Configuration (UI Layer)#

The UI Layer’s configuration object — HaapiUIKitConfiguration on iOS, WidgetConfiguration on Android — carries every parameter the framework needs to connect to the Curity Identity Server, render the flow, and (when configured) bind tokens, fall back to Dynamic Client Registration, or collect risk-assessment context. Build it once at application startup and inject it through HaapiUIKitApplication (iOS) or HaapiUIWidgetApplication (Android).

For presentation knobs (modal vs stack, polling interval, selector style), see Presentation Options. For starting the flow itself, see Flow Lifecycle.

Minimal Configuration#

The five required parameters are the same on both platforms: client ID, base URL, token endpoint, authorization endpoint, and the app redirect URI. Everything else is optional.

Build HaapiUIKitConfiguration once in AppDelegate.application(_:didFinishLaunchingWithOptions:) and use it to construct HaapiUIKitApplication:

@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 {
        haapiUIKitApplication = HaapiUIKitApplicationBuilder(
            haapiUIKitConfiguration: haapiUIKitConfiguration
        ).build()
        return true
    }
}

clientId — the OAuth client ID configured on the Curity Identity Server with the haapi capability.

baseUrl — the base URL of the Curity Identity Server (the runtime endpoint base, typically the issuer URL).

tokenEndpointUrl — the OAuth token endpoint, typically <baseUrl>/oauth/v2/oauth-token.

authorizationEndpointUrl — the OAuth authorization endpoint, typically <baseUrl>/oauth/v2/oauth-authorize.

appRedirect — the app redirect URI registered on the OAuth client (custom scheme or universal link).

For SwiftUI apps, build the configuration in AppDelegate the same way and hook it with @UIApplicationDelegateAdaptor.

Implement HaapiUIWidgetApplication on your Application subclass and expose widgetConfiguration from it:

class ClientApplication : Application(), HaapiUIWidgetApplication {
    private val baseUri = URI.create("https://idsvr.example.com")

    override val widgetConfiguration: WidgetConfiguration
        get() = WidgetConfiguration.Builder(
            clientId = "my-client-id",
            baseUri = baseUri,
            tokenEndpointUri = baseUri.resolve("/oauth/v2/oauth-token"),
            authorizationEndpointUri = baseUri.resolve("/oauth/v2/oauth-authorize"),
            appRedirect = "com.example.app://haapi"
        ).build()
}

clientId — the OAuth client ID configured on the Curity Identity Server with the haapi capability.

baseUri — java.net.URI for the Curity Identity Server runtime base.

tokenEndpointUri — the OAuth token endpoint.

authorizationEndpointUri — the OAuth authorization endpoint.

appRedirect — the app redirect URI registered on the OAuth client. The app module also declares it for the manifest; see Redirect URI.

Register ClientApplication in AndroidManifest.xml via android:name=".ClientApplication". The framework declares HaapiFlowActivity itself; your manifest does not need to.

Redirect URI#

Platform-only: Android.

Some authentication steps run in a browser — a Custom Tab or the default browser — and end with the Curity Identity Server redirecting to appRedirect. The app receives that redirect through an entry point that the framework declares from two manifest placeholders you set in the app module. These values must agree:

  • The huiRedirectScheme and huiRedirectHost placeholders and the optional hui_redirect_path_prefix string resource: appRedirect has their scheme and host, and its path starts with the prefix
  • appRedirect on WidgetConfiguration.Builder
  • A redirect URI registered on the OAuth client
  • redirectTo, when you return a custom HaapiDataMappersFactory from dataMappersFactory: it equals appRedirect

Each disagreement has its own symptom:

ProblemSymptom
The placeholders are missingThe build fails the manifest merge with requires a placeholder substitution but no value for <huiRedirectScheme> is provided
The client does not register appRedirectThe flow fails with the server’s redirect_uri error
The placeholders or path prefix do not match appRedirectThe browser step never returns to the app. With logging enabled, the framework logs a warning when the flow starts
A custom HaapiDataMappersFactory passes a redirectTo other than appRedirectThe flow ignores the redirect and the browser step never completes. With logging enabled, the framework logs a warning when the redirect arrives

During a browser step, the flow accepts only a redirect whose scheme, host, port, and path equal appRedirect, comparing scheme and host ignoring case; the query and fragment are not compared. Any other incoming URI is ignored: a Custom Tab open for the step closes, and the step stays on screen. For enabling the warnings, see Logging.

