Enhancing Secure Mobile Authentication with OAuth, Dynamic Client Registration, and DPoP

Arnab Bdutta
7 min readSep 20, 2023

--

OpenID is a federated identity system designed to support a third party that needs to verify a person’s identity within your domain. For example, an e-commerce website may wish to offer social login, leveraging the identity provider (“IDP”) of Google or Apple. However, third-party authentication raises significant security concerns; we cannot allow the third party to access the user’s password. This is why the home IDP displays the login page via a TLS connection, a process that the RP (Relying Party) cannot intercept.

For first-party websites, using a federated identity protocol is very convenient, even though it was primarily designed for third-party authentication. It’s natural for first-party websites to utilize browser redirection via TLS, enabling the IDP to perform authentication and centralize other domain-specific business logic. However, for first-party mobile applications, OpenID is a square peg in a round hole. The browser redirect experience is subpar, with confusing popups that bewilder end-users, and domains cannot customize these system messages. Most mobile developers prefer a backchannel authentication mechanism, allowing them to keep the entire login experience within the app. Yes, they can access the password, but since it’s a first-party application, security concerns are somewhat reduced.

If passwords were the sole solution for human authentication, the OAuth password grant would suffice. However, most modern authentication technologies involve a series of requests and responses, i.e. they are multi-step. What we truly need is something akin to a backchannel OAuth Code Flow grant, where an authentication workflow can occur, and upon completion, the client can reference it with a code while requesting a token. A new IETF draft, known as OAuth 2.0 for First-Party Native Applications, addresses this need.

There are a few other measures that can enhance security: (1) using “proof of possession tokens” to prevent unauthorized usage in case a token leaks; (2) employing app attestation to mitigate the risk of app tampering; and (3) utilizing FIDO authentication to leverage hardware biometrics for end-user authentication — currently the best alternative to traditional passwords available.

Attestation in Dynamic Client Registration

The Use of Attestation in Dynamic Client Registration enhances security, trust and control in the context of OAuth 2.0 between OAuth clients (applications) and authorization servers. DCR attestation helps ensure that only legitimate and trusted clients can register with an authorization server. By requiring clients to provide a digitally signed attestation of their identity, it becomes much more challenging for malicious or unauthorized clients to register themselves.

To mitigate the risk of app tampering and the use of fraudulent devices during client registration, it is highly advisable to integrate the native app with the Play Integrity API. The Play Integrity API helps you check that interactions and server requests are coming from your genuine app binary running on a genuine Android device.

The integrity verdict obtained through the Play Integrity API can be incorporated as a claim within the evidence parameter (JWT) for attested DCR. This claim can then be subjected to verification by the designated verifier to guarantee the trustworthiness of both the app and the Android device. Based on this verification, the DCR may either be allowed or rejected accordingly.

Combining a classic request to check app integrity using Play Integrity API with the use of attestation in a DCR, we can create the following sequence diagram.

Distributed Proof of Possession (DPoP) token

DPoP ensures that the client possesses the private key corresponding to the public key in the DPoP token. This provides strong proof that the client making a request is the legitimate owner of the associated cryptographic key. It mitigates various types of token-related attacks, including token replay and theft. DPoP tokens are bound to the HTTP request and cannot be reused across different requests. This significantly reduces the risk of token theft and misuse. OAuth 2.0 access tokens are typically bearer tokens, which means anyone with possession of the token can use it. DPoP adds an additional layer of security by requiring proof of possession of a cryptographic key for access, making it more resilient to token leakage or interception.

Here’s a step-by-step guide for creating a first-party native Android app with attestation in Dynamic Client Registration and DPoP token authentication:

1. Attestation in DCR

The first step involves enabling OpenID Connect (OIDC) client registration, commonly known as Dynamic Client Registration (DCR) while ensuring the inclusion of appropriate scopes. In our app’s user interface, we will provide fields where users can input the OpenID Configuration URL and the desired scope for registering an OIDC client. Upon clicking the registration button, the app will initiate a request to fetch crucial configuration details such as the registration_endpoint, authorization_challenge_endpoint, token_endpoint, userinfo_endpoint, and token_revoke_endpoint from the OpenID Configuration response. These URLs serve as essential resources required throughout and will be stored securely in the mobile device’s storage (SQLite database) for efficient and secure communication with the authentication server.

