Introduction

The biid Core SDK is the easiest way to use the biid Platform in an Android app. The biid Core SDK library contains a biid Client. This client handles communication with the biid Platform and manages the biid digital certificates.

system architecture

The developers guide describes how to integrate the biid Core SDK library and use the biid Client. For further details please check the javadoc.

Integration

Android Studio

Include the biid Core SDK library as a dependency in your gradle project:

repositories {

    ...

    maven {
        url "http://nexus.stage-biid.com/content/repositories/client-releases"
    }

    ...
}

dependencies {

    ...

    compile ('com.biid:sdk-core-android:3.2.419'){transitive=true}

    ...
}

Supported Android API levels

The biid Core SDK library supports Android API level 21 and higher.

Manifest

The manifest file contains the configuration required by the SDK to connect to your application. The manifest contains API endpoints, entity & application configuration, SSL certificate pins, and public keys to communicate with the biid APIs. It can be downloaded from the backoffice under the application settings. The manifest is automaticly generated and can not be manually edited. Manual editing will cause checksum failures which will result in a ClientException. Place the manifest file in the directory src/<app_varient>/assets/ within your project, with the filename biid-manifest. The SDK will access this file during initalisation.

Below shows and example manfiest file: src/stage-app/biid-manifest.

Zah2gTkrwCxbFdPokgiCBaijl1Ou/jT/ntLbt1ond7D3kjJXK1g/G7kILYF/ntLbt1ond7D3kjJXK1g
endpoint = api.biid.com
endpoint.pin = sha256/x/35TJJ3uRDDDu6eEoOAU/k35TJJ3uRDDDu6eEoOAU
app.package_name = com.your.application.package.stage
supports_multiple_entities = true
requires_hardware_backed_keystore = true
fcm_sender_id = 1234567
sms_link_scheme = .
app.name = Example Application (Stage)
app.key = 8Xw9T28Xw9T28Xw9T28Xw9T28Xw9T28Xw9T28Xw9T28Xw9T28Xw9T28Xw9T2
app.public_key =
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzejUt4hyiJ/Y72QZsaiy
jA1Ky+fLO9i56U95Of/U5YQIW8JKdFaB/J09cLY9H0Wa5ugAjOLZP4z7HcTA0GWG
pyFOgaxJedLp5S4BcryLhhl9HcNo/9vThZ7dx+zwjihQ6Bct0PEFiJXVKrRy3YEA
WOEGBhaGv9d18w3j5P0NpBpnCxZjAHOvOGa1xFhWVsT1gztCLI6iidq6m9bPDM4e
KN3w6A47057LyaamtZwJugiO1Ts1HIfAoVzVfHktcVS3XSu0M0G3PXxt9xswf3bc
Buo8m4WhxVZihYKuTplb2Ctu03uG15MlAMlyuDjhRJziN1Djzrmd16iJHD1bvdmO
fQIDAQAB

Referencing the biid Client

The biid client, and it’s APIs and properties, is accessible through the SDK’s getClient() function:

Client client = Sdk.getClient();

Initialization

The SDK can be initialized as follows (and should be the first call to the client). This is safe to call on the UI Thread or on a background thread. It should generally be run once per application session. Is can be run at application startup or 'just-in-time' when needed.:

client.initialize(context);

Roles

Once a user is authenticated, a role or roles are assigned to that user. Roles are used to control access to APIs. If the user is not assigned a role required for a particular API, in general a ClientException will be returned with an error message of Access is denied.

The roles assigned to a user are determined by a number of factors including the user’s status, their currently selected entity etc.

The available roles are:

  • ROLE_USER - the default role assigned once a user has authenticated against an entity but is not certified

  • ROLE_USER_EXTENDED - an extended guest role available to certified users on a secondary device

  • ROLE_GUEST - a limited guest role assigned when a user is authenticated but not associated with an entity

  • ROLE_USER_LOCKED - role assigned to an authenticated user who is has a user status of "locked"

  • ROLE_USER_CERTIFIED - the role assigned to a user authenticated against an entity and who is also certified on the new device

All roles can be found here

To get the roles for the currently authenticated user, use the getRoles() API :

List<String> roles = null;
try {
    roles = Sdk.getClient().getRoles();

    if(roles.contains("ROLE_USER_CERTIFIED")){
        Log.i("TAG", "You have ROLE_USER_CERTIFIED, you are certified and can call further API's");
    } else {
        Log.i("TAG", "You do not have the correct role to perform this action.");
    }

} catch (ClientException e) {
    e.printStackTrace();
}

Device Installation ID

A unique installation ID for a device installation is required for successful user authentication and to identity the installation on a particular device. The current installationId can be returned as follows:

