This blog is part of a
series of tutorials explaining how to write serverless functions using the Functions-as-a-Service offering in
SAP Cloud Platform Serverless Runtime
Quicklinks:
Quick Guide
Sample Code
Introduction
In the
previous blog we’ve learned the basics about protecting a function with OAuth
What we didn’t learn: our function didn’t require a
scope
In this blog, let’s learn how to enforce a scope in the JWT token and
- assign the scope to our user and call the function (REST client)
- call the function from a client application (node.js)
Overview
Small recap:
In the previous tutorial, we’ve created a very silly small function and we’ve used framework functionality to protect it with small configuration snippet.
Furthermore, we created a very basic instance of XSUAA and connect the function to it
That was enough to protect our function with OAuth 2.0
The FaaS Runtime took care of rejecting HTTP requests which didn't send a valid JWT token
The FaaS Runtime used the connected XSUAA instance for validation
That was OK.
Fair enough for the beginning
Now, in today's tutorial we’re going to
- add a scope
- and in the function we check if the scope exists in the JWT
These are the steps we're going to cover:
- Create XSUAA
- Create Function
Implement scope check
- Call Function in user centric scenario
- Call Function with client app
Prerequisites
1. Create Instance of XSUAA
We need an instance of XSUAA which is configured with a
scope
We can
update the existing one, or create a new one
In my example, I create a new instance with the following security configuration in a file called
xs-security_faas_scope.json
{
"xsappname" : "xsappforfaaswithscope",
"tenant-mode" : "dedicated",
"scopes": [
{
"name": "$XSAPPNAME.scopeforfunction",
"description": "Scope required for accessing function"
}
],
"role-templates": [ {
"name" : "FunctionRoleTemplate",
"description" : "Role for serverless function",
"default-role-name" : "RoleForFunction",
"scope-references" : ["$XSAPPNAME.scopeforfunction"]
}],
"authorities":["$XSAPPNAME.scopeforfunction"]
}
Explanation:
scopes
We define a scope.
For us, thie means: when calling the function, a JWT token is not enough. The caller must have the scope. If yes, the scope is contained in the token
However, in the security configuration we only define the scope, such that XSUAA knows about it
If things go well (see below) XSUAA will issue a JWT token that contains the scope
However, XSUAA doesn't enforce the scope.
role-templates
A scope is nothing that can be assigned to a human user. For that, we need to define a “role”, along with “role-template”. That can be found in the Cloud Cockpit and an admin can assign the role to users
authorities
This attribute is meant for non-human users, for client apps, in a client-credentials scenario
With this statement, we accept that scope. A client application bound to XSUAA will get the scope in the JWT token
Create XSUAA
We create an instance of XSUAA and we use above JSON parameters
Can be done in the Cloud Cockpit, or on command line:
cf cs xsuaa application xsuaa_faas_scope -c xs-security_faas_scope.json
Create Service Key
As usual, we need a service key in order to reference it from FaaS and also from REST client (see below)
cf csk xsuaa_faas_scope xsuaa_faasscope_servicekey
See
Appendix for all commands
2. Create Function
After creating an instance of XSUAA service, we need to register it in FaaS, and use it in the function definition.
We’ve learned that in a
previous tutorial
Register the service in faas
For your convenience, find here the necessary commands
Always need to login first to Cloud Foundry and/or FaaS client
xfsrt-cli login
The convenient command to register a service interactively (the command line client will propose existing services, so we can choose. Precondition: only service instances with service key are proposed)
xfsrt-cli faas service register
After registration, in the console, we get the info which we need to specify in
faas.json:
the service key and the GUID of the service instance
Use the service in FaaS project
faas.json
"services": {
"xsuaa-with-scope": {
"type": "xsuaa",
"instance": "<your GUID of service instance>",
"key": "xsuaa_faasscope_servicekey"
}
}
See
Appendix for full
faas.json
Configure Security
As learned in previous blog post, with below setting, we tell the FaaS runtime that we want them to enforce a valid JWT token
"triggers": {
"prot": {
"type": "HTTP",
"function": "prot-func-with-scope",
"auth": {
"type": "xsuaa",
"service": "xsuaa-with-scope"
Enforce scope
As mentioned earlier: the above setting will have the following consequence:
Whenever our function is called with HTTP trigger, the request must contain a valid JWT token
Otherwise the call is rejected by the FaaS runtime (with proper status code and error message) and our function code is not even invoked.
As such, the FaaS runtime enforces the authentication.
But it cannot take care of authorization in a generic way
For example, a function might have the following logic:
If EDIT scope is available, the function may accept POST requests, otherwise only GET
Such logic has to be implemented manually by us
We have to look into the JWT token, read the available scopes and check if the one which we require, is there.
The framework offers a convenience method which decodes the JWT token (if available)
We can then access the JWT payload as JSON object
const jwtToken = event.decodeJsonWebToken()
const jwtScopes = jwtToken.payload.scope
In order to check the scope, we need to know the exact scope name.
Background:
We defined the scope name as
$XSAPPNAME.scopeforfunction
The value of the variable
$XSAPPNAME is generated on the cloud platform, as such we have to ask at runtime for the exact value.
The exact value is contained in the service key
And the service key is registered in FaaS
As such, we can ask FaaS for the xsuaa service
Then access the
xsappname property
const xsuaaCredentials = await context.getServiceCredentialsJSON('xsuaa-with-scope')
const requiredScope = xsuaaCredentials.xsappname + '.scopeforfunction'
Based on above 2 code snippets we have the needed information:
Which scope do we expect (for whatever business case)
Which scope is contained in the JWT token
In our example, we require a scope for calling the function.
To check if the scope is sent, we just look into the array of available scopes
If not available, we reject the request.
In our case, the correct status code is 403, because use is authenticated, but lacking authorization
In order to set the status, we need to use the HTTP API (see previous
blog post)
Below snippet puts the snippets together:
const xsuaaCredentials = await context.getServiceCredentialsJSON('xsuaa-with-scope')
const requiredScope = xsuaaCredentials.xsappname + '.scopeforfunction'
// read the JWT token and check required scope
const jwtToken = event.decodeJsonWebToken()
const jwtScopes = jwtToken.payload.scope
if(! jwtScopes.includes(requiredScope)){
// fail with 403
const response = event.http.response // must be enabled in faas.json
response.writeHead(403, {
...
See
Appendix for full sample code
Note:
Before using the HTTP API, it must be enabled, which is done in the
faas.json
Note:
SAP provides security libraries and there’s a little helper function, recommended to use for the scope check
The function I’m talking about:
@Sisn/xssec: checkScope(scope)
Please refer to Links section
In my examples, I’d like to keep the code free from dependencies (as there might be changes, etc), so I’m not using it. Please forgive
But for your convenience, I've created little sample code based on the library. See
Appendix
Deploy function
To deploy the function, you might find this command useful (run from project directory)
xfsrt-cli faas project deploy
3. Call function in user centric scenario
After deploy, we want to test the security of our function
- If we call the function in browser –> error
Reason: no JWT token at all
- If we call the function in REST client with OAuth flow –> error
Reason: JWT token available, but doesn't contain the required scope
Solution: To call the function we need to assign the required role to our user
The Role
In Cloud Foundry, authorization is controlled by means of Role Based Access Control (RBAC)
In the first step, we created an instance of XSUAA, based on a security configuration which defined a scope and a role-template.
After creating our instance of XSUAA, our role-definition has been added to the list of roles in the cockpit.
We can view it there

But viewing is not enough, we have to add the role to our user.
Assign Role to User
First, we create a new
Role Collection, dedicated for your function
In the Cockpit, go to your subaccount->Security-> Role Collections
Second, we have to add the desired role to the new Role Collection
So we “edit” the new role collection and add our new role
Then press “save”

Third, we need to add our Role Collection to the Identity Provider (IDP)
Go to menu entry
Trust Configuration
Click on default IDP
Enter the E-Mail Address of your user
Click “show Assignments”
Then “Assign Role Collection”

Test the function with human user
Now that our user has the required role, we can call the function
We don’t have a user interface in our scenario, so we use a REST client
See
here for a detailed description about how to call an OAuth protected endpoint with REST client
Short description:
1. fetch a JWT token
2. Use token when calling our function
In my example, I’m using Postman which helps to do both steps in one request
To configure postman request, we need to view the service key of our xsuaa instance which we created above
To view the service key, we can use the following command (alternatively, use cockpit)
cf service-key xsuaa_faas_scope xsuaa_faasscope_servicekey
Back in Postman, we have to configure the request as follows
Method: GET
URL: The endpoint of our function, we get the info during deploy, or with xfsrt-cli faas project get
e.g.
https://...-faas.....functions.xfs.cloud.sap/prot/
Don’t forget the slash at the end
Authorization: Oauth 2.0
Access Token:
press "Get new Access Token"
"Get Access Token" Dialog:
Grant Type: Password Credentials
Access Token URL: we copy the “url” property from the service key and append
/oauth/token
Example:
https://<subaccount>.authentication.....hana.ondemand.com/oauth/token
Username: <your cloud user>
Password: <your cloud user password>
Client ID: copy the value of property
clientid from service key
Client Secret: copy the value of property
clientsecret from service key
Client Authentication: choose "Send as Basic Auth header"
Then press “Request Token” on the dialog
After getting the token response, press “Use Token”
Then, back in the main Postman window, press “Send” to send the request to the Function endpoint
As a result, we get our success message, which proves that the scope was included in the JWT token

Now we want to do the negative test, to check our function implementation works correctly and if the correct status code is sent
To do so, we have to remove the role from our user.
We can simply unassign the role collection in the "Trust Configuration"
Afterwards, we need to fetch a new JWT token via “Get New Access Token”
Then send the request again to call the function without scope
The result is
403, as coded by us, and the response message shows that the scope is not contained in the JWT token

Note:
In this scenario, we’re using only one instance of XSUAA
The function uses an instance of XSUAA to protect its endpoint
The user who calls the function, uses the same ClientID to call the function
This is OK, because we as developers of the function trust our user
In case that 2 instances of XSUAA are required, please refer to
this blog for more information
4. Call function in client credentials scenario
Now we want to call the protected function from an application
Again, we’re using only one instance of XSUAA.
We bind our application to the same instance which is used to protect the function
(Sure, in a future blog, we’ll describe a scenario with different XSUAA instances)
In client credentials scenario, we may wonder: how to assign the required scope to the calling app?
We cannot assign it in the cockpit like we did for our (human) user.
To answer this question, somebody wrote
this useful blog post
Our example is a bit different from above blog post, because both the function and the client app are "bound" to the same instance of XSUAA
The solution in our example:
When creating an instance of XSUAA, we defined a parameter called
authorities
This is interesting and might be a new learning for us:
The function is attached to the XSUAA instance and the client app is bound to the same instance
As such, we would expect that the xsuaa instance (== OAuth client) trusts itself, because it is the same instance
In fact, trust is there. But the scope is not automatically there
In a client-credentials scenario, the scope must be explicitly granted, just like we assign a role to a user
To assign the scope to the client itself, no “grant” statement is required, but the
authorities statement is necessary
This statement declares: our application wants to take the scope
"authorities":["$XSAPPNAME.scopeforfunction"]
Create Client app
The only intention of our app is to call the function – and to send a JWT token which contains the required scope
Well... we’ve just learned how to get the required scope into the JWT token
Nothing special needs to be done in the app code
Just fetch a JWT token and then call the function
So we can skip explanations.
We can go ahead and copy the code from the
Appendix
Note:
Don’t forget to replace the
FUNC_HOST URL with the URL of your function
Note:
You might need to change the app name in the
manifest.yml
Then deploy the clientapp with
cf push
Finally, invoke the endpoint of our function caller app and hope to get a success message
http://functioncallerapp.cfapps.sap.hana.ondemand.com/call

To test the negative scenario, we have to remove the
authorities statement from our
xs-security_faas_scope.json file and then execute an update-service command in Cloud Foundry
cf update-service xsuaa_faas_scope -c xs-security_faas_scope.json
Note:
Instead of deleting the “authorities” statement, we can invalidate it by changing the name of the scope to anything non-existing
e.g.
"authorities":["$XSAPPNAME.scopeforfunctionXX"]
In fact, after running update-service, we can invoke our endpoint again and we get the expected error message:
It says that the function responded with 403, which was expected
Summary
We’ve learned that the FaaS runtime uses XSUAA for token validation, there’s no automatic check of available scopes
To manually check the available scopes, we can access the decoded JW T token
We need to access the scope prefix from xsuaa service key
In client-credentials scenario, we need to use the
authorities statement to assign the scope to ourselves
Quick Guide
Few Code snippets
// access registered xsuaa service in order to get the value of variable $XSAPPNAME
const xsuaaCredentials = await context.getServiceCredentialsJSON('xsuaa-with-scope')
const requiredScope = xsuaaCredentials.xsappname + '.scopeforfunction'
// the decoded JWT token
const jwtTokenDecoded = event.decodeJsonWebToken()
// the raw JWT token
const jwtTokenRaw = event.auth.credentials
Links
Appendix 1: Console Commands
- cf cs xsuaa application xsuaa_faas_scope -c xs-security_faas_scope.json
- cf csk xsuaa_faas_scope xsuaa_faasscope_servicekey
cf service-key xsuaa_client xsuaa_client_servicekey
- cf update-service xsuaa_faas_scope -c xs-security_faas_scope.json
- xfsrt-cli login
- xfsrt-cli faas service register
- xfsrt-cli faas project deploy
- xfsrt-cli faas project get
- xfsrt-cli faas project logs
Appendix 2: All sample project files
For your convenience, I'm pasting the structure of my example project.
Folder names can be changed

Protected Function
These are the files required for the function
They are located in the function project folder, with "lib" subfolder
xs-security_faas_scope.json
{
"xsappname" : "xsappforfaaswithscope",
"tenant-mode" : "dedicated",
"scopes": [
{
"name": "$XSAPPNAME.scopeforfunction",
"description": "Scope required for accessing function"
}
],
"role-templates": [ {
"name" : "FunctionRoleTemplate",
"description" : "Role for serverless function",
"default-role-name" : "RoleForFunction",
"scope-references" : ["$XSAPPNAME.scopeforfunction"]
}],
"authorities":["$XSAPPNAME.scopeforfunction"]
}
faas.json
{
"project": "protectedwithscope",
"version": "0.0.1",
"runtime": "nodejs10",
"library": "./lib",
"functions": {
"prot-func-with-scope": {
"module": "functionImpl.js",
"httpApi": true,
"services": ["xsuaa-with-scope"]
}
},
"triggers": {
"prot": {
"type": "HTTP",
"function": "prot-func-with-scope",
"auth": {
"type": "xsuaa",
"service": "xsuaa-with-scope"
}
}
},
"services": {
"xsuaa-with-scope": {
"type": "xsuaa",
"instance": "d8819eda-41e6-40b0-9286-0afa1a59d12c",
"key": "xsuaa_faasscope_servicekey"
}
}
}
package.json
{}
functionImpl.js
module.exports = async function (event, context) {
// access registered xsuaa service in order to get the value of variable $XSAPPNAME
const xsuaaCredentials = await context.getServiceCredentialsJSON('xsuaa-with-scope')
const requiredScope = xsuaaCredentials.xsappname + '.scopeforfunction'
// read the JWT token and check required scope
const jwtToken = event.decodeJsonWebToken()
const jwtScopes = jwtToken.payload.scope
if(! jwtScopes.includes(requiredScope)){
// HTTP API required for configuring response
const response = event.http.response // must be enabled in faas.json
response.writeHead(403, {
'Content-Type': 'text/plain'
});
response.write(`Unauthorized: required scope '${requiredScope}' not found in JWT. Availbale scopes: '${jwtScopes}' ;-(`);
response.end();
} else{
return `Reached protected function. Scope check successful: required scope '${requiredScope}' found in JWT. Availbale scopes: '${jwtScopes}'`
}
}
Function Caller Client App
These are the files required for a little node.js app which calls the protected function
Make sure to adapt the URL of the function in the application code
manifest.yml
The application is bound to the same instance of XSUAA which we registered in FaaS
---
applications:
- name: functioncallerapp
memory: 128M
buildpacks:
- nodejs_buildpack
services:
- xsuaa_faas_scope
server.js
const express = require('express')
const app = express()
const https = require('https');
const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES)
const CREDENTIALS = VCAP_SERVICES.xsuaa[0].credentials
//oauth
const CLIENTID = CREDENTIALS.clientid;
const SECRET = CREDENTIALS.clientsecret;
const OAUTH_HOST = CREDENTIALS.url;
const FUNC_HOST = 'https://abcd1234-...-...-faas-...-functions.xfs.cloud.sap' // adapt URL
const FUNC_TRIGGER = 'prot'
app.get('/call', function(req, res){
// call function endpoint
doCallEndpoint(FUNC_HOST, FUNC_TRIGGER, OAUTH_HOST, CLIENTID, SECRET)
.then((response)=>{
res.status(202).send('Successfully called remote endpoint. Function response: ' + response);
}).catch((error)=>{
res.status(500).send(`Error while calling remote endpoint: ${error} `);
})
});
// helper method to call the endpoint
const doCallEndpoint = function(host, endpoint, token_uri, client_id, client_secret){
return new Promise((resolve, reject) => {
return fetchJwtToken(token_uri, client_id, client_secret)
.then((jwtToken) => {
const options = {
host: host.replace('https://', ''),
path: `/${endpoint}/`,
method: 'GET',
headers: {
Authorization: 'Bearer ' + jwtToken
}
}
const req = https.request(options, (res) => {
res.setEncoding('utf8')
const status = res.statusCode
const statusMessage = res.statusMessage
let response = ''
res.on('data', chunk => {
response += chunk
})
res.on('end', () => {
if (status !== 200 && status !== 201) {
return reject(new Error(`Failed to call function. Message: ${status} - ${statusMessage} - ${response}`))
}
resolve(response)
})
});
req.on('error', (error) => {
return reject({error: error})
});
req.write('done')
req.end()
})
.catch((error) => {
reject(error)
})
})
}
// jwt token required for calling REST api
const fetchJwtToken = function(token_uri, client_id, client_secret) {
return new Promise ((resolve, reject) => {
const options = {
host: token_uri.replace('https://', ''),
path: '/oauth/token?grant_type=client_credentials&response_type=token',
// path: '?grant_type=client_credentials&response_type=token',
headers: {
Authorization: "Basic " + Buffer.from(client_id + ':' + client_secret).toString("base64")
}
}
https.get(options, res => {
res.setEncoding('utf8')
let response = ''
res.on('data', chunk => {
response += chunk
})
res.on('end', () => {
try {
const jwtToken = JSON.parse(response).access_token
resolve(jwtToken)
} catch (error) {
return reject(new Error('Error while fetching JWT token'))
}
})
})
.on("error", (error) => {
return reject({error: error})
});
})
}
// Start server
app.listen(process.env.PORT || 8080, ()=>{})
package.json
{
"dependencies": {
"express": "^4.16.3"
}
}
Appendix 3: Sample code using @Sisn/xssec
package.json
{
"dependencies": {
"@sap/xssec": "latest"
}
}
functionImpl.js
const xssec = require('@sap/xssec')
const util = require('util');
const createSecurityContext = util.promisify(xssec.createSecurityContext);
module.exports = async function (event, context) {
// access registered xsuaa service in order to get the value of variable $XSAPPNAME
const xsuaaCredentials = await context.getServiceCredentialsJSON('xsuaa-with-scope')
const requiredScope = xsuaaCredentials.xsappname + '.scopeforfunction'
const jwtTokenRaw = event.auth.credentials
const securityContext = await createSecurityContext(jwtTokenRaw, xsuaaCredentials)
const jwtScopes = securityContext.getTokenInfo().getPayload().scope
if(! securityContext.checkScope(requiredScope)){
// HTTP API required for configuring response
const response = event.http.response // must be enabled in faas.json
response.writeHead(403, {
'Content-Type': 'text/plain'
});
response.write(`Unauthorized: required scope '${requiredScope}' not found in JWT. Availbale scopes: '${jwtScopes}'`);
response.end();
} else{
return `Reached protected function. Scope check successful: required scope found in JWT. All availbale scopes: '${jwtScopes}'`
}
}