Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
CarlosRoggan
Product and Topic Expert
Product and Topic Expert
8,264
Scenario: we're calling an endpoint which is protected with OAuth 2 and scope.
Problem: although fetching a valid JWT token, were getting 403 error, because of missing scope.
Solution: this blog post 👍
Environment: SAP BTP. Cloud Foundry environment, XSUAA

Quicklinks:
Takeaway
Sample Code



Content


0. Introduction
1. Create Backend Application
2. Create Frontend Application
3. Run the Scenario
4. Optional: Manual Request with REST Client
Appendix: Sample Project Files

Introduction


Our scenario is as follows:



We have an application running in the cloud.
I like to call it Backend Application, to make clear that it is consumed by others.
It is protected with OAuth 2.0 and in addition it requires a certain scope.
We know about scopes:
They are defined by the application and they are assigned as “roles” to a user.
Later, when the user does login, he receives a JWT token which contains these scopes.
Fine.

While this sounds like a Happy End, the story has not finished.
We want to call that backend app from a different application.
I like to name it Frontend Application to make clear that it calls the backend app.
When our frontend app calls the backend app, the request fails with
HTTP Status 403 – Forbidden.
Background:
Status 403 means that the request is somewhat correct, in terms of URL and authentication, but the app rejects it, because authorization is wrong.

We don’t understand why it fails?
If we try calling the backend app with human user, then the request works fine.
The JWT token which is sent after user-login contains the required scope.
But when we send the request in app-to-app scenario, then the JWT token doesn’t contain the required scope. And the request fails.

How does it come?
With other words:
The OAuth flows password-credentials or Authorization Code are working fine.
But the client-credentials flow doesn’t work.

The problem is:
In user-centric scenario, the scope is assigned to the user when assigning the corresponding role.

The solution is:
In app2app scenario, this assignment has to be done as well.
To do this assignment, we need to declare it in the security-configuration, aka xs-security.json file.
And that is already all we need to do.
Add one statement to xs-security.json, stop reading this blog (give it a like...) and be happy.
Finally, the Happy End.

While the Happy End has been reached…. The story doesn’t end here.
To make things reproducible, as usual, let’s get our hands dirty and go through a hands-on tutorial.

The hands-on scenario:

We create an instance of XSUAA.
It is used to protect the backend app and to generate a token to access the app.
We create the backend app.
We create the frontend app which calls the backend app.


Below diagram shows the security configuration which defines a scope and makes sure that the issued JWT token contains that scope in client-credentials flow.


Note:
In case you're not interested in frontend app, you may find useful information about manual request with REST client in chapter 4.

Prerequisites


To follow the tutorial, we need:
Access to SAP Business Technology Platform (SAP BTP) Cloud Foundry environment.
Basic knowledge of Node.js
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 give clear guidance, I’m describing my project layout on my Windows machine.
You can of course ignore it or adapt to your needs.

1. Create Project

On filesystem, we create a root project folder C:\scopetest   containing 2 subfolders for the 2 applications.
We create the required files in the folders and copy the content from the Appendix.

C:\scopetest
backend
package.json
server.js
frontend
package.json
server.js
manifest.yml
xs-security.json

Or see this screenshot:


2. Create Instance of XSUAA Service

In our scenario, we have just one instance of XSUAA service.
It is bound to the backend application and used to protect it.
The consumer application (Frontend), is bound as well to the same instance.
Since both apps are bound to the same instance. we don't need to “grant” any scope (see this blog post for detailed explanation about "grant").

Our scenario doesn’t contain any human user – to make the scenario small and clear and focus on the relevant topic: "How to get the scope into the JWT token in case of client-credentials".
As such, we don't need any "role-template".

