Skip to Content
Technical Articles
Author's profile photo Carlos Roggan

SAP BTP – Security – OAuth 2.0 – Understanding Token Exchange

You heard about token exchange and would you like to have a closer look into why it is useful and how it works?
In this blog post we create a simple example scenario and verify the difference between token exchange and client credentials.

Quicklinks:
Quick Guide
Sample Code

Content

0. Introduction
1. Token Exchange Scenario
2. Native Code Sample
3. Client Credentials Scenario
Appendix 1: Token Exchange Scenario Sample
Appendix 2: Client Credentials Scenario Sample

Next blog post: using destination
Next blog post: multitenancy example

Introduction

The example scenario
We have a user-centric frontend-application that calls a backend-application via HTTP:

Both apps are protected with OAuth and are bound to an instance of XSUAA.
The frontent-app uses an approuter to handle the user-login (means to fetch a JWT token for the user). All endpoints require a JWT token.

Below diagram shows the token flow in detail:

1)
The end user opens the main app entry point which is the approuter URL.
Approuter handles the communication with XSUAA, to present a login screen.
After entering correct credentials, a JWT token is issued.
This token is specifically for the user and for the frontend application.
2)
Approuter forwards the call and the token to the core server app.
The server app validates and accepts the token.
The server app wants to call the backend endpoint which requires a JWT token as well.
However, the existing user-token is not valid for the backend.
Reason: it was issued by different instance of XSUAA.
As such, a new JWT token needs to be fetched.
This is done via token exchange.
3)
The existing token is sent to the XSUAA instance and is used to issue a new token.
4)
This new token (blue) can now be sent to the backend endpoint.
5)
The backend endpoint validates the incoming token with its own instance of XSUAA

Note:
The RFC 7523 specifies the token exchange flow. Quote:
“This specification defines the use of a JSON Web Token (JWT) Bearer Token as a means for requesting an OAuth 2.0 access token as well as for client authentication.”

Grant
We need another detailed look.
The token exchange mechanism doesn’t solve the problem of the 2 different XSUAA instances.
A token issued from XSUAA1 will never be accepted by XSUAA2
In addition, both applications define their own scopes and require that they are contained in the tokens. But XSUAA1 doesn’t even know about XSUAA2, so how could it add any foreign scope?
The mechanism to solve these hurdles is the grant statement.
The backend-XSUAA grants its scope to the frontend-XSUAA.
See below diagram:

Note:
The terms in the diagram are not precise, but I hope that makes it more readable.

We can see that the backend-XSUAA defines a scope called backendscope.

This scope is granted to the frontend-XSUAA – which in fact is the OAuth client with name frontendxsapp. (this is the value of property xsappname in the configuration of the XSUAA service instance).
However, this grant is not enough.
The frontend app (more precise, the frontend XSUAA instance, resp. OAuth client) needs to accept the granted scope.
This is done with the authorities property in the frontend-xsuaa.
However, this accept not enough.
The frontend-OAuth-client has the scope now, this is enough for client-credentials, but not for our user. The user needs a role which contains the backend-scope. Otherwise, the JWT token won’t contain the scope and the scope-check in the backend endpoint will reject the request.
So finally, a third declaration is required to add the backend-scope to the role-template of the frontend-OAuth-client.

Note:
After deploy, the roles need to be assigned to the user in the cloud cockpit.

Note:
More detailed tutorial about all this grant stuff can be found in this blog post.

After this overview, we can now go ahead and create a minimalistic scenario.

Prerequisites

To follow the tutorial, we need access to SAP Business Technology Platform (SAP BTP) Cloud Foundry environment and permissions to create service instances and deploy applications.
However, token exchange is not specific to SAP BTP nor XSUAA, so the code samples can be used similarly in other environments.
We use the command line client for Cloud Foundry, but the same can be achieved in the cockpit.
Basic knowledge about OAuth 2.0 flows is expected.

Preparation

To follow this tutorial, we’re creating a project tex (token exchange) which we’ll deploy later.

Create Project

On filesystem, we create a root project folder C:\tex containing 2 subfolders for the 2 applications (which themselves have subfolders for their modules)

C:\tex
backend
app
frontend
app
approuter

Or see this screenshot:

Each app folder contains a few files required for little node server apps.
We create the required files in the folders and copy the content from the appendix 1

C:\tex
backend
app
package.json
server.js
backend-security.json
manifest.yml
frontend
app
package.json
server.js
approuter
package.json
xs-app.json
frontend-security.json
manifest.yml

Looks confusing, right?
Here’s an additional screenshot, for your convenience:

1. Token Exchange Scenario

We’re going in detail through the scenario which should be standard in SAP BTP.
Later we may compare it to client credentials, if interested.

1.1. Create Backend App

Let’s start with a backend application which is used as kind of API provider or reuse service.

1.1.1. Create Service Instance

Our backend app uses XSUAA for protecting the endpoint.
The configuration of the XSUAA instance is stored in a file called C:\tex\backend\backend-security.json
Typically, this file is called xs-security.json, but for today’s tutorial, I prefer to have different name, so we don’t confuse this file with the one of the frontend app.