Generate a key pair consisting of a public key and a private key to sign the evidence used for attestation. If you are developing an Android app in Java, you can utilize `java.security.KeyPairGenerator` to generate the key pair.


KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEYSTORE);

kpg.initialize(new KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
.setCertificateSerialNumber(BigInteger.valueOf(777)) //Serial number used for the self-signed certificate of the generated key pair, default is 1
.setCertificateSubject(new X500Principal("CN=" + KEY_ALIAS)) //Subject used for the self-signed certificate of the generated key pair, default is CN=fake
.setDigests(KeyProperties.DIGEST_SHA256) //Set of digests algorithms with which the key can be used
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) //Set of padding schemes with which the key can be used when signing/verifying
.setCertificateNotBefore(startDate.getTime()) //Start of the validity period for the self-signed certificate of the generated, default Jan 1 1970
.setCertificateNotAfter(endDate.getTime()) //End of the validity period for the self-signed certificate of the generated key, default Jan 1 2048
.setKeySize(2048)
.setUserAuthenticationValidityDurationSeconds(30)
.build());

kp = kpg.generateKeyPair();

To sign the evidence payload used for attestation you may use `io.jsonwebtoken:jjwt` dependency (in Java). Don’t forget to include the public keys in JWKS format in the request parameters. Refer to this guide regarding details on attestation in DCR using the Janssen server.


String evidenceJwt = Jwts.builder()
.setHeaderParams(headers)
.setClaims(claims)
.signWith(SignatureAlgorithm.RS256, KeyManager.getInstance().getPrivateKey())
.compact();

Sample DCR Request and Response on the Janssen server:

########
Request
########
curl -X POST -k -H ‘Content-Type: application/json’ -i ‘https://your.janssen.server/jans-auth/restv1/register' — data ‘{
“application_type”:”web”,
“client_name”:”DPoPAppClient-bc041d99–2c02–4622–8b6b”,
“evidence”:”eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IlJTMjU2IiwiandrIjp7ImUiOiJBUUFCIiwia3R5IjoiUlNBIiwibiI6IjNwMmN1aXNjOFJ3U0VYN1dEYUxUamlQRUJ1bVZnTlIzVm96MnBobGh0aDV3Y2R4aGZ3aFpUcFRvOHJUWFBSbWozUWM5ck1jOGo5djM5MWZKbXhmSWpWM2Q0YUJVZm5aOS00VFJzdmlXVEJIN2RkODBtWTZhcnl5OTBaY0ZmTHdNektzSG1ITHJfTENJUjNKZ3F1MG1TTm9EUHFjTmNFcVVPZVZxVTV2YnRXUHJMdVNlMEJTNmxaRDQ2aXZBSWttOUJhb2xqT1NEcGJvV2gtUW5nMzBVQTJhZnhHN05JVjlwV1Y2S2hka0h1dUtDUXN1RlRpa2Z6cGlWQVRaLTlFcllUU1hOMDVqT0RJUk5Mc1FnV0NyUnBiMEdheGlick5OWTZmcWF2dkgycDJGaXlnbEFaUnBqTDhRNkVWd0Noek9uT1BySkpDZXdCdTdHVFZ4dVZuaHc0dyJ9fQ.eyJhYXBOYW1lIjoiRFBvUEFwcCIsImlhdCI6MTY5MzU2OTQ0ODUwMywic2VxIjoiYzlmMWFhOTUtNGZhNi00NGQ4LTljNTYtYzhkNDcwNGJlMTQyIiwianRpIjoiZWRlYWQxOWQtYzQ1Yy00NDVhLWFlN2ItZTM2NjRiOTBhY2ZjIn0.OQhT-lzYIUWRb4YV4tZNHHTOKAZOax8yZW3HONxLNIvUsxTeS7zB55G5M8WmdwVA4UTcRL6oBj_j-Ep_r6X1fx9ZaJ-SmGCONgC8LxHPmWJCb1eQxYq-7S-CWTtS-SCK4FW473Kg-Kk_FSlyy5tFbH5p_x_CTiPuTgWK5Ct9OwbJ9tyaaiqeXoRgBUEjjjElSYoZqutFbe1WmKrlwblVtXc47JzaZBbnDt3Goukz1HUln_xL0sT_CnxaTVLKdAT7IIfK5UuVIEiKvoqathMdKlqynrn9kyLXwflpMWbirWrQxIpIbkQ6srHStyDDh1zBEyYNAj-pXz9iyhHULoz0Ig”,
“grant_types”:[“authorization_code”],
“jwks”:”{\”keys\”:[{\”e\”:\”AQAB\”,\”kty\”:\”RSA\”,\”n\”:\”pzVIV71wHi1fG3TEcCUJw0uolOBUryEJPy8IFpI20lAWqWw5prPn2mPqsyaOPpQtNX2_12PmZjlDq3SjInipSK_saSJ4pm-LCTOYZm55n5QbiK-iCR0DgCVlAeNj6YIenk1Tdy0KSuYoZxOL7iOdMAT9f7qvRAlSdWivOef_tDEtCIbZ3aKoNujv0xPUmOgnZ0U1QdxK3bmprV08O6dV6-_GJBeEdGZ5qZezPIxyjaxiGcoDk47QxdFz-aD38md-zQDEr9toU08j08bgeZLvbVr5e_M-fzhGY5yEISg1e-87n_v3HbdKFGuRFqYp3CYVayuYMWJsIIq8aEPZxdi8qw\”}]}”,
“redirect_uris”:[“https://your.janssen.server"],
“response_types”:[“code”],
“scope”:”openid”
}’

