IBM Verify

 View Only

Extending Keycloak's authentication capabilities by integrating with IBM Cloud Identity

By Austin Bruch posted Fri November 22, 2019 05:49 PM

  

Extending Keycloak's authentication capabilities by integrating with IBM Cloud Identity

RedHat SSO (which is based on the open source Keycloak platform) supports a handful of authentication (first and second factor) mechanisms today, including username/password, Kerberos, and TOTP/HOTP via Google Authenticator. While these may be sufficient for some use cases, it's not unforeseeable that administrators or developers maintaining a RedHat SSO or Keycloak deployment may want to supplement the built-in authentication mechanisms, especially with the popularity of technologies like FIDO2 growing in the industry today.

This article describes how one can accomplish such goals by integrating a Keycloak deployment with IBM Cloud Identity (CI) to leverage various authentication APIs provided by CI.

Note: moving forward, I will refer to Keycloak and RedHat SSO interchangeably.

This article will cover the following integration points:
  • One-time password (OTP) via email
  • One-time password (OTP) via SMS
  • QR Code first factor authentication via IBM Verify
  • FIDO2 first factor authentication
Throughout the article, I will repeatedly reference sample code. You can find that sample code on GitHub.

Integration Architecture

At a high level, there's a few pieces involved in this integration:
  1. A Keycloak deployment
  2. An IBM Cloud Identity tenant
  3. Custom Authenticator(s)
  4. The IBM Verify mobile application (available on iOS and Android)

Architecture

Keycloak

The existing Keycloak deployment represents the set of users, applications, and authentication flows that are to be enhanced by integrating with IBM Cloud Identity. Once the integration is complete, these users will be enabled to leverage additional authentication mechanisms when accessing their applications.

IBM Cloud Identity

The IBM Cloud Identity tenant is the instance of Cloud Identity that will be linked to the existing Keycloak deployment. The Cloud Identity tenant is what will perform the various authentication actions based on us driving various API calls. There is various configuration required on the Cloud Identity side when integrating with Keycloak, some of which depending on which authentication mechanisms you are looking to leverage. This will be covered in greater detail below.

Custom Authenticator(s)

In order to leverage authentication mechanisms provided by IBM Cloud Identity as part of an authentication flow in Keycloak, one or more custom authenticators will be implemented, following the Custom Authenticator SPI provided by Keycloak. This gives us a hook into the authentication flow that gets executed at run-time when an end user attempts to authenticate to an application protected by Keycloak.

IBM Verify mobile application

The IBM Verify mobile application is used by the end users of the Keycloak deployment when exercising certain authentication flows provided by IBM Cloud Identity, such as QR Code authentication. Note that it is also possible to leverage a custom mobile application that is implemented via the IBM Verify SDK, instead of directly using the IBM Verify app itself. That is up to the developers implementing the integration(s).

Cloud Identity Requirements

If you do not have administrator access to an IBM Cloud Identity tenant, go here to get a trial tenant.

Depending on which authentication mechanisms you would like to leverage, certain configuration is required. Each piece of configuration is outlined below.

Note: for the sake of consistency, I will use mytenant.ice.ibmcloud.com as the reference Cloud Identity tenant. You will need to substitute your tenant domain for all use cases.

API Client

No matter which authentication mechanism you want to leverage, you will need to create an API Client with the corresponding entitlements. An API Client provides OAuth-based Client ID and Secret information that you can exchange for a short-lived access token. This access token is used to authorize all API calls made into Cloud Identity.

  1. Navigate to https://mytenant.ice.ibmcloud.com/ui/admin/configuration.
  2. Switch to the API access tab, and the API clients sub-tab.
  3. Click on Add API Client to start creating an API client.

From here, you can name your API client, choose to enable/disable it, and select which APIs you would like the API client to be able to access. The API documentation linked on the same page provides such information (link provided here for reference).

Here is a shortlist of APIs to which you'll want to grant access for your API client, based on which integration(s) you are looking to leverage:

Integration Required API access(es)
SMS OTP
  • Generate OTP
  • Authenticate any user
