Authenticate user with PIN

Introduction

The SDK uses the oAuth 2.0 protocol to authenticate the device to access protected resources. To support this protocol the SDK acts as an oAuth 2.0 client. Please read flow details section to learn more.

Register user

The UserClient offers a method to register a new user profile. The new UserProfile value object will be generated during authentication process and will be returned with success callback. This methods is called registerUser and requires two arguments:

  • String[] the scopes authentication is requested for, when no scopes are requested the default scopes of the application will be used,
  • OneginiRegistrationHandler the registration handler to return the authentication result to.

While registering a new user, errors that can occur are reported from the SDK via the specified handler in onError(final OneginiRegistrationError oneginiRegistrationError) method. Please note that you can handle errors which are relevant to you differently. To accomplish this you should compare the OneginiRegistrationError#getErrorType() value with the OneginiRegistrationError error type definitions. The OneginiRegistrationError will also contain an additional error description for debugging and possibly a Throwable object which you can get with the getMessage() and getCause() methods.

Look at example below to see the things explained above in code.

Example code for registering user

  private void registerUser() {
    final OneginiClient oneginiClient = OneginiSDK.getOneginiClient(this);
    oneginiClient.getUserClient().registerUser(Constants.DEFAULT_SCOPES, new OneginiRegistrationHandler() {

      @Override
      public void onSuccess(final UserProfile userProfile) {
        registeredProfile = userProfile;
        userProfileDebugText.setText(userProfile.getProfileId());
        askForProfileName();
      }

      @Override
      public void onError(final OneginiRegistrationError oneginiRegistrationError) {
        handleRegistrationErrors(oneginiRegistrationError);
      }
    });
  }

  private void handleRegistrationErrors(final OneginiRegistrationError oneginiRegistrationError) {
    @OneginiRegistrationError.RegistrationErrorType final int errorType = oneginiRegistrationError.getErrorType();
    switch (errorType) {
      case OneginiRegistrationError.DEVICE_DEREGISTERED:
        showToast("The device was deregistered, please try registering again");

        new DeregistrationUtil(this).onDeviceDeregistered();
        break;
      case OneginiRegistrationError.ACTION_CANCELED:
        showToast("Registration was cancelled");
        break;
      case OneginiRegistrationError.NETWORK_CONNECTIVITY_PROBLEM:
      case OneginiRegistrationError.SERVER_NOT_REACHABLE:
        showToast("No internet connection.");
        break;
      case OneginiRegistrationError.OUTDATED_APP:
        showToast("Please update this application in order to use it.");
        break;
      case OneginiRegistrationError.OUTDATED_OS:
        showToast("Please update your Android version to use this application.");
        break;
      case OneginiRegistrationError.GENERAL_ERROR:
      default:
        // General error handling for other, less relevant errors
        handleGeneralError(oneginiRegistrationError);
        break;
    }
    // start login screen again
    startActivity(new Intent(this, LoginActivity.class));
    finish();
  }

  private void handleGeneralError(final OneginiRegistrationError error) {
    final StringBuilder stringBuilder = new StringBuilder("Error: ");
    stringBuilder.append(error.getMessage());
    stringBuilder.append(" Check logcat for more details.");

    error.printStackTrace();

    showToast(stringBuilder.toString());
  }

Handling user's authentication in the web browser

When registering the end-user the Onegini SDK has to redirect the end-user to the authentication endpoint on the Token Server via the specified browser. The app will be notified about it via the OneginiRegistrationRequestHandler implementation passed in the OneginiClientBuilder. When the user and client credentials are valid the Token Server will redirect the user back to the app.

Example OneginiRegistrationRequestHandler implementation

public class RegistrationRequestHandler implements OneginiRegistrationRequestHandler {

  private static OneginiRegistrationCallback CALLBACK;

  /**
   * Finish registration action with result from web browser
   */
  public static void handleRegistrationCallback(final Uri uri) {
    if (CALLBACK != null) {
      CALLBACK.handleRegistrationCallback(uri);
      CALLBACK = null;
    }
  }

  /**
   * Cancel registration action in case of web browser error
   */
  public static void onRegistrationCanceled() {
    if (CALLBACK != null) {
      CALLBACK.denyRegistration();
      CALLBACK = null;
    }
  }

  private final Context context;

  public RegistrationRequestHandler(final Context context) {
    this.context = context;
  }

  @Override
  public void startRegistration(final Uri uri, final OneginiRegistrationCallback oneginiRegistrationCallback) {
    CALLBACK = oneginiRegistrationCallback;

    // We're going to launch external browser to allow user to log in. You could also use embedded WebView instead.
    final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);

