Module HAAPI iOS Sdk Documentation
The Hypermedia Authentication API (HAAPI) SDK for the Curity Identity Server allows integration of this API into your applications for smarter, simpler login using native UI widgets. It will enable any login method supported by the Curity Identity Server and strictly follows the principle of REST. The SDK is meant to make the security aspects of consuming this API easier.
Requirements
- iOS 14+
How to get started
Swift Package Manager
With Swift Package Manager, add the following dependency
to your Package.swift
.
dependencies: [
.package(url: "https://github.com/curityio/ios-idsvr-haapi-sdk-dist")
]
Cocoapods
With Cocoapods, add the following line to your Podfile.
pod 'IdsvrHaapiSdk'
Usage
Configuration
Start by creating a HaapiConfiguration
for the client access to the server.
let baseURL = URL(string: "https://localhost:8443")!
let haapiConfiguration = HaapiConfiguration(name: "foo-name",
clientId: "foo-client-id",
baseURL: baseURL,
tokenEndpointURL: URL(string: "/oauth/token",
relativeTo: baseURL)!,
authorizationEndpointURL: URL(string: "/oauth/authorize",
relativeTo: baseURL)!,
appRedirect: "foo-app-redirect",
httpHeadersProvider: nil,
authorizationParametersProvider: nil)
Using the HaapiAccessorBuilder
to instantiate HAAPI and OAuth managers - the recommended way (required when using DCR fallback)
HaapiAccessorBuilder 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 HaapiAccessorBuilder
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 HaapiAccessorBuilder
is to create and configure a single instance and invoke build
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.
let haapiConfiguration: HaapiConfiguration = ...
let dcrConfiguration: DCRConfiguration = ...
let haapiAccessor: HaapiAccessor = HaapiAccessorBuilder(haapiConfiguration: haapiConfiguration)
.setDCRConfiguration(configuration: dcrConfiguration)
.build()
// HAAPI flow
let haapiManager: HaapiManager = haapiAccessor.haapiManager
haapiManager.start()
// ... haapiManager.submit/follow ...
Manually instantiating HaapiManager
- use only if supporting only Attestation
backed HAAPI flows
Instantiate HaapiManager
with the HaapiConfiguration
.
var haapiManager: HaapiManager!
// ...
do {
haapiManager = try HaapiManager(haapiConfiguration: haapiConfiguration)
} catch let haapiError as HaapiError {
handleHaapiError(haapiError) // Check the section `Handling HaapiError`
} catch { // Error
handleError(error)
}
Running the HAAPI flow (authorization flow)
Invoke HaapiManager.start
to start the HAAPI flow that returns a HaapiResult
.
The HaapiResult
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.Error
: The object represents an error that may lead to an interruption of the Haapi flow.
The HaapiResult
needs to be handled as illustrated below.
haapiManager.start { [weak self] haapiResult in
self?.handleHaapiResult(haapiResult)
}
//...
private func handleHaapiResult(_ haapiResult: HaapiResult) {
switch haapiResult {
case .representation(let haapiRepresentation): handleHaapiRepresentation(haapiRepresentation)
case .problem(let problemRepresentation): handleProblemRepresentation(problemRepresentation)
case .error(let anError): handleError(anError)
}
}
private func handleHaapiRepresentation(_ haapiRepresentation: HaapiRepresentation) {
switch haapiRepresentation {
case let redirectionStep as RedirectionStep:
print(redirectionStep)
case let authenticatorSelectorStep as AuthenticatorSelectorStep: print(authenticatorSelectorStep)
case let interactiveFormStep as InteractiveFormStep: print(interactiveFormStep)
case let pollingStep as PollingStep: print(pollingStep)
case let continueSameStep as ContinueSameStep: print(continueSameStep)
case let userConsentStep as UserConsentStep: print(userConsentStep)
case let genericRepresentationStep as GenericRepresentationStep: print(genericRepresentationStep)
case let clientOperationStep as ClientOperationStep: print(clientOperationStep)
case let oAuthAuthorizationResponseStep as OAuthAuthorizationResponseStep: print(oAuthAuthorizationResponseStep)
default:
print(haapiRepresentation)
}
}
With a HaapiRepresentation
, invoke HaapiManager.submit
or HaapiManager.followLink
to move forward on the HAAPI flow and handle the new HaapiResult
.
private func submit(formActionModel: FormActionModel, parameters: [String: Any]) {
haapiManager.submitForm(formActionModel, parameters: parameters) { [weak self] haapiResult in
self?.handleHaapiResult(haapiResult)
}
}
private func followLink(_ link: Link) {
haapiManager.followLink(link) { [weak self] haapiResult in
self?.handleHaapiResult(haapiResult)
}
}
When obtaining an OAuthAuthorizationResponseStep
, the HAAPI flow reaches the end. OAuthAuthorizationResponseStep
contains a code
that is required to get an access_token
.
OAuthTokenManager
Using the HaapiAccessorBuilder
helper to instantiate Haapi and OAuth managers - the recommended way (mandatory when using DCR fallback)
The recommended way to use HaapiAccessorBuilder
is to create and configure a single instance and invoke create
once before going through an OAuth operation.
let haapiConfiguration: HaapiConfiguration = ...
let dcrConfiguration: DCRConfiguration = ...
let oauthAccessor: OAuthAccessor = HaapiAccessorBuilder(haapiConfiguration: haapiConfiguration)
.setHaapiAccessorOption(option: .oauth)
.setDCRConfiguration(configuration: dcrConfiguration)
.build()
// Fetch token with OAuthTokenManager
let oAuthTokenManager: OAuthTokenManager = oauthAccessor.oAuthTokenManager
// ... oAuthTokenManager.fetch/refresh ...
Manually instantiating OAuthTokenManager
- use only if supporting only Attestation
backed HAAPI flows
Instantiate an OAuthTokenManager
with the HaapiConfiguration
Running the OAuth flow
Invoke OAuthTokenManager.fetchAccessToken
with the code in OAuthAuthorizationResponseStep.oauthAuthorizationResponseProperties
to get a TokenResponse
.
When receiving a TokenResponse
, the object can be:
- a
SuccessfulToken
: 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. - an
Error
: An error that is returned by the SDK. Check the section on how to handle errors with OAuthTokenManager.
let code = authResponseStep.oauthAuthorizationResponseProperties.code!
let oauthTokenManager = OAuthTokenManager(oauthTokenConfiguration: haapiConfiguration)
oauthTokenManager.fetchAccessToken(with: code) { [weak self] tokenResponse in
self?.handleTokenResponse(tokenResponse)
}
// ...
private func handleTokenResponse(_ tokenResponse: TokenResponse) {
switch tokenResponse {
case let .successfulToken(successfulToken):
successfulToken.accessToken // access_token
successfulToken.refreshToken // refresh_token
case let .errorToken(errorTokenResponse):
errorTokenResponse.error // error as a String
errorTokenResponse.errorDescription // error description as a String
case let .error(anError): handleError(anError)
}
}
ℹ️ It is recommended to keep the SuccessfulToken
especially the access_token
and the refresh_token
in a secure storage such as the Keychain
.
If the access_token
is expired, invoke OAuthTokenManager.refreshAccessToken
to get a new TokenResponse
.
oauthTokenManager.refreshAccessToken(with: "refresh_token") { [weak self] tokenResponse in
self?.handleTokenResponse(tokenResponse)
}
ℹ️ Check this for a concrete example.
Configurations options
- HaapiConfiguration: A class that conforms to
HaapiConfigurable
andOAuthTokenConfigurable
. It can be used forHaapiManager
andOAuthTokenManager
. 👍 It is the recommended class to use. - HaapiConfigurable: A protocol that lists the configuration options of the
HaapiManager
. - OAuthTokenConfigurable: A protocol that lists the configuration options for
OAuthTokenManager
.
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, HaapiManager.dpop
has to be provided to OAuthTokenManager
when requesting the access_token
as demonstrated below:
let code = authResponseStep.oauthAuthorizationResponseProperties.code!
let dPoP = haapiManager.dpop
let oauthTokenManager = OAuthTokenManager(oauthTokenConfiguration: haapiConfiguration)
oauthTokenManager.fetchAccessToken(with: code, dpop: dPoP) { [weak self] tokenResponse in
self?.handleTokenResponse(tokenResponse)
}
⚠️ Omitting the DPoP
object when invoking OAuthTokenManager.fetchAccessToken
results in a server error:
{
"error": "invalid_dpop_proof",
"error_description": "Missing DPoP Proof Token"
}
In that case, retry by invoking OAuthTokenManager.fetchAccessToken
with the dpop
parameter set as illustrated in the example above
Issue unbounded 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, provide only the code
when requesting the access_token via OAuthTokenManager
as demonstrated below:
let code = authResponseStep.oauthAuthorizationResponseProperties.code!
let oauthTokenManager = OAuthTokenManager(oauthTokenConfiguration: haapiConfiguration)
oauthTokenManager.fetchAccessToken(with: code) { [weak self] tokenResponse in
self?.handleTokenResponse(tokenResponse)
}
Valid token binding configurations
The following table exposes all correct combinations:
Server configuration issue-token-bound-authorization-code |
Client configuration TokenBoundConfiguration |
Pass DPoP to OAuthTokenManager.fetchAccessToken | Consequences |
---|---|---|---|
false | UnboundedTokenConfiguration | No | The refresh_token is not bounded to the DPoP. |
false | Is_NOT_UnboundedTokenConfiguration[^1] | Yes | The refresh_token is bound to the DPoP. |
true | Is_NOT_UnboundedTokenConfiguration | Yes | The refresh_token is bound to the DPoP. |
true | Is_NOT_UnboundedTokenConfiguration | No | Server error: { "error": "invalid_dpop_proof", "error_description": "Missing DPoP Proof Token"} |
[^1]: Client is configured with BoundedTokenConfiguration
or the default configuration.
DPoP-Nonce / 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 contain a SuccessfulTokenResponse
, ErrorTokenResponse
, or Error
.
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 such as a dpop-nonce
. 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
@available(iOS 14.0, *)
class MyTokenEndpointResponseListener: OAuthTokenManager.TokenEndpointResponseListener {
func onSuccess(_ value: OAuthTokenManager.SuccessTokenHTTPURLResponseContent) {
// SuccessfulTokenResponse
value.successfulTokenResponse.refreshToken
value.successfulTokenResponse.accessToken
value.successfulTokenResponse.expiresIn
// DataAsMap
if let accessToken = value.dataAsDictionary["access_token"] as? String {
// ...
}
if let refreshToken = value.dataAsDictionary["refresh_token"] as? String {
// ...
}
// HTTPURLResponse
if let httpURLResponse = value.httpURLResponse {
// Headers
if let contentType = httpURLResponse.allHeaderFields["Content-Type"] as? String {
// ...
}
}
// DPOP_NONCE
value.headerFields["dpop-nonce"]
// Error
assert(value.error == nil)
}
func onError(_ value: ErrorHTTPURLResponseContent) {
// ERROR
value.error.localizedDescription
// DataAsMap
if let errorDescription = value.dataAsDictionary["error_description"] as? String {
// ...
}
if let error = value.dataAsDictionary["error"] as? String {
// ...
}
// HTTPURLResponse
if let httpURLResponse = value.httpURLResponse {
// Headers
if let contentType = httpURLResponse.allHeaderFields["Content-Type"] as? String {
// ...
}
}
errorContent = value
}
func onTokenError(_ value: OAuthTokenManager.ErrorTokenHTTPURLResponseContent) {
// ErrorTokenResponse.
value.errorTokenResponse.error
value.errorTokenResponse.errorDescription
// DataAsMap
if let errorDescription = value.dataAsDictionary["error_description"] as? String {
// ...
}
if let error = value.dataAsDictionary["error"] as? String {
// ...
}
// HTTPURLResponse
if let httpURLResponse = value.httpURLResponse {
// Headers
if let contentType = httpURLResponse.allHeaderFields["Content-Type"] as? String {
// ...
}
}
}
}
// Create a HaapiConfiguration with MyTokenEndpointResponseListener
let haapiConfiguration = HaapiConfiguration(name: "foo-name",
...
tokenEndpointResponseListener: MyTokenEndpointResponseListener())
// Create an instance of OAuthTokenManager with the configuration
OAuthTokenManager(oauthTokenConfiguration: haapiConfiguration)
The listener is triggered when receiving a response from the token endpoint: OAuthTokenManager.SuccessTokenHTTPURLResponseContent
, OAuthTokenManager.ErrorTokenHTTPURLResponseContent
, or ErrorHTTPURLResponseContent
.
When opting for this configuration, the TokenResponse
can be ignored as HTTPURLResponseContent
contains the raw response.
When using risk assessment services (ex: BankID's risk assessment functionality)
To enable the collection of necessary information about the device for the risk assessment service, setting the application bundle is required, as demonstrated below.
let haapiConfiguration = HaapiConfiguration(name: "foo-name",
...
applicationBundle: Bundle.main)
It allows the framework to collect and manage the necessary information to provide the service.
For reference about the collected information please refer to the official BankID Relying Party Guidelines for version 6, and API documentation.
⚠️ Server support for the risk assessment functionality integration requires a version of the Curity Identity Server starting from 9.7.0.
Client Authentication Method
Using Attestation
to enforce API security should be the default behavior, 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.
let clientAuthenticationMethod = ClientAuthenticationMethodSecret("foo")
Certificate
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
let clientAuthenticationMethod = try ClientAuthenticationMethodMTLS(pkcs12Filename: pkcs12Filename,
pkcs12Passphrase: passphrase,
serverPEMFilename: serverPEMFilename,
isValidatingHostname: true,
bundle: appBundle))
// using server public key hash
let mtlsClientAuth2 = try! ClientAuthenticationMethodMTLS(pkcs12Filename: pkcs12Filename,
pkcs12Passphrase: passphrase,
serverKeyPinnings: [KeyPinning(hostname: "192.168.1.107",
publicKeyHash: "Kjuy4mT3fbeDozRNP6rTjWRYmbs79Begb5Roq+DUu7s=")],
bundle: appBundle)
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.
// Asymmetric keypair
let asymConfig = try ClientAuthenticationMethodJWTAsymmetric(pemFilename: rsaPrivateKeyPEM, signatureAlgorithm: .rs256, bundle: appBundle)
// Symmetric keypair
let secretKeyJWT = "_UQ49FAPrM0XOdjNROeVIrbfPvTzqVgfLBi66Nk0mIBttc9ZBakcC9ZiuAu9WCvg"
let symConfig = ClientAuthenticationMethodJWTSymmetric(signatureAlgorithm: .hs256, secretKey: secretKeyJWT)
DCR - Dynamic Client Registration
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.
let dcrEndpointURL = URL(string: "/oauth/oauth-registration", relativeTo: serverBaseURL)
let dcrConfiguration = DCRConfiguration(templateClientId: "dcr-client-template-id", clientRegistrationEndpointUrl: dcrEndpointURL)
⚠️ When setting
HaapiConfiguration.useAttestation
value tofalse
or when providing aDCRConfiguration
it is required to set theClientAuthenticationMethod
to a value different thanClientAuthenticationMethodNone
otherwise it will fail.
Error handling
IdsvrError
When using HaapiManager or OAuthTokenManager, their respective completion handler can return an Error
. This error can be cast to an HaapiError
as demonstrated below:
private func handleError(_ error: Error) {
guard let haapiError = error as? HaapiError else { return }
switch haapiError {
case .assertionFailure(let cause): cause
case .attestationFailure(let cause): cause
case .attestationKeyGenFailure(let cause): cause
case .attestationNotSupported:
case .attestationRefusedByServer(let cause): cause
case .communication(let message, let cause): cause
case .dpopKeyCreationFailure(let cause): cause
case .dpopProofCreationFailure(let cause): cause
case .dpopProofFailure(let message, let cause):
message
cause
case .haapiTokenManagerAlreadyExists(let name): name
case .haapiTokenManagerIsClosed:
case .haapiTokenManagerIsExpired:
case .illegalState(let message): message
case .invalidConfiguration(let reason): reason
case .invalidStatusCode(let statusCode): statusCode
case .serverError(let error, let errorDescription, let statusCode):
error
errorDescription
statusCode
}
}
An HaapiError is conforming to IdsvrError
that provides messages describing why an error occurred and provides more information about the error. It is possible to try to cast an Error to IdsvrError
and get the following information:
- error: The error code.
- errorDescription: The error description.
- failureReason: The failure reason.
- recoverySuggestion: The suggestion for handling this Error.
- cause: The cause of the error. (Optional)
private func handleHaapiError(_ error: HaapiError) {
guard let haapiError = error as IdsvrError else { return }
// check the error metadata for information about the error.
// for example, check the recovery suggestion to find out if the error can be recovered
switch haapiError.recoverySuggestion {
case .retryable(condition: let condition):
// The error can be recovered by retrying the same operation when the `condition` is met.
// For example, retrying the same operation at a later time, when the app is running in the foreground.
// This is likely to happen when the device's resources are currently unavailable
case .newHaapiFlow:
// The error can be recovered by triggering a new Authentication flow from the start.
case .nonRecoverable(action: let action):
// This error cannot be recovered without compile time or runtime corrective measures as instructed by the `action`.
}
}
Common errors
HaapiError.invalidConfiguration in OAuthTokenManager
💥 This error happens when using UnboundedTokenConfiguration
and providing a DPoP
object to OAuthTokenManager.fetchAccessToken()
.
🛠 Align the client configuration and the framework API usage as explained [here](#Issue token-bound authorization code).
Dpop proof related errors - HaapiError.dpopKeyCreationFailure, HaapiError.dpopProofCreationFailure and HaapiError.dpopProofFailure
💥 When receiving an HaapiError.dpopKeyCreationFailure
it is recommended to trigger a retry at a later time, if possible, after the device has been rebooted as it provides the better chances of recovering gracefully.
If the error is persistent, it means the device cannot fulfill the requirements for the current client configuration settings and a different token binding configuration is required.
💥 When fetching the access_token and implementing dpop token binding, errors may occur (e.g. while interacting with the Secure Enclave API or the secure storage) when invoking OAuthTokenManager.refreshToken
preventing the DPoP proof mechanism from functioning correctly. In such cases, the client developer code will receive a HaapiError.dpopProofCreationFailure
or a HaapiError.dpopProofFailure
to allow the app to react to such errors. More information about the origin of the error can be found in the underlying cause value.
When the error is persistent, the app can switch the token binding configuration and not use the Secure Enclave
by providing a different keyPairType
parameter such as the P256
NIST algorithm keypair.
❗️ If HaapiError.dpopXXXFailure
is persistent when invoking OAuthTokenManager.refreshToken
, the reasons may be caused by:
- the device's environment state prevents the mechanism from working correctly (e.g. the application's boot sequence event; some other unknown inconsistent device state like a silent soft reboot)
- the device's resources are unavailable when the code is being executed, for example in App Extensions (App Extensions mostly do not have access to the
Secure Enclave
) or the application state isbackground
,.
🛠 It is recommended to trigger the retry when the application is in the foreground for better chances of recovering gracefully.
If the error is still happening in the foreground, the application might be required to switch the client configuration BoundedTokenConfiguration
to use different settings.
Switching configuration involves:
- The hold refresh_token has to be dropped/deleted from the application token storage and a revocation of the token is advised by invoking
OAuthTokenManager.revokeRefreshToken
. This results in the user being logged out. - The BoundedTokenConfiguration has to be configured with a different keyPairType such as P256. ☢️ Doing this effectively laxes the security of the framework's Dpop binding mechanism. Only use if it is strictly necessary.
- Redirect the user to a new authentication flow with this new configuration.
Below is a snippet of code suggestion on how to handle the HaapiError.dpopProofFailure
and switch to a fallback configuration
let secureEnclaveHaapiConfiguration = HaapiConfiguration(name: CONFIG_ALIAS,
...)
let tokenManager = OAuthTokenManager(oauthTokenConfiguration: secureEnclaveHaapiConfiguration)
tokenManager.refreshAccessToken(with: token) { tokenResponse in
if case let .error(cause) = tokenResponse, case let HaapiError.dpopProofFailure(message, error) = cause {
// Error is from the dpop binding mechanism
// message and error can be inspected for details about the problem
// Suggested handling:
// Check the application state:
// - if is `background` ignore the error and backoff as subsequent calls in this code execution path are likely to also fail
// - else implement a retry mechanism
// - store some marker/counter that provides info whether is the first time the refresh is called or a retry
// - based on the value of the marker/counter infer if the error is persistent
// - if the error is persistent, trigger a new authentication flow with different configuration
return
}
// handle other OAuthCompletion
}
// when the persistent error conditions are met use the fallback configuration to start a new authentication
let fallbackTokenBoundConfiguration = BoundedTokenConfiguration(keyPairType: CryptoKeyType.p256)
let fallbackHaapiConfiguration = HaapiConfiguration(name: CONFIG_ALIAS,
...
tokenBoundConfiguration: fallbackTokenBoundConfiguration)
let haapiManager = HaapiManager(haapiConfiguration: fallbackHaapiConfiguration)
haapiManager.start(...)
Error Domain=com.apple.devicecheck.error Code=3
💥 This error happens when using DCAppAttestService
and asking Apple to attest the validity of a generated cryptographic key. To our knowledge, very few users are impacted by this problem. It can be linked to jailbroken devices, to Apple risk score for the user or device, corporate devices (MDM), and unknown cases not detailed in the Apple documentation.
🛠 Unfortunately, the cause is unknown and the Apple documentation does not explain the reason for the failure. Devices that encounter this problem cannot execute the HAAPI flow and therefore, they cannot get an access_token
. Please contact Curity Support to know how to handle devices that cannot support attestation on iOS.
HaapiLogger
When using IdsvrHaapiSdk, it is possible to display the logs in the console as demonstrated below.
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ...
HaapiLogger.followUpTags = DriverFollowUpTag.allCases + SdkFollowUpTag.allCases // To filter logs.
return true
}
// ...
}
Display logs by configuring HaapiLogger.followUpTags
// Driver and Sdk logs are displayed in the console
HaapiLogger.followUpTags = DriverFollowUpTag.allCases + SdkFollowUpTag.allCases
// Only SDK logs are displayed in the console.
HaapiLogger.followUpTags = SdkFollowUpTag.allCases
⚠️ If HaapiLogger.followUpTags
is not set, then no logs are displayed in the console.
Supported log levels
HaapiLogger only uses the following log levels:
Log level | How to enable | Default value |
---|---|---|
Debug | HaapiLogger.isDebugEnabled | It is set to true in DEBUG mode. Otherwise, it is set to false . |
Info | HaapiLogger.isInfoEnabled | It is set to true . |
Warning | HaapiLogger.isWarningEnabled | It is set to true . |
Error | HaapiLogger.isErrorEnabled | It is set to true . |
Display masking sensitive data
By default, HaapiLogger.isSensitiveValueMasked
is set to true. With this configuration, logs are displayed with masking value such as:
2023-08-08 22:03:13.866367+0200 IdsvrHaapiUIKitApp[19098:404410] [HAAPI_DRIVER_HTTP] [DEBUG] HaapiTokenManager.378 : Requesting challenge from *****/cat
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-08-08 22:06:18.982975+0200 IdsvrHaapiUIKitApp[19269:408868] [UNSECURED] ********** SENSITIVE VALUE IS UNMASKED **********
2023-08-08 22:06:18.983071+0200 IdsvrHaapiUIKitApp[19269:408868] [UNSECURED] ********** HaapiLogger.isSensitiveValueMasked must be set to true in `release` mode. **********
2023-08-08 22:06:18.983182+0200 IdsvrHaapiUIKitApp[19269:408868] [HAAPI_DRIVER_HTTP] [DEBUG] HaapiTokenManager.503 : Requesting CAT from https://localhost:8443/dev/oauth/token/cat
❗️Setting this value to false
is only recommended when debugging.
Read the logs
All logs are structured like the following:
2023-06-29 16:31:50.845676+0200 IdsvrHaapiUIKitApp[66219:26995872] [HAAPI_UI_THEMING] [DEBUG] UIStylesContainer.34 : Loading styles definitions from IdsvrHaapiUIKit
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_FLOW: Logs related to the driver flow.
- HAAPI_DRIVER_STORAGE: Logs related to the storage in the driver
- HAAPI_DRIVER_HTTP: : Logs related to any http calls in the driver.
- HAAPI_DRIVER_ATTESTATION: Logs related to the attestation flow.
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 the mapping in the sdk.
- HAAPI_SDK_OAUTH: Logs related to OAuth in the sdk.
Write logs to another destination
When using IdsvrHaapiDriver, it is possible to write the logs to another destination as demonstrated below.
class MyLogSink: LogSink {
func writeLog(logType: LogType,
followUpTag: any FollowUpTag,
message: String,
file: String,
line: Int)
{
// Filter/export to your designed tool
}
}
HaapiLogger.appendLogSink(MyLogSink())
Reference Documentation
Protocols
- ClientAuthenticationMethod
- ClientOperationStep
- FollowUpTag
- HaapiAccessor
- HaapiConfigurable
- HaapiManagerAccessor
- HaapiRepresentation
- HaapiResponse
- IdsvrError
- LogSink
- Masking
- OAuthAccessor
- OAuthResponse
- OAuthTokenConfigurable
- ProblemRepresentation
- Properties
- RawJsonRepresentable
- RepresentationActionModel
- Storage
- TokenBoundConfiguration
- TokenEndpointResponseListener
Structs
- AuthenticatorSelectorStep
- AuthenticatorSelectorStep.AuthenticatorOption
- BankIdClientOperationStep
- ClientOperationAction.Properties
- ContinueSameStep
- EncapClientOperationStep
- ExternalBrowserClientOperationStep
- FormAction.Properties
- FormActionModel
- GenericClientOperationStep
- GenericProperties
- GenericRepresentationStep
- InteractiveFormStep
- InvalidInputProblem.InvalidField
- Link
- OAuthAuthorizationResponseProperties
- OAuthAuthorizationResponseStep
- PollingProperties
- PollingStep
- RedirectionStep
- ResponseAndData
- SelectFormField.Option
- SelectorAction.Properties
- SelectorActionModel
- UserConsentStep
- UserMessage
- WebAuthnAuthenticationClientOperationStep
- WebAuthnRegistrationClientOperationStep
Classes
- AccessToken
- Action
- AttestationConfiguration
- AuthorizationProblem
- BankIdClientOperationActionModel
- BoundedTokenConfiguration
- CheckboxFormField
- ClientAuthenticationMethodJWTAsymmetric
- ClientAuthenticationMethodJWTSymmetric
- ClientAuthenticationMethodMTLS
- ClientAuthenticationMethodNone
- ClientAuthenticationMethodSecret
- ClientOperationAction
- ClientOperationActionModel
- ContextFormField
- DCRConfiguration
- Dpop
- DpopAccessTokenInfo
- EncapAutoActivationClientOperationActionModel
- ErrorHTTPURLResponseContent
- ErrorTokenResponse
- ExternalBrowserClientOperationActionModel
- FormAction
- FormField
- GenericClientOperationActionModel
- HTTPURLResponseContent
- Haapi
- HaapiAccessorBuilder
- HaapiClient
- HaapiConfiguration
- HaapiLogger
- HaapiManager
- HaapiTokenManager
- HaapiTokenManagerBuilder
- HaapiTokenResult
- HiddenFormField
- IdsvrHaapiSdkTestUtils
- InternalTestUtility
- InvalidInputProblem
- KeyPinning
- KeychainStorage
- Message
- Metadata
- OAuthAuthorizationParameters
- OAuthTokenManager
- OAuthTokenManager.ErrorTokenHTTPURLResponseContent
- OAuthTokenManager.SuccessTokenHTTPURLResponseContent
- PasswordFormField
- Problem
- SelectFormField
- SelectorAction
- SuccessfulTokenResponse
- TextFormField
- UnboundedTokenConfiguration
- UsernameFormField
- WebAuthnAuthenticationClientOperationActionModel
- WebAuthnAuthenticationClientOperationActionModel.AllowedCredential
- WebAuthnAuthenticationClientOperationActionModel.CredentialRequestOptions
- WebAuthnRegistrationClientOperationActionModel
- WebAuthnRegistrationClientOperationActionModel.CredentialParams
- WebAuthnRegistrationClientOperationActionModel.CredentialRequestOptions
- WebAuthnRegistrationClientOperationActionModel.CrossPlatformCredentialRequestOptions
- WebAuthnRegistrationClientOperationActionModel.ExcludedCredential
- WebAuthnRegistrationClientOperationActionModel.PlatformCredentialRequestOptions
Enums
- ActionKind
- ClientOperationName
- CryptoImportError
- CryptoKeyType
- DriverFollowUpTag
- HaapiAccessorOption
- HaapiError
- HaapiModel
- HaapiModel.ActionFactory
- HaapiModel.ActionModelFactory
- HaapiModel.ContentFactory
- HaapiModel.FormFieldFactory
- HaapiModel.ProblemFactory
- HaapiModel.PropertiesFactory
- HaapiModel.StepFactory
- HaapiModel.TokenResponseFactory
- HaapiResult
- HttpHeaderNames
- IdsvrErrorHandling
- JWTAsymmetricAlgorithm
- JWTSymmetricAlgorithm
- LogType
- MimeTypes
- PollingStatus
- ProblemType
- RepresentationType
- RetryCondition
- SdkFollowUpTag
- StorageError
- TextFormField.Kind
- TokenResponse
- TokenRevocationResponse
- UnrecoverableAction
Extensions
- ActionKind
- ActionTemplate
- ClientOperationName
- Data
- Dictionary
- FollowUpTag
- FormFieldType
- HaapiAccessorBuilder
- HaapiTokenManager
- JOSEHeader
- NSObject
- OAuthTokenManager
- PollingStatus
- ProblemType
- RepresentationType
- Signer
- StorageError
- String
- StringProtocol
- URL
- URLRequest
- URLResponse
- URLSessionConfiguration
- Verifier
Typealiases
- ActionKind.RawValue
- ActionTemplate.RawValue
- ClientOperationName.RawValue
- FormFieldType.RawValue
- HaapiCompletionHandler
- HttpHeadersProvider
- Kind.RawValue
- OAuthAuthorizationParametersProvider
- OAuthCompletion
- OAuthRevocationCompletion
- PollingStatus.RawValue
- ProblemType.RawValue
- RepresentationType.RawValue