Skip to Content
Technical Articles
Author's profile photo Carlos Roggan

SAP Cloud Platform Backend service: Tutorial [28]: Scenario: Approuter>Node>TokenExchange>API

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

 

Assigned Tags

      5 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Wolfgang Röckelein
      Wolfgang Röckelein

      Hi Carlos,

      thanks for this very helpful blog!

      One question: how long is the token forwarded from the approuter to the node.js application valid? Ie could the application store the token and eg use it to access the backend data each morning and send the user a notification depending on the data without the user logging in once more? What is this token technically in oauth terms?

      And one wish: pleased describe the same scenario just with multitenancy added!

      Thanks,

      Wolfgang

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hello Wolfgang Röckelein,
      thank you very much for your feedback!
      As far as I understand, the OAuth 2 spec doesn’t describe the “format” of a token, leaving it up to the “Authorization Server”, but usually it will be a JWT token.
      Token contains base64 encoded information about user and roles, and some more metadata.
      Metadata include the duration of validity.
      I’ve briefly mentioned it in one of my OAuth blogs
      So when creating your xsuaa instance, you can control the duration of validity. with this param:
      “oauth2-configuration”: {
      “token-validity”: 7200
      }
      However, for security aspects, I guess that the token shouldn’t be stored (although I assume you mean to store it in memory only?)
      Access tokens are meant to be short-living, so extending the duration is probably not the way to go
      But there’s another interesting info:
      along with the access token, the Auth Server sends a “refresh_token” property
      This one is long-lived (e.g. several days).
      You can use it to directly request a new access token from xsuaa (without the need of asking the user for login)
      Its duration can be controlled via parameter
      “refresh-token-validity”
      I assume that this mechanism is used by the TokenExchange destination internally, so you don’t need to do anything yourself, safely relying on the destination ?
      Hope this has answered your questions
      Reference about xsuaa params
      BTW, I’m afraid, wrt multitenancy you need to search the community
      Kind Regards,
      Carlos

      Author's profile photo Wolfgang Röckelein
      Wolfgang Röckelein

      Hi Carlos,

      thanks.

      Regarding oauth token type, I meant eg access token vs. refresh token.

      So the node.js application receives only an access token and not the refresh token?

      Regarding storing I meant of course encrypted storage. Cf eg https://answers.sap.com/questions/654706/job-scheduler-on-cloud-foundry-to-access-onpremise.html for elaboration on a use case.

      But as I understand this, the token exchange would not help me here cause it would fail if the access token provided has already timed out. The token exchange simply covers the case that the access token is for the node.js application but an access token for the backend service is needed.

      Regards,

      Wolfgang

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hello Wolfgang,
      you’re right:

      Destination needs a valid access_token in order to get a refresh_token itself.
      Which makes sense…

      Also, approuter does the token forwarding as authorization header.
      I mean, only the token value is sent, no metadata, no refresh_token etc.
      Means, you can connect an oauth-protected target-service with approuter and it will work, because the target-service is called with valid authorization header and bearer token.

      In my scenario, the exchange will always work, because the node-application can only be called via approuter, so the token (which is sent to destination) is always valid.

      In your scenario, you would need to implement your own authorization, such that you get the whole response from xsuaa, which then includes the refresh_token, which you can store.
      You can have a look this tutorial. The sample code doesn’t use destination yet, so you can add the destination and remove the approuter (or at least, remove the auth part from approuter)

      Kind Regards,
      Carlos

       

      Author's profile photo Former Member
      Former Member

      Hi Calros ,

      We are trying to expose Odata service from an on premise system  to a Data Intelligence suite hosted on Cloud Foundry ( which worked with a direct link to the server:host name of the Odata service )  . In order to have a secure transmission of data i created a created a node js app to expose the data as an application over cloud  , as mentioned in the link below  (https://developers.sap.com/tutorials/cp-connectivity-consume-odata-service-approuter.html#d867b44b-9eeb-4616-853b-492693debae2)

      Now the situation is that the redirect via the URL for this new app works and i can retrieve data as an XML format.When we try to access data via Data intelligence or data hub it doesn't work. So i tried to replicated the process of retrieving this data manually via postman .

      The node JS app does the rerouting and the xsuaa , and then the connectivity / Destination takes care of the rest redirections .

      When i tried this process via Postman , I could get the Oauth tokens which were called via the grant type = code (using the redirect etc )  , however when i call this application URL  via this Token , the application doesn't return any data . I always receive a Status code 200 which does a redirect to the authentication URL .

      Do you have any clue why this is happening ? Am i doing something wrong here . It would be great if you could point something out regarding the approach which we took for exposing the Odata as a proxy app - maybe there is another approach which could be used !

      Best Regards ,

      Pranav