"xsappname": "backendxsuaa",
"scopes": [{
    "name": "$XSAPPNAME.backendscope",
    "granted-apps" : ["$XSAPPNAME(application, frontendxsuaa)"]

The config shows:
Our OAuth client has the name backendxsuaa.
We define a scope with name backendscope.
We use prefix ($XSAPPNAME.backendscope) to make the scope name unique.
At runtime, he variable contains the xsappname (“backendxsuaa”), but it contains as well some hash which is generated during instance creation.
Important for us:
We define the grant statement granted-apps.
What we want to do is to assign our backendscope to the OAuth client frontendxsuaa (we define later).
Note that granted-apps is used only in user-centric scenario (see chapter below for app2app scenario)

The syntax:
In our backend-config, we want to refer to the frontend-application.
More precise, we refer to the XSUAA-instance.
More precise, the OAuth client which is represented by the XSUAA instance.
We reference it via its unique xsappname, which is “frontendxsuaa” (we will create it later)
To actually find it, the service plan has to be given: “application”.
Both infos are passed to the variable $XSAPPNAME which will resolve it at runtime.

To create the service instance, we jump to the folder c:\tex\backend and execute the following command:
cf cs xsuaa application texBackendXsuaa -c backend-security.json

1.1.2. Create Backend App

The app does really nothing.
It just represents a service that requires some user info for audit logging.
In our example, we just log the user info to the console.
In addition– for our own convenience – we return the JWT token, which we receive when we’re called.
That’s all.

The backend app has few requirements:
The service endpoint is protected with OAuth, so it requires a valid JWT token.
Furthermore, it requires a certain scope to be contained in the JWT token.
The app is a standalone reuse service app, it is bound to its own xsuaa instance.

So how can a JWT, which was issued by foreign XSUAA, contain that scope?
We’ve explained it above.
OK, we’ll explain it again below.
But not now.

The application code, extract from file C:\tex\backend\app\server.js

app.get('/endpoint', passport.authenticate('JWT', {session: false}), (req, res) => {
    const auth = req.authInfo  
    if (! auth.checkScope(UAA_CREDENTIALS.xsappname + '.backendscope')) {
        res.status(403).end('Forbidden. Missing authorization')
    }      

    // The fake audit logging
    console.log(`===> [AUDIT] backend called by user ${auth.getGivenName()} with oauth creds: '${auth.getClientId()}' - subdomain: ${auth.getSubdomain()}`)

    res.json({'jwtToken': auth.getAppToken()})
})

We can see that the endpoint is protected with OAuth and passport library.
When the request has successfully passed the validation done by passport and xssec, then we still have to validate the scope.
We use the convenience object authInfo, which was added previously to the request object by the library xssec.
The authInfo provides us with helper methods and easy access to some info contained in the JWT token.
We use it to log some minimal info about the user which is contained in the JWT token.
At the end, we just return the JWT token.
The full application code can be found in the appendix 1.

1.1.3. Deploy

After deploy, we can try to invoke the endpoint, to make sure that it accessible – but forbidden.
https://texbackend.cfapps.sap.hana.ondemand.com/endpoint
We leave it – it is up to our frontend to properly invoke this endpoint…

1.2. Create Frontend App

Our frontend application will have to do the token exchange – as this is the topic of our tutorial.
To do so, it needs a JWT-token which is issued to a human user.
Most easy way to get a hold of a user token: use Approuter.
Approuter together with XSUAA will display a login screen, so the end user can enter his user/password and under the hood, a JWT token will be issued and forwarded to our server application.

1.2.1. Create Service Instance

BTW, we first need to create an instance of XSUAA.

The configuration is stored in C:\tex\frontend\frontend-security.json

{
    "xsappname": "frontendxsuaa",
    "scopes": [{"name": "$XSAPPNAME.frontendscope"}],
    "role-templates": [{
            "name": "FrontendUserRole",
            "scope-references": [   "$XSAPPNAME.frontendscope", 
                                    "$XSAPPNAME(application,backendxsuaa).backendscope"]}],
    "foreign-scope-references": ["$XSAPPNAME(application,backendxsuaa).backendscope"],

Above configuration shows that we define a scope, which will be required and enforced by our app, whenever the homepage is accessed (the homepage is just a simple endpoint).
Since the homepage is accessed by human user, we need to wrap the scope in a role-template, such that afterwards, a role can be assigned to the user.
The interesting parts:
1) The foreign-scope-references statement is required, to accept the scope that was granted by the backend application (more precise: by backendxsapp)
Note that this statement is only used in user-centric scenario (see chapter below).
However, it is not enough to accept the granted scope.
Why ?
2) Since it is a human user who logs into our app and gets the scope in the JWT token, we also need to assign the backendscope to the user, as part of the role-assignment.
As such, we need to add the foreign scope to the role.
We define a role template and we add 2 roles.
We can see the difference in the syntax:
We use the variable $XSAPPNAME to refer to a scope defined in the same file (frontendscope):
$XSAPPNAME.frontendscope
We refer to the foreign backendscope via the extended syntax which includes the foreign xsappname and service plan:
XSAPPNAME(application,backendxsuaa).backendscope

To create the service instance, we jump into directory C:\tex\frontend and execute the following command:
cf cs xsuaa application texFrontendXsuaa -c frontend-security.json

1.2.2. Create Core Application

What do we want to achieve with our frontend core app?
This app is supposed to call the backend app – that’s all.
To call the backend app, a valid JWT token is required, which is fetched with token exchange.
In addition, the app displays some token info in the browser:
1) Since the app itself is protected and requires a JWT token, this (user login) token is printed.
2) After exchanging this user token for backend token, this exchanged token is printed as well.

