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 '@curity/identityserver-haapi-react-native-sdk'
const iosConfig = createIOSConfiguration({
clientId: 'haapi-ios-client',
appRedirect: 'com.example.app://haapi',
clientAuthenticationMethodConfig: secretAuthentication('foo'),
})
const androidConfig = createAndroidConfiguration({
clientId: 'haapi-android-client',
appRedirect: 'com.example.app://haapi',
clientAuthenticationMethodConfig: secretAuthentication('foo'),
})Both platform factories take the method as clientAuthenticationMethodConfig — passing any other key fails type-checking.
Mutual TLS#
Mutual-TLS authentication: the client presents a certificate during the TLS handshake, and the identity server validates it against its configured trust store. In the other direction the SDK checks the server’s certificate before any request is sent; each platform tab below says how.
// 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()The platform validates the server’s certificate chain (signatures, validity, hostname) before the pin is compared against a certificate of that validated chain, so a server whose root the device does not trust is refused until its root is installed on the device. With mutual TLS configured, a request to a non-https URL is refused with HaapiErrorCode.insecureTransport, and buildForHaapi() / buildForOAuth() fail with the same code when the token endpoint is not https. Which certificate to pin, how to compute the pin, key rotation and isValidatingHostname are described on Client Authentication (Driver Layer); the behaviour change is in Upgrade — Server Key Pinning.
// Using bundled client keystore + server trust store:
val mtls = ClientAuthenticationMethodConfiguration.Mtls(
clientKeyStore = myClientKeyStore,
clientKeyStorePassword = "passphrase".toCharArray(),
serverTrustStore = myServerTrustStore
)
// Or with pinned server public-key hash — pin the current key and the next
// key the server will rotate to, so a future rotation needs no app release:
val mtlsPinned = ClientAuthenticationMethodConfiguration.MtlsKeyHash(
clientKeyStore = myClientKeyStore,
clientKeyStorePassword = "passphrase".toCharArray(),
serverKeyPinnings = setOf(
ClientAuthenticationMethodConfiguration.MtlsKeyHash.KeyPinning(
hostname = "idsvr.example.com",
publicKeyHash = "Kjuy4mT3fbeDozRNP6rTjWRYmbs79Begb5Roq+CNwiG="
),
ClientAuthenticationMethodConfiguration.MtlsKeyHash.KeyPinning(
hostname = "idsvr.example.com",
publicKeyHash = "L1sN0kkq8t1BR8nGZ+58vN5FQ4URtT/nRhrEUUwj9ZQ="
)
),
isValidatingHostname = true
)
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val haapiAccessor = HaapiAccessorFactory(haapiConfiguration)
.setClientAuthenticationMethodConfiguration(mtls)
.createForHaapi(onCoroutineContext = this.coroutineContext)
}publicKeyHash is the hash of a certificate in the server’s chain, usually the leaf. The platform validates the chain first, exactly as for any HTTPS connection; the pin is then required on one of the validated certificates, so pinning tightens platform trust and never replaces it. A server whose certificate authority the device does not trust is refused. hostname is the bare host the SDK connects to — no port or scheme — compared without regard to case. With Mtls or MtlsKeyHash, a request to a non-https endpoint fails with insecure_transport before anything is sent. See Upgrade — Server Key Pinning for the full trust model, the rotation playbook, and how to compute the hash.
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 '@curity/identityserver-haapi-react-native-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,
})
// Pin the current key and the next key the server will rotate to, so a
// future rotation needs no app release:
const androidMtlsPinned = AndroidMtlsKeyPinningAuthentication({
clientKeyStoreOption: KeyStoreOption.Client,
serverKeyPinnings: [
{
hostname: 'idsvr.example.com',
publicKeyHash: 'Kjuy4mT3fbeDozRNP6rTjWRYmbs79Begb5Roq+CNwiG=',
},
{
hostname: 'idsvr.example.com',
publicKeyHash: 'L1sN0kkq8t1BR8nGZ+58vN5FQ4URtT/nRhrEUUwj9ZQ=',
},
],
isValidatingHostname: true,
})On iOS, isValidatingHostname: false skips only the hostname match; the platform still validates the server’s certificate chain, and a server whose root the device does not trust is refused (see Upgrade — Server Key Pinning).
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 — the SDK registers no KeyStore Resolver of its own, so for any KeyStoreOption the host app must register a Resolver<KeyStore> in Kotlin before initialising the accessor.
AndroidMtlsKeyPinningAuthentication maps directly onto the Android MtlsKeyHash trust manager described in Upgrade — Server Key Pinning — the same platform-first validation and rotation behavior applies. iOSMtlsKeyPinningAuthentication follows the iOS behavior described on the same page.
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".toCharArray(),
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 '@curity/identityserver-haapi-react-native-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({ clientAuthenticationMethodConfig: … }) 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