SAP BTP Security: How to realize client-credentials flow with IAS [2]: mTLS

This blog post shows how to do client-credentials flow with IAS using “identity” service in SAP BTP.
We create a minimalistic sample app2app scenario where 2 application communicate with each other while authentication is done with the OAuth flow called “client-credentials”.
The difference to previous tutorial is that this time we’re using a client certificate in order to fetch the access token (mTLS), instead of using basic authentication with clientsecret.

Used technologies:
SAP Business Technology Platform (SAP BTP), Cloud Foundry Environment,
SAP Cloud Identity Services – Identity Authentication (IAS),
Node.js.

Quicklinks:
Quick Guide
Sample Code

Content

0. Introduction
1. Preparation
2. Sample Scenario
2.1. Identity Service
2.2. Backend Application
2.3. Frontend Application
2.4. Deploy
3. Run
Appendix: Sample Code

0.1. Introduction

In this hands-on tutorial, we’re using the same setup and scenario like in previous tutorial, so please see here for the description.
Just a few words to introduce the focus of the tutorial.
We have a backend application that is meant to be used by other applications in SAP BTP.
Like all good backends, the endpoint is protected with OAuth.
The backend endpoint requires an incoming JWT token for authentication and will validate it.
The backend doesn’t care HOW the JWT token was requested.
The app is bound to an instance of identity service which is connected to the IAS tenant.
IAS is used to validate the JWT token

The frontend app wants to call the backend app in order to get some data.
It is bound to the same instance of identity service, which acts as OAuth client.
To fetch the required JWT token, the app sends a request to the IAS which acts as Authorization Server.
The request must be authenticated.
To do so, a client certificate and key is sent along with the request (instead of user/password i.e. client/secret)

That’s already it:
The difference to previous blog post is the way how we fetch the JWT token.
So why do we need another blog?
Because today we’re answering questions like
How to get such certificate?
How does it look like?
What is a certificate?
How to configure the request?
Why use certificate at all?
Is it secure?
What to do if it fails?
What to do if a hacker steals my certificate?

The answers are hidden in below text like easter eggs – if you don’t find them, don’t worry, I might not know the answer neither.

For your convenience, a quick look at the scenario:

Out of scope:
Authorization handling. The backend application doesn’t require any scope/role for accessing the endpoint.

Disclaimer:
This is not an official reference application, I’m just sharing my personal experiences.

0.2. Prerequisites

To follow this tutorial, we need

  • Access to an IAS tenant with admin permission.
  • Access to SAP Business Technology Platform (SAP BTP) and permission to create instances and to deploy applications.
  • Basic Node.js skills.
  • Some basic understanding of OAuth and JWT token.
  • The tutorial is using the Cloud Foundry command line client, but all tasks can be done in the cockpit as well

1. Preparation

Before we start with the sample application, we need 2 preparation tasks:
Establish trust between IAS and BTP
Create project

1.1. Configure Trust

If not already done, you need to connect your subaccount to the IAS tenant.
This is done by just pressing a button.
Go to your subaccount -> Security -> Trust Configuration
Press “Establish Trust”
Afterwards, we can check the IAS tenant and view the newly created Application entry at
https://<tenant>.accounts400.ondemand.com/admin/#/applications

1.2. Create Project Structure

To follow this tutorial, we create a project iasclicre which contains our scenario with 2 apps:

C:\iasclicre
backend
package.json
server.js
frontend
package.json
server.js
config_ias.json
manifest.yml

Or see this screenshot:

The file content can be copied from the appendix.

2. Create Sample Scenario

As mentioned, we’re creating 2 apps that are bound to the same instance of identity service.
Both apps will be deployed together.

2.1. Create instance of Identity Service

The only thing worth mentioning about instance creation:
We don’t need to configure anything with respect to mTLS here:

config_ias.json

{ "display-name": "clicreias"
}

To create the instance, we jump with command prompt into the project root folder and run the following command:
cf cs identity application clicreIas -c config_ias.json

2.2. Create Backend Application

In today’s mTLS scenario, we don’t need to change anything on the backend side.
As mentioned above, the backend requires a valid JWT token, but it doesn’t care how it was obtained.
The JWT token itself looks very similar like in previous scenario: it is a bunch of silly characters.
One difference is: it contains an additional claim, which we add to the list of interesting claims in the response:

claims.push(`cnf: ${JSON.stringify(jwtDecoded.cnf)}`)

The cnf is the confirmation hash which is added to the JWT token in case of mTLS.

The full code can be found in the appendix.

2.3. Create Frontend Application

The frontend app has nothing more to do than calling the backend app.

Nevertheless, now we’re coming to the interesting part of the tutorial:
Getting and sending the certificate.

The frontend app is bound to an instance of identity service.
In the binding info, we get the credentials required to call the service.
In case of mTLS, we get the certificate instead of the secret.
Along with the certificate we get the private key.
The certificate itself is just a piece of information.
Only together with the private key, we can achieve what we want:
We want to authenticate against IAS, we want to prove that we’re allowed to execute request.
As such, when fetching a JWT token, we have to send the certificate plus the private key.

