//HAAPI Android SDK Documentation
HAAPI Android SDK Documentation
[androidJvm] Android library with classes and functions required to access Curity Identity Server Hypermedia Authentication API (HAAPI) from Android devices. The library allows a client to go through an authorization flow via HAAPI using a set of models that represent the different responses/steps involved.
For details about the representations used in HAAPI please refer to the HAAPI Data Model documentation.
Requirements
- The HAAPI SDK framework works on Android 8.0+ (API level 26+)
Getting started
Setting up HAAPI SDK
In your app/build.gradle
, add identityserver.haapi.android.sdk
to the dependencies block as demonstrated below.
apply plugin: 'com.android.application'
android { ... }
dependencies {
implementation("se.curity.identityserver:identityserver.haapi.android.sdk:4.3.0")
...
}
Usage
Configuration
Start by creating a HaapiConfiguration for the client access to the server.
// Configuration parameters
val clientId = "haapi-android-client"
val baseURLString = "https://10.0.2.2:8443"
val intermediatePath = "/oauth/v2"
val baseUri = URI.create(baseURLString)
val tokenEndpointUri = URI.create("$baseURLString$intermediatePath/oauth-token")
val authorizationEndpointUri = URI.create("$baseURLString$intermediatePath/oauth-authorize")
val appRedirect = "app://haapi"
// HaapiConfiguration
val haapiConfiguration = HaapiConfiguration(
keyStoreAlias = "keyStoreAlias",
clientId = clientId,
baseUri = baseUri,
tokenEndpointUri = tokenEndpointUri,
authorizationEndpointUri = authorizationEndpointUri,
appRedirect = appRedirect
)
Instantiate a HaapiAccessor via the HaapiAccessorFactory to access HaapiManager and OAuthTokenManager.
HaapiAccessorFactory allows obtaining the accessors to access HAAPI and OAuth from the current device, based on an initial static configuration and the device's capabilities.
The preferred access strategy is to obtain HAAPI access tokens using client attestation, i.e. the device’s key attestation capabilities. If attestation is not supported, or if the Curity Identity Server deems the attestation data as invalid, an optional fallback strategy based on Dynamic Client Registration can be used.
The DCR-based fallback uses templatized client registration: the client configured in HaapiConfiguration
is used to register a dynamic client based on a template ID configured via setDCRConfiguration
. The registration happens on the first time the fallback is used for a given template client ID. The resulting client data is stored on the device and considered by HaapiAccessorFactory
on subsequent runs.
The HaapiAccessor instances created by this class include:
- A ready-to-use HaapiManager to execute authorization flows.
- A ready-to-use OAuthTokenManager to execute OAuth requests like refreshToken and revoke.
ℹ️ When the DCR-based access is used, both HaapiManager and OAuthTokenManager use credentials different from what’s supplied in the initial configuration.
The recommended way to use HaapiAccessorFactory
is to create and configure a single instance and invoke create
once before going through an authorization flow via HAAPI.
Note that HaapiManager internally uses a HaapiTokenManager
to manage the necessary resources to enforce security and identity when the app communicates with the Curity Identity Server. Please refer to the IdsvrHaapiDriver documentation to know more about the underlying framework.
// Configurations
val haapiConfiguration: HaapiConfiguration = ... // See `Configuration` section.
// Coroutine exception handler
private val coroutineExceptionHandler =
CoroutineExceptionHandler { _, throwable ->
// Handle the exception
handleThrowable(throwable)
}
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val haapiAccessor = HaapiAccessorFactory(
haapiConfiguration = myApplication.haapiConfiguration
)
.create(onCoroutineContext = this.coroutineContext)
val haapiManager = haapiAccessor.haapiManager
val oAuthTokenManager = haapiAccessor.oAuthTokenManager
}
Configuring HTTPCookie management
To establish and maintain a potentially long-lived session between client and server, HttpURLConnection includes an extensible cookie manager. For more information on setting up cookie management for HttpUrlConnection
please refer to Android API docs. The HAAPI SDK
framework does not handle this automatically as it should be setup at the application level as per use case requirement. To do so, only a couple of lines of code are required to enable the cookie management behaviour like demonstrated below.
// override default behaviour to accept all cookies
CookieHandler.setDefault(CookieManager(null, CookiePolicy.ACCEPT_ALL))
// keep default behaviour to accept only cookies from original server
val cookieManager = CookieManager()
CookieHandler.setDefault(cookieManager)
Running the Haapi flow (authorization flow)
Invoke HaapiManager.start
to start the HAAPI flow that returns a HaapiResponse.
The HaapiResponse
can return one of these cases:
HaapiRepresentation
: The object has to be cast to the corresponding step as illustrated below. The step's properties have to be presented to the user. Based on the user's action or chosen link,HaapiManager.submit
orHaapiManager.followLink
has to be invoked to move forward.ProblemRepresentation
: The object represents a problem that occurred and it requires corrections from the user or the client.ClientOperationStep
: The object represents a client operation, which requires the client to trigger an external action like opening a web browser or an application.
⚠️ Exceptions can be thrown such as HaapiManagerUnsupportedContentTypeException
, HaapiManagerUnsupportedHttpMethodException
, HaapiManagerUnexpectedException
and Exception
.
The HaapiResponse
needs to be handled as illustrated below.
GlobalScope.launch(Dispatchers.IO) {
val haapiResponse = haapiManager.start(this.coroutineContext)
handleHaapiResponse(haapiResponse)
}
// Handlers
private fun handleHaapiResponse(haapiResponse: HaapiResponse) {
when (haapiResponse) {
is HaapiRepresentation -> {
handleHaapiRepresentation(haapiResponse)
}
is ClientOperationStep -> {
handleClientOperationStep(haapiResponse)
}
is ProblemRepresentation -> {
handleProblemRepresentation(haapiResponse)
}
}
}
private fun handleHaapiRepresentation(haapiRepresentation: HaapiRepresentation) {
when (haapiRepresentation) {
is AuthenticatorSelectorStep -> { /* handle the AuthenticatorSelectorStep */ }
is PollingStep -> { /* handle the PollingStep */ }
is InteractiveFormStep -> { /* handle the InteractiveFormStep */ }
is UserConsentStep -> { /* handle the UserConsentStep */ }
is GenericRepresentationStep -> { /* handle the GenericRepresentationStep */ }
is RedirectionStep -> { /* handle the RedirectionStep */ }
is ContinueSameStep -> { /* handle the ContinueSameStep */ }
is OAuthAuthorizationResponseStep -> { /* handle the OAuthAuthorizationResponseStep */ }
else -> {
throw IllegalStateException("HaapiRepresentation is not handled.")
}
}
}
private fun handleClientOperationStep(clientOperationStep: ClientOperationStep) {
when (clientOperationStep) {
is ExternalBrowserClientOperationStep -> { /* handle the ExternalBrowserClientOperationStep */ }
is GenericClientOperationStep -> { /* handle the GenericClientOperationStep */ }
is WebAuthnRegistrationClientOperationStep -> { /* handle the WebAuthnRegistrationClientOperationStep */ }
is WebAuthnAuthenticationClientOperationStep -> { /* handle the WebAuthnAuthenticationClientOperationStep */ }
is EncapClientOperationStep -> { /* handle the EncapClientOperationStep */ }
else -> {
throw IllegalStateException("ClientOperationStep is not handled.")
}
}
}
private fun handleProblemRepresentation(problemRepresentation: ProblemRepresentation) {
// Handle the ProblemRepresentation.
}
With a HaapiRepresentation
, invoke HaapiManager.submit
or HaapiManager.followLink
to move forward on the HAAPI flow and handle the new HaapiResult
.
private fun submit(formActionModel: FormActionModel, parameters: Map<String, Any>) {
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val haapiResponse = haapiManager.submitForm(
form = formActionModel,
parameters = parameters,
onCoroutineContext = this.coroutineContext
)
handleHaapiResponse(haapiResponse)
}
}
private fun followLinK(link: Link) {
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val haapiResponse = haapiManager.followLink(
link = link,
onCoroutineContext = this.coroutineContext
)
handleHaapiResponse(haapiResponse)
}
}
When obtaining an OAuthAuthorizationResponseStep
, the HAAPI flow reaches the end. OAuthAuthorizationResponseStep
contains a code
that is required to get an access_token
.
Running the OAuth flow
Invoke OAuthTokenManager.fetchAccessToken
with the code in OAuthAuthorizationResponseStep.properties
to get a TokenResponse
.
When receiving a TokenResponse
, the object can be:
- a
SuccessfulTokenResponse
: the access_token is present with other properties such as arefresh_token
if present. - an
ErrorTokenResponse
: an error that is returned by the server. Check theerror
and theerrorDescription
and retry if possible with the missing configuration.
⚠️ Exceptions can be thrown such as HaapiManagerUnsupportedContentTypeException
and Exception
.
val code = oauthAuthorizationResponseStep.properties.code
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val tokenResponse = oAuthTokenManager.fetchAccessToken(
authorizationCode = response.code,
onCoroutineContext = this.coroutineContext,
additionalParameters = emptyMap()
)
handleTokenResponse(tokenResponse)
}
private fun handleTokenResponse(tokenResponse: TokenResponse) {
return when (tokenResponse) {
is SuccessfulTokenResponse -> {
val accessToken = tokenResponse.accessToken
val refreshToken = tokenResponse.refreshToken
}
is ErrorTokenResponse -> { /* Handle the ErrorTokenResponse */ }
}
}
ℹ️ It is recommended to keep the SuccessfulTokenResponse
especially the access_token
and the refresh_token
in a secured storage.
If the access_token
is expired, invoke OAuthTokenManager.refreshAccessToken
using the stored refresh_token
to get a new TokenResponse
.
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val tokenResponse = oAuthTokenManager.refreshAccessToken(
refreshToken = "your_refresh_token",
onCoroutineContext = this.coroutineContext
)
handleTokenResponse(tokenResponse)
}
ℹ️ Check this for a concrete example.
Additional configurations/usages
Binding authorization code
Issue token-bound authorization code
When using issue-token-bound-authorization-code
(true) in the Identity Server configuration, it is mandatory to bind the tokens on the client side.
To bind tokens on the client side, it is required to configure TokenBoundConfiguration
to the HaapiConfiguration
as demonstrated below.
// It is recommended to provide a secured storage for`TokenBoundConfiguration.storage` such as SharedPreferences. This property is important when the KeyStore is not reliable enough to keep a generated KeyPair.
val securedStorage = object: Storage {
override fun delete(key: String) {
// Implements the deletion
}
override fun get(key: String): String? {
// Implements the getter
}
override fun getAll(): Map<String, String> {
// Implements the getter all
}
override fun set(value: String, key: String) {
// Implements the setter
}
}
val tokenBoundConfiguration = TokenBoundConfiguration(
keyAlias = "token_bound_configuration_key_alias",
keyPairAlgorithmConfig = KeyPairAlgorithmConfig.ES256,
storage = securedStorage,
currentTimeMillisProvider = { System.currentTimeMillis() }
)
// HaapiConfiguration
val haapiConfiguration = HaapiConfiguration(
keyStoreAlias = "keyStoreAlias",
clientId = clientId,
baseUri = baseUri,
tokenEndpointUri = tokenEndpointUri,
authorizationEndpointUri = authorizationEndpointUri,
appRedirect = appRedirect,
tokenBoundConfiguration = tokenBoundConfiguration
)
Issue unbound authorization code
On the other hand, when issue-token-bound-authorization-code
is set to false
in the Identity Server configuration, binding the tokens is a choice on the client side.
To not bind the token on the client side, TokenBoundConfiguration
should not be configured to HaapiConfiguration
as demonstrated below:
// HaapiConfiguration
val haapiConfiguration = HaapiConfiguration(
keyStoreAlias = "keyStoreAlias",
clientId = clientId,
baseUri = baseUri,
tokenEndpointUri = tokenEndpointUri,
authorizationEndpointUri = authorizationEndpointUri,
appRedirect = appRedirect,
tokenBoundConfiguration = null
)
Client Authentication Method
Using Attestation
to enforce API security should be the default behavior, ClientAuthenticationMethod.None
, which should work for most users. For the signing key-based client attestation method to work, there must be hardware support, and this can sometimes be uncertain due to the multitude of different device models and user setup in day to day use.
Non-compliant devices provide their alternative proof via the Client Credentials Flow
, in a request for an access token with the dcr scope. There are multiple ways in which the credential can be supplied during this request.
Secret
A simple client secret can be used to request the client credentials, which will need to be the same for all users. This is not a secure option, but it can be useful in some setups, such as when first getting integrated, or as a solution for a development stage of the deployment pipeline.
val secretClientAuthMethod = ClientAuthenticationMethodConfiguration.Secret(secret = "foo")
MTLS
The app can use a client certificate bundled with it or import it from its public key hash representation. Then, it sends the proof of ownership to the Curity Identity Server over a Mutual TLS connection. The Curity Identity Server is configured to verify the trust chain of the client certificate.
// using bundled certificates
val mtls = ClientAuthenticationMethodConfiguration.Mtls(
clientKeyStore = ...,
clientKeyStorePassword = "foo",
serverTrustStore = ...
)
// using server public key hash
val mtls2 = ClientAuthenticationMethodConfiguration.MtlsKeyHash(
clientKeyStore = ...,
clientKeyStorePassword = "foo",
serverKeyPinnings =
setOf(
ClientAuthenticationMethodConfiguration.MtlsKeyHash.KeyPinning(
hostname = "localhost",
publicKeyHash = "Kjuy4mT3fbeDozRNP6rTjWRYmbs79Begb5Roq+CNwiG="
)
),
isValidatingHostname = true
)
Client Assertion
The app can make use of both symmetric and asymmetric keypairs and load them into the device Keychain
, then use it to produce a JWT Client Assertion
, which it then sends as proof of ownership. The Curity Identity Server is then configured with a way to get the public key with which to verify received assertions.
// Asymetric
val asym = ClientAuthenticationMethodConfiguration.SignedJwt.Asymmetric(
clientKeyStore = ...,
clientKeyStorePassword = "foo",
alias = "foo",
algorithmIdentifier =
ClientAuthenticationMethodConfiguration.SignedJwt.Asymmetric.AlgorithmIdentifier.ES256 // according to your certificate
)
// Symmetric
val sym = ClientAuthenticationMethodConfiguration.SignedJwt.Symmetric(
secretKey = "foo",
signatureAlgorithm =
ClientAuthenticationMethodConfiguration.SignedJwt.Symmetric.SignatureAlgorithm.HS256 // according to the server configuration
)
Dynamic Client Registration (DCR) for devices that do not support attestation
Instances of an app running on non-compliant devices will have to prove their identity based on an alternative client credential and perform a Client Credential Flow
, resulting in the creation of a dynamic client. Here you can find more information on how to setup the server environment and prepare the configuration.
Single configuration
The below snippet illustrates configurations for devices that can support or not attestation.
// Configuration parameters
val clientId = "haapi-android-client-secret" // When using DCR, it is required to have a client that supports secret, mtls or jwt.
val clientAuthMethod = ClientAuthenticationMethodConfiguration.Secret(secret = "foo")
val baseURLString = "https://10.0.2.2:8443"
val intermediatePath = "/oauth/v2"
val baseUri = URI.create(baseURLString)
val tokenEndpointUri = URI.create("$baseURLString$intermediatePath/oauth-token")
val authorizationEndpointUri = URI.create("$baseURLString$intermediatePath/oauth-authorize")
val appRedirect = "app://haapi"
// HaapiConfiguration
val haapiConfiguration = HaapiConfiguration(
keyStoreAlias = "keyStoreAlias",
clientId = clientId,
baseUri = baseUri,
tokenEndpointUri = tokenEndpointUri,
authorizationEndpointUri = authorizationEndpointUri,
appRedirect = appRedirect
)
// DCR configuration
val dcrConfiguration = DcrConfiguration(
templateClientId = "dcr-template-client-id",
clientRegistrationEndpointUri = URI.create("$baseURLString$intermediatePath/oauth-token"),
context = this
)
// Create the HaapiAccessorFactory with the DCR configuraiton to get a HaapiManager/OAuthTokenManager. These objects are correctly configured and are aware if your device supports or not attestation.
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val haapiAccessor = HaapiAccessorFactory(
haapiConfiguration = haapiConfiguration
)
.setDcrConfiguration(dcrConfiguration)
.setClientAuthenticationMethodConfiguration(clientAuthMethod)
.create(onCoroutineContext = this.coroutineContext)
val haapiManager = haapiAccessor.haapiManager
val oAuthTokenManager = haapiAccessor.oAuthTokenManager
}
Two configurations
If an application is already using a client configuration that only supports attestation, it is required to have a second client configuration to support non-attestation devices.
The below snippet illustrates this use-case.
// The haapi configuration for attestation only
val attestationHaapiConfiguration = HaapiConfiguration(
keyStoreAlias = "keyStoreAlias",
clientId = "attestation-haapi-client",
baseUri = baseUri,
tokenEndpointUri = tokenEndpointUri,
authorizationEndpointUri = authorizationEndpointUri,
appRedirect = appRedirect
)
// The haapi configuration for non-attestation which requires a client authentication for secret, mtls or jwt
val nonAttestationHaapiConfiguration = HaapiConfiguration(
keyStoreAlias = "keyStoreAlias",
clientId = "non-attestation-haapi-client-using-client-auth-method",
baseUri = baseUri,
tokenEndpointUri = tokenEndpointUri,
authorizationEndpointUri = authorizationEndpointUri,
appRedirect = appRedirect
)
// It can be secret, mtls or jwt
val clientAuthMethod = ClientAuthenticationMethodConfiguration.Secret(secret = "foo")
val dcrConfiguration = DcrConfiguration(
templateClientId = "dcr-template-client-id",
clientRegistrationEndpointUri = URI.create("$baseURLString$intermediatePath/oauth-token"),
context = this
)
// Detecting when to switch configuration
// 1. Using the attestation configuration
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val haapiAccessor = HaapiAccessorFactory(
haapiConfiguration = attestationHaapiConfiguration
)
.create(onCoroutineContext = this.coroutineContext)
// If no error was triggered, then the device supports attestation and HaapiManager/OAuthTokenManager are available.
// Otherwise, check coroutineExceptionHandler for HaapiError.UnsupportedHaapiException
val haapiManager = haapiAccessor.haapiManager
val oAuthTokenManager = haapiAccessor.oAuthTokenManager
}
private val coroutineExceptionHandler =
CoroutineExceptionHandler { _, throwable ->
when (throwable) {
is HaapiError.UnsupportedHaapiException -> {
// The device does not support attestation
useSecondConfiguration()
}
else -> {
// Handle the exception
handleThrowable(throwable)
}
}
}
private fun useSecondConfiguration() {
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val haapiAccessor = HaapiAccessorFactory(
haapiConfiguration = nonAttestationHaapiConfiguration
)
.setDcrConfiguration(dcrConfiguration)
.setClientAuthenticationMethodConfiguration(ClientAuthenticationMethodConfiguration.Secret(""))
.create(onCoroutineContext = this.coroutineContext)
// If the confiration is correct, then HaapiAccessor will use the DCR configuration and HaapiManager/OAuthTokenManager are available.
// Otherwise, check coroutineExceptionHandler to understand the problem.
val haapiManager = haapiAccessor.haapiManager
val oAuthTokenManager = haapiAccessor.oAuthTokenManager
}
}
⚠️ When providing a DcrConfiguration
it is required to set the ClientAuthenticationMethodConfiguration
to a value different than ClientAuthenticationMethodConfiguration.None
otherwise it will fail.
Using risk assessment services
When integrating with services that may require application context information (ex: BankID's risk assessment functionality), it is required to configure the applicationContext
in HaapiConfiguration. This allows the framework to collect and manage the necessary information to provide the service with.
⚠️ For reference about the collected information please refer to the official BankID Relying Party Guidelines for version 6, and API documentation.
To ensure optimal functionality in managing the risk assessment information, it is advised to set the android:allowBackup
flag in your app’s manifest file (AndroidManifest.xml) to ensure persistent state. This is the default setting when creating a new application project.
Listening to the response from the token endpoint via OAuthTokenManager
When invoking OAuthTokenManager.fetchAccessToken
or OAuthTokenManager.refreshAccessToken
, responses are handled and returned as TokenResponse
. The TokenResponse
may be a SuccessfulTokenResponse
or ErrorTokenResponse
.
SuccessfulTokenResponse
and ErrorTokenResponse
contain the attributes as defined in RFC 6749.
With these objects, it is not possible to get the headers or the raw data response. If they are needed, a listener (OAuthTokenManager.TokenEndpointResponseListener
) has to be configured.
The following snippet illustrates how to configure the listener.
// Create a class that conforms to OAuthTokenManager.TokenEndpointResponseListener
class MyTokenEndpointResponseListener : OAuthTokenManager.TokenEndpointResponseListener {
override fun onSuccess(value: OAuthTokenManager.SuccessTokenResponseContent) {
// SuccessfulTokenResponse
value.successfulTokenResponse.accessToken
value.successfulTokenResponse.refreshToken
// responseAsJsonObject
value.responseAsJsonObject?.getString("access_token")
value.responseAsJsonObject?.getString("refresh_token")
// Others
value.headerFields
value.contentType
}
override fun onTokenError(value: OAuthTokenManager.ErrorTokenResponseContent) {
// ErrorTokenResponse
value.errorTokenResponse.error
value.errorTokenResponse.errorDescription
// bodyAsJsonObject
value.bodyAsJsonObject?.getString("error")
// Others
value.headerFields
value.contentType
}
override fun onError(value: HttpClient.Response.Failure) {
// Throwable
value.throwable
// bodyAsJsonObject
value.bodyAsJsonObject
// Others
value.headerFields
value.contentType
}
}
// Create a HaapiConfiguration with MyTokenEndpointResponseListener
val haapiConfiguration = HaapiConfiguration(
clientId = CLIENT_ID,
baseUri = BASE_URI,
tokenEndpointUri = TOKEN_ENDPOINT_URI,
authorizationEndpointUri = AUTHORIZATION_ENDPOINT_URI,
appRedirect = APP_REDIRECT,
useAttestation = false,
tokenEndpointResponseListener = MyTokenEndpointResponseListener()
)
val haapiAccessor = HaapiAccessorFactory(haapiConfiguration)
.setClientAuthenticationMethodConfiguration(...)
.setDcrConfiguration(...)
.create(onCoroutineContext = onCoroutineContext)
val haapiManager = haapiAccessor.haapiManager
val oauthTokenManager = haapiAccessor.oAuthTokenManager
The listener is triggered when receiving a response from the token endpoint: OAuthTokenManager.SuccessTokenResponseContent
, OAuthTokenManager.ErrorTokenResponseContent
, or HttpClient.Response.Failure
.
When opting for this configuration, the TokenResponse
can be ignored as HttpClient.Response
contains the raw response.
Can the Haapi Sdk support app widget?
An app widget enables developers to add custom functionality and content to their application, such as information, collection, control or hybrid widgets. More details can be found here.
❗️When using the Haapi Sdk
in an app widget, use only the OAuth operations to manage the access and refresh tokens. App widgets have specific limitations, such as not being able to access certain APIs or frameworks marked as unavailable for extensions, and limited access to device resources and lifespan. They are best suited for short interactions and specific actions.
⚠️ Running the Haapi flow or retrieving the access_token is not supported in app widget.
To manage/refresh an access token in an app widget, a few requirements are needed:
- The application and the app widget use the same HaapiConfiguration.
- The
SuccessfulTokenResponse
must be stored in a storage (SharedPreferences, which is shared across the app context and its app widgets) that can be used in the application and the app widget when the Haapi flow reaches the end. - The app widget retrieves the
SuccessfulTokenResponse
from the storage. The app widget can manage the access/refresh token via OAuthTokenManager. Upon a successful refresh, theSuccessfulTokenResponse
must be stored in the storage, similar to the previous point.
Example use
// Shared Haapi Configuration
val haapiConfiguration: HaapiConfiguration
// Shared storage
private val appSharedPreferences: SharedPreferences = getSharedPreferences("SharedPreferences", Context.MODE_PRIVATE)
private val ACCESS_TOKEN = "ACCESS_TOKEN"
private val REFRESH_TOKEN = "REFRESH_TOKEN"
// Saving the SuccessfulTokenResponse
fun saveOauthModelToken(token: SuccessfulTokenResponse) {
appSharedPreferences.edit(commit = true) {
putString(ACCESS_TOKEN, token.accessToken)
putString(REFRESH_TOKEN, token.refreshToken)
}
}
// Getting the refresh_token
fun getRefreshToken(): String? {
return appSharedPreferences.getString(REFRESH_TOKEN, null)
}
// Getting the access_token
fun getAccessToken(): String? {
return appSharedPreferences.getString(ACCESS_TOKEN, null)
}
//In the app widget
fun example() {
getRefreshToken()?.let {
GlobalScope.launch(Dispatchers.IO) {
val accessor = HaapiAccessorFactory(haapiConfiguration)
.create(onCoroutineContext = this.coroutineContext)
val token = accessor.oAuthTokenManager.refreshAccessToken(
refreshToken = it,
onCoroutineContext = this.coroutineContext
)
when (token) {
is SuccessfulTokenResponse -> {
saveOAuthModelToken(token)
}
is ErrorTokenResponse -> {
// Handle the error token response.
}
}
}
}
}
HaapiLogger
When using the HAAPI Sdk
it is possible to display the logs in the console as demonstrated below. When running in a project configuration set to DEBUG
mode, the HaapiLogger enabled
property is set to true
. Otherwise, default value is false
but can be set by the developer.
class ClientApplication : Application(), HaapiUIWidgetApplication {
override fun onCreate() {
super.onCreate()
HaapiLogger.enabled = true
}
}
Supported log levels
HaapiLogger only uses the following configurable log levels:
Log level | How to enabled | Usage |
---|---|---|
Error | setLevel(LogLevel.ERROR) |
Used when the application hits an issue preventing one or more functionalities from properly functioning. |
Warning | setLevel(LogLevel.WARN) |
Used when something unexpected happened in the application that might disturb its functionality. |
Info | setLevel(LogLevel.INFO) |
Used when something happens, the application entered a certain state. |
Debug | setLevel(LogLevel.DEBUG) |
Used for information that may be needed for diagnosing issues and troubleshooting or when running application in test environment. |
Verbose | setLevel(LogLevel.VERBOSE) |
Used in rare cases where you need the full visibility of what is happening. These logs are never compiled into an application except during development. |
The table above reads the log level rank as ERROR
being the lowest and Verbose being the highest. Enabling a level via setting the configuration property isXXXEnabled
will also enable the lower levels logs. A log request of level p in a logger with level q is enabled if p >= q. It assumes that levels are ordered. For the standard levels, we have VERBOSE < DEBUG < INFO < WARN < ERROR. For example configuring the logger for level INFO
will output log statements for INFO
, WARNING
and ERROR
.
When running in a project configuration set to DEBUG
mode, the LogLevel is set to VERBOSE
. Otherwise, default value is ERROR
but can be set by the developer.
Display masking sensitive data
To remove the masking, HaapiLogger.isSensitiveValueMasked
has to be set to false
. Now with this configuration, logs are displayed without masking values but occasionally additional warning logs are added to remind this setting should not be enabled except for testing purposes such as:
2023-11-02 11:12:15.756 32737-32737 se.curity....HaapiTokenManager com.example.myhaapiui D HAAPI_DRIVER_FLOW - Received CAT challenge with: *****OWFFpT2lKT
To remove the masking, HaapiLogger.isSensitiveValueMasked
has to be set to false
. Now with this configuration, logs are displayed without masking value but followed with warnings logs such as:
2023-11-02 11:12:15.756 32737-32737 se.curity....HaapiTokenManager com.example.myhaapiui D ***** SENSITIVE VALUE IS UNMASKED *****
2023-11-02 11:12:15.756 32737-32737 se.curity....HaapiTokenManager com.example.myhaapiui D ***** HaapiLogger.isSensitiveValueMasked must be set to true in `release` mode. *****
2023-11-02 11:12:15.756 32737-32737 se.curity....HaapiTokenManager com.example.myhaapiui D HAAPI_SDK_HTTP - A new session id is set - a-65de12c0-870509f4-e6e5-4269-b651-ce8a6f798753###9d70c13cd8a3c4b218aa3404c99491afc44d5060bf6bba21c3698bb6fb4bfa05
❗️Setting this value to false
is only recommended when debugging.
Read the logs
All logs are structured like the following:
2023-11-02 11:12:15.756 32737-32737 se.curity....HaapiManager com.example.myhaapiui D HAAPI_SDK_FLOW - start() is invoked
It is possible to filter the Haapi logs by using the prefix: HAAPI
.
To filter Driver logs, use the prefix: HAAPI_DRIVER
. Here are the following up tags for the driver:
- HAAPI_DRIVER_ATTESTATION: Logs related to the attestation flow.
- HAAPI_DRIVER_DCR: Logs related to the DCR flow.
- HAAPI_DRIVER_FLOW: Logs related to the driver flow.
- HAAPI_DRIVER_HTTP: Logs related to any http calls in the driver.
- HAAPI_DRIVER_STORAGE: Logs related to any storage related calls in the driver.
- HAAPI_DRIVER_KEYSTORE: Logs related to any keystore related calls in the driver.
To filter Sdk logs, use the prefix: HAAPI_SDK
. Here are the following up tags for the sdk:
- HAAPI_SDK_FLOW: Logs related to the sdk flow.
- HAAPI_SDK_HTTP: Logs related to any http calls in the sdk.
- HAAPI_SDK_MAPPING: Logs related to mapping objects in the sdk.
- HAAPI_SDK_OAUTH: Logs related to OAuth in the sdk.
- HAAPI_SDK_STORAGE: Logs related to any storage related calls in the sdk.
Write logs to another destination
It is possible to write the logs to another destination as demonstrated below.
class MyLogSink : LogSink {
override fun writeLog(
logLevel: HaapiLogger.LogLevel,
sender: String?,
followUpTag: HaapiLogger.FollowUpTag?,
message: String,
throwable: Throwable?
) {
// Filter/export to your designated tool.
}
}
HaapiLogger.appendLogSink(MyLogSink())
WebAuthn and Passkeys
When the app needs to trigger a WebAuthn Authorization interaction flow, it can do so by either delegating responsibility on the OS by opening a browser or by using an integrated native flow inside the app.
Native WebAuthn support is provided by:
- Google Play Services Fido2 API.
- Credential Manager: A Jetpack credentials API. Supports built-in Platform authenticators (Android 7+ devices) as well as Cross-platform external hardware Security Keys authenticators.
⚠️ Cross-platform Security-Keys with user verification are currently not supported.
Native WebAuthn support for Android is available in Curity Identity Server starting from version 8.7.0.
To use passkeys on Android, you need to meet certain requirements. Passkeys are supported on devices running Android 9 (Pie) or later. Here are the key requirements and details:
- Android Version: Your device must be running Android 9 (Pie) or a newer version.
- Biometric Authentication: You need a biometric sensor, such as a fingerprint or facial recognition, or a PIN, swipe pattern, or password to authenticate and create a passkey.
- Google Account: You must be signed in to a Google Account on your device.
- Credential Manager: Passkeys are managed through the Credential Manager Jetpack library, which handles different credential types including passkeys, passwords, and identity federation.
- Google gms Fido2: Passkeys are managed through the Fido2ApiClient which handles passkeys
- Google Password Manager: Passkeys can be stored in the Google Password Manager, which synchronizes them between devices signed into the same Google account. This allows you to use passkeys across multiple devices.
- Third-party Password Managers: Starting with Android 14, users can opt to store their passkeys in a compatible third-party password manager.
- Website and App Support: The website or app you want to access must support passkey login.
- Nearby Device Authentication: For cross-device login, your phone must be near the device you're logging into, and you must approve the sign-in on your phone.
- PIN or Biometric Authentication: When creating or using a passkey, you'll need to authenticate using a PIN, swipe pattern, or biometric sensor.
Discoverable Credentials support for Android is available in Curity Identity Server starting from version 9.3.0.