This article introduces a free, open-source sample application which demonstrates how an external FIDO2 relying party can consume IBM Cloud Identity APIs as-a-service. The application has been written in Node.js and leverages a range of API calls from IBM Cloud Identity (CI) including:
- User Management
- FIDO2 APIs
- OAuth and OpenID Connect Integration
The application has very similar capabilities to one I wrote for IBM Security Access Manager (ISAM) and introduced in the article ISAM FIDO2 – Using the FIDO2 server endpoints.
Unlike that article, very little setup needs to be done on a CI tenant (compared to ISAM configuration) to get this scenario up and running. This is, after all, the real appeal of a SaaS-based IDaaS solution.
The rest of this article is divided into the following activities:
- First, we’ll go through the steps of planning your application deployment. This is very important as decisions such as your application’s fully-qualified hostname also has an effect on the FIDO2 relying-party identifier that may be used.
- Next will be configuring your CI tenant for the FIDO2 scenario that has been planned. There are a few “pieces” of CI configuration needed for the different identity services that will be consumed.
- After that it will be time to get the Node.js application configured for your CI tenant and running. We will also test all the scenarios.
- Finally I’ll point out some of the differences between consuming FIDO2 APIs from CI versus ISAM.
Architecture and planning the deployment
As mentioned above, it’s very important to plan your FIDO2 deployment and in particular decide on the fully qualified hostname (FQHN) that your application will advertise. There is a requirement in WebAuthn that the relying party identifier (RPID) be either equal to or a sub-domain of the FQHN, and the browser will enforce this. The following diagram depicts the deployment architecture that is used for the demonstration application we will use in this article. Note that I have chosen to use the fictitious hostname www.cifido2rp.com, and the same value for RPID for the example deployment in this article. The application can certainly be modified to use any hostname (and conforming RPID) you wish – just be sure to update both the Node.js application configuration, and the configuration for the relying party and OpenID Connect application in Cloud Identity:
You may notice in the above architecture diagram that both browser-based and native mobile application clients are pictured. I do have an example Android application working against the cifido2rp demo application, and that will be the topic for a follow-on blog article. In this article we will focus on browser-based WebAuthn clients and the configuration required to support them.
Configuring your Cloud Identity Tenant
If you don’t already have a cloud identity tenant, a free trial can be obtained here.
At the time of writing, IBM Cloud Identity FIDO2 support is in beta and you will need to have your cloud identity tenant administratively enabled for FIDO2. The process for doing this can be found in this notice. Depending on when you are reading this article though, FIDO2 support may be generally available so please check to see if FIDO2 settings appear in your administration console before deciding if you need to have the beta enabled for your tenant.
Creating the FIDO2 relying party and configuring metadata
Starting from the administration UI, access the FIDO2 configuration panel from the Security hamburger menu, then add a new Relying Party configuration. Use the following information as guidance when configuring the relying party:
Display Name: cifido2rp
Relying party identifier: www.cifido2rp.com (Note: This must be equal to or a conforming sub-domain of the FQHN of your deployed relying-party application. For more information read about relying-party identifiers here).
Devices: leave as-is
Device criteria: leave everything unchecked for now
Allowed origins: https://www.cifido2rp.com:9443 (Note: This must be the protocol host and port that the browser will use to access your deployed relying-party application. My Node.js application run on my local machine listens on port 9443).
Save your relying party when done.
At this point you can also optionally add any FIDO2 metadata files you might have. In the screenshot below I’ve depicted adding the google-madeup-metadata.json file that I described in my earlier article ISAM FIDO2 – Metadata and registration policy enforcement.
Creating an API client
The external relying-party application requires an API client id and secret in order to obtain an access token to call CI APIs for FIDO2 and user management. To create the required API client, use the hamburger menu in the administration UI to navigate to Configuration, then API access. Create a new API client with the following Access permissions:
- Authenticate any user
- Manage second-factor authentication enrollment for all users
- Manage users and standard groups
Be sure to take note of the client id and secret, as these will be needed in later configuration of the application.
Optional: Configuring the application for OpenID Connect single sign-on
The external demonstration application has been written to either accept username/password login for a CI user, or allow the CI user to login via the OpenID Connect federated single sign-on protocol using the CI tenant as the OpenID Connect provider. If you wish to use OpenID Connect, configure a new application in CI, as shown:
After initially configuring your OIDC application, be sure to return to the Sign-on tab to retrieve the OpenID Connect client id/secret associated with the application. You’ll need these to configure the application later:
That completes all of the setup associated with the CI tenant. You may also create a new user to test with, however it’s perfectly ok to use your administrative user for this purpose. In the examples shown below, I have created a test user called emily, simply for consistency with the scenarios I wrote about in my earlier ISAM article.
Configuring and running the cifido2rp application
The github repository for this application is located at: https://github.com/sbweeden/cifido2rp
The application is written to work in the same manner to that which I wrote for ISAM in the article ISAM FIDO2 – Using the FIDO2 server endpoints
Clone the source of the cifido2rp application to your local machine.
Follow the instructions in the README.md of the github repository for how to get the application up and running. Be sure to setup the .env file with the configuration parameters related to your CI tenant setup that we prepared earlier in this article.
Your running application should start up like this:
$ npm run start_local
> email@example.com start_local /Users/sweeden/git/cifido2rp
> node -r dotenv/config server.js
Your SSL app is listening on port 9443
Testing the application
In this section we will walk through a simple registration and authentication use case. Access the application with a browser at https://www.cifido2rp.com:9443
From here, login using either OpenID Connect SSO from your Cloud Identity tenant, or simply with the username/password of a cloud directory user. Watch the console output of the Node.js application as you go – you will see which Cloud Identity APIs are being called, and be able to troubleshoot any issues along the way.
After login, the initial demonstration page loads with an empty table of registrations. Register a new FIDO2 credential – in the example screenshots below I’ve registered a resident key credential (so that we can later do username-less login), using the platform authenticator (so that my Mac Touchbar is automatically selected). You can modify and experiment with the different registration options depending on the browser and authenticator you are using. For example if you are using a FIDO2 external authenticator via USB then don’t select “platform” for the Authenticator Attachment. If you are unsure about any of these settings, just use the defaults (however without Require Resident Key you will not be able to exercise the username-less login flow):
Upon successful registration of an authenticator, you should see an entry in the registrations table, and if you have metadata loaded, the Brand column should be populated as well:
Given that in my case I registered a resident key, I can now logout of the application, and perform username-less login using my FIDO2 authenticator:
There are other capabilities in the demonstration application, such as being able to test the authentication operation while already being logged in (this is essentially a second-factor authentication flow), and being able to show the details of a registration:
Again, if your goal is to understand how the CI FIDO2 and user APIs work, watch the Node.js application console output as you are using the demonstration application as a wealth of information on which APIs are being called is traced as the application executes.
That concludes the setup and runtime execution of the demonstration application. In the next section of this article I’ll compare the differences between the FIDO2 APIs from Cloud Identity and ISAM.
Comparing FIDO2 APIs from Cloud Identity with ISAM
It’s quite interesting to look at the example code from this article, and compare it with the example for ISAM that I documented in ISAM FIDO2 – Using the FIDO2 server endpoints. These two applications are really offering the same end-user experience, but with completely different backend implementations of the FIDO2 service.
Both the ISAM and CI FIDO2 APIs are based on the non-normative FIDO2 server specification published here:
ISAM’s FIDO2 server APIs are the closest direct match to this specification, with the only real extension in ISAM being that for username-less login flows (which the FIDO2 server specification makes absolutely no mention of), the username parameter may be passed in as the empty string in calls to the /assertion/options endpoint. Access to all of the FIDO2 server endpoints is also controlled by ISAM access control policy, which means that the endpoints can be either left wide open (not recommended), or locked down to authenticated callers depending on the administrators configuration.
Cloud Identity takes a slightly different approach. First, ALL FIDO2 APIs require authentication via an access token to access them. Second, you never pass a “username” parameter to any of the FIDO2 APIs. Instead you either explicitly pass a userId, or provide neither username or userId and let the access token implicitly identify the user. This difference in approach is primarily because of a referential design constraint in Cloud Identity that requires every user-related record to have an associated user entry in the user directory. Even for federated and social identity sources, a shadow user record is created in Cloud Identity and every user is assigned a unique id. Let’s take a look at a user record in Cloud Identity (retrieved using the /v2.0/Users API) to understand this more:
Notice that for the username emily, there is an immutable id associated with the record (600000GYW3 in my example). This will be true for every user in the CI tenant, regardless of which identity source they come from. So let’s see how this id is applied when calling FIDO2 endpoints.
As I mentioned earlier, in CI every FIDO2 endpoint needs to be called in an authenticated context via an access_token. In the demonstration application there are two different ways the application obtains an OAuth access token. The manner in which the access token is obtained and the privileges it contains can be quite different.
Administrative Access Tokens
If you logged in to the demonstration application using a cloud directory username and password, the demonstration application would first have used the administrative OAuth client id/secret to obtain an administration access token, and then used this access_token to both check your user’s password, and for all subsequent FIDO2 operations. This access token on its own has no relationship to an individual user – it was obtained using the OAuth client_credentials grant type flow with just the client id and secret. You will recall when we set up this client it required the following access permissions:
- Authenticate any user
- Manage second-factor authentication enrollment for all users
- Manage users and standard groups
The access token is used to resolve the username and password to a user record, and retrieve the user.id, which can then be used as the userId parameter in subsequent FIDO2 API calls. For example during a FIDO2 registration for emily, the demonstration application will actually call the FIDO2 /attestation/options server endpoint with the following payload (this can be see in the console trace of the Node.js app):
Notice how the userId field is set to the id of emily’s user record? This is required when using an administrative access token to make API calls relating to a specific user. The same approach is used for all FIDO2 server APIs that need to pass username information.
OpenID Connect Delegated User Access Tokens
An alternative way for the demonstration application to obtain an access token is via OpenID Connect single sign-on. During single sign-on an access token granted by emily is made available to the application. This is quite different from an administrative access token in that it cannot be used to lookup users or perform operations on behalf of other users.
It is possible to make calls to the FIDO2 server endpoints with a delegated user access token and leave the userId field empty, in which case the userId will be derived from the authenticated identity of the access token. In my demonstration application, I always know the user.id of the logged in user because it is provider as part of the result of the OpenID Connect login. Because of this, the demonstration application chooses to always provide the userId field in FIDO2 server API operations. Similarly the displayName field may be left blank, however I don’t recommend that because if not provided, the displayName will default to the CI user’s username, which in my case would be just “emily” instead of “Emily Scillion“. It is a recommended best practice to always provide the userId and displayName from your application client when calling the Cloud Identity FIDO2 server endpoints.
The only other noteworthy difference between the ISAM and Cloud Identity FIDO2 server endpoints is related to the /attestation/result endpoint (i.e. completing a registration flow). In order to remain consistent with other second-factor enrollments in Cloud Identity, each registration has an “enabled” attribute, which defaults to false. Therefore during the proxying of the /attestation/result request, the demonstration application adds the “enabled” attribute with a value of true.
You can see these transformations of requests to the FIDO2 server endpoints coded into ciservices.js, in particular the proxyFIDO2ServerRequest function.
If you’ve made it this far through the article, and got the demonstration application up and running – great. What you should have discovered is that its relatively easy to make use of IBM Cloud Identity APIs to implement user authentication functions, and in the case of this demonstration app, build a simple user self-care portal for registration management. There is one major thing lacking in the demonstration application – enforcing what I call the chicken-and-egg problem. The application should not just allow enrollment of FIDO2 credentials after performing only a username/password login if that user already has one or more other second-factor enrollments in place – otherwise you have created a weakest link security hole where an attacker knowing only the username and password can work around any other location where 2FA for this user is enforced by creating their own 2FA enrollment in your app.
Cloud Identity has an easy fix for this though – first drop support for direct username/password authentication in the application itself, and only adopt federated login via OpenID Connect. Then apply an access policy to the application’s OpenID Connect configuration requiring 2FA if the user has any available methods. This is a far easier approach than coding conditional 2FA into the application itself, and is a recommended adoption pattern for Cloud Identity SSO.
If you have any questions about Cloud Identity, FIDO2, or anything else you’ve seen in this article, please feel free to reach out and I’ll do my best to get you an answer.
Originally Published: 9/16/2019