I’ve enriched the diagram from previous post by adding certificate and key to the binding info.
Also added the cert (+ key) which we send to the IAS in order to get a token.

Note:
The diagram is not precise, when sending the token-fetch request, we call directly the IAS. The service provides only the credentials and the URL.

Execute request with client certificate
The difference to previous tutorial is in authenticating with client certificate (mTLS) instead of basic auth with client secret.
Below code snippet uses the axios module, which makes things easy.

async function _fetchJwtToken() { const requestBody = new url.URLSearchParams({ grant_type: 'client_credentials', response_type: 'token', client_id: CREDENTIALS.clientid }).toString() const options = { headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, url: CREDENTIALS.url + '/oauth2/token', method: 'POST', data: requestBody, httpsAgent: new https.Agent({ cert: CREDENTIALS.certificate, key: CREDENTIALS.key }) } const response = await axios(options); return response.data.access_token
}

The request body contains the usual form based content for client-credentials.
The response_type property is optional, as the value “token” is the default anyways.

For client certificate authentication, we need an Agent, which is responsible for managing the underlying TLS connection.
We create a new https.Agent and configure it with the certificate and private key which we read from the binding.

Note:
Compared to XSUAA, we have to note these 2 differences:
certurl: there’s no certurl property in case of IAS.
tokenendpoint: in case of IAS, it contains the 2: /oauth2/token

The response of the token-request is a JSON object which contains the JWT token in the access_token property.

Once we have the token, we can call the backend endpoint and print the result, no change here.

The full file content can be found in appendix.

2.4. Deploy

Before we deploy we need to have a look at the manifest.yml:

applications: - name: backendclicer routes: - route: backendclicer.cfapps.eu11.hana.ondemand.com services: - name: clicreIas - name: frontendclicer routes: - route: frontendclicer.cfapps.eu11.hana.ondemand.com services: - name: clicreIas parameters: credential-type: "X509_GENERATED" 

The interesting part is the binding parameter of the identity service:
Here we have to specify that we want mTLS.
Remember that there was no config required when creating the service instance.
It is here in the binding where we can add configuration options.
The configuration is done in the service binding which has the property “credential-type” which allows to configure which kind of credentials we want, password or certificate.

The 4 supported values are:

SECRET
If we specify SECRET (or nothing, as it is the default), we get clientid and clientsecret in the app env.
When calling the IAS to fetch a JWT token, then we configure basic authentication and send the clientid as user and the secret as password.
X509_GENERATED
This setting will tell IAS to generate a certificate (plus key) for us and provide it in the binding.
This is the usual value for testing and not recommended as it reveals the private key.
X509_PROVIDED
If this is chosen, we have to generate our own cert (plus key) and specify it in the manifest, in the “certificate” property.
NONE
This means we don’t need credentials.

Note that in our manifest we don’t specify the credential-type for the backend, as we don’t need it (the backend app uses IAS for protection and does not fetch any token)
We only specify the credential-type for the frontend app, such that we can fetch the required token based on mTLS.
We use the GENERATED setting because it is shortcut to get a certificate, as the focus of this blog is the configuration and usage of client certificate.

Note:
We don’t need to specify a route for cert url, as this is obsolete for IAS.

Note:
When service key is required, then we need the binding param as well. It can be specified as JSON file or in the command, like this:
cf csk clicreIas sk -c “{\”credential-type\”: \”X509_GENERATED\”}”

To deploy the 2 apps we run
cf push

Afterwards we can check the certificate in the environment:
cf env frontendclicer

As a result, we get:

Note:
I’ve marked the relevant properties with red rectangle.
And I’ve marked the newlines: the newlines after BEGIN and before END are required, so they’re green.
The orange newlines are used to mark the limit of 64 characters. When sending a certificate, these \n characters are not allowed.
In our frontend code, they are removed automatically.
In manual request, it would be necessary to remove them manually, or with scripting. Only the green newlines would be still required.

3. Run

After deploy, we open our frontendapp at
https://frontendclicer.cfapps.eu11.hana.ondemand.com/homepage

As a result, we can see that the client-credentials flow has been successful and we can see some info from the JWT token.

As promised, we can see the “cnf” claim.
“cnf” stands for “confirmation” and is used as proof of possession.
When we requested the JWT token, we’ve sent a client certificate for authentication.
And afterwards, the token contains an information about that certificate.
With other words: the JWT is bound to a certificate.
This needs to be confirmed.
To do so, there are several methods.
One method is called x5t#S256

To bind a JWT to a certificate, it is obviously not desired to copy the whole certificate into the cnf claim.
Instead, the hash of the certificate is used.
Such hash is also called “thumbprint” (== fingerprint == digest).
In our screenshot, this thumbprint of the x509 certificate (-> x5t) is created with “SHA-256” algorithm (-> S256).

4. Cleanup

For your convenience, here are the commands to remove the artifacts created within this tutorial:

cf d -r -f frontendclicer
cf d -r -f backendclicer
cf ds -f clicreIas

Summary

In this blog post we’ve learned how to fetch a JWT token via the client-credentials grant type as specified by OAuth 2.0.
To get some more insights with a hands-on tutorial, we’ve created 2 apps, to show
how to fetch the token
how to use it
how it looks like

This is nothing new.
The interesting part: we’ve learned how to use mTLS:
How to configure the identity service.
How to execute a request using client certificate and private key.

Quick Guide

How to use mTLS for fetching JWT token:

– No special setting for instance creation of identity
– no certurl property
– configure X509 binding parameter for identity service in manifest
parameters: {“credential-type”: “X509_GENERATED”}
– tokenendpoint is: /oauth2/token
– when requesting token, read cert and key from binding and pass to the request in https.Agent:
const options = {
url: CREDENTIALS.url + ‘/oauth2/token’,
httpsAgent: new https.Agent({
cert: CREDENTIALS.certificate,
key: CREDENTIALS.key

IAS in SAP Help Portal
IAS main entry: https://help.sap.com/docs/IDENTITY_AUTHENTICATION
IAS main docu page.
OpenID Connect with client credentials flow
Configure Secrets in IAS

Identity Service
Main docu entry page
Reference for Params

Client libs
Passport homepage and download.
Node.js package xssec.

OAuth 2.0
Main: OAuth 2.0.
Grant Type Client Credentials.
The spec.
Some more info
Understanding of OAuth for dummies like me.

More blog posts
What is mTLS?
mTLS with Destinations.
Glossary and link list.

Appendix: Sample Code

Note:
You might need to adapt the app names in manifest and the domain of the routes.
Also, the hardcoded URL pointing to backendapp would have to be adapted accordingly.

config_ias.json

{ "display-name": "clicreias"
}

manifest.yml

---
applications: - name: backendclicer path: backend memory: 64M routes: - route: backendclicer.cfapps.eu11.hana.ondemand.com services: - name: clicreIas - name: frontendclicer path: frontend memory: 64M routes: - route: frontendclicer.cfapps.eu11.hana.ondemand.com services: - name: clicreIas parameters: credential-type: "X509_GENERATED" 

backend

package.json

{ "dependencies": { "@sap/xsenv": "latest", "@sap/xssec": "^3.2.13", "express": "^4.17.1", "passport": "^0.4.0" }
}

server.js

const xsenv = require('@sap/xsenv') const IAS_CREDENTIALS = xsenv.getServices({myIas: {label:'identity'}}).myIas const xssec = require('@sap/xssec')
const passport = require('passport')
const JWTStrategy = xssec.JWTStrategy
passport.use('JWT', new JWTStrategy(IAS_CREDENTIALS, "IAS"))
const express = require('express')
const app = express();
app.use(passport.initialize())
app.use(passport.authenticate('JWT', { session: false, failWithError: true })); // start server
app.listen(process.env.PORT) app.get('/endpoint', async (req, res) => { const claims = _formatClaims(req.tokenInfo) res.send(`<h5>Content of received JWT:</h5>${claims}`)
}) function _formatClaims(tokenInfo){ const jwtDecoded = tokenInfo.getPayload() console.log(`===> The full JWT decoded: ${JSON.stringify(jwtDecoded)}`) const claims = new Array() claims.push(`subject: ${tokenInfo.getSubject()}`) claims.push(`<br>zone_uuid: ${tokenInfo.getZoneId()}</br>`) claims.push(`issuer: ${tokenInfo.getIssuer()}`) claims.push(`<br>aud: ${jwtDecoded.aud}</br>`) claims.push(`cnf: ${JSON.stringify(jwtDecoded.cnf)}`) return claims.join('')
}

frontend

package.json

{ "dependencies": { "@sap/xsenv": "latest", "express": "^4.17.1", "axios": "^1.1.2" }
}

server.js

const xsenv = require('@sap/xsenv')
const CREDENTIALS = xsenv.getServices({myIas: {label:'identity'}}).myIas const axios = require('axios')
const url = require('url');
const https = require('https');
const express = require('express')
const app = express(); app.listen(process.env.PORT) app.get('/homepage', async (req, res) => { const jwtToken = await _fetchJwtToken() const result = await _callBackend(jwtToken) res.send(`<h3>Response from backend:</h3><p>${result}</p>`)
}) async function _fetchJwtToken() { const requestBody = new url.URLSearchParams({ grant_type: 'client_credentials', response_type: 'token', client_id: CREDENTIALS.clientid }).toString() const options = { headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, url: CREDENTIALS.url + '/oauth2/token', method: 'POST', data: requestBody, httpsAgent: new https.Agent({ cert: CREDENTIALS.certificate, key: CREDENTIALS.key }) } const response = await axios(options); return response.data.access_token
} async function _callBackend(token){ const options = { method: 'GET', url: 'https://backendclicer.cfapps.eu11.hana.ondemand.com/endpoint', headers: { 'Authorization': 'bearer ' + token } } const response = await axios(options) return response.data }