Module HAAPI iOS UIKit Documentation
An iOS XCFramework with classes and layout definitions that are used to perform an Haapi Flow
authorization with Curity Identity Server Hypermedia Authentication API (HAAPI) from iOS devices.
The framework allows a client to implement an authorization flow via HAAPI by using a set of prebuilt UI screens that represent the different responses/steps that may be involved in the API-driven flow.
Getting started
UIKit application
Start by creating the configurations for the client access to the server and user flow (HaapiUIKitConfiguration), and create an HaapiUIKitApplication
in your AppDelegate.
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var haapiUIKitConfiguration = HaapiUIKitConfigurationBuilder(clientId: Constants.clientId,
baseUrl: Constants.baseURL,
tokenEndpointUrl: Constants.tokenEndpointURL,
authorizationEndpointUrl: Constants.authorizationEndpointURL,
appRedirect: Constants.appRedirectURIString)
.build()
var haapiUIKitApplication: HaapiUIKitApplication!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
haapiUIKitApplication = HaapiUIKitApplicationBuilder(haapiUIKitConfiguration: haapiUIKitConfiguration).build()
return true
}
// ...
}
Start the Haapi flow in your main UIViewController that is implementing HaapiFlowResult
to receive a result at the end of the Haapi flow or an error during the flow.
extension UIApplication {
var haapiUIKitApplication: HaapiUIKitApplication {
get {
guard let appDelegate = delegate as? AppDelegate else { fatalError("...") }
return appDelegate.haapiUIKitApplication
}
}
}
// ... Presented UIViewController
do {
try HaapiFlow.start(from: self, // self is an UIViewController that is presented.
haapiUIKitApplication: UIApplication.shared.haapiUIKitApplication,
haapiDeepLinkManageable: HaapiDeepLinkManager.shared)
} catch {
// handle error if the HaapiFlow does not start.
}
// ...
SwiftUI application
Start by creating the configurations for the client access to the server and user flow (HaapiUIKitConfiguration) in an AppDelegate.
class AppDelegate: NSObject, UIApplicationDelegate {
var haapiUIKitConfiguration = HaapiUIKitConfigurationBuilder(clientId: Constants.clientId,
baseUrl: Constants.baseURL,
tokenEndpointUrl: Constants.tokenEndpointURL,
authorizationEndpointUrl: Constants.authorizationEndpointURL,
appRedirect: Constants.appRedirectURIString)
.build()
var haapiUIKitApplication: HaapiUIKitApplication!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
haapiUIKitApplication = HaapiUIKitApplicationBuilder(haapiUIKitConfiguration: haapiUIKitConfiguration).build()
return true
}
}
Hook the AppDelegate with the struct App
.
@main
struct SwiftUIApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView(haapiApplication: appDelegate.haapiUIKitApplication)
}
}
// ...
}
Implement HaapiFlowResult
in the View
that can start the Haapi flow and start the Haapi flow. HaapiFlowResult
provides a set of methods to receive a result at the end of the Haapi flow or an error during the flow
struct ContentView: View, HaapiFlowResult {
let haapiApplication: HaapiUIKitApplication
@State private var showingHaapiVC = false
var body: some View {
VStack {
Button {
showingHaapiVC = true
} label: {
Text("Start Haapi flow")
}
}
.sheet(isPresented: $showingHaapiVC) {
HaapiFlow.start(self,
haapiUIKitApplication: haapiApplication,
haapiDeepLinkManageable: HaapiDeepLinkManager.shared)
}
// ...
}
func didReceiveOAuthTokenModel(_ oAuthTokenModel: IdsvrHaapiUIKit.OAuthTokenModel) { /* ... */ }
func didReceiveError(_ error: Error) { /* ... */ }
}
DCR configuration for iOS devices that do not support Key Attestation
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
in HaapiUIKitConfigurationBuilder
. 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 is used on subsequent runs.
To configure it, please perform the following steps:
- In your Curity Identity Server, create and configure a client that supports one of the following authentication methods:
- Secret
- Mutual TLS
- Signed JWT
- In your Curity Identity Server, create and configure a
DCR template client ID
, and add the client that was created in point 1 to the Dynamic Client Registration section. - In your Curity Identify Server, find (or configure) the DCR client registration endpoint (
oauth-registration
) and take note as it will be required in the next point. - In your application, update your configuration by setting the Client Authentication method and the DCR configuration as the example demonstrates below:
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var haapiUIKitConfiguration = HaapiUIKitConfigurationBuilder(clientId: clientId,
baseUrl: baseURL,
tokenEndpointUrl: tokenEndpointURL,
authorizationEndpointUrl: authorizationEndpointURL,
appRedirect: appRedirectURIString)
.setClientAuthenticationMethod(method: ClientAuthenticationMethodSecret("foo"))
.setDCRConfiguration(configuration: DCRConfiguration(templateClientId: "dcr-client-template-id",
clientRegistrationEndpointUrl: URL(string: "/oauth/oauth-registration",
relativeTo: baseURL)))
.build()
var haapiUIKitApplication: HaapiUIKitApplication!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
haapiUIKitApplication = HaapiUIKitApplicationBuilder(haapiUIKitConfiguration: haapiUIKitConfiguration).build()
return true
}
// ...
}
HaapiUIKitConfiguration options
As demonstrated in Getting started, HaapiUIKitConfiguration
is being used to build an HaapiUIKitApplication
via HaapiUIKitApplicationBuilder
. When using HaapiUIKitConfiguration, it is possible to configure a few options:
- autoPollingDuration
- shouldAutoHandleFlowErrorFeedback
- presentationMode
- authenticationSelectionPresentation
- shouldConfirmFlowInterruption
- useDefaultExternalBrowser
- setDCRConfiguration
- setClientAuthenticationMethod
- setTokenBoundConfiguration
- setDCRConfiguration
- setClientAuthenticationMethod
- setApplicationBundle
autoPollingDuration
During the Haapi Flow
, when a polling process is invoked then an automatic polling waits a few seconds between triggering polling requests to the server. The default value is 3 seconds. It is possible to configure a number of seconds for the automatic polling. When the value is greater than 0 then the Haapi Flow
will automatically handle the polling process, else it will disable automatic polling behavior.
shouldAutoHandleFlowErrorFeedback
It is possible that during the Haapi Flow
, the server returns an unexpected problem or an internal exception occurs on the framework which prevents the flow from proceeding and completing. In this case, as a good practice, the user is notified when something bad happened, thus preventing them from completing an action.
When set to true
, an AlertDialog is presented and gives the user some feedback about the error which user interaction causes the flow to finish and to return the error information to HaapiFlowResult
.
When set to false
, the flow is directly interrupted and returns the error information to HaapiFlowResult which delegates the user feedback responsibility to the application developer.
presentationMode
This configuration informs HaapiFlow.start()
how HaapiFlowViewController
is presented. The presentation determines the type of animation and transition when presenting HaapiFlowViewController to the client's application. The default value is set to PresentationMode.modal
. PresentationMode.stack
is another option.
With Presentation.modal
, HaapiFlowViewController is presented as a modal" navigation, presenting the flow view controller entering from the bottom to the top and closing with an animation from top to bottom.
With Presentation.stack
, HaapiFlowViewController is presented as a stack navigation, presenting the flow view controller entering from right to left and closing with an animation from left to right.
authenticationSelectionPresentation
This configuration informs how HaapiFlowViewController presents the AuthenticatorSelectorStep
. The presentation determines how the elements of an AuthenticatorSelectorStep
are presented. The default value is set to AuthenticatorSelectionPresentation.list
. AuthenticatorSelectionPresentation.tabs
is another option.
With AuthenticatorSelectionPresentation.list
, authenticators are rendered as a scrollable list.
With AuthenticatorSelectionPresentation.tabs
, authenticators are rendered with a TabbedPager
. A Tab element displays the authenticators from left to right and a "Pager" displays the selected authenticator's content.
shouldConfirmFlowInterruption
When pressing the back button or the close button during the Haapi Flow
, it is possible to display an alert that informs the user that the flow is going to be interrupted. The default value is true
. When the value is set to false
then no alert is displayed when pressing the back button or the close button.
useDefaultExternalBrowser
When the app needs to trigger some kind of external interaction action, it does so by delegating the action URL to be handled by the OS, either by using an ASWebAuthenticationSession
which embeds the browser in the app, or by using the default external browser. The default value for this configuration is false
which instructs the use of ASWebAuthenticationSession
.
setTokenBoundConfiguration
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, call the setTokenBoundConfiguration
and provide a BoundedTokenConfiguration
instance configured with the desired security options.
var haapiUIKitConfiguration = HaapiUIKitConfigurationBuilder(clientId: clientId,
baseUrl: baseURL,
tokenEndpointUrl: tokenEndpointURL,
authorizationEndpointUrl: authorizationEndpointURL,
appRedirect: appRedirectURIString)
.setTokenBoundConfiguration(configuration: BoundedTokenConfiguration(keyPairType: CryptoKeyType.secureEnclave))
.build()
⚠️ Omitting the binding mechanism configuration results in a server error:
{
"error": "invalid_dpop_proof",
"error_description": "Missing DPoP Proof Token"
}
In that case, check and fix the necessary configuration either on the server side or the client side configuration.
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, don't set any configuration as it is the default behaviour. Or alternatively explicitly provide an UnboundedTokenConfiguration
instance configuration.
var haapiUIKitConfiguration = HaapiUIKitConfigurationBuilder(clientId: clientId,
baseUrl: baseURL,
tokenEndpointUrl: tokenEndpointURL,
authorizationEndpointUrl: authorizationEndpointURL,
appRedirect: appRedirectURIString)
.setTokenBoundConfiguration(configuration: UnBoundedTokenConfiguration())
.build()
Token binding configurations
The following table exposes all correct combinations:
Server configuration issue-token-bound-authorization-code |
Client configuration TokenBoundConfiguration |
Consequences |
---|---|---|
false | UnboundedTokenConfiguration | The refresh_token is not bounded to the DPoP. |
false | BoundedTokenConfiguration | The refresh_token is bound to the DPoP. |
true | BoundedTokenConfiguration | The refresh_token is bound to the DPoP. |
true | UnboundedTokenConfiguration | Server error: { "error": "invalid_dpop_proof", "error_description": "Missing DPoP Proof Token"} |
setDCRConfiguration
The IdsvrHaapiUIKit
security features require that iOS devices support hardware-backed Key Attestation. Unfortunately, some of them don't support the security requirements. For those devices, users will not be able to start the HAAPI flow unless the HaapiUIKitApplicationConfiguration
has a DCR configuration in place. In this case, devices that do not have valid Key Attestation support can automatically fallback to the DCR configuration and continue using Haapi in a secure way.
setClientAuthenticationMethod
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)
setApplicationBundle
To enable collection of necessary information about the device for the risk assessment service (ex, BankID's risk assessment feature), setting the application bundle is required when configuring HaapiTokenManagerBuilder
as demonstrated below.
let configurationBuilder: HaapiUIKitConfigurationBuilder = HaapiUIKitConfigurationBuilder(...)
configurationBuilder.setApplicationBundle(Bundle.main)
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.
HaapiFlowViewModel
The HaapiFlowViewModel
is the main driver for the execution and stepping through the authentication flow and it is used internally by the framework to mediate the interaction with the Curity Identity Server. However it is also possible to use it as a standalone functional component allowing a client developer to build a completely custom UI and use the viewmodel as the internal driver to their authentication flow.
To do so, there's a public constructor available and then it is possible to subscribe to state changes on the @Published
state variables.
var haapiUIKitConfiguration = HaapiUIKitConfigurationBuilder(...).build()
...
var haapiUIKitApplication = HaapiUIKitApplicationBuilder(haapiUIKitConfiguration: haapiUIKitConfiguration).build()
...
let haapiFlowViewModel = HaapiFlowViewModel(haapiUIKitApplication: haapiUIKitApplication)
haapiFlowViewModel.$isLoading
.receive(on: DispatchQueue.main)
.sink(receiveValue: { isLoading in
// handle isLoading
})
.store(in: &cancellables)
haapiFlowViewModel.$uiModel
.receive(on: DispatchQueue.main)
.sink(receiveValue: { result in
// handle uiModel change
})
.store(in: &cancellables)
haapiFlowViewModel.$error
.receive(on: DispatchQueue.main)
.sink(receiveValue: { error in
// handle error
})
.store(in: &cancellables)
haapiFlowViewModel.$oAuthModel
.receive(on: DispatchQueue.main)
.sink(receiveValue: { oAuthModel in
// handle OAuthModel
}
.store(in: &cancellables)
Starting the Haapi Authentication flow
The framework provides some convenience helper methods to setup and start the authentication flow inside the application.
To do so, an HaapiFlow
API is provided and the user is able to easily trigger the presentation of the flow by using it like below.
HaapiFlow.start(from: parentVC,
haapiUIKitApplication: haapiApplication,
haapiDeepLinkManageable: HaapiDeepLinkManager.shared)
When the configuration for the presentation mode is set to STACK
, using the convenience flow helpers from SwiftUI code OR when running the app in iOS version below 16.0, a back navigation button is placed with a label that can be customized by providing a translation for the key hui_back
and the tint for the button text and image can be customized by providing the theme's HaapiFlowViewControler
style color configuration with the key closeButtonTintColorName
.
Alternatively, if more control over the presentation and lifecycle management is necessary, when using from UIKit code an instance of HaapiFlowViewController
can be manually created. If using SwiftUI, then a HaapiFlowViewControllerRepresentable
is available or the developer can write their own custom implementation of UIViewControllerRepresentable
wrapping the HaapiFlowViewController
.
⚠️ Important:
Always make sure to thoroughly test the app's authentication flow navigation.
On iOS below 16.0, running from SwiftUI to present the flow in a
NavigationView
, using aNavigationLink
and set it'sdestination
to callHaapiFlow.start()
may cause unexpected behavior due to the destinationView
being immediately initialized when the link is rendered and it's state internally kept until used when navigating. To avoid this behaviour it is possible and recomended to use the modifier flagisActive
and bind it to a state variable that will trigger the init only when set to true, like the example below by clicking a button that triggers the NavigationLink. Also, make sure to setnavigationViewStyle
to.stack
to avoid unexpected behaviour.
NavigationView {
NavigationLink(destination: HaapiFlow.start(self,
haapiUIKitApplication: haapiApplication,
haapiDeepLinkManageable: HaapiDeepLinkManager.shared),
isActive: $isShowingStackVC) { EmptyView() }
Button("Start Haapi Stack") {
self.isShowingStackVC = true
}
}.navigationViewStyle(.stack)
Starting from iOS 16.0, it is recomended to replace
NavigationView
withNavigationStack
due to API deprecation. The example below demonstrates a fleshed out version of the changes. More information onNavigationView/NavigationStack
migration here.
@State private var path: [NavigationPath] = []
NavigationStack {
NavigationLink("Start Haapi Stack", value: NavigationPath.haapiVC)
.navigationDestination(for: NavigationPath.self) { navigation in
switch navigation {
case .haapiVC:
HaapiFlow.start(self,
haapiUIKitApplication: haapiApplication,
haapiDeepLinkManageable: HaapiDeepLinkManager.shared)
}
}
}
Guide
Reference documentation
- ActionableButton.LinkView
- ActionableButton.Primary
- ActionableButton.Secondary
- ActionableButton.Text
- CheckboxView
- Colors
- FormViewController
- GenericViewController
- HaapiFlowViewController
- HeaderView
- Images
- InputTextField
- InputTextField.Curity
- InputTextField.Filled
- InputTextField.Outlined
- LinkView
- LoadingIndicatorView
- LoadingIndicatorView.ButtonLinkView
- LoadingIndicatorView.ButtonPrimary
- LoadingIndicatorView.ButtonSecondary
- LoadingIndicatorView.ButtonText
- LocalizableStrings
- MessageView.Content
- MessageView.Error
- MessageView.Heading
- MessageView.Info
- MessageView.RecipientOfCommunication
- MessageView.Username
- MessageView.Warn
- NotificationBannerView
- PollingViewController
- ProblemViewController
- SelectorViewController
- StackView
- StackView.Interactions
- StackView.Links
- StackView.Polling
- TabView
- TextAppearance.Body
- TextAppearance.Body.Bold
- TextAppearance.BodySubHeadline
- TextAppearance.Callout
- TextAppearance.Callout.Bold
- TextAppearance.Content
- TextAppearance.Link
- TextAppearance.SubHeadline
- TextAppearance.Text
- TextAppearance.Text.Center
- TextAppearance.Title2
- TextAppearance.Title2.Bold.Center
- TextAppearance.Title2.Center
- TextPickerView
- UIThemingConfigurations
- WebAuthnViewController
OAuthLifecycle
With the OAuthTokenModel
at hand, it is possible to request the server to refresh the currently held token in order to be able to continue accessing the resources for longer without having to go through the Haapi Flow
.
To do so, an OAuthLifecycle
API is provided and the user is able to easily refresh the token by using it like below.
// if OAuthTokenModel's refreshToken is nil or empty, we cannot execute the refresh token request.
if let refreshToken = (oauthModel as? OAuthTokenModel)?.refreshToken, !refreshToken.isEmpty {
OAuthLifecycle.refreshToken(refreshToken: refreshToken,
haapiUIKitApplication: haapiUIKitApplication,
lifecycleResultListener: self)// self is an instance of OAuthLifecycleResultListener
}
Configuring external apps integration
When integrating with external providers, third party apps required for the authentication flow that are launched from within the application must have their scheme's declared in the app's plist info file. The third party app's scheme
list must be declared in the Queried URL Schemes
configuration section like below. You can find relevant information here.
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...
<key>LSApplicationQueriesSchemes</key>
<array>
<string>bankid</string>
...
<string>other_app_scheme</string>
</array>
</dict>
</plist>
AutoFill for username and password text fields
AutoFill simplifies login and account creation tasks. As explained here, users can create and save new passwords or log in to an existing account with just a few taps. Users do not need to know their password; the system handles everything.
When using IdsvrHaapiUIKit in your application, a few conditions are required to support autoFill:
- AutoFill only works on a physical device where the user is logged in with their Apple account.
- The iOS app association has to be configured on your Curity Identity Server admin page to enable the server to start serving requests for the
apple-app-association
file. To do so, the most straightforward way is to add a new entry in System -> Deployment -> Zones -> Mobile App Associations -> iOS app association -> New. The entry is your App ID. (For example: XXXXXX.io.curity.myapplication) - In your iOS application -> Signing & Capabilities -> Add a new capability named
Associated Domains
. Configure your Curity Identity Server domain. (For example:webcredentials:your-server-adress
)
When these conditions are met, users can save their username and password and use them again when logging in.
HaapiLogger
When using IdsvrHaapiUIKit, 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 + UIKitFollowUpTag.allCases // To filter logs.
return true
}
// ...
}
Display logs by configuring HaapiLogger.followUpTags
// Driver, Sdk, and UIKit logs are displayed in the console
HaapiLogger.followUpTags = DriverFollowUpTag.allCases + SdkFollowUpTag.allCases + UIKitFollowUpTag.allCases
// Only UIKit logs are displayed in the console.
HaapiLogger.followUpTags = UIKitFollowUpTag.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 enabled | Default value |
---|---|---|
Debug | HaapiLogger.isDebugEnabled | It is set to true in DEBUG mode. Otherwise, default value is false but can be set by the developer if needed. |
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.
To filter UI logs, use the prefix: HAAPI_UI
. Here are the following up tags for the UIKit:
- HAAPI_UI_DATA: Logs related to the data in the UI.
- HAAPI_UI_COMPONENT: Logs related to the components in the UI.
- HAAPI_UI_THEMING: Logs related to the theming in the UI.
- HAAPI_UI_FLOW: Logs related to the flow in the UI.
- HAAPI_UI_SWIFTUI: Logs related to SwiftUI in the UI.
- HAAPI_UI_MAPPING: Logs related to the mapping in the UI.
- HAAPI_UI_LAYOUT: Logs related to the layout of the displayed component.
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())
Error handling - IdsvrError
When using the HaapiIdsvrUIKit
framework's functional classes, it's possible that their respective completion handlers can return an Error
. This error can be cast to an HaapiUIKitError
as demonstrated below:
private func handleError(_ error: Error) {
guard let haapiUIKitError = error as? HaapiUIKitError else { return }
switch haapiUIKitError {
case .illegalState(let reason): reason
case .other(let error): error
case .unsupported(let cause): cause
case .unsupportedMap(let objName, let expectedObjName): expectedObjName, objName
}
An HaapiUIKitError conforms to IdsvrError
that provides metadata describing why an error occurred and provides more information about the error. It is possible to cast an HaapiUIKitError 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: HaapiUIKitError) {
guard let idsvrError = 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`.
}
}
Additional features
Can the IdsvrHaapiUIKit support app extension?
An app extension enables developers to add custom functionality and content to their applications, such as Widgets or Actions. More details can be found here.
❗️When using IdsvrHaapiSdk in an app extension such as a Widget, use only the OAuth operations to manage the access and refresh tokens. App extensions have specific limitations, such as not being able to access certain APIs or frameworks marked as unavailable for extensions and also have limited access to device resources and lifespan. They are best suited for short interactions for specific actions
⚠️ Running the Haapi flow or retrieving the access_token is not supported in app extensions due to limitations in DCAppAttestService API support as mentioned here. The IdsvrHaapi frameworks rely on attestation to secure the app's authenticity when comunicating with the Curity Identity Server and depending on the provided configuration it can use other mechanism to achieve this purpose like Dynamic Client Registration which is however a less secure option.
To manage/refresh an access token in an app extension, a few requirements are needed:
- Configure the app's entitlements setting
App Group
which will be used to setup the shared storage.
When the device supports attestation
- The application and the app extension use the same HaapiConfiguration.
- In the application, when the end of the Haapi authentication flow is reached, and a SuccessfulTokenResponse is retrieved, the token has to be stored securely on the client application code side. The client code is required to implement the token sharing mechanism between the application and the app extension
- In the app extension, when creating HaapiAccessorBuilder, set the
setHaapiAccessorOption
to.oauth
. - In the app extension, the SuccessfulTokenResponse is retrieved by the client code from the shared storage. The app extension can manage the access/refresh token via OAuthTokenManager. Upon a successful refresh, the SuccessfulTokenResponse has to be stored in the storage by the app extension code.
When the DCR configuration takes over because the device does not support attestation
- Implement a shared
Storage
that can be used between the application and the app extension, such as Shared keychain storage - Configure
DCRConfiguration
with the sharedStorage
. - The application and the app extension use the same HaapiConfiguration.
- In the application, when the Haapi flow is reached, and a SuccessfulTokenResponse is retrieved, the token has to be stored. The storage has to be shared between the application and the app extension.
- In the app extension, when creating HaapiAccessorBuilder, set the
setHaapiAccessorOption
to.oauth
. - In the app extension, the SuccessfulTokenResponse is retrieved by the client code from the shared storage. The app extension can manage the access/refresh token via OAuthTokenManager. Upon a successful refresh, the SuccessfulTokenResponse has to be stored in the storage by the app extension code.
Example use
// Implementing shared storage using UserDefaults
struct MySharedStorage: Storage {
func read(key: String) throws -> Data? {
guard let data = userDefaults.data(forKey: key) else { return nil }
return data
}
func write(key: String, data: Data) throws {
userDefaults.set(data, forKey: key)
}
func delete(key: String) throws {
userDefaults.removeObject(forKey: key)
}
private let userDefaults: UserDefaults
init(appGroup: String? = nil) {
guard let suiteName = appGroup else {
userDefaults = UserDefaults.standard
return
}
userDefaults = UserDefaults(suiteName: suiteName)!
}
}
extension OAuthTokenModel {
func toMyStoredToken() -> MyStoredToken {
return MyStoredToken(accessToken: accessToken,
tokenType: tokenType ?? "",
scope: scope ?? "",
expiresIn: expiresIn,
refreshToken: refreshToken ?? "",
idToken: idToken ?? "")
}
}
struct MyStoredToken: Codable {
var accessToken: String
var tokenType: String
var scope: String
var expiresIn: Int
var refreshToken: String
var idToken: String
}
// Setting up DCR to use the shared storage to manage client data
let storage = MySharedStorage(appGroup: "group.shared.storage.example")
var haapiUIKitConfiguration =
HaapiUIKitConfigurationBuilder(
clientId: Constants.clientId,
baseUrl: Constants.baseURL,
tokenEndpointUrl: Constants.tokenEndpointURL,
authorizationEndpointUrl: Constants.authorizationEndpointURL,
appRedirect: Constants.appRedirectURIString
)
.setDCRConfiguration(
configuration: DCRConfiguration(
templateClientId: "dcr-template-client-haapi-ios",
clientRegistrationEndpointUrl: URL(string: "/oauth/oauth-registration",
relativeTo: Constants.baseURL)!,
storage: storage)
)
.build()
// Using shared storage to store access token - HaapiResult implementation
...
func didReceiveOAuthModel(_ oAuthModel: OAuthModel) {
switch oAuthModel {
case is OAuthTokenModel:
// save model
let storableToken = (oAuthModel as! OAuthTokenModel).toMyStoredToken()
let tokenData = try JSONEncoder().encode(storableToken)
storage.write(key: "accessToken", tokenData)
default:
throw Error()
}
}
Additional note
If more control over transport/serialization or custom behavior is needed, the HAAPI iOS Driver and HAAPI iOS SDK can be used as they define lower-level building blocks to issue HAAPI requests.
Reference Documentation
Protocols
- AuthorizationRequestModel
- ClientAuthenticationMethod
- ClientOperationStep
- ContinueSameModel
- DataMapper
- FollowUpTag
- FormModel
- FormOperationModel
- GenericClientOperationHandler
- GenericModel
- GenericOperationModel
- HaapiAccessor
- HaapiConfigurable
- HaapiDeepLinkManageable
- HaapiFlowDeepLinkable
- HaapiFlowResult
- HaapiFlowResultViewController
- HaapiFlowViewControllerDelegate
- HaapiManagerAccessor
- HaapiRepresentation
- HaapiResponse
- HaapiUIKitApplication
- HaapiUIViewController
- IdsvrError
- InfoMessageModel
- InteractionActionModel
- InteractionErrorModel
- InteractionItemButtonModel
- InteractionItemCheckboxModel
- InteractionItemInputTextModel
- InteractionItemModel
- InteractionItemSectionModel
- InteractionItemSelectModel
- InteractionItemTextContentModel
- InteractionValueModel
- LinkItemModel
- LogSink
- Masking
- OAuthAccessor
- OAuthDataMapper
- OAuthErrorModel
- OAuthModel
- OAuthResponse
- OAuthTokenConfigurable
- OAuthTokenModel
- PollingModel
- PollingOperationModel
- ProblemModel
- ProblemRepresentation
- Properties
- RawJsonRepresentable
- RepresentationActionModel
- SelectorItemInteractionActionModel
- SelectorItemModel
- SelectorModel
- Storage
- TokenBoundConfiguration
- TokenEndpointResponseListener
- UIInteractionModel
- UIModel
- UIOperationModel
- UIProblemModel
- UIStylable
- UIStylableThemeDelegate
- ViewLoadable
- WebAuthnOperationModel
- WebAuthnOperationParametersModel
Structs
- AuthenticatorSelectorStep
- AuthenticatorSelectorStep.AuthenticatorOption
- BankIdClientOperationStep
- ClientOperationAction.Properties
- ContinueSameStep
- EncapClientOperationStep
- ExternalBrowserClientOperationStep
- FormAction.Properties
- FormActionModel
- GenericClientOperationStep
- GenericProperties
- GenericRepresentationStep
- HaapiFlowVCRepresentable
- HaapiFlowViewControllerRepresentable
- HaapiUIKitConfiguration
- HaapiUIViewControllerStyle
- HaapiUIViewControllerStyle.MessageViewStyles
- InputTextConfiguration
- InteractiveFormStep
- InvalidInputProblem.InvalidField
- Link
- OAuthAuthorizationResponseProperties
- OAuthAuthorizationResponseStep
- PollingProperties
- PollingStep
- RedirectionStep
- ResponseAndData
- SelectFormField.Option
- SelectorAction.Properties
- SelectorActionModel
- UserConsentStep
- UserMessage
- WebAuthnAuthenticationClientOperationStep
- WebAuthnRegistrationClientOperationStep
Classes
- AccessToken
- Action
- ActionableButtonStyle
- AttestationConfiguration
- AuthorizationProblem
- BankIdClientOperationActionModel
- BaseViewController
- BaseViewControllerStyle
- BoundedTokenConfiguration
- CheckboxFormField
- ClientAuthenticationMethodJWTAsymmetric
- ClientAuthenticationMethodJWTSymmetric
- ClientAuthenticationMethodMTLS
- ClientAuthenticationMethodNone
- ClientAuthenticationMethodSecret
- ClientOperationAction
- ClientOperationActionModel
- ContextFormField
- DCRConfiguration
- DataMapperBuilder
- Dpop
- DpopAccessTokenInfo
- EncapAutoActivationClientOperationActionModel
- ErrorHTTPURLResponseContent
- ErrorTokenResponse
- ExternalBrowserClientOperationActionModel
- FormAction
- FormField
- FormViewController
- FormViewControllerStyle
- GenericClientOperationActionModel
- GenericViewController
- GenericViewControllerStyle
- HTTPURLResponseContent
- Haapi
- HaapiAccessorBuilder
- HaapiClient
- HaapiConfiguration
- HaapiDeepLinkManager
- HaapiFlow
- HaapiFlowViewController
- HaapiFlowViewControllerStyle
- HaapiFlowViewModel
- HaapiLogger
- HaapiManager
- HaapiTokenManager
- HaapiTokenManagerBuilder
- HaapiTokenResult
- HaapiUIKitApplicationBuilder
- HaapiUIKitConfigurationBuilder
- HiddenFormField
- InputTextFieldStyle
- InvalidInputProblem
- KeyPinning
- KeychainStorage
- LinkViewStyle
- LoadingIndicatorViewStyle
- Message
- MessageViewStyle
- Metadata
- NotificationBannerViewStyle
- OAuthAuthorizationParameters
- OAuthDataMapperBuilder
- OAuthLifecycle
- OAuthTokenManager
- OAuthTokenManager.ErrorTokenHTTPURLResponseContent
- OAuthTokenManager.SuccessTokenHTTPURLResponseContent
- PasswordFormField
- PollingViewController
- PollingViewControllerStyle
- Problem
- ProblemViewController
- ProblemViewControllerStyle
- SelectFormField
- SelectorAction
- SelectorViewController
- SelectorViewControllerStyle
- StackViewStyle
- SuccessfulTokenResponse
- TextAppearance
- TextFormField
- TextPickerViewStyle
- UIStyle
- UnboundedTokenConfiguration
- UsernameFormField
- ViewControllerFactoryRegistry
- WebAuthnAuthenticationClientOperationActionModel
- WebAuthnAuthenticationClientOperationActionModel.AllowedCredential
- WebAuthnAuthenticationClientOperationActionModel.CredentialRequestOptions
- WebAuthnRegistrationClientOperationActionModel
- WebAuthnRegistrationClientOperationActionModel.CredentialParams
- WebAuthnRegistrationClientOperationActionModel.CredentialRequestOptions
- WebAuthnRegistrationClientOperationActionModel.CrossPlatformCredentialRequestOptions
- WebAuthnRegistrationClientOperationActionModel.ExcludedCredential
- WebAuthnRegistrationClientOperationActionModel.PlatformCredentialRequestOptions
- WebAuthnViewController
- WebAuthnViewControllerStyle
- WebAuthnViewControllerStyle.TextualKeys
Enums
- ActionKind
- AuthenticatorSelectionPresentation
- ClientOperationName
- CryptoImportError
- CryptoKeyType
- DriverFollowUpTag
- HaapiAccessorOption
- HaapiError
- HaapiModel
- HaapiModel.ActionFactory
- HaapiModel.ActionModelFactory
- HaapiModel.ContentFactory
- HaapiModel.FormFieldFactory
- HaapiModel.ProblemFactory
- HaapiModel.PropertiesFactory
- HaapiModel.StepFactory
- HaapiModel.TokenResponseFactory
- HaapiResult
- HaapiUIKitError
- HandleableProblemType
- HttpHeaderNames
- IdsvrErrorHandling
- InteractionItemButtonType
- JWTAsymmetricAlgorithm
- JWTSymmetricAlgorithm
- LogType
- MessageStyleAttribute
- MimeTypes
- PollingStatus
- PresentationMode
- ProblemType
- RepresentationType
- RetryCondition
- SdkFollowUpTag
- StorageError
- TestingArgument
- TestingEnvironment
- TextFormField.Kind
- TokenResponse
- TokenRevocationResponse
- UIKitFollowUpTag
- UnrecoverableAction
Extensions
- ActionKind
- ActionTemplate
- ClientOperationName
- Data
- Dictionary
- FollowUpTag
- FormFieldType
- HaapiAccessorBuilder
- HaapiFlow
- HaapiFlowViewController
- HaapiTokenManager
- HaapiUIKitError
- InfoMessageModel
- InteractionItemModel
- JOSEHeader
- LinkItemModel
- NSObject
- OAuthTokenManager
- PollingStatus
- ProblemType
- Publisher
- RepresentationType
- SelectorItemModel
- Signer
- StorageError
- String
- StringProtocol
- TextPickerView
- UIImage
- UIModel
- URL
- URLRequest
- URLResponse
- URLSessionConfiguration
- Verifier
- WebAuthnViewController
Typealiases
- ActionKind.RawValue
- ActionTemplate.RawValue
- ClientOperationName.RawValue
- FormFieldType.RawValue
- HaapiCompletionHandler
- HaapiFlowViewController.AssociatedStyle
- HaapiFlowViewControllerRepresentable.UIViewControllerType
- HaapiUIViewControllerFactory
- HttpHeadersProvider
- Kind.RawValue
- OAuthAuthorizationParametersProvider
- OAuthCompletion
- OAuthLifecycleResultListener
- OAuthModelFactory
- OAuthRevocationCompletion
- PollingStatus.RawValue
- ProblemType.RawValue
- RepresentationType.RawValue
- UIModelFactory