Enable OIDC in your NodeJS application

This topic describes how to enable OpenID Connect (OIDC) in a NodeJS client application where CyberArk Identity is the Identity Provider (IdP).

NodeJS-based OIDC Client is a client web application that encapsulates the authentication flows adhering to the OpenID Connect (OIDC) spec while interacting with CyberArk Identity as the Identity Provider (IdP) .

The client demonstrates the authentication and authorization flows conforming to the 'OpenID based on OAuth 2.0' standards. The flows demonstrated by the client are Authorization (Auth) Flow with PKCE, Implicit Flow and hybrid flows. To know more about the differences in these flows while configuring SSO, visit How SSO with OpenID Connect works

The client application showcases two different authentication mechanisms:

  • CyberArk-hosted authentication
  • Embedded authentication

To know more information about the different authentication flows, visit Authentication Workflows in OIDC

The client application is designed to show the tokens, claims, and user information using the tokens after the authentication is successful.

The Recipe

Code base

Prerequisites

  • Download Node.js (>=12.x.x) and npm
  • Install Express.js:
  • npm install express --save
    
  • Create a role:'SampleRole' in the CyberArk Identity Admin Portal
  • Create a user:'SampleUser' and assign to the 'SampleRole'
  • Create a policy:'SamplePolicy' with an authentication profile and assign the policy to 'SampleRole'
  • Configure a custom OIDC Web Application in the CyberArk Identity Admin Portal
  • On the Permissions page of the Web App configuration, add 'SampleRole'. This deploys the web app to the users in 'SampleRole'.
  • On the Trust page of the OIDC application, configure 'Authorized Redirect URI' and 'Resource application URL' in the CyberArk Identity Admin Portal . These are the endpoints of the host url of the client application.

Quick start

Configure the application

