Client Authentication (SDK Layer)#
When attestation cannot be used — older devices, simulators, or any Dynamic Client Registration fallback path — the SDK needs a client authentication method. The available methods (Secret, MTLS, Signed JWT) are the same as at the Driver Layer; the SDK Layer wires them onto HaapiAccessorBuilder (iOS) or HaapiAccessorFactory (Android). For the concept and trade-offs, see Client Authentication .
When to Use Client Authentication#
Configure a client authentication method whenever the SDK cannot rely on attestation alone. In practice this means:
- DCR fallback is configured — the SDK authenticates the main HAAPI client when calling the registration endpoint to spawn a per-device dynamic client. Without an authentication method, the registration call fails before reaching the server. See DCR .
- Attestation is disabled or unavailable on the deployment — for example, simulators, App Extensions that can’t access attestation APIs, or development builds where
useAttestationisfalse. - The main HAAPI client on the Curity Identity Server uses a non-
no-authenticationmethod — the server-side method and the SDK-side method must match.
When the device passes attestation and the deployment is attestation-only, the SDK doesn’t use client authentication on the main HAAPI flow, and configuring it is optional. Configuring it anyway is safe — the SDK uses it only when needed — and is the recommended posture because it also covers the DCR-fallback path on devices that fail attestation.
Secret#
A static shared secret. Easy to set up; appropriate only for development or controlled environments because the secret is identical for every install.
let clientAuth = ClientAuthenticationMethodSecret("foo")
let haapiAccessor = HaapiAccessorBuilder(haapiConfiguration)
.setClientAuthenticationMethod(method: clientAuth)
.buildForHaapi() val authMethod = ClientAuthenticationMethodConfiguration.Secret(secret = "foo")
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val haapiAccessor = HaapiAccessorFactory(haapiConfiguration)
.setClientAuthenticationMethodConfiguration(authMethod)
.createForHaapi(onCoroutineContext = this.coroutineContext)
} secretAuthentication is shared across both platforms — pass the same value into createIOSConfiguration and createAndroidConfiguration:
import {
createIOSConfiguration,
createAndroidConfiguration,
secretAuthentication,
} from 'identityserver.haapi.reactnative.sdk'
const iosConfig = createIOSConfiguration({
clientId: 'haapi-ios-client',
appRedirect: 'app://haapi',
clientAuthenticationMethodConfiguration: secretAuthentication('foo'),
})
const androidConfig = createAndroidConfiguration({
clientId: 'haapi-android-client',
appRedirect: 'app://haapi',
clientAuthenticationMethodConfig: secretAuthentication('foo'),
})The platform config record names differ (clientAuthenticationMethodConfiguration on iOS, clientAuthenticationMethodConfig on Android) — passing the wrong key fails type-checking.
Mutual TLS#
Mutual-TLS authentication: the client presents a certificate during the TLS handshake. The server validates the certificate against a configured trust store, or against pinned public-key hashes.
// Using a bundled PKCS12 keystore + server PEM:
let clientAuth = try ClientAuthenticationMethodMTLS(
pkcs12Filename: "client.p12",
pkcs12Passphrase: "passphrase",
serverPEMFilename: "server.pem",
isValidatingHostname: true,
bundle: Bundle.main
)
// Or with pinned server public-key hash:
let clientAuthPinned = try ClientAuthenticationMethodMTLS(
pkcs12Filename: "client.p12",
pkcs12Passphrase: "passphrase",
serverKeyPinnings: [
KeyPinning(
hostname: "idsvr.example.com",
publicKeyHash: "Kjuy4mT3fbeDozRNP6rTjWRYmbs79Begb5Roq+DUu7s="
)
],
bundle: Bundle.main
)
let haapiAccessor = HaapiAccessorBuilder(haapiConfiguration)
.setClientAuthenticationMethod(method: clientAuth)
.buildForHaapi() // Using bundled client keystore + server trust store:
val mtls = ClientAuthenticationMethodConfiguration.Mtls(
clientKeyStore = myClientKeyStore,
clientKeyStorePassword = "passphrase",
serverTrustStore = myServerTrustStore
)
// Or with pinned server public-key hash:
val mtlsPinned = ClientAuthenticationMethodConfiguration.MtlsKeyHash(
clientKeyStore = myClientKeyStore,
clientKeyStorePassword = "passphrase",
serverKeyPinnings = setOf(
ClientAuthenticationMethodConfiguration.MtlsKeyHash.KeyPinning(
hostname = "idsvr.example.com",
publicKeyHash = "Kjuy4mT3fbeDozRNP6rTjWRYmbs79Begb5Roq+CNwiG="
)
),
isValidatingHostname = true
)
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val haapiAccessor = HaapiAccessorFactory(haapiConfiguration)
.setClientAuthenticationMethodConfiguration(mtls)
.createForHaapi(onCoroutineContext = this.coroutineContext)
} MTLS uses per-platform factories — the native sides hold the certificate material, so the JS factories take only enum selectors for which keystore / bundle to use:
import {
iOSMtlsAuthentication,
iOSMtlsKeyPinningAuthentication,
AndroidMtlsAuthentication,
AndroidMtlsKeyPinningAuthentication,
BundleOption,
KeyStoreOption,
} from 'identityserver.haapi.reactnative.sdk'
// iOS — PKCS12 + server PEM bundled in the iOS resources:
const iosMtls = iOSMtlsAuthentication({
pkcs12Filename: 'client.p12',
pkcs12Passphrase: 'passphrase',
serverPEMFilename: 'server.pem',
isValidatingHostname: true,
bundleOption: BundleOption.Main,
})
// iOS — pinned server public-key hash:
const iosMtlsPinned = iOSMtlsKeyPinningAuthentication({
pkcs12Filename: 'client.p12',
pkcs12Passphrase: 'passphrase',
serverKeyPinnings: [
{
hostname: 'idsvr.example.com',
publicKeyHash: 'Kjuy4mT3fbeDozRNP6rTjWRYmbs79Begb5Roq+DUu7s=',
},
],
bundleOption: BundleOption.Main,
})
// Android — keystore selection via enum (native side resolves to the actual KeyStore):
const androidMtls = AndroidMtlsAuthentication({
clientKeyStoreOption: KeyStoreOption.Client,
serverKeyStoreOption: KeyStoreOption.Server,
})
const androidMtlsPinned = AndroidMtlsKeyPinningAuthentication({
clientKeyStoreOption: KeyStoreOption.Client,
serverKeyPinnings: [
{
hostname: 'idsvr.example.com',
publicKeyHash: 'Kjuy4mT3fbeDozRNP6rTjWRYmbs79Begb5Roq+CNwiG=',
},
],
isValidatingHostname: true,
})PKCS12 and Keystore files are not passed across the bridge. iOS resolves pkcs12Filename against the bundle (selected by BundleOption); Android resolves KeyStoreOption.Client / KeyStoreOption.Server to native KeyStore instances provided by the host app’s native registry. Ship the actual certificates with your app’s native resources, and see Native Resolvers for the Resolver registration mechanism — for keystore options other than the defaults, the host app must register a Resolver<KeyStore> in Kotlin before initialising the accessor.
Signed JWT#
Build a JWT signed with an asymmetric or symmetric key and present it as client_assertion. The server is configured with the matching public key (asymmetric) or shared secret (symmetric) to verify the assertion. Asymmetric is recommended for production — the private key never leaves the device.
// Asymmetric (recommended for production):
let asymConfig = try ClientAuthenticationMethodJWTAsymmetric(
pemFilename: "rsa-private-key.pem",
signatureAlgorithm: .rs256,
bundle: Bundle.main
)
// Symmetric (HMAC):
let symConfig = ClientAuthenticationMethodJWTSymmetric(
signatureAlgorithm: .hs256,
secretKey: "shared-secret-key"
)
let haapiAccessor = HaapiAccessorBuilder(haapiConfiguration)
.setClientAuthenticationMethod(method: asymConfig)
.buildForHaapi() // Asymmetric:
val asym = ClientAuthenticationMethodConfiguration.SignedJwt.Asymmetric(
clientKeyStore = myClientKeyStore,
clientKeyStorePassword = "passphrase",
alias = "signing-key",
algorithmIdentifier =
ClientAuthenticationMethodConfiguration.SignedJwt.Asymmetric.AlgorithmIdentifier.ES256
)
// Symmetric:
val sym = ClientAuthenticationMethodConfiguration.SignedJwt.Symmetric(
secretKey = "shared-secret-key",
signatureAlgorithm =
ClientAuthenticationMethodConfiguration.SignedJwt.Symmetric.SignatureAlgorithm.HS256
)
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val haapiAccessor = HaapiAccessorFactory(haapiConfiguration)
.setClientAuthenticationMethodConfiguration(asym)
.createForHaapi(onCoroutineContext = this.coroutineContext)
} Asymmetric JWT uses per-platform factories (iOS resolves a PEM file; Android resolves a Keystore alias). Symmetric JWT is shared because HMAC needs only a string secret:
import {
iOSJwtAsymmetricAuthentication,
AndroidJwtAsymmetricAuthentication,
jwtSymmetricAuthentication,
JwtAsymmetricAlgorithm,
JwtSymmetricAlgorithm,
BundleOption,
KeyStoreOption,
} from 'identityserver.haapi.reactnative.sdk'
// iOS asymmetric — RSA private key bundled in the iOS resources:
const iosAsym = iOSJwtAsymmetricAuthentication({
asymmetricAlgorithm: JwtAsymmetricAlgorithm.RS256,
pemFilename: 'rsa-private-key.pem',
bundleOption: BundleOption.Main,
})
// Android asymmetric — Keystore alias:
const androidAsym = AndroidJwtAsymmetricAuthentication({
asymmetricAlgorithm: JwtAsymmetricAlgorithm.ES256,
alias: 'signing-key',
clientKeyStoreOption: KeyStoreOption.Client,
})
// Symmetric — shared across both platforms:
const symmetric = jwtSymmetricAuthentication({
symmetricAlgorithm: JwtSymmetricAlgorithm.HS256,
secretKey: 'shared-secret-key',
})Pass the resulting method into createIOSConfiguration({ clientAuthenticationMethodConfiguration: … }) or createAndroidConfiguration({ clientAuthenticationMethodConfig: … }) (per the Secret tab above). Use asymmetric JWT in production — the private key stays in native iOS bundle or Android Keystore and is never visible to JS.
When configuring DCR fallback, 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. See DCR for the matching server-side prerequisites.
How to implement this: Client Authentication (Driver Layer) · Client Authentication (concept) · DCR · How to Configure DCR Fallback