Dev tutorials

8 mins read

Configuring Node.js Applications to Authenticate Using OAuth mTLS and Certificate Bound Tokens

Learn how to configure a Node.js application to authenticate itself with Cloudentity using OAuth mTLS client authentication specification and get an access token that is certificate bound to ensure only the systems that have access to the certificate key pair can use the access token.

Overview

Cloudentity provides implementation of RFC8705 for OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens. This specification enables developers to integrate and transform existing application integration architectures into more secure access patterns.

mtls rfc support.

In this tutorial, we will create a Node.js application and fetch a regular access token as well as a certificate bound access token obtained using the mTLS OAuth token endpoint.

mTLS Certificate Bound Tokens

Let’s look at an overview of how mTLS client authentication and certificate bound access token helps to secure your application from rogue callers that have obtained an access token. Anyone in possession of the access token can access a protected resource using that access token unless the token is bound to a certificate of the actual client that requested the token.

mtls overview.

Mutual-TLS as described in RFC 8705 presents a solution to provide a proof of possession of tokens to prevent a rogue caller from using stolen access tokens. Even if a rogue caller obtains the access token issued to a client, it won’t be able to access the protected resources, since mTLS client authentication is used and the client certificate is bound to the access token. Since the rogue caller does not have access to the certificate, the rogue caller attempts to use the access token and the resource server can now deny access to the protected resource since the presented certificate does not match the certificate thumbprint bound to the access token.

Prerequisites

Reference Repository

Check out the below GitHub repo for a complete source code of the reference application in this tutorial

Node.js OAuth mTLS Client Authentication

Building Node.js Application

Import Required Packages

In the index.js file. import the required npm packages.

var axios = require('axios');
var qs = require('qs');
var express = require('express');
var app = express();
var jwt_decode = require('jwt-decode');
var fs = require('fs');
var https = require('https');

var mustacheExpress = require('mustache-express');
var bodyParser = require('body-parser');
var path = require('path')
require('dotenv').config();

Set Up Views and Pages

  1. Set up the express app to serve some views and html pages.

    app.set('views', `${__dirname}/views`);
    app.set('view engine', 'mustache');
    app.engine('mustache', mustacheExpress());
    app.use (bodyParser.urlencoded( {extended : true} ) );
    app.use(express.static(path.join(__dirname, "/public")));
    
  2. Set up the port and log the URL for the starting point of the application. Then a /health endpoint is set up to verify that everything is up and running.

    const port = process.env.PORT;
    app.listen(port);
    
    console.log(`Server listening at http://localhost:${port}/home`);
    
    app.get('/health', function (req, res) {
      res.send('Service is alive and healthy')
    });
    
  3. Define a /home route to render the home page that displays various. It will serve the traffic for the OAuth flow.

    app.get('/home', function(req, res) {
      res.render('home', {} )
    })
    

Define Environment Variables

Next, lets define some variables to configure the client credentials and OAuth token URL for the non-mTLS OAuth client application from our environment variables.

const client_id = process.env.OAUTH_CLIENT_ID;
const client_secret = process.env.OAUTH_CLIENT_SECRET;
const token_url = process.env.OAUTH_TOKEN_URL;
const auth_token = Buffer.from(`${client_id}:${client_secret}`, 'utf-8').toString('base64');

Using Mutual-TLS requires the use of a certificate and public key. These are read from the file system used to initialize the https.Agent. The environment variables used with mTLS OAuth server are then read in.

const httpsAgent = new https.Agent({
  cert: fs.readFileSync('client.crt'),
  key: fs.readFileSync('client.key'),
});

const mtls_client_id = process.env.MTLS_OAUTH_CLIENT_ID;
const mtls_token_url = process.env.MTLS_OAUTH_TOKEN_URL;

Tip - Insight

We will use the above HTTPS Agent to ensure the API calls uses the above certificate and key files during the TLS handshake with both Cloudentity and the actual resource server.

Route for Regular Access Token

When the user selects Get Access Token from the application’s UI, the route /auth is called which fetches a regular access token that is not certificate bound. Here the client_credentials grant type is used. The token is then decoded and displayed in the UI.


app.get('/auth', function(req, res) {
  getAuth().then(value => {
   if(value !== undefined) {
     var decoded = jwt_decode(value);
    res.render('home', {accessToken: JSON.stringify(decoded, null, 4)} )
   } else {
     res.send("No token fetched!")
   }
 }, err => {
   res.send("Unable to fetch token!")
 })

});