Email OTP
  • Generate OTP
  • Authenticate any user
QR Authentication
  • Read authenticator configuration
  • Manage authenticator registrations for all users
  • Manage users and groups
FIDO Authentication
  • Manage second-factor authentication enrollment for all users
  • Authenticate any user
  • Perform read operations on MFA authentication method configuration
  • Manage users and groups

It's up to you if you'd like to create an API client for each integration, or one API client for all integrations. Either approach will work.

Verify Registration Profile

Note: Setting up a Verify Registration Profile is required if you want to leverage the QR Code Authentication integration.

A Verify Registration Profile is a resource that corresponds to the backing connection made between the IBM Verify mobile application (or an application built using the IBM Verify SDK) and Cloud Identity. When a user registers IBM Verify on their mobile device with your IBM Cloud Identity tenant, the information configured in the chosen Verify Registration Profile is relayed to the user's IBM Verify mobile app.

  1. Navigate to https://mytenant.ice.ibmcloud.com/ui/admin/security.
  2. Switch to the Registration profiles tab.
  3. Click on Add Registration Profile to start creating a Verify Registration Profile.

At the very least a Name and Service Name are required. Service Name is what will show up in your end user's IBM Verify app, so name it appropriately.

FIDO Relying Party


Note: Setting up a new (or modifying the existing) FIDO Relying Party is required if you want to leverage the FIDO Authentication integration.

A FIDO Relying Party (FIDO RP) is a resource that corresponds to the relying party in the FIDO authentication flow. It is "where" your end users will be authenticating to when they use a FIDO device to authenticate.

There is an out-of-the-box Relying Party configured, but it's likely that it's configuration will not suite your needs, as part of the FIDO RP configuration determines the hostname(s) at which the FIDO authentication flow is valid for a given RP. Since we are describing an API-based integration, the domain at which your end users will leverage FIDO authentication is likely not going to match the domain of your Cloud Identity tenant.

  1. Navigate to https://mytenant.ice.ibmcloud.com/ui/admin/security.
  2. Switch to the FIDO2 tab, and the Relying Parties context.
  3. Click on the Relying Parties + button to start creating a new FIDO2 Relying Party.

Configure a display name for the FIDO RP, as well as a Relying party identifier (RP ID) and Allowed origins. The RP ID is where you will need to configure a valid domain value that lines up with the domain at which your authentication flow will be running (likely the runtime domain of your Keycloak deployment). Similarly, Allowed origins allows you to specify URNs that correspond to descendants of the RP ID from which a FIDO authentication flow should be considered valid.

You can read more about Relying parties in the WebAuthn specification.

Email OTP Integration


This section will describe the steps required to complete an Email-based OTP integration. More specifically, the steps will outline the Cloud Identity APIs required to complete the flow, as well as sample steps for integrating with the Keycloak SPI. The sample code provided exercises the same Cloud Identity APIs and Keycloak SPIs described in this flow, so only a high level view of the code will be outlined here.

A custom authenticator can be created that solely performs OTP via email. It is up to you, the Keycloak developer, to determine where in your authentication flow to leverage the custom authenticator. This includes whether to always require the OTP, or to conditionally require it based on some run-time considerations.

We start the process in the authenticator's authenticate method.

First we access the authenticated user's email using
AuthenticationFlowContext#getUser().getEmail();
where AuthenticationFlowContext represents the instance of the same class, supplied as an argument to Authenticator#authenticate.

Ensuring this value exists, we can continue on with the OTP flow.

In order to initiate the OTP flow, we first need to obtain an OAuth access token, using our API client's Client ID and Secret pair.

To do that, we make the following HTTP request (as documented here):
POST /v1.0/endpoint/default/token HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length: 123

client_id=<client_id>&client_secret=<client_secret>&grant_type=client_credentials

If successful (HTTP status code 200), the JSON response payload includes our access token. Save that in memory for later use.

Next, we will generate the OTP and send it to the end user's email address. We do so by making this HTTP request (as documented here).
POST /v1.0/authnmethods/emailotp/transient/verification HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>
Content-Type: application/json
Content-Length: 123