app.get('/app', passport.authenticate('JWT', {session: false}), async (req, res) => {
    const auth = req.authInfo    
    if (! auth.checkScope(UAA_CREDENTIALS.xsappname + '.frontendscope')) {
        res.status(403).end('Forbidden. Authorization for homepage access is missing.')
    }      

    //do token exchange and call backend
    const userJWT = auth.getAppToken()
    const texJwtToken = await _doTokenExchange(userJWT)      
    await _callService(texJwtToken)

    // print token info to browser
    const htmlUser = _formatClaims(userJWT) // login token
    const htmlTEX = _formatClaims(texJwtToken) // after token exchange

    res.send(`  <h4>Claims from user login</h4>${htmlUser}
                <h4>Claims from token exchange</h4>${htmlTEX}`)
})

How to do the token exchange?
In this sample, we’re using the convenience library @sap/xssec, which requires basically one line:

xssec.requests.requestUserToken(jwt, UAA_CREDENTIALS, null, 'backendxsuaa!t14860.backendscope', null, null, (error, token)=>{
   resolve(token)

We pass the user token which we’ve received in the request header.
In addition, we pass the credentials of the frontend-XSUAA, which we have in the binding.
Interesting for our example is the (optional) fourth parameter: the scope.
Here we can pass a filter for scopes.
In our example, the backend endpoint requires only the backendscope.
But the user-token carries some more scopes (frontendscope, openid).
As such, we use this parameter for filtering, such that we send only the 1 required scope to the backend.

After token exchange, we get the new token in the callback, and it will contain only one scope.

Note:
This scope param is optional and we can pass null instead

After we have a hold of the exchanged token, we can use it to call the backend service:

const options = {
   headers: { Authorization: 'Bearer ' + jwtToken }
}
const serviceURL = 'https://texbackend.cfapps.sap.hana.ondemand.com/endpoint'
const response = await fetch(serviceURL, options)

Note:
As usual, to keep all the sample code as small as possible, we’re avoiding any error handling.
Please don’t blame me for that.

Last thing we’re doing in this silly app is to print the content of the 2 tokens.
We decode the token and print some useful properties (claims) of the JWT token.

function _formatClaims(jwtEncoded){
    const jwtBase64Encoded = jwtEncoded.split('.')[1]
    const jwtDecodedAsString = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii')
    const jwtDecodedJson = JSON.parse(jwtDecodedAsString)

    const claims = new Array()
    claims.push(`client_id: ${jwtDecodedJson.client_id}`)
    claims.push(`<br>name: ${jwtDecodedJson.given_name} ${jwtDecodedJson.family_name}</br>`)
    claims.push(`email: ${jwtDecodedJson.email}`)
    claims.push(`<br>xs.system.attributes: ${JSON.stringify(jwtDecodedJson['xs.system.attributes'])}</br>`)
    claims.push(`scopes: ${jwtDecodedJson.scope}`)
    claims.push(`<br>aud: ${jwtDecodedJson.aud}</br>`)

    return claims.join('')
}

Note:
Instead of manually decoding the token, we could use the tokenInfo object, provided by xsssec.
It provides helper methods for accessing the claims, but not all, so I’m doing manually.

1.2.3. Create Approuter

We don’t need to spend many words about the Approuter (see tutorial for beginners).
We’re using it only for convenient user login.
The relevant config in the file  C:\tex\frontend\approuter\xs-app.json

"routes": [{
    "source": "^/tofrontend/(.*)$",
    "target": "$1",
    "destination": "destination_frontend",
    "authenticationType": "xsuaa"

We set authenticationType to xsuaa, which means our endoint is OAuth protected and this will lead to popping up a login-screen.
We define a route which points to our core server app.
The URL of our server app is configured in a destination.
For simplicity, we declare the destination in the manifest, as environment variable:

env:
  destinations: >
    [
      {
        "name":"destination_frontend",
        "url":"https://texfrontend.cfapps.sap.hana.ondemand.com",
        "forwardAuthToken": true

1.2.4. Deploy Frontend App

We can go ahead and jump into folder C:\tex\frontend and deploy our 2 app modules to cloud foundry.

After successful deployment, we should NOT access our app.
First we need to assign the role (required by our apps) to our user.
If we log in without the required role, our wrong login would be kept in a session and we would need to clear the caches of browser, etc

Create Role Collection
We go to our subaccount in BTP Cockpit -> Security -> Role Collections
Press + button to create role collection with name “tex”.

Add role
After creation, we go to role collection details and press “Edit”.
We add the role “FrontendUserRole”.

Add user
To add our cloud user, we type our username and accept the proposal of the UI.
Finally, we don’t forget to press Save

1.3. Run the scenario

Now we finally open our frontend application.
Main entry point is:
ApprouterURL + route + endpoint + slash

In my example:
https://texfrontendrouter.cfapps.sap.hana.ondemand.com/tofrontend/app/

As a result, we get a login screen in which we enter the credentials of our valid BTP user.
Afterwards, we’re redirected to the homepage of our frontend app, which is the /app endpoint of our core server module.
It displays the claims of the 2 tokens.
In my example it looks like this:

We can see that both tokens are almost identical – which is expected, because both were issued by the same instance if XSUAA. This can be verified by the client_id claim, which contains the xsappname of our frontend-XSUAA instance.
We can see that after token exchange, the user info has been preserved.
That is actually the most important learning of today’s tutorial !
We can see that the exchanged token in fact only contains the 1 scope which we filtered.
We can see the aud claim. This is important claim. It contains the audience for which this token was issued. With other words: the recipient. As per default it is always the same like the client_id.
Why the same?
Obvious: our frontend app is bound to frontend-xsuaa. User logs in via frontend-XSUAA. Our app validates the token against frontend-XSUAA.
As such, the xsappname of frontend-XSUAA is the desired receiver, so it is contained in the aud claim.
BUT: the token has to be sent to backend app…

Usually, the backend-XSUAA would NOT be contained in the aud of the token (because issued by frontend-XSUAA). Thus, the token would be rejected by backend.
As per default, the frontend-XSUAA would not add any other client to the aud.
BUT: if a scope is granted from backend to frontend, then the backend-XSUAA will be added to the aud.
That’s why our scenario works fine (hopefully).

Finally, above screenshot shows the actual functionality of our backend: the audit logging that writes the user information to the console.
We can see how useful it is to receive the user-info in the backend via token exchange.

2. Native Code

At this point we’re already done with the tutorial.
However, if you cannot use the @sap/xssec library, you might be interested in alternative sample code below, which uses the native node.js module.
To adapt our scenario, we just need to replace the function _doTokenExchange with below code.
In addition, we need to add the require statement.

const https = require('https')

async function  _doTokenExchange(jwt) {
    const oauthEndpoint = UAA_CREDENTIALS.url 
    return new Promise ((resolve, reject) => {     
       const options = {
          host: oauthEndpoint.replace('https://',''), 
          path: '/oauth/token',
          method: 'POST',
          headers: {
             Authorization: "Basic " + Buffer.from(UAA_CREDENTIALS.clientid + ':' + UAA_CREDENTIALS.clientsecret).toString("base64"),
             'Content-Type': 'application/x-www-form-urlencoded'
          }
       }
 
       const granttype = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
       const data = `grant_type=${granttype}&response_type=token&assertion=${jwt}&scope=backendxsuaa!t14860.backendscope`      
       const req = https.request(options, (res) => {
          let response = ''
          res.on('data', chunk => {
            response += chunk
          })
          res.on('end', () => {
            resolve(JSON.parse(response).access_token)
          })
       })
       req.write(data)
       req.end() 
    })   
 }

Note:
In above sample code, we’re composing the URL manually and we’re adding the scope parameter.
As mentioned earlier, the scope is just a filter and is optional.

Note:
Even in this chapter, we’re hiding any error handling.

Note:
That’s already all for this chapter, nothing more to add in the appendix.

3. Optional: Client Credentials Scenario

As promised in the introduction, to better understand the token exchange, we’d like to compare it to client-credentials flow.
We don’t need to change much in the code.
Let’s quickly go through the changes and leave the full code for the appendix 2.

Note:
We don’t need to recreate everything from scratch, it is enough to update the 2 service instances with new configuration, and to redeploy the frontend app.

3.1. Backend App

The backend app is receiver of the token and it defines a scope and it grants the scope to the frontend. In case of app2app scenario, where one application talks to another one, without involved user, the OAuth flow “client credentials” is used. In this case, the grant mechanism is different.

3.1.1. Update Service Instance

To enable grant in client credentials scenario, we need to modify the configuration of our XSUAA instance.

{
    "xsappname": "backendxsuaa",
    "tenant-mode": "dedicated",
    "scopes": [{
        "name": "$XSAPPNAME.backendscope",
        "grant-as-authority-to-apps" : ["$XSAPPNAME(application, frontendxsuaa)"]

We can see the difference in the grant statement. It is a different property name:
grant-as-authority-to-apps
The value remains the same.

The command (executed from directory c:\tex\backend) for updating our existing service instance:
cf update-service texBackendXsuaa -c backend-security.json

Note:
No need to restage the application

3.1.2. Backend Application

No change required.

3.1.3. Deploy

No deploy required.

3.2. Frontend App

We don’t change much in the frontend app.

3.2.1. Update Service Instance

Again, we need to update the XSUAA instance with a configuration that supports the grant mechanism specific to client-credentials:

    "xsappname": "frontendxsuaa",
    "scopes": [{"name": "$XSAPPNAME.frontendscope"}],
    "role-templates": [{
            "name": "FrontendUserRole",
            "scope-references": [   "$XSAPPNAME.frontendscope", 
                                    "$XSAPPNAME(application,backendxsuaa).backendscope"]}],
    "authorities":["$XSAPPNAME(application,backendxsuaa).backendscope"],

We can see the new statement which is used to accept a granted scope: authorities The value is again same like earlier. It can be a dedicated scope, or just a wildcard to accept all scopes.
To update the service instance, we jump into directory C:\tex\frontend and execute the following command:
cf update-service texFrontendXsuaa -c frontend-security.json

3.2.2. Core Application

We need to adapt the code in order to use a different function for requesting a JWT token based on client credentials.
Note that in this scenario we’re NOT doing token exchange, it is just for comparison.

app.get('/app', passport.authenticate('JWT', {session: false}), async (req, res) => {
    . . .    
    //fetch token with client credentials and call backend
    const clicreJwtToken = await _doClientCredentials()      
    await _callService(clicreJwtToken)

    // print token info to browser
    const htmlUser = _formatClaims(auth.getAppToken()) // login token
    const htmlCliCre = _formatClaims(clicreJwtToken) // fetched with client credentials

    res.send(`  <h4>Claims from user login</h4>${htmlUser}
                <h4>Claims from client credentials</h4>${htmlCliCre}`)

The snippet shows that we are NOT using the user-token for fetching a new token with client-credentials.
The code for fetching a JWT token with client credentials flow:

xssec.requests.requestClientCredentialsToken(null, UAA_CREDENTIALS, null, null, (error, token)=>{
   resolve(token)

It is again very simple, because we’re using the convenience library xssec.
We only need to pass the credentials which we get from the binding of our app to XSUAA.
Filtering scope is not supported here.
At the end, we print the relevant claims to the browser window, such that we can compare them with the token exchange scenario.

3.2.3. Create Approuter

No change here.

3.2.4. Deploy

it is enough to push one module, the modified server app:
cf push texfrontend

After deploy we access our app and we can see the browser window:

In the screenshot we can see that there’s no user information in the JWT token which we’ve fetched via client credentials flow.
Nevertheless, the token is valid and is accepted by the backend application.
The log of the backend application shows that it was called properly, the scope-check has been successfully passed.
The audit logging doesn’t have user data for logging.

Summarizing, the client-credentials flow is not the right flow for our example scenario, where the user information is essential.

4. Optional:  cleanup

For your convenience, find below the commands that can be used to delete all artifacts created during this tutorial.
(Frankly, cleanup shouldn’t optional…)

cf d -r -f texfrontend
cf d -r -f texfrontendrouter
cf d -r -f texbackend
cf ds -f texBackendXsuaa
cf ds -f texFrontendXsuaa

Summary

We have a user-centric app with user-login.
The app calls a protected API that requires different JWT token.
To fetch a token for the API, we use token exchange, because user info is preserved.
Fetching a JWT via token exchange works like a normal token-fetch, but requires additional URL parameter assertion (JWT token) and the special value for grant_type.

In todays blog post we also learned how to grant scope between 2 different instances of XSUAA.
We learned that the grant mechanism is different for user-centric scenario and for app2app.

Short summary:
Token Exchange is used to exchange user-token for other token and preserve user information.

Next tutorial:
Use Token Exchange destination

Quick Guide

Grant scope in user-centric scenario:

"scopes": [{
    ...
    "granted-apps" : ["$XSAPPNAME(application, frontendxsuaa)"]

Accept scope and add to role, in user-centric scenario:

"role-templates": [{
    "scope-references": [   "$XSAPPNAME(application,backendxsuaa).backendscope"]
...
"foreign-scope-references": ["$XSAPPNAME(application,backendxsuaa).backendscope"],

Native request with token exchange and (optional) filtering scope:

...
   method: 'POST',
   headers: {
      Authorization: "Basic " + Buffer.from(UAA_CREDENTIALS.clientid + ':' + UAA_CREDENTIALS.clientsecret).toString("base64"),
      'Content-Type': 'application/x-www-form-urlencoded'
... 
const granttype = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
const data = `grant_type=${granttype}&response_type=token&assertion=${jwt}&scope=backendxsuaa!t14860.backendscope`      
...
req.write(data)

Convenient request with token exchange and (optional) filtering scope:

xssec.requests.requestUserToken(jwt, CREDENTIALS, null, 'xsappname!t14860.scope', null, null, (error, token)=>{

Links

Tutorial for same scenario but using destination configuration with token exchange type.
Tutorial for token exchange in multitenant application.
Tutorial for granting scopes.
Same, but across subaccount borders.
OAuth for dummies, explained by Dummy.
Info about the content of JWT tokens, explained in my dummy way.
Introduction and first dummy steps with approuter.

Spec for token exchange, i.e. request access token via JWT bearer token

Documentation in Cloud Foundry about token exchange.
Github for node-fetch module to execute HTTP requests.
npm site for xssec library.

Reference for xs-security.json file in the SAP Help portal.


Appendix 1: Token Exchange Scenario Code

backend-security.json

{
    "xsappname": "backendxsuaa",
    "tenant-mode": "dedicated",
    "scopes": [{
        "name": "$XSAPPNAME.backendscope",
        "granted-apps" : ["$XSAPPNAME(application, frontendxsuaa)"]
    }]
}

manifest.yml

---
applications:
  - name: texbackend
    path: app
    memory: 64M
    routes:
    - route: texbackend.cfapps.sap.hana.ondemand.com
    services:
      - texBackendXsuaa

app

package.json

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

server.js

const express = require('express')
const app = express();
const xssec = require('@sap/xssec')
const xsenv = require('@sap/xsenv')
const passport = require('passport')
const JWTStrategy = xssec.JWTStrategy
const UAA_CREDENTIALS = xsenv.getServices({myXsuaa: {tag: 'xsuaa'}}).myXsuaa
passport.use('JWT', new JWTStrategy(UAA_CREDENTIALS))
app.use(passport.initialize())
app.use(express.json())

// start server
app.listen(process.env.PORT)

// Endpoint to be called by frontend app
app.get('/endpoint', passport.authenticate('JWT', {session: false}), (req, res) => {
    const auth = req.authInfo  
    if (! auth.checkScope(UAA_CREDENTIALS.xsappname + '.backendscope')) {
        res.status(403).end('Forbidden. Missing authorization.')
    }      

    // The fake audit logging
    console.log(`===> [AUDIT] backend called by user '${auth.getGivenName()}' from subdomain '${auth.getSubdomain()}' with oauth client: '${auth.getClientId()}'`)

    res.json({'jwtToken': auth.getAppToken()})
})

Frontend

frontend-security.json

{
    "xsappname": "frontendxsuaa",
    "tenant-mode": "dedicated",
    "scopes": [
        {
            "name": "$XSAPPNAME.frontendscope",
            "description": "Scope required for human users to login to homepage"
        }
    ],
    "role-templates": [
        {
            "name": "FrontendUserRole",
            "description": "Role for end users, allows to login to app",
            "scope-references": [   "$XSAPPNAME.frontendscope", 
                                    "$XSAPPNAME(application,backendxsuaa).backendscope"]
        }
    ],
    "foreign-scope-references": ["$XSAPPNAME(application,backendxsuaa).backendscope"],
    "oauth2-configuration": {"token-validity": 5}
}

manifest.yml

---
applications:
  - name: texfrontend
    path: app
    memory: 64M
    routes:
    - route: texfrontend.cfapps.sap.hana.ondemand.com
    services:
      - texFrontendXsuaa
  - name: texfrontendrouter
    routes:
    - route: texfrontendrouter.cfapps.sap.hana.ondemand.com
    path: approuter
    memory: 128M
    env:
      destinations: >
        [
          {
            "name":"destination_frontend",
            "url":"https://texfrontend.cfapps.sap.hana.ondemand.com",
            "forwardAuthToken": true
          }
        ]      
    services:
      - texFrontendXsuaa

app

package.json

{
  "dependencies": {
    "@sap/xsenv": "latest",
    "@sap/xssec": "^3.2.12",
    "express": "^4.17.1",
    "node-fetch": "2.6.2",
    "passport": "^0.4.0"
  }
}

server.js

const https = require('https')
const fetch = require('node-fetch')
const express = require('express')
const app = express();
const xssec = require('@sap/xssec')
const passport = require('passport')
const JWTStrategy = xssec.JWTStrategy
const xsenv = require('@sap/xsenv')
const UAA_CREDENTIALS = xsenv.getServices({myXsuaa: {tag: 'xsuaa'}}).myXsuaa
passport.use('JWT', new JWTStrategy(UAA_CREDENTIALS))
app.use(passport.initialize())
app.use(express.json())

// start server
app.listen(process.env.PORT)

// display frontend 
app.get('/app', passport.authenticate('JWT', {session: false}), async (req, res) => {
    const auth = req.authInfo    
    if (! auth.checkScope(UAA_CREDENTIALS.xsappname + '.frontendscope')) {
        res.status(403).end('Forbidden. Authorization for homepage access is missing.')
    }      

    //do token exchange and call backend
    const userJWT = auth.getAppToken()
    const texJwtToken = await _doTokenExchange(userJWT)      
    await _callService(texJwtToken)

    // print token info to browser
    const htmlUser = _formatClaims(userJWT) // login token
    const htmlTEX = _formatClaims(texJwtToken) // after token exchange

    res.send(`  <h4>Claims from user login</h4>${htmlUser}
                <h4>Claims from token exchange</h4>${htmlTEX}`)
})


/* HELPER */

async function _callService (jwtToken){  
    const options = {
       headers: { Authorization: 'Bearer ' + jwtToken }
    }
    const serviceURL = 'https://texbackend.cfapps.sap.hana.ondemand.com/endpoint'
    const response = await fetch(serviceURL, options)
    const responseJson = await response.json()
    return responseJson
}
 
async function _doTokenExchange (jwt){
    return new Promise ((resolve, reject) => {
        xssec.requests.requestUserToken(jwt, UAA_CREDENTIALS, null, 'backendxsuaa!t14860.backendscope', null, null, (error, token)=>{
            resolve(token)
        })  
    })  
}

function _formatClaims(jwtEncoded){
    const jwtBase64Encoded = jwtEncoded.split('.')[1]
    const jwtDecodedAsString = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii')
    const jwtDecodedJson = JSON.parse(jwtDecodedAsString)

    const claims = new Array()
    claims.push(`client_id: ${jwtDecodedJson.client_id}`)
    claims.push(`<br>name: ${jwtDecodedJson.given_name} ${jwtDecodedJson.family_name}</br>`)
    claims.push(`email: ${jwtDecodedJson.email}`)
    claims.push(`<br>xs.system.attributes: ${JSON.stringify(jwtDecodedJson['xs.system.attributes'])}</br>`)
    claims.push(`scopes: ${jwtDecodedJson.scope}`)
    claims.push(`<br>aud: ${jwtDecodedJson.aud}</br>`)
    return claims.join('')
}

Approuter

package.json

{
    "dependencies": {
        "@sap/approuter": "latest"
    },
    "scripts": {
        "start": "node node_modules/@sap/approuter/approuter.js"
    }
}

xs-app.json

{
  "authenticationMethod": "route",
  "routes": [
    {
      "source": "^/tofrontend/(.*)$",
      "target": "$1",
      "destination": "destination_frontend",
      "authenticationType": "xsuaa"
    }
  ]
}

Appendix 2: Client Credentials Scenario Code

backend-security.json

{
    "xsappname": "backendxsuaa",
    "tenant-mode": "dedicated",
    "scopes": [{
        "name": "$XSAPPNAME.backendscope",
        "grant-as-authority-to-apps" : ["$XSAPPNAME(application, frontendxsuaa)"]
    }]
}

manifest.yml

---
applications:
  - name: texbackend
    path: app
    memory: 64M
    routes:
    - route: texbackend.cfapps.sap.hana.ondemand.com
    services:
      - texBackendXsuaa

app

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 UAA_CREDENTIALS = xsenv.getServices({myXsuaa: {tag: 'xsuaa'}}).myXsuaa

const express = require('express')
const app = express();
const xssec = require('@sap/xssec')
const passport = require('passport')
const JWTStrategy = xssec.JWTStrategy
passport.use('JWT', new JWTStrategy(UAA_CREDENTIALS))
app.use(passport.initialize())
app.use(express.json())


// start server
app.listen(process.env.PORT)

// Endpoint to be called by frontend app
app.get('/endpoint', passport.authenticate('JWT', {session: false}), (req, res) => {
    const auth = req.authInfo  
    if (! auth.checkScope(UAA_CREDENTIALS.xsappname + '.backendscope')) {
        res.status(403).end('Forbidden. Missing authorization.')
    }      

    // The fake audit logging
    console.log(`===> [AUDIT] backend called by user '${auth.getGivenName()}' from subdomain '${auth.getSubdomain()}' with oauth client: '${auth.getClientId()}'`)

    res.json({'jwtToken': auth.getAppToken()})
})

Frontend

frontend-security.json

{
    "xsappname": "frontendxsuaa",
    "tenant-mode": "dedicated",
    "scopes": [
        {
            "name": "$XSAPPNAME.frontendscope",
            "description": "Scope required for human users to login to homepage"
        }
    ],
    "role-templates": [
        {
            "name": "FrontendUserRole",
            "description": "Role for end users, allows to login to app",
            "scope-references": [   "$XSAPPNAME.frontendscope", 
                                    "$XSAPPNAME(application,backendxsuaa).backendscope"]
        }
    ],
    "authorities":["$XSAPPNAME(application,backendxsuaa).backendscope"],
    "oauth2-configuration": {"token-validity": 5}
}

manifest.yml

---
applications:
  - name: texfrontend
    path: app
    memory: 64M
    routes:
    - route: texfrontend.cfapps.sap.hana.ondemand.com
    services:
      - texFrontendXsuaa
  - name: texfrontendrouter
    routes:
    - route: texfrontendrouter.cfapps.sap.hana.ondemand.com
    path: approuter
    memory: 128M
    env:
      destinations: >
        [
          {
            "name":"destination_frontend",
            "url":"https://texfrontend.cfapps.sap.hana.ondemand.com",
            "forwardAuthToken": true
          }
        ]      
    services:
      - texFrontendXsuaa

app

package.json

{
  "dependencies": {
    "@sap/xsenv": "latest",
    "@sap/xssec": "^3.2.12",
    "express": "^4.17.1",
    "node-fetch": "2.6.2",
    "passport": "^0.4.0"
  }
}

server.js

const https = require('https')
const fetch = require('node-fetch')
const express = require('express')
const app = express();
const xssec = require('@sap/xssec')
const passport = require('passport')
const JWTStrategy = xssec.JWTStrategy
const xsenv = require('@sap/xsenv')
const UAA_CREDENTIALS = xsenv.getServices({myXsuaa: {tag: 'xsuaa'}}).myXsuaa
passport.use('JWT', new JWTStrategy(UAA_CREDENTIALS))
app.use(passport.initialize())
app.use(express.json())

app.listen(process.env.PORT)

app.get('/app', passport.authenticate('JWT', {session: false}), async (req, res) => {
    const auth = req.authInfo    
    if (! auth.checkScope(UAA_CREDENTIALS.xsappname + '.frontendscope')) {
        res.status(403).end('Forbidden. Authorization for homepage access is missing.')
    }      

    //fetch token with client credentials and call backend
    const clicreJwtToken = await _doClientCredentials()      
    await _callService(clicreJwtToken)

    // print token info to browser
    const htmlUser = _formatClaims(auth.getAppToken()) // login token
    const htmlCliCre = _formatClaims(clicreJwtToken) // fetched with client credentials

    res.send(`  <h4>Claims from user login</h4>${htmlUser}
                <h4>Claims from client credentials</h4>${htmlCliCre}`)
})


/* HELPER */

async function _callService (jwtToken){  
    const options = {
       headers: { Authorization: 'Bearer ' + jwtToken }
    }
    const serviceURL = 'https://texbackend.cfapps.sap.hana.ondemand.com/endpoint'
    const response = await fetch(serviceURL, options)
    const responseJson = await response.json()
    return responseJson
}
 
async function _doClientCredentials (){
    return new Promise ((resolve, reject) => {
        xssec.requests.requestClientCredentialsToken(null, UAA_CREDENTIALS, null, null, (error, token)=>{
            resolve(token)
        })  
    })  
}

function _formatClaims(jwtEncoded){
    const jwtBase64Encoded = jwtEncoded.split('.')[1]
    const jwtDecodedAsString = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii')
    const jwtDecodedJson = JSON.parse(jwtDecodedAsString)

    const claims = new Array()
    claims.push(`client_id: ${jwtDecodedJson.client_id}`)
    claims.push(`<br>name: ${jwtDecodedJson.given_name} ${jwtDecodedJson.family_name}</br>`)
    claims.push(`email: ${jwtDecodedJson.email}`)
    claims.push(`<br>xs.system.attributes: ${JSON.stringify(jwtDecodedJson['xs.system.attributes'])}</br>`)
    claims.push(`scopes: ${jwtDecodedJson.scope}`)
    claims.push(`<br>aud: ${jwtDecodedJson.aud}</br>`)
    return claims.join('')
}

Approuter

package.json

{
    "dependencies": {
        "@sap/approuter": "latest"
    },
    "scripts": {
        "start": "node node_modules/@sap/approuter/approuter.js"
    }
}

xs-app.json

{
  "authenticationMethod": "route",
  "routes": [
    {
      "source": "^/tofrontend/(.*)$",
      "target": "$1",
      "destination": "destination_frontend",
      "authenticationType": "xsuaa"
    }
  ]
}

Assigned Tags

      12 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Volker Buzek
      Volker Buzek

      what an excellent deep dive into the authentication- and authorization-realm!

      any chance to top that off with bringing in an on-prem SAP system, connected via CC and protected via Principal Propagation? That‘s such a common real-world scenario, but still looks like too much voodoo black magic to many out there 🙂

      Author's profile photo Holger Bruchelt
      Holger Bruchelt

      This is really a great blog post, thanks for sharing Carlos Roggan . For the SAP Cloud Connector integration I can also recommend the blog post by Martin Raepple Principal propagation in a multi-cloud solution between Microsoft Azure and SAP Business Technology Platform (BTP), Part II: Connecting the system on-premise | SAP Blogs

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hi Holger Bruchelt thanks for your feedback - anyways great to see you here 😉 and thanks for adding this useful link!
      Cheers,
      Carlos

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hello Volker Buzek ,
      Thank you so much for your feedback !
      And thank you as well for the proposal on subsequent deep dive - I read rarely such proposals and it is a good idea. I hope I can come back on it.
      For the moment, I have a similar blog post here, it helped me to understand the connectivity service and cloud connector, although there is no principal propagation in it:
      https://blogs.sap.com/2020/08/07/sap-cloud-platform-how-to-call-onprem-system-from-node.js-app-via-cloud-connector/

      Kind Regards,
      Carlos

      Author's profile photo neo song
      neo song

      Hello, Carlos Logan.

      It's a very good blog series.

      These days, SAP HANA is creating nodejs backend "CRUD" service criteria,

      which are configured to control Hana DB data to the front end (sapui5).

      Your writing was a great help to me. Thanks once again.

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hello neo song ,
      Thank you very much for your feedback - it is really helpful to know that it has been helpful - I wish you enjoy the BTP and please continue commenting in the Community 😉

      Author's profile photo Kevin Hu
      Kevin Hu

      Is it possible to include role collection creation in the xs-security.json instead of doing it manually in this scenario?

      Thanks.

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hello Kevin Hu ,

      yes, it is possible, I mentioned it somewhere....
      Here's a snippet for you:

      {
          "xsappname": "rctestxsappname",
          "scopes": [
            {
              "name": "$XSAPPNAME.myTestScope"
            }
          ],
          "role-templates": [
            {
              "name": "MyTestRole",
              "scope-references": [ "$XSAPPNAME.myTestScope" ]
            }
          ],
          "role-collections": [
            {
              "name": "MyTestRC",
              "role-template-references": [ "$XSAPPNAME.MyTestRole" ]
            }
          ]
      }

      After create instance of xsuaa, the role collection will be available in the cockpit and it will be configured with the role.

      Kind Regards,

      Carlos

      Author's profile photo Kevin Hu
      Kevin Hu

      Carlos, thanks for the reply.

      One more question, I noticed your previous blog here

      https://blogs.sap.com/2020/06/02/how-to-call-protected-app-from-external-app-as-external-user-with-scope/

      The scenario is very similar to this Token exchange one, but what is the main difference?

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hello Kevin Hu ,
      sorry for the late reply - have just coincidentally seen your comment.
      The difference is that the other blog has nothing to do with token exchange.
      That other blog explains the "grant" mechanism, which is necessary in app2app scenario, where we cannot assign a role, because there is no user.
      Kind Regards,
      Carlos

      Author's profile photo Jian Luo
      Jian Luo

      Very nice! I would say this clearly gives me end 2 end view on how JWT token is generated and validated from UI to Backend!

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hello Jian Luo , thanks for your feedback, really good to know that it helps!