try {
    Log.i("TAG", "Your installation ID: " + Sdk.getClient().getInstallationId());
} catch (ClientException e) {
    e.printStackTrace();
}

By default an installationId is automatically generated for each new app installation. However it is possible to set the installationId to a specific value. For example if it needs to match an ID used in another service in the solution.

In this case the installation ID can be set using the setInstallationId(id) method.

Regardless of how the installation ID is populated (manually set or automatically generated), the ID is persisted by the SDK and only needs to be set once per app installation (if the app is deleted and re-installed from a backup the installationID will be lost).

Note: The installation ID must be unique per installation. If this is not the case, attempting to authenticate the user will result in a ClientException.

Summary points for the installation ID:

  • Only set the ID if the installation ID needs to match that of an existing ID.

  • Set the ID before authentication, set it the first time the application is launched.

  • Do not set it more than once (unless you use the same value).

  • Not setting it will result in installationID being to set by the SDK to a generated GUID.

  • The installation ID can be accessed by getInstallationID().

  • The installation ID is persisted by the SDK.

  • The Installation ID will not be backed up, either locally or to iCloud, and therefore will not be restored from backup.

The installation ID can be set as follows:

try {
    final String installationId = UUID.randomUUID().toString();

    Sdk.getClient().setInstallationId(installationId);

} catch (ClientException e) {
    e.printStackTrace();
}

Language Settings

See Localization section for details on how to return and set the language code used for localised content.

Logging in

After the client is initialized, you must authenticate the user with a username and OAuth access token an OAuth authorization end point or from the biid security provider, as well as an optional entity.

This selects the entity to authenticate with, or if no entity is passed in, the entity linked to the app (ie. in a single entity configuration).

client.authenticate(username, accessToken, entity);

If any call fails with an InvalidTokenException, check the resolution in the exception:

if (exception.getResolution() == InvalidTokenException.Resolution.LOGIN) {
    // get a new access token by authenticating again
if (exception.getResolution() == InvalidTokenException.Resolution.RETRY) {
    // retry the current call one more time.
} else {
    // no resolution, treat this as an error.
}

See Exception Handling for more details on handling different types of exceptions.

Check which entity is selected and authenticated:

client.getSelectedEntity()

Multiple entities

The following is only relevant for apps that support multiple entities.

To get a list of entities the user is linked to, use:

List<Entity> entities = client.requestEntitiesForUser();

Or, if the user wants to add another entity, get an entity by url name with:

Entity entity = client.requestEntity(urlName);

Use this call to select another entity:

client.selectEntity(entity);

When you make any other call to the client, the selected entity will be implied.

In certain integrations, for example when only one entity is available and the entity details are known to the SDK integrator, it makes sense to create the entity model to pass into selectEntity() rather than retrieving it via a checkEntityUrl() call. This can be done by creating an Entity object with it’s ID:

final Entity entity = new Entity("2995046156774445007121104");
// constructor parameter is the Entity Id.

try {
    Sdk.getClient().selectEntity(entity);
} catch (NoConnectionException e) {
    e.printStackTrace();
} catch (InvalidTokenException e) {
    e.printStackTrace();
} catch (ClientException e) {
    e.printStackTrace();
}

In addtion there is also a helper API that creates and selects the Entity in one call:

try {
    Sdk.getClient().selectEntity("2995046156774445007121104");
    // the parameter is the Entity Id.
} catch (NoConnectionException e) {
    e.printStackTrace();
} catch (InvalidTokenException e) {
    e.printStackTrace();
} catch (ClientException e) {
    e.printStackTrace();
}

Certification Process

In order for a user to be able to authenticate online and sign documents, they need to have first completed the "certification process". This process involves three steps: Phone Verification, Registration and Certification.

Phone Verification

The device the app runs on needs to be verified before the user can be certified. Even if the app supports multiple entities, the device only needs to be verified once.

Before the device is verified, the user has status UNVERIFIED.

client.requestUser().getStatus() == User.Status.UNVERIFIED

First the user requests an verification code:

client.requestUserDeviceVerification();

An SMS with the verification code will be send to the phone number the user registered with. The verification code is received by a different channel. The verification code format has no spaces or special characters. When the user receives the code, they can verify the device:

client.verifyUserDevice(code);

Registration

After verification the user will have status UNREGISTERED:

client.requestUser().getStatus() == User.Status.UNREGISTERED

To register an account for the selected entity, the user needs to fill in a registration form. The identity fields for this entity can be retrieved and populated as follows:

List<FieldDefinition> fields = client.requestEntityDetails().getUserFieldDefinitions();

// user enters a value for each field

Which will be send off like this:

User user = new User();
for (FieldDefinition field : fields) {
   user.put(field.getName(), value)
}

client.registerUser(user);

Note that if field.isReadOnly() is true, changes in the value of the field will be ignored.

Certification

For a user to become "certified", a Digital Certificate should be issued by a recognised Certification Authority (CA), and a "private key" should be generated and securely stored in the Secure Enclave of the device.

A registered user on a verified device will have a status of UNACCREDITED.

client.requestUser().getStatus() == User.Status.UNACCREDITED

A user can be certified in different ways, depending on the "Accreditation Level" required for certification.

The accreditation process is the process of verifying the user identity, either online or in person, to enable the CA to issue a Digital Certificate for the user.

The biid Platform supports 3 levels of accreditations (Level 1 to Level 3). For each level there are different legal and procedural requirements, needed to verify the user identity.

Different accreditation levels can be defined for different entities and an entity can support multiple accreditation levels, with each level replacing the previous one.

Once an accreditation process is successfully completed, the user will have the status UNCERTIFIED. Once accredited biid Platform will generate a 16 digit code for the user. This code should be kept safe and secret, in order to protect the user account. This code is known as a "DIAC", which stands for "Digital Identity Activation Code".

Receipt of a DIAC, either from an API or via email, etc., is a prerequisite for completing the Certification Process, as a Digital Certificate can only be generated when providing a valid DIAC for the specific accreditation level.

The following paragraphs detail the requirements and processes a user needs to follow in order to get accredited and certified to a specific level.

Accreditation Level L1

An accreditation Level 1 allows a user to "self accredit" i.e. registering their details online without the need of an operator reviewing the provided information.

Once the Level 1 accreditation process is complete, the user will be issued a "self signed" Digital Certificate from the biid platform directly (not from a Certificate Authority).

This type of certificate does not provide any legal validity when signing documents but it can still be used to authenticate online and authorise transactions.

If a user has a status of UNCERTIFIED and an accreditation Level 1, the biid platform exposes an API to "auto-certify" the user. In this case the user does not need to perform any manual action such as typing their Level 1 DIAC into a certification screen.

Certifying a user for accreditation Level 1 can be done as follows:

user.getAccreditationLevel() == User.AccreditationLevel.L1

The app can request a certificate without user interaction:

try {
    Sdk.getClient().certifyUserAtLevel1(this);
} catch (NoConnectionException e) {
    e.printStackTrace();
} catch (InvalidTokenException e) {
    e.printStackTrace();
} catch (UserNotAccreditedException e) {
    e.printStackTrace();
} catch (DeviceNotVerifiedException e) {
    e.printStackTrace();
} catch (ClientException e) {
    e.printStackTrace();
}

Level 2 and Level 3 Certification Process

Level 2 and Level 3 accreditation may require the user to provide additional information and proof of identity. An operator of the entity will then have to review and validate this information, before the user can be accredited and a DIAC generated.

The amount of details and documentation required to accredit a user Level 2 or Level 3 may differ substantially, but in general the accreditation process depends on the legal framework the generated biid identity needs to comply with.

For instance, if a "Qualified Digital Certificate", according to the European eIDAS Regulation need to be issued, the accreditation process will require the user provides a copy of a valid ID document (e.g. a Passport or national ID card) as well as undertaking an identity verification process that might include biometric verification, face analysis and even a visit to a local checkpoint, to meet an operator in person.

The required accreditation process is always agreed between the organisation that provides the biid service and the Certification Authority that will need to issue Digital Certificates to end users.

  • Level 2 accreditation (known as "Low" in the eIDAS Regulation) does not require the user to visit a checkpoint or undertake a video call in order to verify their identity, it does however require that an operator approves the user accreditation request.

  • Level 3 accreditation (known as "Substantial" in the eIDAS Regulation) requires that the user either visit in person an authorised checkpoint, undertake a video call with an operator or use a recognised ID verification tool, in order to formally verify their identity.

Full details on the eIDAS Regulation can be found here.

Locations at which a user can be accredited can be retrieved using the checkpoint SDK calls.

Once the accreditation process has been completed and the DIAC received, the user can certify by entering the DIAC with the certifyUser API, as follows:

try {
    Sdk.getClient().certifyUser(this, "3924677809504936".toCharArray());
} catch (NoConnectionException e) {
    e.printStackTrace();
} catch (InvalidTokenException e) {
    e.printStackTrace();
} catch (UserNotAccreditedException e) {
    e.printStackTrace();
} catch (ValidationException e) {
    e.printStackTrace();
} catch (DeviceNotVerifiedException e) {
    e.printStackTrace();
} catch (ClientException e) {
    e.printStackTrace();
}

In the background the certifyUser process is made up of the following steps:

  1. An asymmetric key pair is created on the device.

  2. A certificate signing request (CSR) is generated, signed with the private key and sent along with the DIAC, as part of the API request.

  3. If the DIAC and CSR are valid, the CSR is sent off to the appropriate Certification Authority. The certifyRequest will then return a success response and the user status will be set to CERTIFICATE_PENDING.

  4. If the CA issues a signed digital certificate, the next call to requestUser will show the status CERTIFIED. The signed certificate is then retrieved and securely stored on the device.

If requestUser is called immediately after a request for certification and the user is successfully certified, then it is likely that the request will return an InvalidTokenError error as the cached token will no longer be valid, and authentication needs to take place again to aquire the new elevated access toekn. This can be resolved by initiating the action detailed in the resolution property of the InvalidTokenException. See javadoc for further details. In this case all that is needed if the resolution is RETRY is to call the requestUser() method again.

See InvalidTokenException for general token exceptions.

See certifyUser() for more details of certificiation.

See Exception Handling for more details on specific exception handling.

Level 2 and Level 3 Certification Flow

user certification L2 L3

No matter the accreditation level, once successfully certified the user will be able to authenticate online, authorise transactions and sign documents.

The legal validity of the digital signature will instead depend on the accreditation level of the user, with Level 3 providing the highest level of validity.

Uncertify

In certain circumstances the user may want to uncertify for the current selected entity on the device.

This can be done using the uncertify() API.

try {
    Sdk.getClient().uncertify();
} catch (NoConnectionException e) {
    e.printStackTrace();
} catch (InvalidTokenException e) {
    e.printStackTrace();
} catch (ClientException e) {
    e.printStackTrace();
}

After successfully calling uncertify(), subsequent calls to other APIs will result the return of an InvalidTokenException as the cached token will no longer be valid. This can be resolved by initiating the action detailed in the resolution property of the error. See javadoc for further details.

At this point the user’s status for this entity will be UNCERTIFIED.

See InvalidTokenException for general token exceptions.

Transactions

Types of Transactions

A certified user can manage the following three types of transactions:

Transaction Type Description

Authentication

AUTH

Authentication transaction

Documents

DOC

Document signing transaction

Messages

MSG

Message only transaction

Transaction Subtypes

In addition to the transaction type, transactions also have a subType property. This can be used in conjunction with the Transaction Type, to determine the exact type of the transaction.

Currently subTypes only exist for auth (authentication) and doc (document) transactions:

Transaction Type SubType Description

Authentication

AUTH_WEB

Web login transasction

AUTH_IDV

Identity verification transaction

Document

DOC_PDF

PDF document signing transactions

DOC_XML

XML document signing transactions

DOC_MIXED

Mixed PDF & XML document signing transaction

Transaction Actions

Each transaction type has a sepcific set of actions that can be applied to it. These are detailed below:

Transaction Type Confirm Sign Reject Dismiss

Authentication

Document

Message

Requesting Transactions

The requestTransactions() method can be used to request a paged and filtered list of transactions. The results can be filtered on Status (by requesting transactions with a specific status to return) and the Type of transaction to required.

List<Transaction> transactions = client.requestTransactions(offset, max, statuses, type, documentType);

Authentication requests

An authentication request can be confirmed by first requesting the Transaction’s details by it’s ID and then confirming it using confirmAuthentication(). On successful confirmation, the evidence generated is stored in the biid platform with the transaction.

AuthenticationTransaction transaction = requestAuthenticationTransaction(id);

client.confirmAuthentication(transaction);

Or reject the request like so:

client.rejectAuthentication(id);

Document signing requests

Retrieving document details

The details of a Document in a document transaction are accessed by the document’s ID as follows:

try {
    Document document =  Sdk.getClient().requestDocument("id");
} catch (NoConnectionException e) {
    e.printStackTrace();
} catch (InvalidTokenException e) {
    e.printStackTrace();
} catch (ClientException e) {
    e.printStackTrace();
}

Document Image preview

To retrieve a preview image of a document, the document object should first be retrieved as above and then passed in as a parameter to the following request:

try {
    File file = Sdk.getClient().requestDocumentPreview(document);
} catch(NoConnectionException e) {
	e.printStackTrace();
} catch(InvalidUserStateException e) {
	e.printStackTrace();
} catch(InvalidTokenException e) {
	e.printStackTrace();
} catch(UserLockedException e) {
	e.printStackTrace();
} catch(ClientException e) {
	e.printStackTrace();
}

Document content

To download the content of a document to the device, the document object should first be retrieved as above and then passed in as a parameter to the following request:

try {
    ParcelFileDescriptor descriptor = Sdk.getClient().requestDocumentContent(document);

} catch(NoConnectionException e) {
	e.printStackTrace();
} catch(InvalidUserStateException e) {
	e.printStackTrace();
} catch(InvalidTokenException e) {
	e.printStackTrace();
} catch(UserLockedException e) {
	e.printStackTrace();
} catch(ClientException e) {
	e.printStackTrace();
}

Publicly accessible documents

If a document has been marked as isPublic on the server side, it is possible to retrieve the document content, for display or download, via it’s getPublicUrl() method.

Signing a document (Level 1, 2, 3 accredited user)

A user accredited to Level 1, 2 or 3 can sign the documents in a transaction as follows:

 try {
    DocumentsTransaction transaction = Sdk.getClient().requestDocumentsTransaction("id");
    Sdk.getClient().signDocuments(transaction);

} catch(TransactionNotPendingException e) {
	e.printStackTrace();
} catch(NoConnectionException e) {
	e.printStackTrace();
} catch(InvalidUserStateException e) {
	e.printStackTrace();
} catch(InvalidTokenException e) {
	e.printStackTrace();
} catch(UserLockedException e) {
	e.printStackTrace();
} catch(ClientException e) {
	e.printStackTrace();
}

A document signing request can be rejected as follows:

try {
    DocumentsTransaction transaction = Sdk.getClient().requestDocumentsTransaction("id");
    Sdk.getClient().rejectDocuments(transaction.getId());

} catch(TransactionNotPendingException e) {
	e.printStackTrace();
} catch(NoConnectionException e) {
	e.printStackTrace();
} catch(InvalidUserStateException e) {
	e.printStackTrace();
} catch(InvalidTokenException e) {
	e.printStackTrace();
} catch(UserLockedException e) {
	e.printStackTrace();
} catch(ClientException e) {
	e.printStackTrace();
}

Messages

A Message transaction is an information based transaction. It has only one action and that is Dismiss. Dismissing a Message Transaction, generates evidence providing proof that the message has been "seen".

A message can be dismissed by first requesting the message Transaction’s details by it’s ID and then dismissing it using dismissMessage() :

try {
    Sdk.getClient().dismissMessage(messageTransaction);

} catch(TransactionNotPendingException e) {
	e.printStackTrace();
} catch(NoConnectionException e) {
	e.printStackTrace();
} catch(InvalidUserStateException e) {
	e.printStackTrace();
} catch(InvalidTokenException e) {
	e.printStackTrace();
} catch(UserLockedException e) {
	e.printStackTrace();
} catch(ClientException e) {
	e.printStackTrace();
}

Push messages

The biid Platform can send a push message through the Firebase Cloud Messaging service when the user’s certificate has been signed or when a transaction is created.

To enable this, integrate Firebase Cloud Messaging in your app and enter the (long) server key in the biid back-office.

Extend the FirebaseInstanceIdService to pass the token to the biid Client:

public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService {

    @Override
    public void onTokenRefresh() {

        ...

        String token = FirebaseInstanceId.getInstance().getToken();
        client.onFcmTokenRefreshed(token);

        ...
    }
}

And extend the FirebaseMessagingService to handle the messages:

public class MyFirebaseMessagingService extends FirebaseMessagingService {

    @Override
    public void onMessageReceived(RemoteMessage message) {

        ...

        biidNotification notification =
            SDK.getClient().onFcmMessageReceived(message.getFrom(), message.getData());

        if (notification == null) {
            // the push message is not from the biid Platform
        } else {
            // notify the user
        }

        ...
    }
}

SMS messages

The biid Platform can also send an SMS message with a link when the user’s certificate has been signed or when a transaction is created.

To enable this, enter an app identifier in the biid back-office.

Add the following intent-filter to the activity in which you want to handle the transaction notification:

<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="http" android:host="[your app identifier]" />
</intent-filter>

And handle the intent like this:

biidNotification notification = client.onSmsReceived(intent);

if (notification == null) {
    // the SMS is not from the biid Platform
} else {
    // notify the user
}

Note: intercepting text messages is no longer recommended by Google and the permissions are now restricted.

Exception Handling

Successful requests made to the Client SDK are returned from the method itself. Failures from the SDK are flagged using exceptions. This section details some of the important exceptions that are thrown from various APIs. The biid SDK relies heavily on exception handling and as such exceptions should not be ignored on any SDK calls as they represent all the ways an API can fail.

There are five different types of exceptions that can be handled by the SDK. Each is discussed in detail below:

Invalid User State Exceptions

Most useful interactions with the SDK require the user to have certified their device i.e. a signed certificate exists on the device. This is represented by the user status of CERTIFIED. A user with this status can create, sign, accept, confirm and reject different types of transactions. However there are several status iterations that the SDK has to go through to get this status see Certification Process for more information. For example if you try to sign a transaction and you do not have the status of CERTIFIED and therefore no signed certificate exists on your device, you will receive a UserNotCertifiedException. UserNotCertifiedException is a subclass of an InvalidUserStateException. These types of exceptions are a hint that the user is in the incorrect state to perform the action that was requested. These exceptions are very useful in that they are somewhat unavoidable in wider user journeys. User journeys which are interrupted by these exceptions can be caught and corrected easily.

Once an invalid state is caught the best practice is to perform a requestUser() call. The result of this call will always give the current user status. See requestUser() for a more detailed discussion on this topic. The Application implementing the SDK should always be built in a reactive way with the requestUser call at its center. For example, during user on-boarding the user will be taken through the registration, verification and accreditation phases before certification (generally in that order). These type of exceptions detect and inform the developer that something was missed or the user status has changed unexpectedly and the user status should be requested and acted on.

These types of exceptions can be caught specifically using a subclass of the InvalidUserStateException. For example you can catch a UserNotRegisteredException if the user is not registered. Alternatively the InvalidUserStateException can be caught for all invalid states and the direct cause retrieved using the getInvalidState() method, which for the above example would return NOT_REGISTERED. In either case the exception handler should always send the user through a journey that requests the user status and pushes the user to the correct flow to resolve the incorrect state.

Invalid states can happen for a number of reasons, for example; a result of a fraud check done by a payment provider may in extreme cases cause the users certificate to be revoked. This will take the user from the CERTIFIED status to the UNCERTIFIED status, which means the certificate has been revoked and removed from the device and the user will need to go through the Certification Process again (call the certifyUser method). In another example, the user may change their telephone number, this will mean the user will need to re-verify their telephone number and will receive the status of UNVERIFIED. In this case the user should be pushed through to the Phone Verification.

The invalid state exceptions do not give you the current status of the user so somewhere in resolution journey of this error, a requestUser() call should me made to retrieve the new status, and react to it.

The InvalidUserStateException and its subclasses all contain a traceId. As the exception is easily recoverable there is no need to display this traceId to the user, however it can be logged if the developer wishes for error resolution and analytics purposes for example.

All InvalidUserStateExceptions are logged into elastic search, and are addressable via the getTraceId() method.

See available subclasses:

See also:

requestUser documentation for resolution techniques and how to react to a status.

getTraceId() to enrich error logging.

See Exception Handling Examples for some examples in handling these types of exceptions.

Validation Exceptions

Validation Exceptions are represented by one type of exception, a ValidationException. These types of exceptions are very common and should be handled correctly. They are thrown by API’s which accept user input which needs to be validated. When input validation performed by the platform fails, this exception will be thrown with error messages returned from the server.

A single ValidationException is capable of encapsulating multiple errors from a single exception. Each ValidationException will contain a map of field name and errors messages for each validation that failed. A field name is either a user field name (e.g. registration field name, see Registration) as defined in the back office or a parameter name of the biid SDK method. For example; if a user tries to complete a registration user journey and calls the registerUser(User user) method and some of the user fields are incorrectly entered or are invalid, this type of exception will be returned. Each error can be referenced back to the registration field in question using the keys in the getErrors() method of the ValidationException. The messages in this map are designed to be directly displayed to the user and are configurable and localisable in the back office.

Often validation exceptions do not contain a traceID as they are not logged in elastic search due to their frequency and self correcting nature. This exception has no subtypes.

See Exception Handling Examples for some examples in handling these types of exceptions.

Invalid Token Exceptions

Invalid Token Exceptions are represented by a single exception; an InvalidTokenException. These exceptions are thrown when the token that is generated by the SDK is invalid. The following could cause this to happen:

  • Token expiry – The token currently being used by the SDK has expired.

  • Scope change – The token is no longer valid because the access permissions, scope or grant have changed.

  • OAuth token verification failure – A token or credential used in a claim has expired, or is revoked.

There are only two methods for resolving this exception: re-authorisation or re-authentication.

Token expiry and scope changes are mostly resolved by requesting a new token, this is the most common cause of this exception and the SDK handles most of this on behalf of the developer. However all other causes of an InvalidTokenException require the user to re-authenticate to the SDK, this could mean re-entering credentials. This second type of exception is very rare and generally once a user has entered credentials into an application and acquired a token which is sent to the authenticate() method of the SDK, the resultant token lasts for the length of the session.

For tokens that have expired or changed scope, the SDK will reauthorize (request a new token) on behalf of the developer. The exception will still be thrown post reauthorization but the exception gives the developer a resolution hint to fix the problem.

The getResolution() method returns an resolution enum of two types which represent the way to correct the problem (see resolution enum). This enum can be either RETRY or LOGIN.

An InvalidTokenException with RETRY : Simply call the method that threw this exception again. The new token will be used this time.

An InvalidTokenException with LOGIN : This is a more serious token exception and the user may have the re-authenticate with the calling application, as there is a problem with the supplied OAuth token.

See InvalidTokenException for more details.

See Exception Handling Examples for some examples in handling these types of exceptions.

Client Exceptions

Client Exceptions are represented by a single type of exception the ClientException. These exceptions are generally thrown to aid developers during the SDK integration to the application, wrapping error conditions or errors returned from the server during setup. ClientExceptions also encapsulate any unknown errors that happen. The android core SDK will never crash your application as it wraps RuntimeExceptions and throws a ClientException. If an error condition is sent from an API call, these are caught in the SDK and thrown up the stack to the calling application.

ClientExceptions are not recoverable and should not be seen in a production application in normal cases. However errors do happen and these exceptions catch them. All production applications have user journeys which should display screens to the user in the event something unknown or unexpected were to happen. For example if some critical API goes down due to an outage these are the exceptions which will be thrown. An 'unknown error' user journey should be shown to the user here and the appropriate messages displayed. The traceId of the exception is very useful here (see getTraceId() for more details). The traceId is a unique code which identifies the specific error instance to the user at the time the error occurred. In the event of these types of exceptions this code should be logged with any analytics call to help identify the problem if reported by a user. This error code could also be physically shown to the user, this would help third line support identity the problem if the user reports the code to first line support. This code can be recalled from elastic search within the biid platform.

There is one subclass of a ClientException known as ClientNotSupportedException. These can be caught by the developer as a flag that the version of the SDK in not compatible with the version of the platform which it is connecting two. These can be thrown from any API but do not need to be explicitly caught.

ClientExceptions contain a number of methods to help identity the error:

  • getMessage() : An a string providing additional information where relevant

  • getTraceID() : unique ID generated per API call. Can be used for error tracking

See also:

See Exception Handling Examples for some examples in handling these types of exceptions.

SDK Process Specific Exceptions

The last type of exceptions that can be thrown from the SDK are specific to the process that is being called. These range from phone validation processes, the Certification Process, transaction handling or security processes.

These types of exceptions are thrown for very different reasons and should be considered when calling the relevant functions.

Listed below are some important examples of these exceptions. All of these types exceptions have a traceId and are logged to elastic search.

Verification Process Exceptions:

Certification Process Exceptions:

Security Exceptions:

NoConnectionException

A NoConnectionException is thrown if a network request fails. Reasons for failure can include:

  • No active network available

  • No matching certificate found for the pinned endpoint (see Certificate Pinning)

Certificate Pinning

The biid SDK implements an additional level of security through the use of Certificate Pinning (SSL Pinning). Certificate pinning ensures that the SSL certificate returned from an SSL connection exactly matches a hash of the certificate which is stored within the manifest.

The manifest which is generated contains the certificate hash (PIN), there is no need to manually import or add anything.

Using the Trace ID

Some exceptions generate a trace Id which is a unique ID that is associated with the failed request. This allows the request to be tracked though the back end micro-services. This id has been exposed via the getTraceId() method in most exceptions as an aid to debugging any issues with the call i.e. it can be used to find the request in elastic search or the error logs. This ID can also be used to uniquely identify customer errors in production enviroments, the trace ID may want to be surfaced in a log or in the UI to the customer in some situations.

The traceID can be accessed as followed:

try {
    final User user = Sdk.getClient().requestUser();

} catch (NoConnectionException e) {
    // bad connection
} catch (InvalidTokenException e) {

    Log.e("TAG", "Invalid Token, TraceID: " + e.getTraceId(), e);
    e.printStackTrace();
} catch (ClientException e) {

    Log.e("TAG", "Client exception: "+ e.getMessage());
    // e.getMessage() will print a trace ID if relevant.

    e.printStackTrace();
}

Localization

The following APIs are available for managing the language localization. Any changes to language settings, require a login or re-authentication before they will be applied to messages returned from the server.

getLanguage() returns the current language code (ISO 639-1) used for localised content:

try {
    String language = Sdk.getClient().getLanguage();
} catch (ClientException e) {
    e.printStackTrace();
}

setLanguage() overrides the device’s language for returned localised content:

try {
    Sdk.getClient().setLanguage("en");
} catch (NoConnectionException e) {
    e.printStackTrace();
} catch (InvalidTokenException e) {
    e.printStackTrace();
} catch (ClientException e) {
    e.printStackTrace();
}

Additonal Code Samples

Extend Permissions

Extends Permissions allows a certified user to see their transactions on a secondary device without having to have certified on that device itself.

try {

    Sdk.getClient().requestExtendPermissions("3924677809504936".toCharArray());

} catch (NoConnectionException e) {
    e.printStackTrace();
    // no connrection
} catch (InvalidTokenException e) {
    // token expired, reauthentication could be needed.
    e.printStackTrace();
} catch (UserNotAccreditedException e) {
    // user has to be accredited and certified on at least one other device
    e.printStackTrace();
} catch (ValidationException e) {
    e.printStackTrace();
    // incorrect DIAC
} catch (DIACLimitException e) {
    e.printStackTrace();
    // number of requests with DIAC exceeded
} catch (DeviceNotVerifiedException e) {
    e.printStackTrace();
    // device is not verified
} catch (ClientException e) {
    e.printStackTrace();
    // The request failed
}

Exception Handling Examples

Below shows some example code for handling several different types of exceptions referenced earlier in the documentation.

public void registerUser() {

    final Map<String, TextView> errorViewMap = new HashMap<>();

    try {

        Sdk.getClient().registerUser(new User()); // erroneous user

    } catch(NoConnectionException noConnectionException) {

        startInfoActivity("It seems there is problem with your connection, please try again", null);

    } catch(InvalidUserStateException invalidStateException) {

        requestUserStatus(invalidStateException);

    } catch(InvalidTokenException invalidTokenException) {

        if(invalidTokenException.getResolution() == InvalidTokenException.Resolution.RETRY) {
            registerUser();
        } else {
            showLoginActivity();
        }

    } catch(ValidationException validationException) {

        final Map<String, String> errors = validationException.getErrors();

        for(String key: errors.keySet()) {

            Log.i("errors", "incorrect field: \'" + key + "\' failed validation with message: \"" +  errors.get(key) + "\"");

            errorViewMap.get(key).setError(errors.get(key));
        }

    } catch(UserLockedException userLockedException) {

        startUserLockedScreen();

    } catch(ClientException clientException) {
        Log.i("error", clientException.getMessage() + " traceId:"+ clientException.getTraceId());
        // log to server...
    }
}

private void requestUserStatus(InvalidUserStateException invalidStateException) {

    if(invalidStateException != null)
        Log.i("error", "Tryed to call method with user in incorrect state: " + invalidStateException.getState().toString());

    try {

        switch(Sdk.getClient().requestUser().getStatus()) {

            case LOCKED:                startUserLockedScreen(); break;
            case UNACCREDITED:          startInfoActivity("Contact your service provider to accredit user", null); break;
            case UNVERIFIED:            startDeviceVerificationActivity(); break;
            case UNREGISTERED:          startRegistrationActivity(); break;
            case UNCERTIFIED:           startCertificationActivity(); break;
            case CERTIFICATE_PENDING:   waitForPush(); break;
            case CERTIFIED:             onCertifiedUser(); break;
        }

    } catch(NoConnectionException e) {

        startInfoActivity("It seems there is problem with your connection, please try again", null);

    } catch(InvalidTokenException invalidTokenException) {

        if(invalidTokenException.getResolution() == InvalidTokenException.Resolution.RETRY) {
            requestUserStatus(null);
        } else {
            showLoginActivity();
        }

    } catch(UserLockedException userLockedException) {

        startUserLockedScreen();

    } catch(ClientException clientException) {

        Log.i("error", clientException.getMessage() + " traceId:"+ clientException.getTraceId());
        // log to server...
    }

}

private void showLoginActivity() {

    // show login screen, Attempt a logout here.
}

private void startRegistrationActivity() {

    // start registration activity
}

private void startDeviceVerificationActivity() {

    // start device verification activity
}

private void startCertificationActivity() {

    // start the certification activity
}

private void startInfoActivity(final String information, final String traceId ) {

    // start activity that shows some information about the error state, it would help the suuport staff if the user quoted the traceId.

}

private void startUserLockedScreen(){

    // start activity that shows how the user can get in contact with first line support
}

private void waitForPush(){

    // wait for a push notifcaition to inform the user there certifiate has been signed, or poll the status.
}

private void onCertifiedUser() {

    // create tranasctions, sign documents ... etc
}