Deep Dive into Enhancing User Experience with Native Authentication (OAuth 2.0 for First-Party Apps) and Passkeys in Keycloak
Introduction
As an IAM Architect, one common question I encounter from many customers when discussing about authentication experience is:
I don’t want to open a browser when using a mobile app because it kills the user experience.
or
I want to implement the login process within the app, as I don’t want to redirect to the Identity Provider because it increases friction and potential drop-off.
I won’t delve into the reasons why, in some scenarios, you must delegate the authentication process to the Identity Provider (IdP) using OIDC with the traditional redirection method. Instead, I will describe how to implement native authentication in Keycloak to eliminate the need for redirecting or launching a browser to handle the authentication process.
Nowadays, we have a proposed standard called OAuth 2.0 for First-Party Applications, which offers an API-based authentication when the application controls the login experience based on the information returned by the IdP. As usual, I always advocate following identity standards (or proposed standards) to avoid reinventing the wheel and to ensure interoperability.
In this article, I will describe the implementation of this standard in Keycloak to demonstrate the benefits of this approach. Currently, this standard is not supported, so we developed a series of custom extensions (SPIs) we called Keycloak Advanced Authentication in the context of TwoGenIdentity. These include the support OAuth 2.0 for First-Party Applications and also a custom Native passkey authenticator. However, our intention is to add other passwordless authentication mechanisms as well.
I decided that the first authenticator that support this standard will be passkeys, to offer a secure and enhanced experience. Passkeys based on FIDO Alliance and W3C standards. Passkeys replace passwords with cryptographic key pairs, offering strong credentials, protection from server leaks, and defense against phishing.
In the native login experience with passkeys, you can implement something like this:
Lastly, as usual, my articles will show the theory behind the scenes and a real implementation to glue all the pieces together.
Overview
OAuth 2.0 for First-Party Applications is an extension of the OAuth 2.0 standard, so you will notice many common parameters and formats in the authorization request. To explain the details, I’ll walk you through the developed implementation in Keycloak using using a real-world example. For a review of the generic specifications, you can refer to the proposed RFC site.
You are a First-Party Application, right?
First-party application means that the application and the identity server are controlled by the same entity, therefore you have control and trust over them. This is the basic definition, but there are other concepts at play. The standard mentions that the Identity Provider must ensure the application is a first-party client to prevent client impersonation. This is especially critical for public clients, which cannot securely store secrets. I would say upfront that this is not an easy topic, so I will provide some recommendations to address this in the context of a native application.
First of all, you have the possibility to implement application attestation using Apple’s and Google’s app attestation APIs. Google offers this service on Android through the Play Integrity API, which checks that user actions and requests are coming from your unmodified app binary, installed via Google Play, and running on a genuine Android device. Apple provides a similar service on iOS with App Attest and DeviceCheck, which serve as anti-fraud tools to safeguard the application.
Now, you might be wondering how to use this feature in the context of OAuth. Well, there is a proposed standard called OAuth 2.0 Attestation-Based Client Authentication. It introduces key-bound attestation that can be used when interacting with the Authorization Server to prove authenticity. Even after reviewing the standard, you still need to perform app attestation, which means you might still rely on Apple’s and Google’s attestation APIs.
I won’t go into further detail here for now. To simplify this version of the article, I will not include the app attestation or how to generate the Client Attestation JWT and Client Attestation Proof of Possession (PoP) JWT for use when calling the Token endpoint. However, I have provided some general points to give a high-level idea.
Deep Dive into Native Authentication Sequence Diagram
The standard added a new endpoint called the authorization challenge endpoint (/authorization-challenge
) which will receive the HTTP POST authorization request from the first-party client for navigating the defined authentication flow in the Identity Provider.
In this example, the Digital Bank portal wants to offer passkey login while maintaining control over the UX, knowing that passwordless authentication improves security for their customers. On the other hand when managing their account it will require step-authentication.
Now we are going to review the steps involved in the native authentication illustrated in the following diagram in this case.
Based on this, Keycloak is configured with the custom extension and a new flow called “first party login” with the new passkey authenticator in the bank
realm.
The “first-party login” flow includes the new passkeys authenticator, as shown below.
This new authenticator supports sending the Authorization Challenge Response, which indicates to the application the details of the authentication step needed to continue the login process based the information from the IdP. This is how API-based authentication works.
In relation to the Bank Portal, it uses a customization of the keycloak-js
that we developed, which supports communication with the authorization endpoint and handles the authorization challenge response.
Let’s look into each step of the sequence diagram to understand how the login process is done.
Step 1) The user accesses the bank portal to manage their accounts.
Step 2) The bank portal initializes the SDK and sends the authorization request. In this first step, the app is expected to receive an authorization challenge request error because it needs the metadata to trigger the passkeys selection. Here is the request in cURL format for simplicity.
curl --location 'https://labs.identityaccessplus.com/realms/bank/authorization-challenge'
--header 'Accept: application/json'
--header 'Content-Type: application/x-www-form-urlencoded'
--data-urlencode 'client_id: {client-id}'
--data-urlencode 'scope=openid profile'
Step 3) The IdP challenge response endpoint receives the request and generates the authorization challenge error response (HTTP 400), indicating with the error insufficient_authorization
that the authorization server is requesting the client to take additional steps to complete the login process. Here it is in JSON format, as shown below
{
"error": "insufficient_authorization",
"error_description": "Missing passkeys parameters",
"auth_session": "0b574dc89d3982120237HjLOEScM0qc",
"metadata": [
{
"type": "passkeys",
"config": {
"createTimeout": 0,
"userVerification": "not specified",
"shouldDisplayAuthenticators": false,
"challenge": "7uIxhFHZQS2KnKbe99j0FA",
"rpId": "labs.identityaccessplus.com",
"isUserIdentified": "false"
}
}
]
}
To clarify a few things, the standard specifies the format of the error with parameters such as error
, error_description
, and auth_session
. However, there are no specifications for how the IdP returns the metadata needed to define the next step of the authentication. Therefore, I have designed a simple method for specifying the metadata with the required configuration for the authenticator type passkeys
. In the config
parameter, the application will find all the required configuration to initialize el authentication mechanism.
Step 4) The application SDK identifies that the authenticator type is passkeys
and then it initializes navigator.credentials.get()
with the information provided in the metadata to initiate authentication with a passkey.
Step 5) The user selects the passkey and signs in with the fingerprint option. More technically speaking, after the user selects an account and consents using the device’s touch, the promise is resolved, returning a PublicKeyCredential
object under the application’s control.
Step 6) The application SDK completes all the required parameters for passkeys authentication and sends back the authorization challenge response. The payload of this request includes the SAME auth_session
value from the previous request.
curl - location 'https://labs.identityaccessplus.com/realms/bank/authorization-challenge'
--header 'Accept: application/json'
--header 'Content-Type: application/x-www-form-urlencoded'
--data-urlencode 'client_id={client-id}'
--data-urlencode 'scope=openid profile'
--data-urlencode 'clientDataJSON={client-json-data}'
--data-urlencode 'signature={signature-data}'
--data-urlencode 'credentialId={credential-id}'
--data-urlencode 'auth_session=0b574dc89d3982120237HjLOEScM0qc'
Again, to enter a bit more technically, all the parameters are related to passkeys (public key credential object). For example, the signature
is the core of the credential and needs to be verified by the Identity Provider.
Step 7) The IdP challenge response endpoint receives the request and continues with the passkey authentication flow. It finalizes the authentication process because the presented information is valid meaning that credential’s signature is verified with the stored public key.
Step 8) The application receives an OAuth 2.0 authorization code, as specified by the OAuth 2.0 standard, in JSON format, as shown below.
{
"authorization_code": "{code-value}",
}
At this point, we are on common OAuth 2.0 standard ground, meaning the application negotiates the token and show the user information based on the identity tokens.
Another exciting scenario is step-up authentication. The user experience is enhanced because the user remains within the app, and no redirection to the Identity Provider is needed, thanks to the native authentication experience. It is triggered when user wants to access to managing the accounts.
In this case, the API behind the scenes is triggering the step-up because it follows OAuth 2.0 Step-up Authentication Challenge Protocol. I explained in other article, but it gives the possibility to the API to implement step-up authentication thanks to understand the required Authentication Context Level (acr
) and then, if the level is not enough, tells to the client that it needs to trigger step-up authentication ( RFC thanks to Vittorio Bertocci ❤).
If you want to compare the user experience in MFA and step-up with the traditional approach (which means redirection to the IdP), you can check out my previous article that talks about those topics.
Live Demo
In the first use case, I will walk through the login process on both a mobile device and a desktop to highlight the differences in how each device requests passkeys. You will see all the steps described earlier.
In both cases, the user has already registered the passkey in Keycloak. They will access the portal and attempt to manage their user accounts (this last step will require step-up authentication as it is considered a critical operation). For the sake of the demo, I used a web application to simplify the PoC, but in a real scenario, it can be a native app.
Bank Portal native login experience with passkeys and step-up in a Mobile scenario
Bank Portal native login experience with passkeys and step-up in a Browser scenario
Conclusions
Well, here we are. If you went through the articles and watched the videos, it is crystal clear all the benefits of following this approach. Having a standard for API-based authentication is great, as UX and security are enhanced when the application controls the login experience based on the IdP information.
Another thing to keep in mind is that, due to the deprecation of the Resource Owner Credentials Grant in OAuth 2.1, this will be an alternative to offer to users. However, it provides an enhanced experience because the standard supports smooth interaction with the IdP.
If you are interested in this approach, DM me for more information or a demo. In the context of TwoGenIdentity, we will continue evolving the extension for OAuth 2.0 for First-Party Applications and adding other authenticator to further enhance both enhancing the platform.
Lastly, here is the discussion I posted in the Keycloak community to gain traction there.