{
"otpDeliveryEmailAddress": "<email>"
}

Note the use of our access token retrieved earlier in the Authorization request header.

If successful (HTTP status code 202), the JSON response payload includes, among other things, 2 particular fields of note: id, and correlation. The id is what we'll use to submit verification requests when the user submits the OTP they received via email. The correlation is a sort of OTP Prefix that we'll display to the user so they can confirm they are submitting the correct OTP, in case they've received multiple OTPs recently. Even if they've only received one, matching the correlation gives the user confidence they're on the right track. We store these two pieces of information in the authentication session context for later retrieval:
AuthenticationFlowContext#getAuthenticationSession().setUserSessionNote("otp-correlation", "<correlation>");
AuthenticationFlowContext#getAuthenticationSession().setUserSessionNote("otp-trxn-id", "<id>");

As suggested above, at this point in the flow, we need to render a form to the user that has the following:

  • some instructive text telling the user that an OTP has been sent to their email address and that they should submit that OTP here
Note: it is common practice to list the email address in a partially obfuscated format e.g. ****ca@yourdomain.com. This protects the identity of the user, but also gives them context as to which email address was used for the OTP flow.
  • the OTP correlation described above
  • an input field for the user to submit their OTP

When submitted, the form should send the relevant data back to our custom authenticator, where we will take control next via the Authenticator#action method.

We access our correlation and transaction id from authentication session user notes:
AuthenticationFlowContext#getAuthenticationSession().getUserSessionNotes().get("otp-correlation");
AuthenticationFlowContext#getAuthenticationSession().getUserSessionNotes().get("otp-trxn-id");

Additionally, we also need to extract the submitted OTP from the form POST. One way to do that is as follows:
MultivaluedMap<String, String> formParams = context.getHttpRequest().getDecodedFormParameters();
String otpValue = formParams.getFirst("<your_form_field_name_here>");

Now, we can validate the OTP submitted by the user (API reference):

POST /v1.0/authnmethods/emailotp/transient/verification/<transactionId> HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>
Content-Type: application/json
Content-Length: 123

{
"otp": "<otp_submitted_by_user>"
}

A HTTP Status code of 200 in the response means the OTP was successfully verified. The API documentation covers error scenarios, including invalid OTP, maximum attempts reached, etc.

If successful, we can mark the context as success to move on with the rest of the authentication flow:
context.success();

If unsuccessful, we can display an appropriate page template to the user, giving them the chance to resubmit, if appropriate.

At this point, the second factor of authentication has successfully been completed via Email OTP leveraging Cloud Identity APIs. There's no need to get setup with an email delivery service, implement a OTP generation algorithm, track OTP submission attempts, etc. All of that is handled by the Cloud Identity APIs.

SMS OTP Integration

The SMS OTP integration follows a flow nearly identical to that of the Email OTP integration. The deviations are as follows:

Sourcing a mobile number

In the Email OTP flow, we were able to access the email address of a given user by making the following method call in our custom authenticator:
AuthenticationFlowContext#getUser().getEmail();

This is available to us because email is a built-in required attribute of Keycloak's user schema. However, phone number, specifically mobile phone number (that can receive SMS's) is not part of the standard user attribute schema supported by Keycloak.

Therefore, you will need to figure out how you plan to source phone numbers of users in your Keycloak deployment. Making that determination is outside the scope of this document, as there are many ways to accomplish this objective, depending on the information available to you and the desired experience for your consumers. The sample code that demonstrates the SMS OTP integration operates on the assumption that a user's phone number is stored as a custom attribute called phoneNumber. Depending on your implementation strategy, this may need modification.

Cloud Identity SMS OTP APIs

For every API request made in the Email OTP flow, there's an equivalent for the SMS OTP flow. They are as follows:

Obtain an access token


This request is exactly the same as it was in the Email OTP flow. Ensure your API Client is properly entitled to avoid unexpected 403 Forbidden responses.

Request an OTP be sent over SMS