So in our security descriptor, we call it xs-security.json (the usual name, but not mandatory name) we define a scope with name "myscope".
"scopes": [{
"name": "$XSAPPNAME.myscope"

Defining a scope doesn’t mean that it is also assigned.
In case of human user, the scope is assigned by cloud admin via role and role collection.
In case of client-credentials, the scope is assigned by accepting it.
Since we have only one XSUAA instance, we don’t need to grant it.
But still we need to accept it.
This is done with the authorities statement:
"authorities":["$XSAPPNAME.myscope"]

We use this statement to declare that our OAuth client (with name "myxsapp") accepts the assignment of scope $XSAPPNAME.myscope

That’s already all the learning of today’s tutorial.
Nevertheless, the brave among us, continue with the hands-on session to get the full understanding and happy end experience.
The full file content can be found in the Appendix.

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

1. Create Backend Application


After creating the service instance, we can go ahead and create our backend application.
The app is bound to the instance of XSUAA service, which is used to protect and endpoint of the app.
The app does really nothing but exposing one endpoint, which is protected with OAuth and which requires the scope which we defined in the xs-security.json file.
We check the scope manually in the code and we return a status code 403 if we don't find it in the JWT token.
For protection and scope check, we use the library @Sisn/xssec
app.get('/endpoint', passport.authenticate('JWT', {session: false}), (req, res) => {
. . .
const auth = req.authInfo
if (! auth.checkScope(UAA_CREDENTIALS.xsappname + '.myscope')) {
res.status(403).end(`Forbidden. Missing authorization...`)

In addition, we read the “scope” claim from the JWT token, we print it to the console and we also return it in the response of our endpoint.
This is just for fun and for celebrating the happy end:
app.get('/endpoint', passport.authenticate('JWT', {session: false}), (req, res) => {
const token = req.authInfo.getAppToken()
const tokenInfo = new xssec.TokenInfo(token)
const scopes = JSON.stringify(tokenInfo.getPayload().scope)
. . .
res.send(`Backend was called successfully. Received JWT token with scopes: ${scopes}`)

The full application code can be found in the appendix.

We don’t deploy the backend app yet because we have just one single manifest file for both our apps. We deploy both apps together in chapter 2.

2. Create Frontend Application


The purpose of the present blog post is to solve the problem of missing scope.
This has been already solved by creating the XSUAA service instance with proper configuration.
To test the solution, we can either call our backend app manually with a REST client like postman (See chapter 4).
Or we can create a frontend app which consumes the backend app.
I think this second approach is easier and comes closer to reality.

The frontend application is not protected and doesn’t facilitate user-login.
It is not necessary, because the app is just used to call the protected backend app.
To fetch a JWT token, the app is bound to the same XSUAA instance like the backend app.
We use a helper function of the library @Sisn/xssec to fetch a token via client-credentials flow
xssec.requests.requestClientCredentialsToken(null, UAA_CREDENTIALS, null, null, (error, token)=>{
resolve(token)

Once we have the token, we use it to call the endpoint of backend app.
We use the native https module for the GET request:
host: 'mybackend.cfapps.sap.hana.ondemand.com', 
path: '/endpoint',
headers: {
Authorization: "Bearer " + jwtToken
}

https.get(options, res => {
let response = ''
res.on('data', chunk => {
response += chunk
})
res.on('end', () => {
resolve(response)

Finally, our simple frontend app prints the response to the browser screen.
As mentioned above, the response just prints the scope name which were found in the JWT token.
The full application code can be found in the appendix.

3. Run the Scenario


We can go ahead and jump into folder C:\scopetest and deploy our 2 app modules to Cloud Foundry.
Now we can open our frontend application.
In my example:
https://myfrontend.cfapps.sap.hana.ondemand.com/app

And the result, :


The response contains the scopes claim of the JWT token which we've sent to the backend app.
We can see that the request has been successful and that the required scope has been added to the JWT token..
Like that, we can be convinced that the learning of this tutorial really helped to get the scope into the JWT token.

Happy End…


🥳



4. Optional: Manual Request with REST Client


In case you need to call the backend app with REST client from local laptop, let’s quickly go through this scenario as well.
We use a REST client to execute the same 2 requests that we did in our frontend app:

1. fetch JWT token from XSUAA authorization server
2. use the token to call the endpoint of our backend application.


4.0. Preparation: Credentials

In order to fetch a JWT token from the XSUAA instance, we need credentials, used to authenticate against the XSUAA authorization server.
In case of bound application, we get the credentials in the environment variables of our application (after binding and deployment).
In case of remote app (like a REST client) we can create a service key and view the credentials there.

To view the credentials in the application environment:
cf env mybackend

To create a Service Key:
cf csk myXsuaa sk

To view the content of the Service Key:
cf service-key myXsuaa sk

In both cases, we can see the properties which we need, as follows:


We take a note of these 3 relevant properties and values:

"clientid": "sb-myxsapp!t14860"
"clientsecret": "EP/xZARlzygmKlJ92Uu5sE9x/Go="
"url": "https://test.authentication.sap.hana.ondemand.com"

4.1. Fetch JWT Token with REST Client

Compose a new request in postman with the following settings:

HTTP Verb:
POST

URL:
We need to append the segments
/oauth(/token
to the "url" property.
In my example:
https://test.authentication.sap.hana.ondemand.com/oauth/token

Headers:
Name: Content-Type
Value: application/x-www-form-urlencoded

Request Body:
client_id:sb-myxsapp!t14860
grant_type:client_credentials
response_type:token

Authorization:
Type: Basic
User: value_of_clientid
Password: value_of_clientsecret

Below screenshot tries to show all required settings:


After successful request, we see a response body with several properties.
We copy the value of the access_token property into our clipboard.

Optional: Introspect the JWT Token

The JWT token is a decoded string.-..to keep it secret....... and like all children, we'd like to know what is it, this secret string.....🙊
As such, we use a tool to decode the mystic token, with that strange name, and view the secret content....

And example for such a tool: jwt.io -> debugger
It shows that the desired scope is present:


4.2. Call Service with REST Client

We still have the JWT token in our clipboard, so we can go ahead and compose the second request, which is the call to our backend app endpoint.

HTTP Verb:
GET

URL:
https://mybackend.cfapps.sap.hana.ondemand.com/endpoint

Authorization
Authorization Type: Bearer Token
Authorization Value: just the JWT token in clipboard.
No need to enter the literal “Bearer ”, as it is generated.

In my example:


4.3. Optional: negative test

For reasons of curiosity:
We can remove the "authorities" statement from the xs-security.json file.
Then update the XSUAA service instance and run the scenario:
It should fail with our "Forbidden" error.
The update command:
cf update-service myXsuaa -c xs-security.json

5. Optional:  cleanup


For your convenience, find below the commands to delete all artifacts created during this tutorial:

cf d -r -f myfrontend
cf d -r -f mybackend
cf dsk -f myXsuaa sk
cf ds -f myXsuaa

Summary


We have a scenario where a protected app is bound to one XSUAA.
We want to call that app, so we fetch a JWT token from this XSUAA.
We want that this JWT token contains the required scope.
To achieve this, we need to add the “authorities” statement to the xs-security.json file.

Takeaway


Define scope as usual:
"scopes": [{
"name": "$XSAPPNAME.myscope"

Accept scope (root level property)
"authorities":["$XSAPPNAME.myscope"]

Links


Tutorial for granting scopes.
OAuth for dummies, explained by Dummy.
Info about the content of JWT tokens, explained in my dummy way.
npm site for xssec library.

Reference for xs-security.json file in the SAP Help portal.
Understanding Token Exchange
Security Glossary.



Appendix: Sample Project Files


xs-security.json
{
"xsappname": "myxsapp",
"scopes": [
{
"name": "$XSAPPNAME.myscope"
}
],
"authorities":["$XSAPPNAME.myscope"]
}

manifest.yml
---
applications:
- name: myfrontend
path: frontend
memory: 64M
routes:
- route: myfrontend.cfapps.sap.hana.ondemand.com
services:
- myXsuaa
- name: mybackend
path: backend
routes:
- route: mybackend.cfapps.sap.hana.ondemand.com
memory: 64M
services:
- myXsuaa

Backend Application


package.json

{
"dependencies": {
"@sap/xsenv": "latest",
"@sap/xssec": "latest",
"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 token = req.authInfo.getAppToken()
const tokenInfo = new xssec.TokenInfo(token)
const scopes = JSON.stringify(tokenInfo.getPayload().scope)

const auth = req.authInfo
if (! auth.checkScope(UAA_CREDENTIALS.xsappname + '.myscope')) {
res.status(403).end(`Forbidden. Missing authorization. Received JWT token with scopes: ${scopes}`)
}

res.send(`Backend was called successfully. Received JWT token with scopes: ${scopes}`)
})






Frontend Application


package.json

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

server.js
const https= require('https')
const xssec = require('@sap/xssec')
const express = require('express')
const app = express();
const xsenv = require('@sap/xsenv')
const UAA_CREDENTIALS = xsenv.getServices({myXsuaa: {tag: 'xsuaa'}}).myXsuaa

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

// app endpoint
app.get('/app', async (req, res) => {
const jwtToken = await _fetchToken()
const response = await _callBackend(jwtToken)

res.send(`Response from Backend Application:<br>${response}</br>`)
})


/* HELPER */

async function _fetchToken (){
return new Promise ((resolve, reject) => {
xssec.requests.requestClientCredentialsToken(null, UAA_CREDENTIALS, null, null, (error, token)=>{
resolve(token)
})
})
}

async function _callBackend(jwtToken) {
return new Promise ((resolve, reject) => {
const options = {
host: 'mybackend.cfapps.sap.hana.ondemand.com',
path: '/endpoint',
headers: {
Authorization: "Bearer " + jwtToken
}
}

https.get(options, res => {
let response = ''
res.on('data', chunk => {
response += chunk
})
res.on('end', () => {
resolve(response)
})
})
})
}
6 Comments