Android recommends verified App Links for links into an app: Android delivers a verified https link only to the app that the domain names, so no other app can receive the redirect. The Curity Identity Server already publishes /.well-known/assetlinks.json for the Android apps configured on its clients — the package name and signing-certificate registration that attestation uses — including the delegate_permission/common.handle_all_urls relation that App Links verify. The redirect can therefore live on the server’s own host:

  1. Register a redirect URI on a path the server does not serve, such as https://idsvr.example.com/haapi/callback, on the OAuth client.

  2. Set the placeholders to https and the server host:

    // app/build.gradle.kts
    android {
        defaultConfig {
            manifestPlaceholders["huiRedirectScheme"] = "https"
            manifestPlaceholders["huiRedirectHost"] = "idsvr.example.com"
        }
    }
    // app/build.gradle
    android {
        defaultConfig {
            manifestPlaceholders += [huiRedirectScheme: "https", huiRedirectHost: "idsvr.example.com"]
        }
    }
  3. Limit the path in res/values/strings.xml. Without it the app claims every URL of the server host, including the pages of the browser step itself:

    <string name="hui_redirect_path_prefix" translatable="false">/haapi/callback</string>
  4. Set appRedirect = "https://idsvr.example.com/haapi/callback".

Android verifies the domain after installation, and only when the host is publicly resolvable and serves assetlinks.json over https with a publicly trusted certificate. Until the host is verified for the app, Android opens the redirect in the browser: the tab shows the server’s 404 page for the callback path and the app never resumes. With logging enabled, the framework logs a warning when the host is not verified for the app (Android 12 and later). Check the state with adb shell pm get-app-links com.example.app.

A host that cannot be verified — an IP address, a private domain, or a self-signed certificate, as in most local and CI environments — needs a custom scheme instead. For manual testing on Android 12 and later, adb shell pm set-app-links-user-selection --user cur --package com.example.app true idsvr.example.com approves the host for the app on one device.

Custom Scheme#

When an App Link is not possible, use a private-use URI scheme based on a domain you control, written in reverse order as RFC 8252 requires, such as com.example.app://haapi. The application ID is a natural choice when it contains no underscores, which a URI scheme does not allow. Write the scheme in lowercase: Android matches schemes case-sensitively.

// app/build.gradle.kts
android {
    defaultConfig {
        manifestPlaceholders["huiRedirectScheme"] = "com.example.app"
        manifestPlaceholders["huiRedirectHost"] = "haapi"
    }
}
// app/build.gradle
android {
    defaultConfig {
        manifestPlaceholders += [huiRedirectScheme: "com.example.app", huiRedirectHost: "haapi"]
    }
}

Keep the default hui_redirect_path_prefix (empty, any path) and set appRedirect = "com.example.app://haapi".

Avoid generic values such as app://haapi, which earlier versions of these examples showed. Any app can register the same scheme; Android then shows a chooser, or another app receives the redirect.

What Another App Can Reach#

HaapiFlowActivity is not exported, so other apps can neither start it nor receive the tokens it returns. Start it for a result from your own app, as described in Flow Lifecycle. The redirect entry point is exported, as any redirect target must be, and accepts only a VIEW intent with a URI.

A verified App Link stops other apps from receiving the redirect; a custom scheme does not. Neither stops another app from sending a URI that matches appRedirect while a browser step is in progress. Such a URI does not carry the values the server issued for the step, so the server rejects it. The flow then ends with an error, and the user has to start the login again.

Security Configuration#

The six security knobs — DCR fallback, Token Binding, Client Authentication, Risk Assessment, token revocation, plain-http endpoints — are wired on the UI-Layer builder using the same parameter types as at the SDK Layer. The SDK-Layer pages carry the per-parameter depth; the UI-Layer call sites are:

KnobiOS builder callAndroid builder callParameter depth
DCR fallbacksetDCRConfiguration(configuration:)setDcr(...)DCR
Token BindingsetTokenBoundConfiguration(configuration:)setDpopTokenBinding(...)Token Binding
Client AuthenticationsetClientAuthenticationMethod(method:)setClientAuthenticationMethod(...)Client Authentication
Risk AssessmentsetRiskAssessmentConfiguration(_:)setContext(...) ¹Risk Assessment
Token revocationsetRevocationEndpointUrl(endpoint:)setRevocationEndpointUri(...)Token Lifecycle
Plain-http endpoints ²setAllowInsecureHttpEndpoints(value:)setAllowInsecureHttpEndpoints(...)HAAPI Request Target Hardening

¹ On Android, risk assessment is wired by passing the application Context rather than a dedicated configuration object — setContext(applicationContext) is the wiring call. The framework then reads OS name, OS version, device model, and package name from that Context internally and forwards them on every HAAPI request. iOS exposes the same four fields as explicit parameters on RiskAssessmentConfiguration because UIDevice is main-actor-isolated; see Risk Assessment for the field-by-field detail on both platforms.

