Skip to Content
Technical Articles

Writing Function-as-a-Service [9]: How to call OAuth-protected endpoint

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 function code we can do basically anything that is possible in Node.js
Sounds good, but sometimes there are hurdles
Like the (necessary but tedious) security aspects
If we do a demo and call a REST service endpoint from within a function:
then that endpoint will always be open, to make the demo small and nice
However, usually, REST endpoints are protected and users need to authenticate with user/password
-> That’s easy, just provide basic auth
Other endpoints require an apiKey
-> No prob, we set it as header
Prof endpoints are protected with OAuth authentication
-> Here it gets more complicated, but I showed it in previous blog
Really prof endpoints are OAuth protected and require authorization
-> This is a bit more tricky and requires some config in addition to the OAuth flow

Why tricky? Just assign a role to the user…?
Stop:
A function is not a user and we cannot assign the required role to the function like we would assign it to a user
Luckily, this tutorial shows how to achieve that:
Write a function that calls an endpoint that is protected with OAuth and requires scope

With other words, about the tricky part:
How to assign a scope to a function in client-credentials flow

Remember OAuth 2.0:

In order to access a resource (REST endpoint), we need a JWT token.
We get that token from “Authorization Server” (XSUAA)
To get it, there are several “grant types”:
– “Resource Owner Password Credentials”: human user authenticates with password (login screen) and token contains scopes based on his roles
– “client credentials”: from app to app, no login
(see here for more info)

In our scenario, the function runs without user context
As such we’re talking about “client credentials” flow
We don’t have user password.
We cannot get a user password by opening a login screen

Of course, we know how to obtain a JWT token from XSUAA service instance
But that token doesn’t contain the scope which is required by the protected REST endpoint

So what needs to be done in order to get a JWT token containing a certain scope?
The trick is in the configuration of the xsuaa instances

Note:
Of course, it is possible to call a function from e.g. a UI5 application, which is user centric and has user login
In that scenario, the function would receive the JWT token from the UI5 application, so nothing needs to be done
So no tutorial required
This tutorial is for the non-user-centric scenario, no UI, no user, no fun

Prerequisites

– Access to SAP Cloud Platform, productive account. Trial is (currently) not supported
– Instance of Serverless Runtime (see Preparation blog)
– Cloud Foundry CLI installed locally (see same Preparation blog)
– Functions CLI available locally (see still same link)
– Node.js installed
– Basic knowledge of writing functions in SAP Cloud Platform serverless runtime

Overview

In the first part of this tutorial, we’re creating a very basic app which exposes an endpoint which is protected with OAuth and which requires a certain scope
In the second part, we’re creating a Function which tries to call that endpoint

Part 1: The Protected App
Create xsuaa, define scope
Create app, check scope

Part 2: The Calling Function
Create xsuaa, define authority
Create Function, do OAuth flow

Preparation: Create Project Structure

In our example, we’re using one root Project C:\tmp_faas_callsafe
containing 2 working directories,
for the secure app C:\tmp_faas_callsafe\safeapp
and for the Function C:\tmp_faas_callsafe\unsafefunction
So let’s create the folder structure:

Beautiful.

Part 1: The Protected App

We use Node.js and express to create a simple app which provides a REST endpoint
We use passport and @sap/xssec to protect it with OAuth 2.0
We use manual code to enforce the required authorization
We use an instance of XSUAA service for managing the OAuth flow

1.1. Security Configuration

Security for our app is carried out by an instance of XSUAA
We configure it with the following params

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

This content can be pasted during instance creation in cloud cockpit, but for better handling, we store above content in a file with a name of our choice.
In my example, the file has a silly name to avoid confusion: xs-security-safe.json

Short explanation

xsappname
The name of the security artifact (Internally, it is treated like an application).
Security artifacts to be distinguished by XSUAA and by us.

scope
We declare: we require that anybody who calls our endpoint should have special permission.
To distinguish “our” permission from others, we give a name to “our” special permission
This is the “scope”.
Note: we aren’t defining a “role”. This means that no human user will be able to call our endpoint
In our tutorial we don’t need it
So let’s keep the file short and focus on the essential

