Authenticate user with PIN
- Introduction
- Register user
- Determine if we can login user
- Login registered user
- PIN request handlers
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 callOneginiPinCallback#acceptAuthenticationRequest(pinEnteredByUser)
in order to successfully finish PIN creation process. ThepinLength
parameter determines the required pin length. In order to cancel the PIN creation process you can call theOneginiPinCallback#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 theOneginiPinValidationError#getErrorType()
value with theOneginiPinValidationError
's static fields. Please note that there are also theOneginiPinValidationError#getMessage()
andOneginiPinValidationError#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 additionalPinWithConfirmationHandler
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 callOneginiPinCallback#acceptAuthenticationRequest(pinEnteredByUser)
in order to successfully finish the PIN creation process orOneginiPinCallback#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 isAuthenticationAttemptCounter
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.