² The default is false: the base URL, token endpoint, authorization endpoint, a configured revocation endpoint and — when DCR is configured — client registration endpoint must all use https. On both platforms a non-https address makes starting a flow, and every OAuthLifecycle token operation (refresh, revocation, DPoP retrieval), fail with insecure_haapi_configuration. Set it to true only for a local development or test server, and leave it false in anything you ship. On Android, a HaapiConfiguration created with allowInsecureHttpEndpoints = true and passed to the WidgetConfiguration constructor permits it too. On both platforms it only permits an http address to be configured, and still refuses any other scheme: App Transport Security on iOS, and the network security configuration on Android, still decide whether the app may send cleartext traffic, and requests still go only to the configured addresses.

Example — configuring DCR fallback + a Secret client authentication method on the UI-Layer builder (chain these onto the foundational builder shown above):

// ... required parameters from the Minimal Configuration block above
.setClientAuthenticationMethod(
    method: ClientAuthenticationMethodSecret("foo")
)
.setDCRConfiguration(
    configuration: DCRConfiguration(
        templateClientId: "dcr-template-client-haapi-ios",
        clientRegistrationEndpointUrl: URL(
            string: "/oauth/oauth-registration",
            relativeTo: baseURL
        )
    )
)
.build()
// ... required parameters from the Minimal Configuration block above
.setClientAuthenticationMethod(
    WidgetConfiguration.ClientAuthenticationMethod.Secret("my-secret")
)
.setDcr(
    WidgetConfiguration.Dcr(
        templateClientId = "dcr-template-client-haapi-android",
        clientRegistrationEndpointUri = baseUri.resolve("/oauth/oauth-registration"),
        context = this
    )
)
.build()

The UI Layer composes the SDK Layer’s security wiring, but server-side prerequisites must already be in place before the client-side call has any effect:

  • setDCRConfiguration / setDcr — without a configured template HAAPI client on the Curity Identity Server that references the main client via authenticate-client-by, the framework’s first registration call returns 401 or 404 and the flow fails before the user sees a screen.
  • setTokenBoundConfiguration / setDpopTokenBinding — when the server has issue-token-bound-authorization-code=true but the client omits token binding (on iOS, passing UnboundedTokenConfiguration or omitting the call; on Android, omitting setDpopTokenBinding), the token-exchange step returns invalid_dpop_proof. The mismatch is silent until the user has already authenticated; see the matrix in Token Binding.
  • setRiskAssessmentConfiguration / setContext — without a configured risk-assessment integration at the Curity profile level, the device-context fields are forwarded but never consumed, and the auth step that depends on the risk score either runs without a score or fails with a flow-level error.

Configure both sides together; the SDK-Layer pages linked in the table above carry the server-side prerequisites for each knob.

Storage#

Platform-only: iOS.

By default the framework persists the authenticated session — token and DPoP state — in the app-private keychain, and no configuration is required. Call setStorage(_:) to share that session with an App Extension through a keychain access group, or to inject a custom Storage. DCR data inherits this store unless you give DCRConfiguration a storage of its own.

// share the session with an app extension via a keychain access group
.setStorage(StorageBuilder.Keychain().accessGroup("group.example.shared").build())

// optionally set the data-protection level (default: .afterFirstUnlockThisDeviceOnly)
.setStorage(
    StorageBuilder.Keychain()
        .accessGroup("group.example.shared")
        .accessibility(.whenUnlockedThisDeviceOnly)
        .build()
)

Data-protection levels#

Both levels are this-device-only: stored secrets are device-bound and never migrate to another device through an encrypted backup or iCloud. Choose the one that matches when your app needs to reach the session:

LevelWhen to choose itLimitations
.afterFirstUnlockThisDeviceOnly (default)The default for session continuity. Use when the app — or its extension — needs to read the session in the background, or at any point after the device has been unlocked at least once since boot (for example, a silent token refresh while backgrounded).Not accessible before the first unlock after a reboot: a background launch immediately after restart, before any unlock, fails with the keychain-unavailable error.
.whenUnlockedThisDeviceOnlyA stricter posture for when you never need background or locked access — items are reachable only while the device is currently unlocked, which minimizes the accessibility window.Any keychain access while the device is locked fails — including background silent refresh, an extension running while locked, and push-triggered background work.

In both cases a locked or just-booted keychain surfaces a typed, inspectable error rather than an opaque failure, so it can be retried once the app is in the foreground — see Storage Errors.

Was this helpful?