In the folder that was checkout, search for the file .example.env. Rename it as .env and replace the placeholders as mentioned below:

  • Set the TENANT_FQDN (without https) that reflects the CyberArk Identity tenant that you are connecting to.
  • Set the APP_HOST_URL as the url and domain with which you want to host this client application. (By default, this can be localhost, but the 'hosts' file needs to be changed for any specific url (for example, http://example.host.com:3000). If changed, make sure to change the redirect and resource urls in the CyberArk Identity Admin Portal to reflect the new host url.
  • Set the configurations APP_ID, CLIENT_ID, CLIENT_SECRET to reflect the 'Application ID', 'OpenID Connect Client ID', 'OpenID Connect Client Secret' of the Web Application Configuration
  • Add the POST_AUTHORIZE_CALLBACK and RESOURCE_URL as the relative url paths of the full urls provided under the 'Authorized Redirect URI' and 'Resource application URL' fields of the web app in CyberArk Identity Admin Portal.
  • If using the Embedded-authentication flow, then add the APP_KEY to reflect the appkey provided in the web app in CyberArk Identity Admin Portal.

Startup commands

Navigate to the folder where you want to put the code.

git clone https://github.com/idaptive/nodejs-oidc-webapp
npm install
npm start

Steps to Code

Step 1: Discover Issuer Metadata

  • Obtain the issuerUrl. For example, 'OpenID Connect Issuer URL' from the metadata of the web app in the CyberArk Identity Admin portal.
  • Use the Issuer.discover(issuerUrl) to have the issuer discovered to be able to obtain the metadata like client in the callback that follows as shown in the code below:
Issuer.discover(issuerUrl) // => Promise
.then(function (cyberarkIssuer) {
  //code to use issuer, client etc.
}

Step 2: Initialize Client

Through the cyberarkIssuer.Client function, you can initialize the client as configured in the Admin Portal with the client-related parameters as client_id, client_secret, redirect_uris, response_types and post_logout_redirect_uris.
//Initialize Client with the clientid etc. to be able to use the further endpoints within the client
const client = new cyberarkIssuer.Client({
    client_id: client_id,
    client_secret: client_secret,
    redirect_uris: redirect_uri,
    response_types: response_types[authFlow],
    post_logout_redirect_uris: post_logout_redirectUrl
  }); // => Client

Step 3: Call authorization endpoint

Obtain the authorization endpoint using the client.authorizationUrl by sending the required parameters:
  • For Authorization code flow with PKCE: code_challenge, code_challenge_method
  • For implicit or hybrid flows: nonce, response_mode token_endpoint_auth_method, state for all flows

For CyberArk hosted login flow, during this step when a request goes authorisation endpoint, the user is redirected to authentication page.

However, in embedded login case, after the user authenticates successfully, a call navigating to authorizationUrl is done. This step redirecting the request to authorizationUrl can be done in the success handler or the resource url where the user lands after successful authentication.

const authUrl = client.authorizationUrl({
    scope: scope,
    redirect_uri: redirect_uri,
  ...(isAuthCodeFlow) && {code_challenge: code_challenge},
    ...(isAuthCodeFlow) && {code_challenge_method: code_challenge_method},
    ...(!isAuthCodeFlow) && {nonce: nonce},
    ...(!isAuthCodeFlow) && {response_mode: 'form_post'},
    token_endpoint_auth_method: 'none',
    state: currentState
});

Step 4: Callback from authorization endpoint to redirect_uri

Successful authorization results in receiving a callback to redirect_uri with the params containing the authorization code. Depending on the flow, response_mode is sent as form_post (for implicit & hybrid workflows) which implies getting a POST call to the redirect_uri with the token information. Otherwise, a GET call can be expected with the authorization code. Hence, the below snippet works for both API methods (GET and POST). Note that you can retrieve the access_token and id_token directly from the callback parameters directly in the implicit or hybrid flow. For Authorization Code flow, we can go ahead further to initiate a callback through client.callback.

Once received, a call to client.callback will initiate a call back to retrieve requested tokens by submitting required parameters like state, response_type, and code_verifier(if PKCE flow) or nonce (for other flows) along with the authorization code.

A callback function handles the callback response containing the token set containing the claims.

client.callback(redirect_uri, params, {
    ...(isAuthCodeFlow) && {code_verifier: code_verifier},
    ...(!isAuthCodeFlow) && {nonce: nonce},
    responseType: response_types[authFlow],
    state: currentState
    }) // => Promise
      .then(function (tokenSet) {
        req.session.sessionTokens = tokenSet;
        req.session.claims = tokenSet.claims();
      })

Step 5: Call User Info endpoint

Get user information by making a call (promise on previous) using the client.userinfo function by providing the access token received as part of the token set.
client.userinfo(req.session.sessionTokens.access_token)
            .then(function (userinfo) {
                req.session.user = userinfo;
                  }).then(function (userinfo) {
                  res.redirect('/');
             });

Step 6: Logout

To invoke the logout procedure, call a function (client.endSessionUrl) that represents the API to end the sessio. The parameters supplied include the token_type, token, and post_logout_redirectUri. The token presented can be the access_token. After successful redirect, the post_logout_redirectUri any logics like session cleanup can be done.
const logoutUrl = client.endSessionUrl({
            post_logout_redirectUri: post_logout_redirectUrl,
            token: req.session.sessionTokens.access_token,
            token_type_hint: 'access_token'
        });
        res.redirect(logoutUrl);

Step 7: Refresh Tokens

If the refresh token is available in token set, then submit the tokens at.
client.refresh(refresh_token) // => Promise
  .then(function (tokenSet) {
//logic to refresh the the token and retrieve tokenSet.claims()
  });

UI Workflow & Components

pug is the template engine used for demo purposes by configuring it in app.js. Other template engines like ejs, Haml etc. can also be used by setting using app.set.
app.set('view engine', 'pug');

The navbar2.pug hosts the tabbed views of Home, Redirect Login (CyberArk hosted), and Embedded Login. A 'content' object is sent during the rendering of the navbar2 page during the respective get request that is fired. The content object contains the required user info, session tokens, and user info for presenting on the ui.

app.get('/loginwidget', (req,res,next) => {
    res.render('navbar2', {"content":{"user":null,"loginStatus":false,"action": 'loginwidget'}});
});

Post authentication and authorization using any of the login flows and after authorization, the web application client displays the response in clear separation of the tokens retrieved, claims, and the user information obtained using the access_token and the userinfo end point.