########
Response
########
{
"allow_spontaneous_scopes": false,
"jwks": {
"keys": [{
"kty": "RSA",
"e": "AQAB",
"n": "pzVIV71wHi1fG3TEcCUJw0uolOBUryEJPy8IFpI20lAWqWw5prPn2mPqsyaOPpQtNX2_12PmZjlDq3SjInipSK_saSJ4pm-LCTOYZm55n5QbiK-iCR0DgCVlAeNj6YIenk1Tdy0KSuYoZxOL7iOdMAT9f7qvRAlSdWivOef_tDEtCIbZ3aKoNujv0xPUmOgnZ0U1QdxK3bmprV08O6dV6-_GJBeEdGZ5qZezPIxyjaxiGcoDk47QxdFz-aD38md-zQDEr9toU08j08bgeZLvbVr5e_M-fzhGY5yEISg1e-87n_v3HbdKFGuRFqYp3CYVayuYMWJsIIq8aEPZxdi8qw"
}]
},
"application_type": "web",
"rpt_as_jwt": false,
"registration_client_uri": "https://your.janssen.server/jans-auth/restv1/register?client_id=05fd2650-8478-4bca-9488-b14f7219473b",
"tls_client_auth_subject_dn": "",
"run_introspection_script_before_jwt_creation": false,
"registration_access_token": "872ab5ca-457a-4118–95cf-d699dc549967",
"client_id": "05fd2650–8478–4bca-9488-b14f7219473b",
"token_endpoint_auth_method": "client_secret_basic",
"scope": "openid",
"client_secret": "97c734ca-a729–49b7–9ec3–514e43ba530b",
"client_id_issued_at": 1693571537,
"backchannel_logout_uri": [],
"backchannel_logout_session_required": false,
"client_name": "DPoPAppClient-bc041d99–2c02–4622–8b6b",
"par_lifetime": 600,
"spontaneous_scopes": [],
"id_token_signed_response_alg": "RS256",
"access_token_as_jwt": false,
"grant_types": ["authorization_code"],
"subject_type": "pairwise",
"additional_token_endpoint_auth_methods": [],
"keep_client_authorization_after_expiration": false,
"require_par": false,
"redirect_uris": ["https://your.janssen.server"],
"redirect_uris_regex": "",
"additional_audience": [],
"frontchannel_logout_session_required": false,
"client_secret_expires_at": 0,
"access_token_signing_alg": "RS256",
"response_types": ["code"]
}

2. Execute the Authorization Challenge Endpoint to get the Authorization Code

Once Dynamic Client Registration (DCR) is successful, the app saves the OIDC client details in the SQLite database. Every time the user opens the app, it checks whether the client details are present in the database. If they are, the app proceeds directly to the login page, prompting the user for their username and password. Upon submitting the username and password, the app extracts the authorization_challenge_endpoint from the OpenID Provider configuration and initiates a request to obtain the Authorization Code.