Similar to how we generated an OTP and had it sent via email, we'll request for an OTP to be generated and sent via SMS to our user's phone using this API:
POST /v1.0/authnmethods/smsotp/transient/verification HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>
Content-Type: application/json
Content-Length: 123

{
"otpDeliveryMobileNumber": "<mobileNumber>"
}

The response structure is similar to the email response structure, where we will grab the id and correlation of this SMS OTP transaction for later use.

Validate the submitted OTP


We validate the submitted OTP using this API, like this:
POST /v1.0/authnmethods/smsotp/transient/verification/<transactionId> HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>
Content-Type: application/json
Content-Length: 123

{
"otp": "<otp_submitted_by_user>"
}

QR Code first factor authentication via IBM Verify


The QR Code login integration allows end users to perform passwordless authentication by scanning a QR code with their phone instead of having to enter a username and password to identify themselves.

In order for an end user to leverage the QR Code login capability, they first need to install the IBM Verify mobile app (or an app built using the IBM Verify SDK) and register their app installation with their user account. Once registered, users can leverage capabilities provided by IBM Verify. These two flows will be covered next:

IBM Verify User Registration

The high level steps for end user IBM Verify registration are as follows:

  1. Initiate IBM Verify registration flow with Cloud Identity for a specific user
  2. Display registration QR code for user to scan with IBM Verify app (Verify app will call Cloud Identity API to complete registration)
  3. Poll for completion of Verify registration

The sample code provided implements the registration flow as an authenticator so that it can be presented to the user post-login during an authentication flow. The user can choose to opt-in to setup IBM Verify to enable alternative login mechanisms for subsequent login flows. This custom authenticator should be configured at the end of an authentication flow, since it requires that an authenticated user be available in the session context. The steps of this registration implementation will be outlined next.

First, the authenticator receives control with an authenticated user available in the context. If the user chooses to register IBM Verify, the registration record will be associated with the authenticated user.

As soon as this authenticator receives control, it checks to see if this user has already registered an IBM Verify app installation with their account.

Cloud Identity stores IBM Verify registrations associated with user records in their user registry. That means for any Keycloak user to leverage Cloud Identity's authentication functions that depend on IBM Verify, a user record needs to exist within Cloud Identity that corresponds to a user record in Keycloak. These user records stored in Cloud Identity will be referred to as "shadow records".

In order to check if the current user has registered an IBM Verify instance with Cloud Identity, we need to lookup registrations based on the user's Cloud Identity ID. For a user that has never performed any such registrations, they will not yet have a shadow record associated with their account. Therefore, part of this authenticator's duties are to create the shadow record within Cloud Identity, if needed. Once created, the user record in Keycloak will be updated to track the shadow records's unique identifier, for future lookups.

Checking for and creating the shadow record


In our sample implementation, we have chosen to add a custom attribute to Keycloak users to track their Cloud Identity shadow record unique identifier. This attribute is named cloudIdentity.userId. Checking for it's existence is as simple as:
UserModel#getAttribute("cloudIdentity.userId");

If it doesn't exist, we treat that as a signal that we need to go and create our shadow record. We do so using this User management API, forming a request like this one:

Note that we're skipping over the access token generation step for brevity.
POST /v2.0/Users HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/scim+json
Authorization: Bearer <access_token>
Content-Type: application/scim+json
Content-Length: 123

{
"userName": "f4cb772f-a46a-4de6-b8ad-22fb8e49edbb",
"externalId": "f4cb772f-a46a-4de6-b8ad-22fb8e49edbb",
"emails": [
{
"type": "work",
"value": "<emailAddress>"
}
],
"urn:ietf:params:scim:schemas:extension:ibm:2.0:Notification": {
"notifyType": "NONE"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:ibm:2.0:Notification"
]
}

Here we have chosen to use the Keycloak user record's ID for both userName and externalId so we can avoid having to propagate any username updates from Keycloak to Cloud Identity. After all, the Cloud Identity record is simply used as a reference for tracking registration information within Cloud Identity.

