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);
});
Hi Carlos,
Cool blog!
Now I am facing how can I get authorization code via certificate e.g. my test s user certificate within chrome browser?
Really confused and cannot find any resource on this specific topic..
Do you have any solution or clue here?
Thanks and happy holiday
Alex