Skip to Content
Technical Articles

SAP Cloud Platform Backend service: Tutorial [21]: API: called: from: internal: node.js: “authorization code”

This blog is part of a series of tutorials explaining the usage of SAP Cloud Platform Backend service in detail.

Recap
In the previous tutorial, we’ve created a node.js application running in the cloud and calling an OData service, which is hosted by Backend service.
As we know, Backend service APIs are protected with OAuth 2 security mechanism.
Yes, I remember it
As we know, Backend service supports 2 OAuth flows: the grant types “Authorization Code” and “Password Credentials”
Our node.js app was using the “Resource Owner Password Credentials” grant type.
This flow has the advantage that it is a little bit simpler: first request to get the token, second request goes to API
However, it is simpler because the end-user password has to be written in the code.
No prob
I’m tired of checking if I’ve removed my password from the published sample code
Oh, that means that pwd123 is not your real password?
No, I’m not that stupid
Oh I thought that…

OK, in this blog we’re re-writing our app to use “Authorization Code” grant type
I’ve explained the OAuth flow theory in this blog
So in this blog we just need to see the code

Nevertheless, let’s copy and display the nice diagram. Visualizing the flow helps in understanding the code.

Note:
Implementing the “Authorization Code” grant type suits us well because in our app, we’re anyway exposing an endpoint, to trigger the API call

Remember:
One basic requirement of “Authorization Code” grant type is to support redirect, so an endpoint is needed for receiving incoming call which contains the authorization code.

Our app has already one endpoint, we just need to add a second one

Goal

Write a simple node.js application which calls Backend service API like before
NEW: It should be based on OAuth “Authorization Code”, such that a login screen is displayed

Prerequisites

Explained in the same chapter of the previous blog

Preparation

Explained in the same chapter of the previous blog

XSUAA

Explained in the same chapter of the previous blog

Application

Explained in the same chapter of the previous blog

NEW: this code:

'use strict';

const request = require('request-promise');
const express = require('express');
const app = express();
const client_oauth = require('client-oauth2');

// Cloud Foundry environment variables containing xsuaa info
const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES);
const XSUAA_URL = VCAP_SERVICES.xsuaa[0].credentials.url; 
const XSUAA_CLIENTID = VCAP_SERVICES.xsuaa[0].credentials.clientid; 
const XSUAA_CLIENTSECRET = VCAP_SERVICES.xsuaa[0].credentials.clientsecret; 
const VCAP_APPLICATION = JSON.parse(process.env.VCAP_APPLICATION);
const APP_URL = VCAP_APPLICATION.application_uris[0]; 

// OData service, Backend service API
const SERVICE_URL = 'https://backend-service-api.cfapps.eu10.hana.ondemand.com/odatav2/DEFAULT/PRODUCTSERVICE;v=1/Products';

// configure the oauth client instance with required params 
const oauthClient = new client_oauth({
  clientId: XSUAA_CLIENTID,
  clientSecret: XSUAA_CLIENTSECRET,
  accessTokenUri: XSUAA_URL + '/oauth/token',
  authorizationUri: XSUAA_URL + '/oauth/authorize',
  redirectUri:'https://' + APP_URL + '/callback',
  scopes: []
});

// helper method for calling OData service
const _doQUERY = function (serviceUrl, accessToken){
    return new Promise (function(resolve, reject){
        const options = {
            url: serviceUrl,
            resolveWithFullResponse: true ,
            headers: { 
                Authorization: 'Bearer ' + accessToken, 
                Accept : 'application/json'
            }
        };
        
        request(options)
        .then(response => {
            if(response && response.statusCode == 200){
                resolve({responseBody: response.body});
            }
            reject('Error while calling OData service');
        })  
        .catch(error => {
            reject(error);
        });
    });
 };

// the enpoint to access our app
app.get('/mainentry', function (req, res) {
    res.redirect(oauthClient.code.getUri());
});
  
// the endpoint to which xsuaa will redirect
app.get('/callback', function (req, res) { 
    oauthClient.code.getToken(req.originalUrl)
    .then(result => {
        return _doQUERY(SERVICE_URL, result.accessToken);                    
    })
    .then(result => {
        res.send('<h2>OData QUERY Result:</h2>OData service response: <p>' + JSON.stringify(result.responseBody) + '</p>');
    })
    .catch(error => {
        res.send('ERROR: ' + error);
    });    
});
  