    context.startActivity(intent);
  }
}

When the client credentials are invalid the Token Server is not able to redirect the user back to the app. As this will potentially make the app instance useless unless re-installing the app. The SDK will validate the client credentials and refresh them before redirecting to the token server to receive an access grant.

When user authentication in web browser failed or was canceled, you can abort registration action by calling OneginiRegistrationCallback#denyRegistration().

Handling the registration callback during registration

An authentication via the browser will eventually lead to a redirect towards the app. As the app is a layer on top of the SDK, the library can not handle this redirect itself. It is the responsibility of the app to handle the redirect and delegate it to the library. When delegating a redirect to the SDK, the SDK will verify if the redirect has the right syntax and if it should be handled. If the SDK decides the redirect should be handled it will continue to process it. The app can handle a redirect by specifying a scheme in the AndroidManifest.xml of the application. Please note that the scheme is part of the redirect uri that you have set in the OneginiConfigModel and in the Token Server configuration

Example AndroidManifest.xml

<application>
 ...
  <activity
      android:name=".view.activity.LoginActivity"
      android:launchMode="singleTask">
    <intent-filter>
      <action android:name="android.intent.action.VIEW"/>
      <category android:name="android.intent.category.DEFAULT"/>
      <category android:name="android.intent.category.BROWSABLE"/>

      <data android:scheme="oneginiexample"/>
    </intent-filter>
  </activity>
 ...
</application>

Processing a callback includes validation of the callback itself. Errors reported in the callback will be reported to the app via the handler. For a successful callback the SDK will start using the included authorization grant. Based on this authorization grant an access token will be requested for the specified set of scopes. When the client has the refresh token grant type a refresh token will be returned with the created access token for a correct access grant by the Token Server. Once a refresh token is received the SDK will use a CreatePinDialog implementation passed in OneginiClientBuilder to get the users pin. The pin is used to store the refresh token encrypted. Depending on the setting chosen by the app developer the user should confirm the entered pin or not. The received access token will not be persisted and only stored in memory, so it will be gone the next time the app is started.

Example code to handle registration callback

  @Override
  public void onNewIntent(final Intent intent) {
    super.onNewIntent(intent);
    handleRedirection(intent.getData());
  }

  private void handleRedirection(final Uri uri) {
    if (uri == null) {
      return;
    }

    final OneginiClient client = OneginiSDK.getOneginiClient(getApplicationContext());
    final String redirectUri = client.getConfigModel().getRedirectUri();
    if (redirectUri.startsWith(uri.getScheme())) {
      RegistrationRequestHandler.handleRegistrationCallback(uri);
    }
  }

  @OnClick(R.id.cancel_registration_button)
  public void onCancelRegistrationButton() {
    RegistrationRequestHandler.onRegistrationCanceled()
  }

Determine if we can login user

The UserClient contains the getUserProfiles method which returns set of all registered UserProfile value objects. You can check size of that set to determine if there is need to register new user or if there is possibility to login. If the method isRegisteredAtLeastOneUser from the example below will return false you can assume that no user is authenticated on the device. In that case user have to register before logging in.

Example code

private boolean isRegisteredAtLeastOneUser() {
  final Set<UserProfile> userProfiles = OneginiSDK.getOneginiClient(this).getUserClient().getUserProfiles();
  return userProfiles.size() > 0;
}

You can also use UserClient#isUserRegistered() method in order to check if specified user is registered on the device.

Login registered user

When at least one user has already registered there is possibility to log in that user using authenticateUser method from the UserClient. This method requires two arguments:

  • UserProfile the UserProfile ValueObject that we want to authenticate,
  • OneginiAuthenticationHandler the authentication handler to return the authentication result to.

The result of authentication is an access token with optionally a refresh token. The authentication handler contains:

  • an onSuccess method which lets you know that authentication was finished successfully - at this point, you can request data on behalf of the user,
  • an onError method which is called in every other case.

You can find more information about authentication in user authentication section.

PIN request handlers

At this point the app would crash with a NullPointerException due to the lack of PIN request handlers for creating and verifying a PIN. To prevent that you need to provide your own authentication request handlers using the OneginiClientBuilder methods. The OneginiCreatePinRequestHandler and OneginiPinAuthenticationRequestHandler are required in order to perform authentication flows with a PIN.

Example

