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)
})
})
})
}