Authorization Challenge Endpoint allows first-party native clients to obtain authorization code which later can be exchanged on access token. This can provide an entirely browserless OAuth 2.0 experience suited for native applications. This endpoint conforms to OAuth 2.0 for First-Party Native Applications specifications. Click here for its implementation using the Janssen server.

Sample Request and Response:

########
Request
########
curl -k https://your.janssen.server/jans-auth/restv1/authorization_challenge -d 'grant_type=authorization_code' -d 'scope=openid' -d 'username=admin' -d 'password=secret' -d 'state=0x74ab847000' -d 'nonce=8974ab847000' -d 'client_id=05fd2650–8478–4bca-9488-b14f7219473b'

########
Response
########
{"authorization_code":"cdf09c4a-10f4–467e-9f6f-a697db44b108"}

3. Distributed Proof of Possession token

Using the token_endpoint obtained from the OpenID Configuration of the authentication server, the app will send a request to obtain an access token. See the DPoP jwt syntax to be used in the DPoP header of the token request. The Authorization Code should be passed as a request parameter.

For the Android app (in java), `com.nimbusds:oauth2-oidc-sdk` provides method for DPOP header generation.

DefaultDPoPJWTFactory dpopJWTFactory = new DefaultDPoPJWTFactory(rsaKey, JWSAlgorithm.RS256);
SignedJWT signedJwt = dpopJWTFactory.createDPoPJWT(new JWTID(),
httpMethod,
new URI(requestUrl),
iat);

For more control, the signed DPoP jwt header can be created with `io.jsonwebtoken:jjwt`.

Map<String, Object> headers = new HashMap<>();
headers.put("typ", "dpop+jwt");
headers.put("alg", "RS256");
headers.put("jwk", KeyManager.getPublicKeyJWK(KeyManager.getInstance().getPublicKey()).getRequiredParams());

Map<String, Object> claims = new HashMap<>();
claims.put("jti", UUID.randomUUID().toString());
claims.put("htm", httpMethod); //POST
claims.put("htu", requestUrl); //issuer

String token = Jwts.builder()
.setHeaderParams(headers)
.setClaims(claims)
.signWith(SignatureAlgorithm.RS256, KeyManager.getInstance().getPrivateKey())
.compact();

The token_type obtained in response is DPoP.

########
Request
########

curl -k -u '05fd2650-8478-4bca-9488-b14f7219473b:97c734ca-a729-49b7-9ec3-514e43ba530b' -H 'DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IlJTMjU2IiwiandrIjp7ImUiOiJBUUFCIiwia3R5IjoiUlNBIiwibiI6IjNwMmN1aXNjOFJ3U0VYN1dEYUxUamlQRUJ1bVZnTlIzVm96MnBobGh0aDV3Y2R4aGZ3aFpUcFRvOHJUWFBSbWozUWM5ck1jOGo5djM5MWZKbXhmSWpWM2Q0YUJVZm5aOS00VFJzdmlXVEJIN2RkODBtWTZhcnl5OTBaY0ZmTHdNektzSG1ITHJfTENJUjNKZ3F1MG1TTm9EUHFjTmNFcVVPZVZxVTV2YnRXUHJMdVNlMEJTNmxaRDQ2aXZBSWttOUJhb2xqT1NEcGJvV2gtUW5nMzBVQTJhZnhHN05JVjlwV1Y2S2hka0h1dUtDUXN1RlRpa2Z6cGlWQVRaLTlFcllUU1hOMDVqT0RJUk5Mc1FnV0NyUnBiMEdheGlick5OWTZmcWF2dkgycDJGaXlnbEFaUnBqTDhRNkVWd0Noek9uT1BySkpDZXdCdTdHVFZ4dVZuaHc0dyJ9fQ.eyJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly9hZG1pbi11aS10ZXN0LmdsdXUub3JnIiwiaWF0IjoxNjkzNTY5NDg0NTQ0LCJqdGkiOiI1MWY0ZWRkYy00MzZjLTRjMmQtYmU1OS1mZjkzNTNiNDI0OTQifQ.MLCDkwyaWVV5kiowWJdnvT241zL0UwMrZ5Kes-uQ_12tKROC0BSZkf2PQ08PcA9nGEw5Ab4MPJOOQlLZAXqkiI7W2zx9BjLIWnRkkSkCS3ucRIfJWxvuQ4W1bfbtUvy9UTZ3UZSD4pxdsZSoGRADePH6JEJy4GkWH6qMrCHeLr2YEjeaBPzJ3Z4YazX8p7kt7_dbNz56hU78hiqu9JetM0-9_5CLqxbomsc8KzreWZ-Mazt_iJJHp_pEKZouClbieKsI_lWfko7MyzxIZNJm6FKdMtCi8SWsXRnuLUkNES0TBe61-yJjqRFqJGZ9BOmwIo8txkmuFHqd403ibaPiiw' https://your.janssen.server/jans-auth/restv1/token -d 'grant_type=authorization_code' -d 'scope=openid' -d 'code=c13327d9-9a1c-41f7-8d3c-06d83ee7f69f' -d 'redirect_uri=https://your.janssen.server/callback'

