//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

Getting started

Setting up HAAPI SDK

In your app/build.gradle, add identityserver.haapi.android.sdk to the dependencies block as demonstrated below.

groovy
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.

kotlin
// 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:

ℹ️ 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.

kotlin
// 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.

kotlin
// 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:

⚠️ Exceptions can be thrown such as HaapiManagerUnsupportedContentTypeException, HaapiManagerUnsupportedHttpMethodException, HaapiManagerUnexpectedException and Exception.

The HaapiResponse needs to be handled as illustrated below.

kotlin
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.

kotlin
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:

⚠️ Exceptions can be thrown such as HaapiManagerUnsupportedContentTypeException and Exception.

kotlin
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.

kotlin
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.

kotlin
// 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:

kotlin
// 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.

kotlin
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.

kotlin
// 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.

kotlin
// 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.

kotlin
// 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.

kotlin
// 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.

kotlin
// 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:

Example use

kotlin
// 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.

kotlin
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:

kotlin
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:

kotlin
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:

kotlin
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:

To filter Sdk logs, use the prefix: HAAPI_SDK. Here are the following up tags for the sdk:

Write logs to another destination

It is possible to write the logs to another destination as demonstrated below.

kotlin
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:

⚠️ 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:

Discoverable Credentials support for Android is available in Curity Identity Server starting from version 9.3.0.

Packages

Name
se.curity.identityserver.haapi.android.sdk
se.curity.identityserver.haapi.android.sdk.internal
se.curity.identityserver.haapi.android.sdk.models
se.curity.identityserver.haapi.android.sdk.models.actions
se.curity.identityserver.haapi.android.sdk.models.oauth
se.curity.identityserver.haapi.android.sdk.util