A successful response is indicated by a 201 Created status, and the response payload includes the current representation of this new user record. In that representation is an id, which is a string that uniquely identifies the Cloud Identity user record. We will take that string and store it on our Keycloak user record, using the cloudIdentity.userId key mentioned above:
UserModel#setSingleAttribute("cloudIdentity.userId", ciUserId);

Checking for existing Verify registrations


Now that we have the Cloud Identity user ID, we can check to see if our user has any pre-existing Verify registrations. This HTTP GET API is how we can do that, making a request like this one:
GET /v1.0/authenticators?search=owner%3D%22<userId>%22 HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>

A successful response is indicated by a 200 OK status, but you need to interrogate the response payload to see if the results are empty, or if there's any authenticator resources returned. For a user without any previously registered authenticators, the response will be an empty array.

Initiating a new Verify registration


If we've determined that the user does not yet have a Verify registration but they want one, then we will begin the registration process by invoking this API like this:
POST /v1.0/authenticators/initiation?qrcodeInResponse=true HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>
Content-Type: application/json

{
"owner": "<userId>",
"clientId": "<verifyRegistrationProfileId>",
"accountName": "<friendly-name>"
}

A few things to note here:

  • Pay special attention to the qrcodeInResponse query parameter. This is critical to implementing this flow
  • owner: this is the Cloud Identity User ID for our authenticated user
  • clientId: this is the unique ID of the Verify Registration Profile previously configured, as described here. You can choose to configure the ID of this Registration Profile as part of your authenticator static config, or you can dynamically look it up using this API. Consuming that API is left as an exercise to the reader (there's an example in the sample code).
  • accountName: this is a friendly name that is displayed on the user's IBM Verify mobile app, identifying this particular registration instance. It may be valuable to allow the user to control what goes into this field via a text input in the browser.

A 200 OK response status indicates the registration has been properly initiated. In the response payload is a property called qrcode, whose value is a base64-encoded image. That image is our QR code we need to display to the user. Save that string in memory and send it to the browser during the next state transition to display for the user to see and scan.

Polling for completion


Since the completion of the registration is done asynchronously (the IBM Verify app makes the necessary API calls to complete the process), we need to poll on some interval to check on the status of the registration. To poll for completion, we'll invoke the same API we invoked a few steps ago, where we checked to see if any authenticator registrations existed for the current user. We can check for a change (transition from 0 to 1 instances) from the previous result to see when this registration has completed. If you're interested in allowing multiple registrations per user, you will want to keep track of which instances existed before this registration process began, so you know when a new registration shows up.

Once we detect the registration is complete, we can mark this authentication as a success and move on!

Authentication via IBM Verify


Performing a first factor QR Code authentication via IBM Verify involves these high level steps:

  1. Initiate a QR authentication flow
  2. Display the QR Code for the end user to scan with their IBM Verify mobile app
  3. Poll for completion of the QR authentication flow; once complete, associate the login session with the identified user

Initiating a QR authentication flow


In this first factor authentication flow, we don't yet know who is authenticating. So when initiating the QR authentication request, we aren't able to specify anything about a current user. This differs from the flows previously described, as those were all executed knowing who was performing the action. We initiate the QR Login flow by using this API:
GET /v2.0/factors/qr/authenticate?profileId=<verifyRegistrationProfileId> HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>

Again, note that we're supplying our Verify Registration Profile ID as the profileId query parameter, and of course our access token in the Authorization request header.

200 OK indicates a successful response. In the response payload, we need to extract a few pieces of information:

  • id: the unique ID for this authentication request
  • qrCode: the base64-encoded image we will display to the user to actually perform the authentication
  • dsi: the device session index, used when polling for authentication request state updates

Store these pieces of information in memory for later consumption, and display the qrCode as an image on the login page for the user to scan with their mobile application.

Polling for QR authentication completion


You'll want to poll for status updates pretty frequently, to provide a seamless experience for the user i.e. the user should be "logged in" and proceed with their login flow pretty quickly after the QR code scan completes on their mobile device. The sample code polls every 5 seconds; feel free to tune this appropriately.

We'll use our id and dsi from the previous step when polling for status updates on this API:
GET /v2.0/factors/qr/authenticate/<id>?dsi=<dsi> HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>

As usual, 200 OK indicates a successful response. Within the response payload, we're looking for the state property to transition to a value of SUCCESS. Once that happens, we know the authentication has completed successfully. In that case, we also check out the userId property, which will contain the Cloud Identity user ID associated with the user who performed the authentication.

We take that userId and use it to find which Keycloak user signed in by identifying the Keycloak user record who's cloudIdentity.userId custom attribute matches the userId we received from the QR login response.

After we find that user within the Keycloak registry, we associate them with the current authentication session and mark the authentication as a success like this:
context.setUser(user);
context.success();

Of course, if anything went wrong during this process, you'd need to handle those cases gracefully. Potential issues could range from the QR authentication request timing out, the resulting user from the QR authentication request not mapping to a Keycloak user, or many other issues. We're simply covering the happy path to demonstrate how this integration can be pieced together.

At this point, if the happy path was taken, then your user has just completed first factor passwordless authentication by leveraging the IBM Verify mobile app and IBM Cloud Identity, with a pretty minimal amount of work on your end!

First factor authentication using FIDO2

The FIDO2 login integrations allows end users to perform passwordless authentication by leveraging a hardware device (external, or built in to their computer) instead of having to enter a username and password to identify themselves.

In order for an end user to leverage the FIDO2 login capability, they first need to register their hardware device with their user account. Both the registration and the run-time authentication flows will be covered in the upcoming sections:

FIDO2 device registration


The high level steps for end user FIDO2 device registration are as follows:

  1. Initiate a FIDO registration device flow with Cloud Identity for a specific user.
  2. Guide the user through the registration in their web browser.
  3. Forward along registration response from FIDO2 device to Cloud Identity to complete the registration.

Similar to the QR code registration flow outlined above, the provided sample code implements the FIDO2 device registration flow as an authenticator so that it can be presented to the user post-login during an authentication flow. The user can choose to opt-in to set a FIDO2 device to use in subsequent authentication flows. The steps of this registration are outlined next.

First, the authenticator receives control with an authenticated user available in the context. If the user chooses to register a compatible FIDO2 device, the registration record will be associated with the authenticated user.

As soon as the authenticator receives control, it checks to see if this user has already registered a FIDO2 device. For the sake of the demonstration, we are only looking to support 1 FIDO2 device per user. This can easily be expanded upon to support multiple devices per user, if desired.

Similar to IBM Verify registrations, FIDO2 device registrations are stored within Cloud Identity, indexed by the associated user's Cloud Identity unique ID. That means for any Keycloak user to leverage Cloud Identity's FIDO2 authentication functions, a user record needs to exist within Cloud Identity that corresponds to a user record in Keycloak. These user records stored in Cloud Identity will be referred to as "shadow records".

In order to check if the current user has registered a FIDO2 device with Cloud Identity, we need to lookup registrations based on the user's Cloud Identity ID. For a user that has never performed any such registrations, they will not yet have a shadow record associated with their account. Therefore, part of this authenticator's duties are to create the shadow record within Cloud Identity, if needed. Once created, the user record in Keycloak will be updated to track the shadow records's unique identifier, for future lookups.

See the above section on how we implement creation and referencing of Cloud Identity shadow records. The process is exactly the same for both the IBM Verify and the FIDO2 flows.

Checking for existing FIDO2 registrations


At this point in our registration flow, we have the Cloud Identity user ID, so we can now check for any pre-existing FIDO2 registrations. As mentioned above, this is mostly because we're only targeting to support 1 device per user in this demonstration. The FIDO2 devices management API is used to retrieve that list, like so:
GET /v2.0/factors/fido2/registrations?search=userId%3D%22<userId>%22 HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>

A successful response is indicated by a 200 OK status, but you need to parse the response payload to determine how many registration results were found. For a user without any prior FIDO2 device registrations, the response will be an empty array.

Initiating a new FIDO2 device registration


We can begin a new FIDO2 device registration by invoking the FIDO2 attestation API like this:
POST /v2.0/factors/fido2/relyingparties/<rpId>/attestation/options HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>
Content-Type: application/json

{

"attestation": "none",
"userId": "<cloudIdentityUserId>",
"authenticatorSelection": {
"requireResidentKey": true,
"authenticatorAttachment": "cross-platform",
"userVerification": "preferred"
}
}

The API documentation provides additional details about the various options supported, but the sample above closely mirrors what's used in our provided sample code.

Note: `authenticatorSelection.requireResidentKey` set as true is a requirement for this FIDO2 device to perform first factor authentication. Without it, it would only be able to work as second-factor, when we already have authenticated the user in the given authentication context flow.

Note: The `rpId` in the URL path is the unique identifier of the FIDO2 Relying Party configured earlier, as described above. Similar to the Verify Registration Profile used during the QR Code registration flow, you can choose to configure the unique ID of this FIDO2 Relying Party as part of your authenticator's static configuration, or you can dynamically look it up using the FIDO2 relying party management API. See the sample code for an example of how to consume this API.

200 OK indicates the registration has begun successfully, and the response payload itself is very important to hang on to, as that information will be sent back down to the browser to instrument the registration flow on the end user's side of things.

At this point, you need to render the HTML page where the user can choose to register their FIDO device. The response from above needs to be included in the web page, so that you can invoke the following JavaScript code:
navigator.credentials.create({
publicKey: response
}).then(...)

where response is the JSON object returned from the API, after some massaging (see sample code for details).

This is what kicks off the browser taking control and initiating the FIDO device registration on the client's side of things. The browser will guide the user through registering their device, and ultimately will produce a credential object that your JavaScript will have access to. That credential needs to be submitted back to the server, and our sample code chooses to do that using an HTML form.

Completing the FIDO2 registration


Once the custom authenticator has received the credential information generated by the FIDO2 device through the browser, we can now submit that back to Cloud Identity to complete the registration flow. This is accomplished via the FIDO2 attestation result API, with a request like this:
POST /v2.0/factors/fido2/relyingparties/<rpId>/attestation/result HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>
Content-Type: application/json

{
"type": "<type>",
"enabled": true,
"id": "<id>",
"nickname": "<nickname>",
"rawId": "<rawId>",
"response": {
"clientDataJSON": "<clientDataJSON>",
"attestationObject": "<attestationObject>"
}
}

Note that type, id, rawId, response.clientDataJSON, and response.attestationObject all come from the browser's response to navigator.credentials.create. nickname should also come from the user, but that can be obtained via a regular HTML text input. enabled can also be controlled by the user, if you choose to do so.

If the response returns with a 200 OK status, then the FIDO2 registration is complete! This device can now be used for first factor authentication for this user.

FIDO2 authentication


Performing a first factor FIDO2 authentication involves these high level steps:

  1. Initiate a FIDO2 authentication flow from Cloud Identity.
  2. Route the FIDO2 assertion data to the browser to guide the user through handling the authentication challenge.
  3. Take the response back from the browser and validate it with Cloud Identity.

Initiating a FIDO2 authentication flow


Like the QR authentication flow, we don't know anything about who is attempting to authenticate. So when we initiate the FIDO authentication request, we aren't able to specify any information about the current user. We'll see the difference in the request payload below, which is what separates the first and second factor authentication flows. We'll be using the FIDO assertion API:
POST /v2.0/factors/fido2/relyingparties/<rpId>/assertion/options HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>
Content-Type: application/json

{
"userVerification": "preferred"
}

Had we been performing a second factor auth, we would have also included userId in the JSON payload.

Once again, note the rpId in the URL path, as well as the bearer token in the Authorization header.

We'll pass the entire JSON response down to the browser in the rendered HTML page for the user to perform the login (just like we did in the registration flow).

Once the page loads and the user activates the FIDO login flow (via button click or however you choose), you can invoke this JavaScript code:
navigator.credentials.get({
publicKey: response
}).then(...)

where response is (a massaged version of) the JSON object returned from the API above.

Once this code executes, the browser takes over again, prompting the user to plug in and/or activate their FIDO2 device. Once the user finishes following the native browser prompts, the async navigator.credentials.get call returns an assertion object, which is asserting the user's identity, based on the encryption key used to sign the assertion, stored in the FIDO device. We then send that data back to the server via HTML form submission.

Completing the FIDO2 authentication


Once the assertion is received by the custom authenticator, we can submit that information back to Cloud Identity to validate the identity assertion. We accomplish that by invoking the FIDO assertion result API like so:
POST /v2.0/factors/fido2/relyingparties/<rpId>/assertion/result HTTP/1.1
Host: mytenant.ice.ibmcloud.com
Accept: application/json
Authorization: Bearer <access_token>
Content-Type: application/json

{

"type": "<type>",
"rawId": "<rawId>",
"response": {
"clientDataJSON": "<clientDataJSON>",
"authenticatorData": "<authenticatorData>",
"userHandle": "<userHandle>",
"signature": "<signature>"
},
"id": "<id>"
}

Just like the registration validation flow, all of this data comes from the assertion received from the browser.

A 200 OK indicates success, and a successful payload includes, among other things, a userId field which tells us which user has performed the authentication. Just like with the QR login flow, we can take this Cloud Identity user ID and map it to a user in our Keycloak user registry, associate that user with the authentication session, and we're in!

Keycloak Requirements


Once you've got a custom authenticator built (either your own, or one of the samples provided in the sample repository), there's a few things you need to do from the Keycloak side to consume the authenticator.

Make Keycloak aware of the new authenticator


In order to leverage your new custom authenticator, you need to "expose it" to Keycloak. Depending on your Keycloak deployment architecture, these steps may vary. They are documented here. In essence, there's a specific way you need to bundle up your Java Class files along with some related metadata files, and there's a specific location where you need to drop in your JAR file that contains the various classes that collectively implement your custom authenticator.

Modify your authentication flow to consume the new authenticator


Once your new authenticator is "added" to Keycloak, you can now include it in an authentication flow.

Which authentication flow you modify will depend on where/how you want to incorporate the custom authenticator. Keycloak provides some basic guidance here. Essentially, the steps are as follows:

  1. Within the Keycloak admin UI, select the Realm for which you want to use the custom authenticator.
  2. Navigate to the Authentication menu item
  3. Within authentications, go to the Flows tab
  4. Choose which Authentication Flow you want to modify. This will depend on which flow is actively in use for the applications/endpoints for which you want to leverage the new authenticator.

    Note: If the flow you've chosen is an out-of-the-box flow, you'll need to copy it to make any changes. If it's a custom flow you've created, you can make changes directly.

  5. Click on Add execution, then choose the new authenticator from the dropdown list. The name here corresponds to the name provided in your authenticator implementation.
  6. Once the authenticator execution is added to your flow, you can move it around and manage it's configuration. In the sample authenticators provided, there's a few pieces of common configuration required:
    1. Cloud Identity Tenant FQDN - This is required to make API calls to Cloud Identity
    2. API Client ID - The Client ID associated with the API Client you created. This API Client performs all the various API calls required for your authentication flow
    3. API Client Secret - The Client Secret associated with the API Client you created. The Client ID and Secret are exchanged for a short-lived OAuth access token.
  7. Once all of this configuration is completed, you'll want to bind your modified authentication flow to a "Runtime flow", which can be done on the "Bindings" tab of the "Authentication" page.

At this point, your new authentication flow is live for any application or endpoints that is protected by the given runtime flow to which you bound your authentication flow.

Wrap-up


We've demonstrated how easy it is to leverage Cloud Identity APIs to enhance your authentication experience within Keycloak. If you're already familiar with authoring custom authenticators, the only new piece for you is figuring out which Cloud Identity APIs to invoke to accomplish your specific authentication goal. Even if you're not, you are now empowered to start building custom authentication modules to solve whatever business challenge you're facing.

If you have any questions, please feel free to reach out and I will do my best to get you an answer.
0 comments
44 views

Permalink