########
Response
########

{"access_token":"fb49e7e7-2831-4dfd-8947-30ea2c6f305e","scope":"openid","id_token":"eyJraWQiOiJjb25uZWN0X2E4MWQyZWIzLTY2YTYtNGEzOS1hN2I2LTEzODBiMzAzYTYwNl9zaWdfcnMyNTYiLCJ0eXAiOiJqd3QiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiTG5ocnFjR0ZiYmw1X3ZCaG1Yanc1ZyIsInN1YiI6IkxVS0RHR0Q3aXQtb3lqUFE3akV6RV90WF9IZVVvcVRjeU1weTNNZ0ZIa0EiLCJhbXIiOltdLCJpc3MiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmciLCJub25jZSI6Ijg5NzRhYjg0NzAwMCIsImphbnNPcGVuSURDb25uZWN0VmVyc2lvbiI6Im9wZW5pZGNvbm5lY3QtMS4wIiwiYXVkIjoiMDVmZDI2NTAtODQ3OC00YmNhLTk0ODgtYjE0ZjcyMTk0NzNiIiwicmFuZG9tIjoiMDg4ZmQ5MTUtNDA5NC00NDgxLTlkODEtZTllZDdhMTY3MGE3IiwiYWNyIjoiZGVmYXVsdF9jaGFsbGVuZ2UiLCJjX2hhc2giOiJjZmJ0d0ZWek51QzhpVDFFQWtJdGlnIiwiYXV0aF90aW1lIjoxNjkzNTc0Mzg0LCJleHAiOjE2OTM1NzgwMTgsImdyYW50IjoiYXV0aG9yaXphdGlvbl9jb2RlIiwiaWF0IjoxNjkzNTc0NDE4fQ.ODhYZTdC0IJjidUAMyVbRKKoovyW1-2AdzVPtn6tx232Vjx6Mxs-Sng57YV6lUdjFLbYfmUjn5JM90Zg3buJHpu6gZKaLs_44j-1i9NbJrAFhe_6libXO0Bib4v5vacYx_0zllXO5FklDdvctahxFc9-xG8pTolkqEGJGjILyNI70NOjodZxBTPPZDBSER9MnD0V_CmRM0izonroV_HyvWDCbTJm9sjBufoY2UkpKCYpyKytu5zSUFq6gq3DEX661Vx_ANgpzy_0BkJgfQ4-juvVPoBdcQnqK7akpwgqyf82NK-pFSZXdduO1Z343L_uYyzzurEkMHf6s_8jre4EXw","token_type":"DPoP","expires_in":299}

4. Fetch User Info using the access token

The app will utilize the access_token from the token endpoint to fetch user info from the authorization server.

########
Request
########

curl -k -H 'Authorization=Bearer fb49e7e7-2831-4dfd-8947-30ea2c6f305e' https://your.janssen.server/jans-auth/restv1/userinfo -d 'access_token=fb49e7e7-2831-4dfd-8947-30ea2c6f305e'

########
Response
########

{"sub":"LUKDGGD7it-oyjPQ7jEzE_tX_HeUoqTcyMpy3MgFHkA"}

Conclusion

In conclusion, this tutorial has outlined the steps to create a first-party native Android app with DPoP (Distributed Proof of Possession) token authentication and Attestation in Dynamic Client Registration, focusing on security features supported by the Janssen Server. DPoP enhances OAuth 2.0 security by ensuring the client’s possession of the private key corresponding to the public key in the DPoP token.

--

--