//HAAPI Android Driver Documentation
HAAPI Android Driver Documentation
[androidJvm] Android library with classes and functions required to access Curity Identity ServerHypermedia Authentication API (HAAPI) from android devices. The library handles client attestation and provides the building blocks to issue HAAPI requests, namely obtaining DPoP access tokens.
Framework Structure
se.curity.identityserver.haapi.android.driver
This package contains classes for HAAPI access token management.
se.curity.identityserver.haapi.android.driver.internal
This package contains internal classes that are not meant for external usage or required for using Haapi Android Driver.
se.curity.identityserver.haapi.android.driver.okhttp
This package contains functions and types to configure an OkHttpClient so that it can access HAAPI resources, including access token management and flow session state management.
When using functions or types in this package, then the OkHttp dependency needs to be explicitly added to the build configuration. Example:
implementation("com.squareup.okhttp3:okhttp:3.14.9")
The SDK doesn't include OkHttp as a transitive dependency.
Usage examples
Start by creating an HaapiTokenManager
// 0 - Create a HaapiTokenManager
val tokenEndpointUri: URI = ...
val clientId: String = ...
val haapiTokenManager = HaapiTokenManager(tokenEndpointUri, clientId) {
// define additional configuration properties
}
Using the OkHttp client library
When using the OkHttp HTTP client library, it is possible to include an interceptor that will automatically add all required request headers and manage the session identifier. This includes DPoP tokens and managing the associated nonces.
// 1 - Create an OkHttpClient using haapiTokenManager
val httpClient = OkHttpClient.Builder()
.addHaapiInterceptor(tokenManager)
// define other builder properties
.build()
// 2 - Use httpClient to access HAAPI resources
// access token, proof tokens, associated nonces and session identifiers will be handled automatically
// when doing requests with httpClient
Using HaapiTokenManager directly
Otherwise, the DPoP tokens and associated nonces need to be explicitly requested from the HaapiTokenManager and added to the outgoing HAAPI requests:
// 1 - use the HaapiTokenManager to retrieve the DPoP access and proof tokens
// required for the outgoing HTTP request
val httpRequestMethod: String = ...
val httpRequestTargetUri: URI = ...
val tokens = tokenManager.getDPoPTokensFor(httpRequestMethod, httpRequestTargetUri)
val authorizationHeaderValue = "DPoP ${tokens.accessTokenString}"
val dpopHeaderValue = tokens.proofTokenString
// Any Identity Server response may contain a DPoP-Nonce header with a nonce string.
// This nonce needs be provided to all subsequent calls to this method.
val dpopNonceHeaderValue = tokens.dpopNonce
// 2 - add authorizationHeaderValue, dpopHeaderValue and dpopNonceHeaderValue to the Authorization,
// DPoP request headers and DPoP nonce headers respectively
...
In this case, the session identifier also needs to be handled explicitly.
⚠️ When using <use-legacy-dpop>false</use-legacy-dpop>in the Identity Server configuration and receiving a 401 status code
If a response with a 401 status code is received, www-authenticate header should be checked.
If this key is present and contains error=\"use_dpop_nonce\", the new DPoP nonce should be extracted from the dpop-nonce header. The failed request should be retried using the new nonce.
⚠️ When using <issue-token-bound-authorization-code>true</issue-token-bound-authorization-code> in the Identity Server configuration. It is mandatory to set TokenBoundConfiguration to the configuration for HaapiTokenManager as demonstrated below.
val tokenBoundConfiguration = TokenBoundConfiguration(
keyAlias = "uniqueAlias",
keyPairAlgorithmConfig = KeyPairAlgorithmConfig.ES256,
storage = object : Storage {
override fun set(value: String, key: String) {
TODO("Not yet implemented")
}
override fun get(key: String): String? {
TODO("Not yet implemented")
}
override fun delete(key: String) {
TODO("Not yet implemented")
}
override fun getAll(): Map<String, String> {
TODO("Not yet implemented")
}
},
currentTimeMillisProvider = { System.currentTimeMillis() }
)
// Using the Builder (the recommended way)
HaapiTokenManager.Builder(
clientId = "clientID",
tokenEndpointUri = URI.create("tokenEndpointUri")
)
.setTokenBoundConfiguration(tokenBoundConfiguration)
.build()
// Using HaapiTokenManager.MutableConfig
val mutableConfig = HaapiTokenManager.MutableConfig(
clientId = "clientID",
tokenEndpointUri = URI.create("tokenEndpointUri")
)
mutableConfig.tokenBoundConfiguration = tokenBoundConfiguration
HaapiTokenManager(mutableConfig.toConfig())
When receiving the code, HaapiTokenManager.dpopHelper has to be used to generate the proof token when requesting the access_token.
⚠️ When integrating with services that may require application context information (ex: BankID's risk assessment functionality), it is required to configure the applicationContext in HaapiTokenManager. 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.
HaapiLogger
When using the HAAPI Driver 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. When setting a log level, it 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 default LogLevel is set to VERBOSE. Otherwise, default value is ERROR but can be set by the developer.
Display masking sensitive data
By default, HaapiLogger.isSensitiveValueMasked is set to true. With this configuration, logs are displayed with masking value such as:
2023-11-02 11:12:15.756 32737-32737 se.curity....HaapiTokenManager com.example.myhaapiui D Received CAT challenge with: *****hoNzdLdyJ9
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 E ***** SENSITIVE VALUE IS UNMASKED *****
2023-11-02 11:12:15.756 32737-32737 se.curity....HaapiTokenManager com.example.myhaapiui E ***** 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_DRIVER_FLOW - Received CAT challenge with: eyJzdGF0ZSI6ImV5SnJhV1FpT2lJdE16Z3dOelE0TVRJaUxDSjROWFFpT2lKT
❗️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....HaapiTokenManager com.example.myhaapiui D HAAPI_DRIVER_FLOW - Fetching a Haapi Access Token.
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.
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())
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 Driver 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)
IdsvrHaapiException Error Handling Guide
The IdsvrHaapiException hierarchy provides structured error handling with two main categories:
- Retryable errors - Temporary issues that can be resolved by retrying
- Unrecoverable errors - Fundamental issues requiring manual intervention
Basic Error Handling Pattern
try {
val result = throwableOperation()
} catch (e: IdsvrHaapiException) {
when (e) {
is IdsvrHaapiException.Retryable -> handleRetryableError(e)
is IdsvrHaapiException.Unrecoverable -> handleUnrecoverableError(e)
}
}
Handling Retryable Errors
Retryable errors indicate temporary issues that may be resolved by attempting the operation again under specific conditions.
suspend fun handleRetryableError(exception: IdsvrHaapiException.Retryable) {
when (exception.condition) {
is RetryCondition.Now -> {
// Retry immediately with exponential backoff
retryWithBackoff(exception)
}
is RetryCondition.WhenAppForeground -> {
// Queue for retry when app comes to foreground
queueForForegroundRetry(exception)
}
}
}
Handling Unrecoverable Errors
Unrecoverable errors require manual intervention and cannot be resolved through automatic retry.
fun handleUnrecoverableError(exception: IdsvrHaapiException.Unrecoverable) {
when (exception.action) {
is UnrecoverableAction.ModifyConfiguration -> {
handleConfigurationError(exception)
}
is UnrecoverableAction.InvalidPlatform -> {
handlePlatformError(exception)
}
is UnrecoverableAction.IntrospectCause -> {
handleInvestigationRequired(exception)
}
}
}
Error codes
When getting IdsvrHaapiException, the error is one of the DriverErrorCodes.
object DriverErrorCodes {
const val PARSING_EXCEPTION = "parsing_exception"
const val INVALID_INPUT = "invalid_input"
const val USE_DPOP_NONCE = "use_dpop_nonce"
const val ACCESS_DENIED = "access_denied"
const val UNSUPPORTED_DEVICE = "unsupported_device"
const val KEYSTORE_EXCEPTION = "keystore_exception"
const val UNSUPPORTED_HAAPI_EXCEPTION = "unsupported_haapi_exception"
const val CONTENT_TYPE = "content_type"
const val UNEXPECTED_EXCEPTION = "unexpected_exception"
const val UNSUPPORTED_CONFIGURATION = "unsupported_configuration_exception"
}
Packages
| Name |
|---|
| se.curity.identityserver.haapi.android.driver |
| se.curity.identityserver.haapi.android.driver.internal |
| se.curity.identityserver.haapi.android.driver.okhttp |