public static OneginiClient getOneginiClient(final Context context) {
  OneginiClient oneginiClient = OneginiClient.getInstance();
  if (oneginiClient == null) {
    final Context applicationContext = context.getApplicationContext();
    final CreatePinRequestHandler createPinRequestHandler = new CreatePinRequestHandler(applicationContext);
    final PinAuthenticationRequestHandler pinAuthenticationRequestHandler =
        new PinAuthenticationRequestHandler(applicationContext);

    // will throw OneginiConfigNotFoundException if OneginiConfigModel class can't be found
    oneginiClient = new OneginiClientBuilder(
        applicationContext, createPinRequestHandler, pinAuthenticationRequestHandler)
        .build();
  }
  return oneginiClient;
}

Create PIN request handler

The OneginiCreatePinRequestHandler interface is responsible for handling the PIN creation process. Create a class that implements this interface and overrides the following methods:

  • startPinCreation(final UserProfile userProfile, final OneginiPinCallback oneginiPinCallback, final int pinLength) - this method will be invoked by the SDK whenever there will be a need to create a new PIN (during registration, or during the change pin action). You have to call OneginiPinCallback#acceptAuthenticationRequest(pinEnteredByUser) in order to successfully finish PIN creation process. The pinLength parameter determines the required pin length. In order to cancel the PIN creation process you can call the OneginiPinCallback#denyAuthenticationRequest() method.
  • onNextPinCreationAttempt(final OneginiPinValidationError oneginiPinValidationError) - this method will be called when the PIN provided by user hasn't met the PIN policy. You can check the exact error by comparing the OneginiPinValidationError#getErrorType() value with the OneginiPinValidationError's static fields. Please note that there are also the OneginiPinValidationError#getMessage() and OneginiPinValidationError#getCause() methods which can return more information about the error.
  • finishPinCreation() is called whenever correct PIN was created or the action was canceled by the user. Below is sample code for creating a custom PIN request handler. In this example we created an additional PinWithConfirmationHandler to support the PIN verification step. You can feel free to skip this step if it's not relevant for your use-case. You can check the full solution in our Example App which is available on GitHub.

Example code

public class CreatePinRequestHandler implements OneginiCreatePinRequestHandler {

  public static PinWithConfirmationHandler oneginiPinCallback;
  private final Context context;

  public CreatePinRequestHandler(final Context context) {
    this.context = context;
  }

  @Override
  public void startPinCreation(final UserProfile userProfile, final OneginiPinCallback oneginiPinCallback, final int pinLength) {
    PinActivity.setIsCreatePinFlow(true);
    notifyActivity(context.getString(R.string.pin_title_choose_pin), "");

    CreatePinRequestHandler.oneginiPinCallback = new PinWithConfirmationHandler(oneginiPinCallback);
  }

  @Override
  public void onNextPinCreationAttempt(final OneginiPinValidationError oneginiPinValidationError) {
    handlePinValidationError(oneginiPinValidationError);
  }

  @Override
  public void finishPinCreation() {
    Toast.makeText(context, "CreatePinRequestHandler#finishPinCreation", Toast.LENGTH_LONG).show();
  }

  /**
   * Extended pin handler, used to create PIN verification step
   */
  public class PinWithConfirmationHandler {

    private final OneginiPinCallback originalHandler;

    private char[] pin;

    public PinWithConfirmationHandler(final OneginiPinCallback originalHandler) {
      this.originalHandler = originalHandler;
    }

    public void onPinProvided(final char[] pin) {
      if (isPinSet()) {
        secondPinProvided(pin);
      } else {
        firstPinProvided(pin);
      }
    }

    private void firstPinProvided(final char[] pin) {
      OneginiSDK.getOneginiClient(context).getUserClient().validatePinWithPolicy(pin, new OneginiPinValidationHandler() {
        @Override
        public void onSuccess() {
          PinWithConfirmationHandler.this.pin = pin;
          notifyActivity(context.getString(R.string.pin_title_verify_pin), "");
        }

        @Override
        public void onError(final OneginiPinValidationError oneginiPinValidationError) {
          handlePinValidationError(oneginiPinValidationError);
        }
      });
    }

    public void secondPinProvided(final char[] pin) {
      final boolean pinsEqual = Arrays.equals(this.pin, pin);
      nullifyPinArray();
      if (pinsEqual) {
        originalHandler.acceptAuthenticationRequest(pin);
      } else {
        notifyActivity(context.getString(R.string.pin_title_choose_pin), context.getString(R.string.pin_error_not_equal));
      }
    }

    public void pinCancelled(){
      nullifyPinArray();
      originalHandler.denyAuthenticationRequest();
    }

    private boolean isPinSet() {
      return pin != null;
    }