“grant-as-authority-to-apps”
This is the essential line:
Instead of assigning a “role” to a human user, we assign it to an application.
Because that’s what we want to learn in this tutorial: how a function from FaaS can call a protected app that requires a special “scope”
Our app is protected with oauth and requires a scope.
And “grant” is the mechanism how an external application (FaaS in our case) can get the permission/authorization to call the protected endpoint
Note:
We have to understand that we’re not assigning the scope to the name of a deployed Node.js application (or whatever)
What we’re explicitly writing here: the name of the security artifact. The name of the security artifact is the value of the property xsappname
To avoid confusion, I always give stupid names to all artifacts, that makes it easier to distinguish.
As such, if a name starts with xsapp…, it is the name of a security artifact
In the current tutorial, we will create a second instance of xsuaa and we will name it xsappforfaas
So that’s what we write here: We “grant” the new scope to the security artifact with name xsappforfaas
Note:
The syntax for defining the permitted application:
“$XSAPPNAME(<service_plan>, <name_of_xsapp_attribute>)”
Note:
In the section below, there will be a note to type the value of xsappname exactly like here
Or we can say: here we have to type the value of xsappname exactly like we’ll type it in the section below
Doesn’t matter: No typos allowed for xsappname attribute, otherwise we’ll get errors

Anyways, creating a file is not enough: this isn’t a deployment artifact, it is just a source file containing the security description
So now we have to go ahead and create an instance of XSUAA service
As everybody knows, it can be done in the cockpit (then point to this file) or on command line (and point to this file)
In my example, we jump into C:\tmp_faas_callsafe\safeapp and run the following command

cf cs xsuaa application xsuaaforsafetyapp -c xs-security-safe.json

1.2. Create Application

Now it is time to protect our created application
Sorry, my fault
I wanted to say:
Now it is time to create our protected application

We’ve already created the project structure, we only need to copy and paste the file contents from the appendix

As usual, the app does nothing.
Only be there
And be protected

So what we have to do:
In order to “be there” we have to do nothing
In order to “be protected” we have to do two things:

We have to “wish” that the caller has that scope when he or it calls our endpoint
We have to enforce that the caller has the “wished” scope

This is done manually by checking the scope and refusing the access to our endpoint
To manually check the scope, we have to do 2 things:

We have call a helper to do the work (check scope)
We have to act upon the result:
If we don’t like the result of the scope check, we say it

This has been confusing.
Let’s try again:

The “passport” node module does the authentication check before our code is reached

const jwtStrategy = new JWTStrategy(xsuaaCredentials)
passport.use(jwtStrategy)
...
app.use(passport.authenticate('JWT', { session: false }));

The “passport” module itself is configured with specific xsuaa implementation for OAuth
As such, passport can check if a JWT token is present and if it is valid
Afterwards, our endpoint-implementation is invoked and we can check manually if the JWT token contains the scope that we wish.
We use a helper method for that check:

const fullScopeName = xsuaaCredentials.xsappname + '.scopeformysafety'
if(! req.authInfo.checkScope(fullScopeName)){
   return res.status(403).json({
      message: "We don't like the scopes in the JWT token"

Our error response is not polite, but clear

1.3. Deploy and Run the App

We create a manifest.yml file according to the appendix section.
It might be required to change the app name in the manifest, in case it is already taken

In the manifest, we declare that we want to bind the app to the instance of XSUAA service which we created above
Like that, our app has access to the XSUAA server which is necessary to protect our app with OAuth 2.0

We deploy the app to our SAP Cloud Platform space, located in the same subaccount like our serverless runtime instance

After deployment, we can try to invoke our endpoint in the browser

https://safetyapp.cfapps.eu10.hana.ondemand.com/securedEntry

The response is Unauthorized with status code 401
This response sounds quite polite, so it is not ours… it is provided by the FWK which we’ve used
Since we’ve invoked the endpoint in the browser, there’s no JWT token present at all, so our call is immediately rejected

1.4. Small Recap

We’ve created an instance of XSUAA service
We’ve configured it with our desired scope and grant
We’ve created an application with an endpoint, to be secured
We’ve bound it to the instance of XSUAA, such that XSUAA can help in protecting the app
In the code, we’ve added authentication and implemented authorization check

Part 2: The Calling Function

Now we’re coming to the main part of our tutorial:
How to access the secured app from within a function?

Basically, our function has to perform the OAuth flow.
It has to go to XSUAA and ask for a JWT token.
Then go to the safeguarded app and handover the token
And it has to pray that the token contains the required scope

Aha!

How to ensure that the JWT token contains the required scope?
Praying is good – but in this tutorial we’re learning the security mechanism

2.1. Security Configuration

We create an instance of XSUAA service, dedicated for our function.
Again, this xsuaa instance is configured with params stored in a json file.

The file is located in the working directory for the function unsafefunction and we name this file xs-security-faas.json, to make sure that we don’t mix it.

The content is copied from the appendix and looks like this:

{
  "xsappname" : "xsappforfaas",
  "tenant-mode" : "dedicated",
  "authorities":["$XSAPPNAME(application,xsappforsafeapp).scopeformysafety"]
}

The security configuration contains the essential line, to acquire the “authority”.
Basically, once created, the security artifact already HAS the authority “…scopeformysafety”, because it is GRANTED by the other XSUAA security artifact, the other xsapp
Remember the attribute grant-as-authority-to-apps
Nevertheless, the faas-xsapp has to explicitly “accept” the granted scope

The syntax:
$XSAPPNAME(<service_plan>,<name_of_foreign_xsapp>).<name_of_foreign_scope>

To create the XSUAA-instance for FaaS, we jump into the function folder unsafefunction and we execute the following command:

cf cs xsuaa application xsuaaforfaas -c xs-security-faas.json

As usual, in order to use it from FaaS, we need to create a service key:

cf csk xsuaaforfaas servicekeyforfaas

Of course, both can be done from the Cloud Cockpit, if you prefer:

First create the service instance of XSUAA, along with above JSON parameters
Then click on the new instance and create a service key with above name

2.2. Create Function

At this point, we’ve already learned everything we’re supposed to learn in this tutorial:
We’ve done the security configuration which allows to call an app from a function
We could say good-bye now, or good night…  but that wouldn’t be good
So let’s quickly finish

2.2.1. Register Service Instance

We know how to use a service instance from FaaS: There are 2 ways, the second one is more comfortable: The service registration allows to store service credentials in FaaS itself
It can be done in Extension Center, but currently not all service types are supported.
So let’s use the command line

First we do the login

xfsrt-cli login

Then we run the interactive service registration command

xfsrt-cli faas service register

When prompted, we choose the instance xsuaaforfaas from the list

Note:
If you don’t see it, although it is created and shown by the CF CLI, then you’ve probably forgotten to create a service key

After creation, we may view the content of the service key

cf service-key xsuaaforfaas servicekeyforfaas

2.2.2. Declare Usage

Once the service instance is registered in FaaS, we have to declare the usage of it in our faas project.
In faas.json, we need to reference the registered service by its guid
To get that guid, we ask the FaaS itself, with the command

xfsrt-cli faas service list -o yaml

The result of this command prints all registered services and additional information, like the guid:

Note:
The parameter -o yaml (alternatively, -o json) specifies the format of the output

We copy the guid from the service-section
In faas.json, we can now add the following section

  "services": {
    "regxsuaa":{
      "type": "xsuaa",
      "instance": "ab11ab11-ab11-ab11-ab11-ab11ab11ab11",
      "key": "servicekeyforfaas"      
    }
  }

Here we declare that in the project we’re using that concrete registered service, and we assign a name to it

2.2.3. Declare Usage Usage

Next: declare the usage of the declared usage
In faas.json, we’ve declared the usage of the registered service and we’ve assigned an alias: “regxsuaa”
Now we can declare that our function declaration wants to make use of that registered service instance

  "functions": {
    "oauthcallerfun": {
      "module": "caller.js",
      "handler": "doCallProtectedSrv",
      "services": ["regxsuaa"]
    }
}

2.3. Implement Function

Coming to the function code, in folder
C:\tmp_faas_callsafe\unsafefunction\lib
and file
caller.js

High-level overview of what the code is doing:
Nothing

I mean, there’s nothing new in this section, the interesting trick, to get the scenario running, was in the configuration of the xsuaa.
So we can go ahead and copy the code from the appendix

Less-high-level overview:
It does nothing but executing the OAuth flow and calling our app (which does nothing)
It does nothing with the result, only return it

Detailed overview:

1. Access the registered service instance to obtain the credentials for XSUAA Authorization server

const xsuaaServiceKey = await context.getServiceCredentialsString('regxsuaa')	
const xsuaaCredentials = JSON.parse(xsuaaServiceKey)
const oauthUrl = xsuaaCredentials.url
const clientId = xsuaaCredentials.clientid
const clientSecret = xsuaaCredentials.clientsecret	

2. Call the XSUAA server to get a JWT token

const jwtToken = await _fetchJwtToken(oauthUrl, clientId, clientSecret)

3. Use the JWT token to call the silly endpoint

const result = await _callEndpoint(jwtToken)

The implementation of this function is a normal request (using native module to avoid dependency)
The URL of the protected endpoint is hard-coded here

const options = {
   host: 'safetyapp.cfapps.eu10.hana.ondemand.com',
   path: '/secureEntry',
   headers: {
      Authorization: 'Bearer ' + jwtToken
   }
}		
https.get(options, res => {
   let response = ''
   res.on('data', chunk => {
      response += chunk
   })
   res.on('end', () => {
         resolve({message: `Function called protected service. Endpoint response: \n${response}`})
. . .

4. Get the response and return it

return result.message

2.4. Deploy and Run the Function

To deploy the function project we jump into folder C:\tmp_faas_callsafe\unsafefunction
and execute

xfsrt-cli faas project deploy

Afterwards, we can  invoke the function via HTTP trigger.
The URL looks similar like this:

https://abc123-faas-http.tenant.eu10.functions.xfs.cloud.sap/callendpoint/

As a result, we get the response of our function which contains the response of our protected app

Summary

If an OAuth-and-scope-protected app should be called by a function (no user-login), two configuration steps are required:

  • The app has to explicitly “grant” the scope to the function
  • The function has to accept it

These configuration steps are done in the xs-security.json files of the 2 XSUAA instances

In this tutorial, we’ve learned how to protect an app with OAuth 2 and how to enforce a scope. And we’ve learned how to call such protected app from a serverless function

Quick Guide

An app that requires OAuth and scope, can allow to be accessed by other app (without user-login)
The xsappname of consuming app has to be entered

  "scopes": [{
      "grant-as-authority-to-apps" : [ "$XSAPPNAME(application, xsappforfaas)"]

An app or function that wants to call the protected (and granting) app, has to declare the access

"authorities":["$XSAPPNAME(application,xsappforsafeapp).scopeformysafety"]
  • About OAuth: see this blog
  • Manual OAuth flow for Password credentials: explained in detail here
  • Programmatic OAuth flow: see this example
  • Today’s tutorial is based on an older blog where I already explained same topic

Appendix: All Project Files

Here you can find all the code required for this tutorial, ready for copy&paste
You only need to adapt:
– the app name in the manifest
– the instanceID in the faas.json

For your convenience, see here the Project Structure again:

Part 1: The Protected App

These are the files of the app which provides an endpoint and which is called by the function
These files are located in the folder “safeapp”

xs-security-safe.json

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

manifest.yml

---
applications:
- name: safetyapp
  memory: 128M
  buildpacks:
    - nodejs_buildpack
  services:
    - xsuaaforsafetyapp

package.json

{
  "main": "server.js",
  "dependencies": {
    "@sap/xsenv": "latest",
    "@sap/xssec": "latest",
    "express": "^4.16.3",
    "passport": "^0.4.1"
  }
}

server.js

const express = require('express');
const passport = require('passport');
const xsenv = require('@sap/xsenv');
const JWTStrategy = require('@sap/xssec').JWTStrategy;

const xsuaaService = xsenv.getServices({ myXsuaa: { tag: 'xsuaa' }});
const xsuaaCredentials = xsuaaService.myXsuaa; 
const jwtStrategy = new JWTStrategy(xsuaaCredentials)
passport.use(jwtStrategy);

const app = express();
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));

// our protected endpoint
app.get('/secureEntry', function(req, res){       
   console.log('===> Endpoint has been reached. Authentication ok. Now checking authorization')

   const fullScopeName = xsuaaCredentials.xsappname + '.scopeformysafety'
   if(! req.authInfo.checkScope(fullScopeName)){
      return res.status(403).json({
         error: 'Unauthorized',
         message: "We don't like the scopes in the JWT token"
      })      
   }

   res.send('Successfully passed security control' );
});

// start server
app.listen(process.env.PORT, () => {})

Part 2: The Calling Function

These are the files required for the function
They are located in the folder “unsafefunction”

xs-security-faas.json

{
  "xsappname" : "xsappforfaas",
  "tenant-mode" : "dedicated",
  "authorities":["$XSAPPNAME(application,xsappforsafeapp).scopeformysafety"]
}

faas.json

{
  "project": "calloauth",
  "version": "0.0.1",
  "runtime": "nodejs10",
  "library": "./lib",
  "functions": {
    "oauthcallerfun": {
      "module": "caller.js",
      "handler": "doCallProtectedSrv",
      "services": ["regxsuaa"]
    }
  },

  "triggers": {
    "callendpoint": {
      "type": "HTTP",
      "function": "oauthcallerfun"
    }
  },
  "services": {
    "regxsuaa":{
        "type": "xsuaa",
        "instance": "ab12ab12-ab12-ab12-ab12-ab12ab12ab12",
        "key": "servicekeyforfaas"      
    }
  }
}

package.json

{}

caller.js

const https = require('https');

// main entry
const _faasHandler = async function (event, context) {

	const xsuaaServiceKey = await context.getServiceCredentialsString('regxsuaa')	
	const xsuaaCredentials = JSON.parse(xsuaaServiceKey)
	const oauthUrl = xsuaaCredentials.url
	const clientId = xsuaaCredentials.clientid
	const clientSecret = xsuaaCredentials.clientsecret	
	
	// call the target endpoint
	const jwtToken = await _fetchJwtToken(oauthUrl, clientId, clientSecret)
	const result = await _callEndpoint(jwtToken)
	
	return result.message
} 

const _fetchJwtToken = async function(oauthUrl, oauthClient, oauthSecret) {
	return new Promise ((resolve, reject) => {
	   	const options = {
		  	host:  oauthUrl.replace('https://', ''),
		  	path: '/oauth/token?grant_type=client_credentials&response_type=token',
		  	headers: {
			 	Authorization: "Basic " + Buffer.from(oauthClient + ':' + oauthSecret).toString("base64")
		  	}
	   	}
	   	https.get(options, res => {
		  	res.setEncoding('utf8')
		  	let response = ''
		  	res.on('data', chunk => {
				response += chunk
			})
			res.on('end', () => {
				try {
					const responseAsJson = JSON.parse(response)
					const jwtToken = responseAsJson.access_token            
					if (!jwtToken) {
						return reject(new Error('Error while fetching JWT token'))
					}
					resolve(jwtToken)
				} catch (error) {
					return reject(new Error('Error while fetching JWT token'))               
				}
			})
		})
	   .on("error", (error) => {
		  console.log("Error: " + error.message);
		  return reject({error: error})
	   });
	})   
 }
 
const _callEndpoint = async function(jwtToken){
	return new Promise((resolve, reject) => {
		const options = {
			host: 'safetyapp.cfapps.eu10.hana.ondemand.com',
			path: '/secureEntry',
			headers: {
				Authorization: 'Bearer ' + jwtToken
			}
		}		
		https.get(options, res => {
			res.setEncoding('utf8')
			let response = ''
			res.on('data', chunk => {
			  response += chunk
			})
			res.on('end', () => {
				const status = res.statusCode
				if(status < 400){
					resolve({message: `Function called protected service. Endpoint response: \n${response}`})
				}else{
					reject({error: {message: `Error calling endpoint with status ${status}`}})
				}
		  	})
		})
		.on("error", (error) => {
			reject({error: error})
		});		  
	})
}

// export the handler function with readable alias
module.exports = {
	doCallProtectedSrv : _faasHandler
}
Be the first to leave a comment
You must be Logged on to comment or reply to a post.