const getAuth = async () => {
  try {
    const data = qs.stringify({'grant_type':'client_credentials'});
    const response = await axios.post(token_url, data, {
      headers: {
        'Authorization': `Basic ${auth_token}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    })
    return response.data.access_token;
  } catch(error){
    console.log(error);
  }
}

Route for Certificate Bound Access Token

Now let’s define the /mtlsauth route that fetches a certificate bound access token. The getMtlsAuth function is invoked with the grant type set to client credentials and then the token endpoint is called. RFC 8705 states For all requests to the authorization server utilizing mutual-TLS client authentication, the client MUST include the “client_id” so the client ID is included. The httpsAgent is used which includes the certificate and the public key for establising the mTLS connection.

app.get('/mtlsauth', function (req, res) {
  getMtlsAuth().then(value => {
    if (value !== undefined) {
      var decoded = jwt_decode(value);
      res.render('home', { certificate_bound_access_token: JSON.stringify(decoded, null, 4) })
    } else {
      res.send("No token fetched!")
    }
  }, err => {
    res.send("Unable to fetch token!")
  })
});

const getMtlsAuth = async () => {
  try {
    const data = qs.stringify({ 'grant_type': 'client_credentials', 'client_id': mtls_client_id });

    const httpOptions = {
      url: mtls_token_url,
      method: "POST",
      httpsAgent: httpsAgent,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      data: data
    }

    const response = await axios(httpOptions)
    return response.data.access_token;
  } catch (error) {
    console.log(error);
  }
}

Application Configuration

Prepare Certificate and JWKS

Create a self-signed certificate used to establish communication between the OAuth client application, the Cloudentity authorization server and the resource server. Once you have an RSA key pair, use the public key to generate the JWKS that includes the alg and x5c keys.

Tip - Reuse

In case you want to use one of the existing cert/key pairs, use the follwing artifacts:

Below, you can see a sample RSA Public key in pem format:

-----BEGIN CERTIFICATE-----
MIIFezCCA2OgAwIBAgIJAJPiYxyvuQq0MA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxDTALBgNVBAMMBHRlc3QwHhcNMjIwMzA4MDA1MDEyWhcN
MjMwMzA4MDA1MDEyWjBUMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0
ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ0wCwYDVQQDDAR0
ZXN0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw+zfZjdHRm36hvCw
hQjWlfzSJ47KHri+pdMNt5DJxgrDMkNIiu2NvYS7seJ2FRbUL0auOP84icBhRfF3
xdeJK7OLTvty4KnBY3iU2Uh1xt2KwLe3biyZpVBziCf0zfFkkTX0eT2WV5eunuQ5
+tq6V2qsXy0gSoBsolBFr3CfyXsfxzdaBseja8H4kTIpGv9lTRpNcFNOGl66VtXm
I1q4m00IfTpWzj0EivsvHIvPwHhSFw2oHYTUzabpttv6tyuZXnZqfBqw27F6Tljn
PR3W0mAxKugYrBbfcFY8dN3TETLDpFs9NiLgdHBNBwUsieLoHwJRPuBUxcCcjdMD
57+ksCLtof/0uCzZdbut8oapS8s4946TiM9YgiVy57OLzVMm7sa7RMpxvQgmNleM
1sYP4qvHf9o1HIBezwPum98la0zbR6zMWKBjCYWq5cPWC8V8+hASMbGx483str1T
A+J7nBzmQBK1v1w4Cpcho2iPSMvnbBRhLRbmYQTdD93e1EXTG/TbHiZrygRx+hJY
rx4PYi8JDSSqHwxKKEEIUKUdbFzkIWde0grgZDuxRvRnKZVc9PYv7LuqxABMyaXs
cPWMrbhAbfXvaFQKmTHomDk9Zn3go3ZuNVSR6kZ6/ht6edONfMKbLgWRLrFyhaTo
oM2wErJhIOxb+gZpIcVYlMou+VUCAwEAAaNQME4wHQYDVR0OBBYEFDbdhIObv4Gv
pY9awCqeLU8hs9rUMB8GA1UdIwQYMBaAFDbdhIObv4GvpY9awCqeLU8hs9rUMAwG
A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAELdLrm+wkvVlpQoG41a+nNC
vzqYbDLJs2DFQqoibicYo+kQvqSVIZM3lFvRMOyXgnO+a0Qk5tQ+bPmpgQV8X43c
D9awzJYz0Cua7nSPgyAI2hFzg1bk0Ohn5gihAJ0NRnqBP53FEmoygCZJUF8Trcvb
E+vHiqgjiHPpBokcm0qoUcojQz453B0XR3io00vmvXeWH+IVxOtqQGKgjOee0B8a
RuUHp3e0UhG+2FhAzKbKZ6uYSw5XmrybRV8sNDbTPfMYd/EmjK/wuc6C29VysuXr
uSSBW5eEE/Ltvb43MhQpt+ylmWY2YM0FcPGJfuSCuzYby1uiAdiAGvI7iW1DKi88
7R9L+LH9rbV2LM6q5jX1MiMPNDq0btGtcpjP2uPF5F0vkwBrdL1u98WDbCqeHoS4
GSTV0DUMNqNfzRVVcu9BLA5xa396hvBjsY3vskw9YwYlKwoN25Umtccy5QxLN+eG
PB7JYvHmM1X/iw27G2RGSq3Fzj9Ib1NVhrMv9ZjkceV9ZR1S08Y415iuzLGfAQKm
DpCT6D4jwPnRv4RcPSWz45fFUo8fflP14tkTX1W93KCA65LHTVbetuBSCyu459Ia
nJB8N3lL8S5CUyhmoQHvmDtzH2u9lPYfb2K7xQKRJaOEqLOy9hD2fTJvSSNeUG8K
Ymm1oFHEg+8e5ZYTEsc8
-----END CERTIFICATE-----

Generated JWKS should be of the given format:

{
"keys": [
    {
      "kty": "RSA",
      "kid": "534877c8-1af6-43fd-b073-9be9eaea72fa",
      "alg": "RS256",
      "n": "w-zfZjdHRm36hvCwhQjWlfzSJ47KHri-pdMNt5DJxgrDMkNIiu2NvYS7seJ2FRbUL0auOP84icBhRfF3xdeJK7OLTvty4KnBY3iU2Uh1xt2KwLe3biyZpVBziCf0zfFkkTX0eT2WV5eunuQ5-tq6V2qsXy0gSoBsolBFr3CfyXsfxzdaBseja8H4kTIpGv9lTRpNcFNOGl66VtXmI1q4m00IfTpWzj0EivsvHIvPwHhSFw2oHYTUzabpttv6tyuZXnZqfBqw27F6TljnPR3W0mAxKugYrBbfcFY8dN3TETLDpFs9NiLgdHBNBwUsieLoHwJRPuBUxcCcjdMD57-ksCLtof_0uCzZdbut8oapS8s4946TiM9YgiVy57OLzVMm7sa7RMpxvQgmNleM1sYP4qvHf9o1HIBezwPum98la0zbR6zMWKBjCYWq5cPWC8V8-hASMbGx483str1TA-J7nBzmQBK1v1w4Cpcho2iPSMvnbBRhLRbmYQTdD93e1EXTG_TbHiZrygRx-hJYrx4PYi8JDSSqHwxKKEEIUKUdbFzkIWde0grgZDuxRvRnKZVc9PYv7LuqxABMyaXscPWMrbhAbfXvaFQKmTHomDk9Zn3go3ZuNVSR6kZ6_ht6edONfMKbLgWRLrFyhaTooM2wErJhIOxb-gZpIcVYlMou-VU",
      "e": "AQAB",
      "x5c": [
     "MIIFezCCA2OgAwIBAgIJAJPiYxyvuQq0MA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDTALBgNVBAMMBHRlc3QwHhcNMjIwMzA4MDA1MDEyWhcNMjMwMzA4MDA1MDEyWjBUMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ0wCwYDVQQDDAR0ZXN0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw+zfZjdHRm36hvCwhQjWlfzSJ47KHri+pdMNt5DJxgrDMkNIiu2NvYS7seJ2FRbUL0auOP84icBhRfF3xdeJK7OLTvty4KnBY3iU2Uh1xt2KwLe3biyZpVBziCf0zfFkkTX0eT2WV5eunuQ5+tq6V2qsXy0gSoBsolBFr3CfyXsfxzdaBseja8H4kTIpGv9lTRpNcFNOGl66VtXmI1q4m00IfTpWzj0EivsvHIvPwHhSFw2oHYTUzabpttv6tyuZXnZqfBqw27F6TljnPR3W0mAxKugYrBbfcFY8dN3TETLDpFs9NiLgdHBNBwUsieLoHwJRPuBUxcCcjdMD57+ksCLtof/0uCzZdbut8oapS8s4946TiM9YgiVy57OLzVMm7sa7RMpxvQgmNleM1sYP4qvHf9o1HIBezwPum98la0zbR6zMWKBjCYWq5cPWC8V8+hASMbGx483str1TA+J7nBzmQBK1v1w4Cpcho2iPSMvnbBRhLRbmYQTdD93e1EXTG/TbHiZrygRx+hJYrx4PYi8JDSSqHwxKKEEIUKUdbFzkIWde0grgZDuxRvRnKZVc9PYv7LuqxABMyaXscPWMrbhAbfXvaFQKmTHomDk9Zn3go3ZuNVSR6kZ6/ht6edONfMKbLgWRLrFyhaTooM2wErJhIOxb+gZpIcVYlMou+VUCAwEAAaNQME4wHQYDVR0OBBYEFDbdhIObv4GvpY9awCqeLU8hs9rUMB8GA1UdIwQYMBaAFDbdhIObv4GvpY9awCqeLU8hs9rUMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAELdLrm+wkvVlpQoG41a+nNCvzqYbDLJs2DFQqoibicYo+kQvqSVIZM3lFvRMOyXgnO+a0Qk5tQ+bPmpgQV8X43cD9awzJYz0Cua7nSPgyAI2hFzg1bk0Ohn5gihAJ0NRnqBP53FEmoygCZJUF8TrcvbE+vHiqgjiHPpBokcm0qoUcojQz453B0XR3io00vmvXeWH+IVxOtqQGKgjOee0B8aRuUHp3e0UhG+2FhAzKbKZ6uYSw5XmrybRV8sNDbTPfMYd/EmjK/wuc6C29VysuXruSSBW5eEE/Ltvb43MhQpt+ylmWY2YM0FcPGJfuSCuzYby1uiAdiAGvI7iW1DKi887R9L+LH9rbV2LM6q5jX1MiMPNDq0btGtcpjP2uPF5F0vkwBrdL1u98WDbCqeHoS4GSTV0DUMNqNfzRVVcu9BLA5xa396hvBjsY3vskw9YwYlKwoN25Umtccy5QxLN+eGPB7JYvHmM1X/iw27G2RGSq3Fzj9Ib1NVhrMv9ZjkceV9ZR1S08Y415iuzLGfAQKmDpCT6D4jwPnRv4RcPSWz45fFUo8fflP14tkTX1W93KCA65LHTVbetuBSCyu459IanJB8N3lL8S5CUyhmoQHvmDtzH2u9lPYfb2K7xQKRJaOEqLOy9hD2fTJvSSNeUG8KYmm1oFHEg+8e5ZYTEsc8"
      ]
    }
  ]
}

Register mTLS OAuth Client

Let’s register a trusted OAuth Client Application in Cloudentity and configure the client application for mTLS specifications. We chose the client application type as service as this is a trusted backend application as it can securely store the secrets/credentials used in this application for OAuth flow. Then, internally set the grant type for the application as client_credentials.

Creating Client Applications

If you need help with creating your client application, check out the Creating and Configuring Client Applications article.

For your application:

  1. Select the Token Endpoint Authentication Method as Self Signed TLS Client Authentication.

  2. Configure the JSON Web Key Set with the above generated JWKS block that has the self-signed certificate.

  3. Check the box for Certificate bound access token to get the certificate thumbprint bound to the access token. This enables a new JWT Confirmation Method member "x5t#S256" that adheres to the RFC-8700 - Proof of Possession semantics specifications for JSON web tokens.

    mtls configuration

Register Regular OAuth Client

Let’s register a regular trusted OAuth Client Application. We chose the client application type as service as this is a trusted backend application as it can securely store the secrets/credentials used in this application for OAuth flow. Then, internally set the grant type for the application as client_credentials.

Creating Client Applications

If you need help with creating your client application, check out the Creating and Configuring Client Applications article.

For your new client application, set the Token Endpoint Authentication Method to Client Secret Basic.

Configure Node.js Application with OAuth Client

Now that we have 2 OAuth clients that are required to demonstrate the flow in the Node.js application, let’s go ahead and configure the application with the above OAuth client info.

Go to the root of the sample-nodejs-mtls-oauth-client project. From the root of the repository, enter the following command in the terminal:

cd sample-nodejs-mtls-oauth-client

In the .env file enter the OAuth server variables copied before:

OAUTH_CLIENT_ID="`<your oauth client id that is not using mtls>`"
OAUTH_CLIENT_SECRET="`<your oauth client secret that is not using mtls>`"
OAUTH_TOKEN_URL="`<your oauth client token url that is not using mtls>`"
MTLS_OAUTH_CLIENT_ID="`<your oauth client id that is using mtls>`"
MTLS_OAUTH_TOKEN_URL="`<your oauth client token url that is using mtls>`"

Run Application

To run the application, enter the following command from the root of the project in the terminal after you have added the required environment variables to .env.

npm start

Once the application is running and the end user visits http://localhost:5002/home, the user is presented with the following UI.

token access ui

Select Get Access Token which invokes the /auth route in the Node.js application that fetches and displays a regular access token from Cloudentity in the decoded format.

Regular access token

Select Get Certificate Bound Access Token which invokes the /mtlsauth route in the Node.js application that fetches and displays a certificate bound access token in the decoded format. The main difference between the previous token is the presence of the cnf claim with the x5t thumbprint within this access token.

Certificate bound access token

Summary

This wraps up our tutorial for the Node.js application that fetches two different flavors of access tokens: one certificate bound with the certificate thumbprint of the certificate presented during the mTLS handshake and the other one with no binding to a certificate.

After going through the tutorial, you will have accomplished the following:

  • Build a simple Node.js application to obtain certificate bound access token
  • Create an OAuth Application with the Cloudentity Authorization Platform
  • Authorize and fetch OAuth access token in the Node.js app using OAuth mTLS client specification
Updated: Jul 11, 2022