Dynamic Client Registration (DCR)#

Dynamic Client Registration is configured at the SDK Layer. When attestation is unavailable or rejected, the SDK uses the configured DCRConfiguration to register a per-device dynamic OAuth client and uses that client for subsequent token requests. For the concept and security trade-offs, see Dynamic Client Registration (DCR).

Configuration#

DCR fallback requires two things from your app: a DCRConfiguration pointing at the server-side registration endpoint and template client ID, and a non-attestation client authentication method (Secret, MTLS, or Signed JWT). See Client Authentication for the available methods.

let dcrConfiguration = DCRConfiguration(
    templateClientId: "dcr-template-client-haapi-ios",
    clientRegistrationEndpointUrl: URL(
        string: "/oauth/oauth-registration",
        relativeTo: baseURL
    )!
)

let haapiAccessor = HaapiAccessorBuilder(haapiConfiguration)
    .setDCRConfiguration(dcrConfiguration)
    .setClientAuthenticationMethod(method: ClientAuthenticationMethodSecret("foo"))
    .buildForHaapi()
val dcrConfiguration = DcrConfiguration(
    templateClientId = "dcr-template-client-haapi-android",
    clientRegistrationEndpointUri = URI.create("$baseURLString/oauth/oauth-registration"),
    context = applicationContext
)

GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
    val haapiAccessor = HaapiAccessorFactory(haapiConfiguration)
        .setDcrConfiguration(dcrConfiguration)
        .setClientAuthenticationMethodConfiguration(
            ClientAuthenticationMethodConfiguration.Secret(secret = "foo")
        )
        .createForHaapi(onCoroutineContext = this.coroutineContext)
}

DCR is configured per platform by attaching dcrConfiguration to the platform config record. The native bridge selects attestation or DCR at runtime — your JS code does not branch.

import {
    createIOSConfiguration,
    createAndroidConfiguration,
    secretAuthentication,
    ApplicationContextOption,
} from '@curity/identityserver-haapi-react-native-sdk'

const iosConfig = createIOSConfiguration({
    clientId: 'haapi-ios-client',
    appRedirect: 'com.example.app://haapi',
    clientAuthenticationMethodConfig: secretAuthentication('foo'),
    dcrConfiguration: {
        templateClientId: 'dcr-template-client-haapi-ios',
        clientRegistrationEndpointUrl:
            'https://idsvr.example.com/oauth/oauth-registration',
    },
})

const androidConfig = createAndroidConfiguration({
    clientId: 'haapi-android-client',
    appRedirect: 'com.example.app://haapi',
    clientAuthenticationMethodConfig: secretAuthentication('foo'),
    dcrConfiguration: {
        templateClientId: 'dcr-template-client-haapi-android',
        clientRegistrationEndpointUrl:
            'https://idsvr.example.com/oauth/oauth-registration',
        contextOption: ApplicationContextOption.Default,
    },
})

AndroidDcrConfiguration extends the shared DcrConfiguration with a required contextOption (usually ApplicationContextOption.Default, the host application’s context); IOSDcrConfiguration adds no extra fields. Both platforms still require a non-noAuthentication client authentication method.

Server-Side Prerequisites#

DCR is a coordinated client-and-server feature. Configure two clients on the Curity Identity Server before the client-side configuration above will work:

  • A main HAAPI client with a real authentication method (Secret, MTLS, or Signed JWT). These are the credentials that ship with your app and that the SDK uses to bootstrap the dynamic-registration request. The main client must not use no-authentication.
  • A template HAAPI client carrying the haapi capability. This client defines the shape of the per-device dynamic clients that will be created — capabilities, allowed scopes, token settings. The template client must reference the main client via the authenticate-client-by setting, which is how the server ties the registration request (authenticated as the main client) to the template it spawns from.

The OAuth profile must also expose the dynamic-client-registration endpoint (oauth-registration). Its URL goes into the client-side configuration as clientRegistrationEndpointUrl (iOS) or clientRegistrationEndpointUri (Android). It must use https: on both platforms, creating an accessor — buildForHaapi() or buildForOAuth() on iOS, createForHaapi() or createForOAuth() on Android — fails with insecure_haapi_configuration before anything is sent when it does not, unless the HaapiConfiguration permits http with allowInsecureHttpEndpoints — see HAAPI Request Target Hardening.

On both platforms, the registration request and the token request that precedes it refuse every HTTP redirect, and fail with haapi_redirect_refused, so the client credentials and the registration token never leave the configured endpoints. For programmatic handling, compare the error to HaapiErrorCode.haapiRedirectRefused on iOS or DriverErrorCodes.HAAPI_REDIRECT_REFUSED on Android; the Driver-layer cause is a HaapiRedirectRefusal carrying the redirect’s status code. Configure the registration and token endpoints with the URLs that answer directly, with no reverse-proxy redirect in between — see Upgrade — Redirects Are Refused.

For the full server-side walkthrough, see Implementing HAAPI Attestation Fallback on the Curity resources site.

Runtime Selection#

Configure DCR unconditionally. HaapiAccessorBuilder (iOS) and HaapiAccessorFactory (Android) decide at runtime which path to take — attestation when the device supports it, DCR fallback when it does not. The SDK ignores the DCR configuration on attestation-capable devices, so there is no need to detect device capabilities yourself or branch on them. Ship one configuration that covers both cases.

On iOS, once the fallback has registered a dynamic client, later builds reuse it without checking attestation again. To move a device back to attestation — for example after a server-side problem that rejected genuine devices is fixed — build with shouldClearExistingDCRClient set to true (HaapiAccessorBuilder.buildForHaapi(shouldClearExistingDCRClient:), or setShouldClearExistingDCRClient(value:) on the UIKit configuration builder). The stored dynamic client is removed and the attestation check runs again. A device that has reached the App Attest key limit is not treated as attestation-incapable, so the limit never triggers the fallback; see App Attest Key Limit (iOS).

When configuring DCR, the client authentication method must be set to one of Secret, MTLS, or Signed JWT — not the default None. DCR bootstraps a per-device dynamic client; doing so requires the SDK to authenticate the main client when calling the registration endpoint, and that authentication is what this method provides. Without it, the registration call fails before reaching the server.

Was this helpful?