    private void nullifyPinArray() {
      if (isPinSet()) {
        final int arraySize = pin.length;
        for (int i = 0; i < arraySize; i++) {
          pin[i] = '\0';
        }
        pin = null;
      }
    }
  }

  private void handlePinValidationError(final OneginiPinValidationError oneginiPinValidationError) {
    @OneginiPinValidationError.PinValidationErrorType int errorType = oneginiPinValidationError.getErrorType();
    switch (errorType) {
      case OneginiPinValidationError.WRONG_PIN_LENGTH:
        notifyActivity(context.getString(R.string.pin_title_choose_pin), context.getString(R.string.pin_error_invalid_length));
        break;
      case OneginiPinValidationError.PIN_BLACKLISTED:
        notifyActivity(context.getString(R.string.pin_title_choose_pin), context.getString(R.string.pin_error_blacklisted));
        break;
      case OneginiPinValidationError.PIN_IS_A_SEQUENCE:
        notifyActivity(context.getString(R.string.pin_title_choose_pin), context.getString(R.string.pin_error_sequence));
        break;
      case OneginiPinValidationError.PIN_USES_SIMILAR_DIGITS:
        notifyActivity(context.getString(R.string.pin_title_choose_pin), context.getString(R.string.pin_error_similar));
        break;
      case OneginiPinValidationError.DEVICE_DEREGISTERED:
        new DeregistrationUtil(context).onDeviceDeregistered();
      case OneginiPinValidationError.GENERAL_ERROR:
      default:
        notifyActivity(context.getString(R.string.pin_title_choose_pin), oneginiPinValidationError.getMessage());
        break;
    }
  }

  private void notifyActivity(final String title, final String message) {
    final Intent intent = new Intent(context, PinActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.putExtra(PinActivity.EXTRA_TITLE, title);
    intent.putExtra(PinActivity.EXTRA_MESSAGE, message);
    context.startActivity(intent);
  }
}

For more info on error handling see the error handling topic guide.

PIN authentication request handler

Create a class that implements the OneginiPinAuthenticationRequestHandler interface, which is responsible for handling PIN authentication requests, and override the following methods:

  • startAuthentication(final UserProfile userProfile, final OneginiPinCallback oneginiPinCallback, final AuthenticationAttemptCounter attemptCounter) - this method will be invoked by the SDK whenever there will be a to need authenticate the user. You have to call OneginiPinCallback#acceptAuthenticationRequest(pinEnteredByUser) in order to successfully finish the PIN creation process or OneginiPinCallback#denyAuthenticationRequest() to cancel it.
  • onNextAuthenticationAttempt(final AuthenticationAttemptCounter attemptCounter) - this method is called when the PIN that is provided by the user is incorrect, but his failed attempts limit hasn't been reached yet. The method's parameter is AuthenticationAttemptCounter object providing information about number of failed attempts, remaining attempts and maximum attempts counters.
  • finishAuthentication() - this method is called whenever the authentication process is finished regardless whether it was successful or failed.

Example code

public class PinAuthenticationRequestHandler implements OneginiPinAuthenticationRequestHandler {

  public static OneginiPinCallback oneginiPinCallback;
  private static UserProfile userProfile;
  private final Context context;
  private final UserStorage userStorage;

  public PinAuthenticationRequestHandler(final Context context) {
    this.context = context;
    userStorage = new UserStorage(context);
  }

  @Override
  public void startAuthentication(final UserProfile userProfile, final OneginiPinCallback oneginiPinCallback, final AuthenticationAttemptCounter attemptCounter) {
    PinAuthenticationRequestHandler.userProfile = userProfile;
    PinAuthenticationRequestHandler.oneginiPinCallback = oneginiPinCallback;

    PinActivity.setIsCreatePinFlow(false);
    startPinActivity(userProfile);
  }

  @Override
  public void onNextAuthenticationAttempt(final AuthenticationAttemptCounter attemptCounter) {
    PinActivity.setRemainingFailedAttempts(attemptCounter.getRemainingAttempts());
    startPinActivity(userProfile);
  }

  @Override
  public void finishAuthentication() {
    PinActivity.setRemainingFailedAttempts(0);
  }

  private void startPinActivity(final UserProfile userProfile) {
    final Intent intent = new Intent(context, PinActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.putExtra(PinActivity.EXTRA_TITLE, context.getString(R.string.pin_title_enter_pin));

    final User user = userStorage.loadUser(userProfile);
    intent.putExtra(PinActivity.EXTRA_USER_NAME, user.getName());

    context.startActivity(intent);
  }
}

For more info on error handling see the error handling topic guide.