This blog is part of a
series of tutorials explaining the usage of
SAP Cloud Platform Backend service in detail.
In this tutorial, we take another step to create professional applications.
We’re going to write and configure a little scenario, using Backend service as backend.
Main learning: how to configure and use Destination of type “Token Exchange”
Quicklinks:
Destination
App.js
All Project Files
Goal
We want to consume
Backend service
NEW use Destination auth type "Token Exchange"
We want a node app which consumes Backend service
We want to have approuter to handle user login
NEW we want to reuse user login
Solution
Configure App Router to handle user login and forward access token
Use node.js app to add application logic and reuse token
Configure Token Exchange Destination to handle API endpoint
Use Backend service to host and access data
Our scenario looks like this:
Note:
Currently our scenario doesn’t contain any user interface
The detailed flow:
Our end-user opens the URL which is exposed by our App Router, in browser
Our App Router requires authentication with xsuaa,
As such, the user has to enter credentials in a login-screen, presented by XSUAA
Afterwards, our App Router delegates the call to our node.js application, forwarding the access token
Our node.js app stores the token and sends it to the destination service
The destination service is connected to XSUAA instance, to handle authorization
The destination service reads the requested destination configuration and sends the details
Furthermore, the destination is configured to handle the OAuth token
So it sends a valid token for accessing Backend service API
Our node.js app uses this token to call our API in Backend service, which requires token
Finally, our API returns the data, which is sent back to the browser of end-user
Overview:
Following steps are required for implementing the scenario:
Configuration
Create a service instance of Authorization & Trust Management service (XSUAA)
Create a service instance of Destination service
Create a configuration of Destination pointing to URL of API in Backend Service
Development
Create API in Backend service (no coding)
Create App Router application (no coding)
Create node.js application (little bit of node.js coding)
Test
Call App Router endpoint and view payload from Backend service API
Prerequisites
Understand
Backend service and have an API ready to
use
Understand
App Router
Understand
destination service
Optionally: node.js
installed on your machine
Preparation
in this blog we deploy 2 applications, as such we need a kind of “project structure” on our file system
Project Structure
We have one main project folder:
scenario
Here we store 2 configuration files which are not required by the apps at runtime
The project folder contains 2 subfolders, for our 2 apps, which we deploy independently
Each sub folder contains a
manifest.yml file and an
appfolder directory
Each
appfolder contains a
package.json file (both apps are node apps)
Furthermore, the
appfolder of the
approuter contains an
xs-app.json file, to configure the app router
Create files and folders according to the following tree
C:\scenario
destination.properties
xs-security.json
C:\scenario\approuter
manifest.yml
C:\scenario\approuter\appfolder
package.json
xs-app.json
C:\scenario\clientapp
manifest.yml
C:\scenario\clientapp\appfolder
package.json
app.js
1 Create API in Backend service
See
here
2 Create XSUAA service instance
For our scenario we need a new instance of XSUAA service
It will be bound to App Router, for user-login, and to our node.js app
Also the destination service will use it.
The XSUAA instance needs to be configured with scopes:
Destination service requires
uaa.user scope
Backend service requires access token and
AllAccess scope
Both destination service and Backend service will check if the required scope is contained in the JWT token which they receive
As such, when creating an instance of XSUAA service, we have to define the required scopes:
{
. . .
"scopes": [{
"name": "uaa.user",
. . .
"foreign-scope-references": [
"$XSAPPNAME(application,4bf2d51c-1973-470e-a2bd-9053b761c69c,Backend-service).AllAccess"
. . .
We have to set the
tenant-mode as
"dedicated", otherwise the default would be "shared" for service plan "application", which would cause some issues
Now create the service instance:
Choose "application" as service plan
Point to the xs-security.json file to configure the parameters (or copy&paste the content)
Save the instance with name "XsuaaForScenario"
Please refer to
appendix section for the content of
xs-security.json file, to use when creating the XSUAA service instance
More details needed? See
here
3 Create App Router app
We use the App Router to have an entry point to our scenario, adding authentication and authorization.
xs-app.json
We configure our App Router deployment to require authentication via XSUAA
"authenticationType": "xsuaa",
"scope": [ "Backend-service!t6131.AllAccess" ]
This setting will lead to a login screen being displayed when the user opens the route and it should also check that the user has the role which is required to call Backend service API
Our App Router exposes an endpoint which is the root URL:
"source": "^/(.*)$",
See
appendix section for the full
textxs-app.json file content
manifest.yml
In the manifest, we define a dependency to the XSUAA service instance which we created above
services:
- XsuaaForScenario
Furthermore, we define a destination which points to the endpoint of our node.js application (we’ll deploy it later, but I know the URL already…)
env:
destinations: >
[
{
"name": "destination_client",
"url": "https://bsclient.cfapps.eu10.hana.ondemand.com/mainentry",
"forwardAuthToken": true
Important:
We declare that the access token, which is handled by App Router, should be forwarded to the destination
Concrete:
When the App Router application calls the URL specified above (bsclient……../mainentry), then it will send the access token in the "Authorization" header.
Although our node app doesn’t really require it for authentication. But our app can read it and store it
package.json
The third file which is needed in our
textapprouter/appfolder directory is the
textpackage.json
It is just the same like in the other approuter blogs, please refer to the
appendix section for the full content.
For more info about App Router, please check the nice 3 blogs mentioned in the prerequisites section.
Deploy
Deploy the app like you're used to and afterwards read the environment variables of the app (explanation
here or use cf env ApprouterForShop).
Since we’ve defined a binding to xsuaa, we get the clientid/secret in the environment of the deployed app.
We take a note of clientid/clientsecret . We need it below, when creating the destination configuration.
Troubleshooting
If you get an error on deployment: 'No UAA service found', try deleting App Router app and deploy from scratch.
4 Create Destination service instance
A service instance of Destination service is required if we want to read destination configuration from our node app.
Creating an instance doesn’t require any special parameters
Give the name as “mydestination”
(need details to
create instance?)
5 Create Destination of Type "Token Exchange"
One of our goals, within all the series of tutorials, is to learn how to call Backend service
In a typical professional scenario, we have a user-centric application (client app. In our example (still) without UI)
Such application should of course be protected, like every enterprise application
So, when the end-user logs in to the application, an access token is issued.
Then the client application calls the Backend service to get the desired data
Backend service requires again a login with token
But of course, we don’t want to force the end-user to enter his credentials again
Instead, the first login token should be re-used for the call to backend service
This can be achieved with the help of "Token Exchange" destination.
Imagine a UI, where the user presses a button and backend data is fetched. The access token, which is obtained from the first login, might be outdated in the meantime, but it doesn’t matter: it will be sent to the destination service and the destination service will take care and respond with a fresh new token for Backend service
That’s quite cool.
The destination configuration is easy, nothing really new to explain. (see
here for info about creating destination configuration )
Explanation
Name
We enter "BackendServiceAPI"
It is The name of the configuration.
We need this name in our node app, to access the values programmatically
Authentication
We choose "OAuth2UserTokenExchange"
We know that the target URL to which this destination is pointing, requires OAuth authentication
The destination configuration supports it in the following way:
The destination service supports us in fetching a valid token for the target
In this case, an existing User Token is required.
The destination service will send the token to the Authorization server, so it needs
Client ID
Here we enter the value which we’ve noted above, after deploying the App Router.
It is coming from the “credentials” section of the xsuaa instance, to which we’ve bound the App Router (and later also our node.js app)
Client Secret
Same
Token Service URL
The oauth endpoint, also read from the Environment Variables (like clientId).
Explained e.g.
here
In my case:
https://bssubaccount.authentication.eu10.hana.ondemand.com/oauth/token
Token Service Type
Leave "Dedicated", the default
Note:
After creation of destination, you can press “Check Connection”.
The result will be green, to indicate that the destination service is reachable, but response will be 401 because the target URL cannot be called without a valid token
6 Node.js client application
At this point in time, we have the App Router app which is waiting to call our node app, and we have the destination which is waiting to be called by our node app
So now we can create our node.js application, which calls Backend service
Application code
We’ve
learned that we can route directly from App Router to Backend service.
So why do we need a client app?
Yes, we don’t really need it in our playground scenario
Obviously, in a real world scenario we would have a User Interface client which consumes the Backend service API
However, for us it is interesting to learn the mechanisms, how to deal with tokens and the destination
Furthermore, it can make sense to add a node or java application, to have the chance to add some logic: e.g in the code we could check the roles of the user and filter the data accordingly, etc
So what does our app have to do?
1. Expose an endpoint
Like that, it can be called in browser
app.get('/mainentry', function (req, res) {
2. Call Backend service
We know already the javascript code to call the OData service and display the response data in browser.
However, in this blog we deal with Token Exchange Destination.
See below the code.
2.1. Read token
We read and store the token which is received when endpoint is called by App Router (we specified in App Router that the auth token should be forwarded)
How is it received?
It is an "Authorization" header
var authHeader = req.headers.authorization;
If auth header is not present, then the xsuaa instance is not properly configured. Or approuter not correct configured
2.2. Call destination service
Our node app calls the destination service to get the target URL and the target authorization
Before using the destination service, an access token has to be fetched (like
described earlier).
But this time, we’re using the “Token Exchange” destination type, so we have to send the existing access token, which we received from App Router and stored in a variable above.
To send the token, we have to use a special header.
NEW: Token Exchange Header name:
X-user-token
Value is the end-user-token, as forwarded from App Router
Yes, we have to send 2 tokens to get 1 token back.
But it makes sense
One we send to authenticate for destination service, the other we send for token exchange
Advantage: we don’t have to care if token is outdated, etc, we always get a fresh new token.
2.3. Call API
After calling the destination service, we parse the response and read the details of the chosen destination configuration
let bsApiUrl = result.destinationInfo.destinationConfiguration.URL;
let tokenTypeForBsApi = result.destinationInfo.authTokens[0].type; // type is 'bearer'
let tokenForBsApi = result.destinationInfo.authTokens[0].value;
Then we call the API in Backend service (the URL we’ve read above), using the access token received from destination.
See
appendix section for the whole code
Note:
One final comment:
Our node app itself is not protected , so after deployment the endpoint can be called without login.
Furthermore, if our node app should be more intelligent, e.g. to read the authorization of the current end-user after login, we would need more security-related code.
How to implement that? See
here and
here in the SAP Help Portal
Deploy
To deploy the node app, we need the
textmanifest.yml file, the
textpackage.json file and the
textapp.js file, see appendix
In the manifest, we have to specify a binding to the XSUAA service instance which we created above
Furthermore, we have to specify a binding to the destination service instance created above
Need description for
deploy? Or
here
Test
After deploy open the endpoint of the node app in the browser
https://bsclient.cfapps.eu10.hana.ondemand.com/mainentry
It should give the expected error message, as written by us
Run the scenario
The correct way of calling our app to get the data from Backend service is to open the App Router endpoint
Since we’ve configured the endpoint as root (just a slash in the source property), we can just click on the hyperlink in the application details in the cockpit.
https://shelf.cfapps.eu10.hana.ondemand.com/
First thing we see is the user login screen.
Here have to enter the credentials of our Trial user
The user needs to have assigned the role required by Backend service
This role is already required by App Router (as we defined above)
After successful login, we see the data coming from Backend service, displayed in the browser
Happy.
Troubleshooting
Unhappy.
If you get strange errors while testing around, try deleting the apps and re-deploy App Router then Client app
If you retest after re-deploy, you might need to close and reopen the browser window (using private mode)
If you get "Forbidden", try analyzing the JWT tokens
See
this blog for a Troubleshooting guide
Links
Blogs:
Configure
XSUAA
Understanding OAuth
1
Understanding OAuth
2
Understanding App Router
1
Understanding App Router
2
Understanding App Router
3
Node,js app
1
Node,js app
2
Node,js app
3
Node,js app
4
Node,js app
5
SAP Help Portal:
App Router
Configuration
Appendix: All Project Files
Find below all files required to run the scenario.
From here you can copy&paste the content into the prepared project structure
Configuration files
xs-security.json
{
"xsappname": "XsuaaForScenario",
"tenant-mode": "dedicated",
"description": "XSUAA for Backend service and TokenExchange",
"scopes": [
{
"name": "uaa.user",
"description": "Sscope for UAA user, required by Token Exchange Destination Type"
}
],
"foreign-scope-references": [
"$XSAPPNAME(application,4bf2d51c-1973-470e-a2bd-9053b761c69c,Backend-service).AllAccess"
],
"role-templates": [
{
"name": "RoleTemplate_UaaUser",
"description": "Role template for Destination type 'Token Exchange'. Contains UAA user scope.",
"scope-references": [
"uaa.user"
]
}
]
}
Command for creating XSUAA instance with xs-security.json file:
cf create-service xsuaa application XsuaaForScenario -c xs-security.json
destination.properties
#clientSecret=<< Existing password/certificate removed on export >>
Name=AA_BackendServiceAPI
Description=Destination pointing to Productservice
Type=HTTP
clientId=sb-XsuaaForScenario\!t13020
Authentication=OAuth2UserTokenExchange
tokenServiceURL=https\://bssubaccount.authentication.eu10.hana.ondemand.com/oauth/token
ProxyType=Internet
URL=https\://backend-service-api.cfapps.eu10.hana.ondemand.com/odatav2/DEFAULT/PRODUCTSERVICE;v\=1/Products/
tokenServiceURLType=Dedicated
App Router files
manifest.yml
---
applications:
- name: ApprouterForShop
host: shelf
path: appfolder
memory: 128M
services:
- XsuaaForScenario
env:
destinations: >
[
{
"name": "destination_client",
"url": "https://bsclient.cfapps.eu10.hana.ondemand.com/mainentry",
"forwardAuthToken": true
}
]
xs-app.json
{
"authenticationMethod": "route",
"routes": [
{
"authenticationType": "xsuaa",
"scope": [ "Backend-service!t6131.AllAccess" ],
"source": "^/(.*)$",
"target": "$1",
"destination": "destination_client"
}
]
}
package.json
{
"name": "myapprouter",
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
},
"dependencies": {
"@sap/approuter": "^6.0.1"
}
}
Node.js application files
manifest.yml
---
applications:
- name: BsClientApp
host: bsclient
path: appfolder
memory: 128M
services:
- XsuaaForScenario
- mydestination
app.js
'use strict';
const oauthClient = require('client-oauth2');
const request = require('request-promise');
const express = require('express');
const app = express();
const cfenv = require("cfenv");
const appEnv = cfenv.getAppEnv();
const credentials = appEnv.getServiceCreds('mydestination');
const destClientId = credentials.clientid;
const destClientSecret = credentials.clientsecret;
const destUri = credentials.uri; //https://destination-configuration.cfapps.eu10.hana.ondemand.com
const destAuthUrl = credentials.url;//https://bssubaccount.authentication.eu10.hana.ondemand.com
// destination service is protected with OAuth "client credentials"
const _getTokenForDestinationService = function() {
return new Promise((resolve, reject) => {
let tokenEndpoint = destAuthUrl + '/oauth/token';
const client = new oauthClient({
accessTokenUri: tokenEndpoint,
clientId: destClientId,
clientSecret: destClientSecret,
scopes: []
});
client.credentials.getToken()
.catch((error) => {
return reject({message: 'Error: failed to get access token for Destination service', error: error});
})
.then((result) => {
resolve({message:'Successfully fetched token for Destination service.', tokenInfo: result});
});
});
}
// call the REST API of the Cloud Foundry Destination service to get the configuration info as configured in the cloud cockpit
const _getDestinationConfig = function (destinationName, authorizationHeaderValue, existingJwtToken){
return new Promise (function(resolve, reject){
let fullDestinationUri = destUri + '/destination-configuration/v1/destinations/' + destinationName;
const options = {
url: fullDestinationUri,
resolveWithFullResponse: true ,
headers: { Authorization: authorizationHeaderValue,
'X-user-token' : existingJwtToken //header for token exchange
}
};
request(options)
.catch((error) => {
return reject({ message: 'Error occurred while calling Destination service', error: error });
})
.then((response) => {
if(response && response.statusCode == 200){
let jsonDestInfo = JSON.parse(response.body);
return resolve({ message: 'Successfully called Destination service.' , destinationInfo: jsonDestInfo });
}else{
reject('Error: failed to call destination service. ' + response.body);
}
});
});
};
// call OData service (API defined in Backend service)
const _doQUERY = function (serviceUrl, authorizationHeaderValue){
return new Promise (function(resolve, reject){
const options = {
url: serviceUrl,
resolveWithFullResponse: true ,
headers: {
Authorization: authorizationHeaderValue,
Accept : 'application/json'
}
};
request(options)
.then((response) => {
if(response && response.statusCode == 200){
resolve({responseBody: response.body});
}
return reject({ message: 'Error while calling OData service'});
})
.catch((error) => {
reject({ message: 'Error occurred while calling OData service', error: error });
});
});
};
// endpoint will be called by App Router
app.get('/mainentry', function (req, res) {
var authHeader = req.headers.authorization;// contains token to be exchanged by dest srv
if (! authHeader){
res.send('ERROR: No authorization header found. This endpoint should not be called directly. ');
}
var theJwtToken = authHeader.substring(7);// removes 'bearer ' from string
// 1a) get access token for destination service
_getTokenForDestinationService()
.then(result => {
// 1b) call the destination service
return _getDestinationConfig('BackendServiceAPI', result.tokenInfo.tokenType + ' ' + result.tokenInfo.accessToken, theJwtToken);
})
.then(result => {
let bsApiUrl = result.destinationInfo.destinationConfiguration.URL;
let tokenTypeForBsApi = result.destinationInfo.authTokens[0].type; // type is 'bearer'
let tokenForBsApi = result.destinationInfo.authTokens[0].value;
// 2. call BS-API with Url + oauth token retrieved from destination
return _doQUERY(bsApiUrl, tokenTypeForBsApi + ' ' + tokenForBsApi);
})
.then(result => {
res.send('<h2>RESULT of request to Backend service:</h2>OData service response: <p>' + JSON.stringify(result.responseBody) + '</p>');
})
.catch(error => {
res.send('ERROR: ' + error.message + ' - FULL ERROR: ' + error.error);
});
});
// start the server
app.listen(process.env.PORT, function () {
})
package.json
{
"scripts": {
"start": "node app.js"
},
"dependencies": {
"cfenv": "^1.1.0",
"client-oauth2": "^4.2.3",
"express": "^4.16.3",
"request": "^2.88.0",
"request-promise": "^4.2.4"
}
}