// the server
const port = process.env.PORT || 3000;  // cloud foundry will set the PORT env after deploy
app.listen(port, function () {
    console.log('=> Server running. Port: ' + port);
});

 

Explanation:

In our app, we expose an endpoint which we use as main entry point to start the application

app.get('/mainentry', function (req, res) { 
   res.redirect(oauthClient.code.getUri());

What it does is a redirect.
To which target?
As specified by OAuth 2, the app redirects to the “Authorization Server”, in order to get the “Authorization Code”
As required by OAuth 2, the URL looks like this:

https://acc.authentication...com/oauth/authorize?client_id=sbname!t12345&redirect_uri=https://myapp.cfapps...com/callback&response_type=code

The good thing is that we don’t need to compose it manually.
We can use the oauth client:

oauthClient.code.getUri()

Why redirect? Why not just call that URL?
It has to be a redirect, because there’s nothing to fetch with a “request”. We want the “Authorization Server” to show a login screen
OK, user enters the credentials…and then?
If credentials are correct, then the “Authorization Server” will call the redirect-URL which our app has sent.
Has it?
Yes, the URI which was composed by the oauth-client .
How does it know how to compose?
We had given all required information in the constructor:

const oauthClient = new client_oauth({
  clientId: XSUAA_CLIENTID,
  clientSecret: XSUAA_CLIENTSECRET,
  accessTokenUri: XSUAA_URL + '/oauth/token',
  authorizationUri: XSUAA_URL + '/oauth/authorize',
  redirectUri:'https://' + APP_URL + '/callback',

We’ve specified the redirect-URL such that it matches the second endpoint which our app exposes
As such the “Authorization Server” calls the redirect-endpoint of our app and sends the “Authorization Code” as URL parameter

After receiving the incoming request from the “Authorization Server”, our endpoint implementation uses again the oauth-client library to fetch the access token:

app.get('/callback', function (req, res) {
    oauthClient.code.getToken(req.originalUrl)

Note:
In the previous blog, the syntax was different, because we used the “Resource Owner Password Credentials” grant type: client.owner.getToken(usr, pwd)

What is the originalUrl?
To fetch the token with code, the input param should be the “Authorization Code”, but what we pass is  req.originalUrl. The value of originalUrl contains the required param:
e.g.
/callback?code=V2X0H17fvN&state=
And the oauth client knows how to extract it

What happens next?
Nothing new: the Backend service API is called and the result is presented in the browser window

Deploy application

OK, I know how to do that

Run application

After deployment, click the URL which is displayed in the “Application Routes” section, as explained in previous blog 
However, this time we need to add the endpoint path:

/mainentry

Example URL:
https://yourapp.cfapps.eu10.hana.ondemand.com/mainentry

Immediately, the login screen is presented:

The URL shows that the login screen is sent by XSUAA. So the user can safely enter the credentials without need to worry: the credentials aren’t stored in our app and they aren’t sent to Backend service over the net

After entering correct credentials, the XSUAA follows the redirect which we specified in our app coding, and it sends the “Authorization Code” (as URL param)

Note:
BTW, you know that the redirect URL can be configured in the XSUAA service instance, so it doesn’t have to be specified by the app coding

Once the redirect endpoint of our app has been invoked, our app uses the “Authorization Code” to fetch an access token. Then it can call the Backend service API and display the result in the browser

The following screenshot shows that the redirect endpoint of our app (/callback) has been reached
Furthermore, the authorization code is there, like sent by xsuaa:

Note:
The sample code is for learning the OAuth flow. A real web application with user interface would have slightly different handling, e.g. to avoid showing the “Authorization Code” in the URL

Troubleshooting

What to do if you happily deploy everything and try testing and get….. Forbidden ???
See this blog for a Troubleshooting guide

Summary

In this tutorial we’ve learned how it feels to use the grant type “Authorization Code” in a node.js application.
What we wanted to see: the logon screen.
We don’t have user interface in our app, so we can be happy that the authentication is done by XSUAA
We only need to bind the service instance to our app (in Cloud Foundry)

Advertisement

OAuth explained:
Grant type Resource Owner Password Credentials
Grant type Authorization Code

OAuth in real world:
Using REST client: this blog
Using node.js application locally, outside cloud: this blog
Using node.js application deployed to Cloud Foundry: this blog
Using node.js application in Cloud Foundry with service binding to xsuaa: this blog

Overview of blogs about SAP Cloud Platform Backend service
How to configure XSUAA for our use case
If you don’t have a “Space” in SAP Cloud Platform, Cloud Foundry Environment, BETA

Links

Node.js oauth client package documentation: https://www.npmjs.com/package/client-oauth2

Appendix: the files

For your convenience, here are the files that you need:
Don’t forget to adapt the files to your personal env (names, service instance etc)

manifest,yml

---
applications:
- name: myBsConsumerApp
  host: bsappauthcode
  path: appfolder
  memory: 64M
  buildpack: nodejs_buildpack
  services:
    - XsuaaForNodeApp

Note:
The attributes memory and buildpack can be removed.
Buildpack is automatically detected, because we have a package.json file

package.json

{
  "name": "bsconsumer",
  "version": "1.0.0",
  "description": "This app calls Backend service API",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "client-oauth2": "^4.2.4",
    "express": "^4.17.0",
    "request": "^2.88.0",
    "request-promise": "^4.2.4"
  }
}


app.js

'use strict';

const request = require('request-promise');
const express = require('express');
const app = express();
const client_oauth = require('client-oauth2');

// Cloud Foundry environment variables containing xsuaa info
const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES);
const XSUAA_URL = VCAP_SERVICES.xsuaa[0].credentials.url; 
const XSUAA_CLIENTID = VCAP_SERVICES.xsuaa[0].credentials.clientid; 
const XSUAA_CLIENTSECRET = VCAP_SERVICES.xsuaa[0].credentials.clientsecret; 
const VCAP_APPLICATION = JSON.parse(process.env.VCAP_APPLICATION);
const APP_URL = VCAP_APPLICATION.application_uris[0]; 

// OData service, Backend service API
const SERVICE_URL = 'https://backend-service-api.cfapps.eu10.hana.ondemand.com/odatav2/DEFAULT/PRODUCTSERVICE;v=1/Products';

// configure the oauth client instance with required params 
const oauthClient = new client_oauth({
  clientId: XSUAA_CLIENTID,
  clientSecret: XSUAA_CLIENTSECRET,
  accessTokenUri: XSUAA_URL + '/oauth/token',
  authorizationUri: XSUAA_URL + '/oauth/authorize',
  redirectUri:'https://' + APP_URL + '/callback',
  scopes: []
});

// helper method for calling OData service
const _doQUERY = function (serviceUrl, accessToken){
    return new Promise (function(resolve, reject){
        const options = {
            url: serviceUrl,
            resolveWithFullResponse: true ,
            headers: { 
                Authorization: 'Bearer ' + accessToken, 
                Accept : 'application/json'
            }
        };
        
        request(options)
        .then(response => {
            if(response && response.statusCode == 200){
                resolve({responseBody: response.body});
            }
            reject('Error while calling OData service');
        })  
        .catch(error => {
            reject(error);
        });
    });
 };

// the enpoint to access our app
app.get('/mainentry', function (req, res) {
    res.redirect(oauthClient.code.getUri());
});
  
// the endpoint to which xsuaa will redirect
app.get('/callback', function (req, res) { 
    oauthClient.code.getToken(req.originalUrl)
    .then(result => {
        return _doQUERY(SERVICE_URL, result.accessToken);                    
    })
    .then(result => {
        res.send('<h2>OData QUERY Result:</h2>OData service response: <p>' + JSON.stringify(result.responseBody) + '</p>');
    })
    .catch(error => {
        res.send('ERROR: ' + error);
    });    
});
  
// the server
const port = process.env.PORT || 3000;  // cloud foundry will set the PORT env after deploy
app.listen(port, function () {
    console.log('=> Server running. Port: ' + port);
});

 

Be the first to leave a comment
You must be Logged on to comment or reply to a post.