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
@Sisn/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
@Sisn/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
OAuth 2.0 Token Exchange
rfc8693
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.
Security Glossary.
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"
}
]
}