Create a Node.js Application via Cloud Foundry Command Line Interface
- How to create a simple “Hello World” application in Node.js
- How to run authentication checks via XSUAA service
- How to run authorization checks by setting XSUAA scopes
Prerequisites
- You have a productive account for SAP Business Technology Platform (SAP BTP). If you don’t have such yet, you can create one so you can try out services for free.
- You have created a subaccount and a space on Cloud Foundry Environment.
- cf CLI is installed locally.
- Node.js and npm are installed locally.
- You have installed an integrated development environment, for example Visual Studio Code.
This tutorial will guide you through creating and setting up a simple Node.js application by using cf CLI. You will start by building and deploying a web application that returns simple data – a Hello World! message, and then invoking this app through another one - a web microservice (application router).
- Step 1
First, you need to connect to the SAP BTP, Cloud Foundry environment with your productive subaccount. Your Cloud Foundry URL depends on the region where the API endpoint belongs to. To find out which one is yours, see: Regions and API Endpoints Available for the CF Environment
In this tutorial, we use
eu20.hana.ondemand.com
as an example.-
Open a command-line console.
-
Set the Cloud Foundry API endpoint for your subaccount. Execute (using your actual region URL):
Bash/ShellCopycf api https://api.cf.eu20.hana.ondemand.com
- Log in to SAP BTP, Cloud Foundry environment: Bash/ShellCopy
cf login
-
When prompted, enter your user credentials – the email and password you have used to register your productive SAP BTP account.
IMPORTANT: If the authentication fails, even though you’ve entered correct credentials, try logging in via single sign-on.
RESULT
Details about your personal SAP BTP subaccount are displayed (API endpoint, user, organization, space).
-
- Step 2
You’re going to create a simple Node.js application.
In your local file system, create a new directory (folder). For example:
node-tutorial
From your Visual Studio Code, open the
node-tutorial
folder.In this folder, create a file
manifest.yml
with the following content:YAMLCopy--- applications: - name: myapp routes: - route: node-1234-aaaa-5678.cfapps.eu20.hana.ondemand.com path: myapp memory: 128M buildpack: nodejs_buildpack
The
manifest.yml
file represents the configuration describing your application and how it will be deployed to Cloud Foundry.IMPORTANT: Make sure you don’t have another application with the name
myapp
in your space! If you do, use a different name and adjust the whole tutorial according to it.Also bear in mind that your application’s technical name (in the route) must be unique in the whole Cloud Foundry landscape. We advice that you use, for example, your subdomain name or part of your subaccount ID to construct the technical name. In this tutorial, we use:
node-1234-aaaa-5678
Inside
node-tutorial
, create a subfoldermyapp
.In the
myapp
directory, execute:Bash/ShellCopynpm init
This will walk you through creating a
package.json
file in themyapp
folder. Press Enter on every step.Then, still in the
myapp
directory, execute:Bash/ShellCopynpm install express --save
This operation adds the
express
package as a dependency in thepackage.json
file.After the installation is completed, the content of
package.json
should look like this:JSONCopy{ "name": "myapp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.18.1" } }
Add engines to the
package.json
file and update thescripts
section. Yourpackage.json
file should look like this:JSONCopy{ "name": "myapp", "version": "1.0.0", "description": "My simple Node.js app", "main": "index.js", "engines": { "node": "14.x.x" }, "scripts": { "start": "node start.js" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.18.1" } }
Inside the
myapp
folder, create another file calledstart.js
with the following content:JavaScriptCopyconst express = require('express'); const app = express(); app.get('/', function (req, res) { res.send('Hello World!'); }); const port = process.env.PORT || 3000; app.listen(port, function () { console.log('myapp listening on port ' + port); });
This creates a simple web application returning a Hello World! message when requested. The
express
module represents the web server part of this application. Once these steps are completed, you can see that theexpress
package has been installed in thenode_modules
folder.Deploy the application on Cloud Foundry. To do that, in the
node-tutorial
directory, execute:Bash/ShellCopycf push
Make sure you always execute
cf push
in the folder where themanifest.yml
file is located! In this case, that’snode-tutorial
.When the staging and deployment steps are completed, the
myapp
application should be successfully started and its details displayed in the command console.Now open a browser window and enter the URL of the
myapp
application (see the route).That is:
https://node-1234-aaaa-5678.cfapps.eu20.hana.ondemand.com
RESULT
Your Node.js application is successfully deployed and running on the SAP BTP, Cloud Foundry environment. A Hello World! message is displayed in the browser.
Which file contains information about the buildpack that provides the runtime on which you deploy your application?
- Step 3
Authentication in the SAP BTP, Cloud Foundry environment is provided by the Authorization and Trust Management (XSUAA) service. In this example, OAuth 2.0 is used as the authentication mechanism. The simplest way to add authentication is to use the Node.js
@sap/approuter
package. To do that, a separate Node.js micro-service will be created, acting as an entry point for the application.In the
node-tutorial
folder, create anxs-security.json
file for your application with the following content:JSONCopy{ "xsappname" : "myapp", "tenant-mode" : "dedicated" }
Create an
xsuaa
service instance namednodeuaa
with planapplication
. To do that, execute the following command in thenode-tutorial
directory:Bash/ShellCopycf create-service xsuaa application nodeuaa -c xs-security.json
Add the
nodeuaa
service inmanifest.yml
so the file looks like this:YAMLCopy--- applications: - name: myapp routes: - route: node-1234-aaaa-5678.cfapps.eu20.hana.ondemand.com path: myapp memory: 128M buildpack: nodejs_buildpack services: - nodeuaa
The
nodeuaa
service instance will be bound to themyapp
application during deployment.Now you have to create a microservice (the application router). Go to the
node-tutorial
folder and create a subfolderweb
.IMPORTANT: Make sure you don’t have another application with the name
web
in your space! If you do, use a different name and adjust the rest of the tutorial according to it.Inside the
web
folder, create a subfolderresources
. This folder will provide the business application’s static resources.Inside the
resources
folder, create anindex.html
file with the following content:HTMLCopy<html> <head> <title>Node.js Tutorial</title> </head> <body> <h1>Node.js Tutorial</h1> <a href="/myapp/">My Application</a> </body> </html>
This will be the start page of the
myapp
application.In the
web
directory, execute:Bash/ShellCopynpm init
This will walk you through creating a
package.json
file in theweb
folder. Press Enter on every step.Now you need to create a directory
web/node_modules/@sap
and install anapprouter
package in it. To do that, in theweb
directory execute:Bash/ShellCopynpm install @sap/approuter --save
In the
web
folder, open thepackage.json
file and replace the scripts section with the following:JSONCopy"scripts": { "start": "node node_modules/@sap/approuter/approuter.js" },
Now you need to add the
web
application to your project and bind the XSUAA service instance (nodeuaa
) to it. To do that, insert the following content at the end of yourmanifest.yml
file.YAMLCopy- name: web routes: - route: web-1234-aaaa-5678.cfapps.eu20.hana.ondemand.com path: web memory: 128M env: destinations: > [ { "name":"myapp", "url":"https://node-1234-aaaa-5678.cfapps.eu20.hana.ondemand.com", "forwardAuthToken": true } ] services: - nodeuaa
Here you can follow the same pattern for constructing the technical name of the
web
application - by using your subaccount ID.In the
web
folder, create anxs-app.json
file with the following content:JSONCopy{ "routes": [ { "source": "^/myapp/(.*)$", "target": "$1", "destination": "myapp" } ] }
With this configuration, the incoming request is forwarded to the
myapp
application, configured as a destination. By default, every route requires OAuth authentication, so the requests to this path will require an authenticated user.In the
myapp
directory, execute the following commands (one by one) to download packages@sap/xssec
,@sap/xsenv
, andpassport
:Bash/ShellCopynpm install @sap/xssec --save npm install @sap/xsenv --save npm install passport --save
Verify that the request is authenticated. Check the JWT token in the request using the
JWTStrategy
provided by the@sap/xssec
package. To do that, go to themyapp
directory, and replace the content of thestart.js
file with the following:JavaScriptCopyconst express = require('express'); const passport = require('passport'); const xsenv = require('@sap/xsenv'); const JWTStrategy = require('@sap/xssec').JWTStrategy; const app = express(); const services = xsenv.getServices({ uaa:'nodeuaa' }); passport.use(new JWTStrategy(services.uaa)); app.use(passport.initialize()); app.use(passport.authenticate('JWT', { session: false })); app.get('/', function (req, res, next) { res.send('Application user: ' + req.user.id); }); const port = process.env.PORT || 3000; app.listen(port, function () { console.log('myapp listening on port ' + port); });
Go to the
node-tutorial
directory and execute:Bash/ShellCopycf push
This command will update the
myapp
application and deploy theweb
application.What’s going on?
As of this point of the tutorial, the URL of the
web
application will be requested instead of themyapp
URL. It will then forward the requests to themyapp
application.When the staging and deployment steps are completed, the
web
application should be successfully started and its details displayed in the command console.Open a new browser tab or window, and enter the URL of the
web
application.That is:
https://web-1234-aaaa-5678.cfapps.eu20.hana.ondemand.com
Enter the credentials for your SAP BTP user.
Both the
myapp
andweb
applications are bound to the same Authorization and Trust Management (XSUAA) service instancenodeuaa
. In this scenario, the authentication is handled by XSUAA through the application router.
RESULT
Click the
My Application
link. The browser window displays Application user:<e-mail>
, where<e-mail>
is the one you have logged to Cloud Foundry with.Check that the
myapp
application is not accessible without authentication. To do that, refresh its previously loaded URL in a web browser – you should get a response401 Unauthorized
.
Which service provides the authentication for your application?
- Step 4
Authorization in the SAP BTP, Cloud Foundry environment is also provided by the XSUAA service. In the previous example, the
@sap/approuter
package was added to provide a central entry point for the business application and to enable authentication. Now to extend the example, authorization will be added through the implementation of ausers
REST service. Different authorization checks will be introduced for the GET and CREATE operations to demonstrate how authorization works. The authorization concept includes elements such as roles, scopes, and attributes provided in the security descriptor filexs-security.json
. For more information, see: Application Security Descriptor Configuration Syntax-
To introduce application roles, open the
xs-security.json
in thenode-tutorial
folder, and add scopes and role templates as follows:JSONCopy{ "xsappname": "myapp", "tenant-mode": "dedicated", "scopes": [ { "name": "$XSAPPNAME.Display", "description": "Display Users" }, { "name": "$XSAPPNAME.Update", "description": "Update Users" } ], "role-templates": [ { "name": "Viewer", "description": "View Users", "scope-references": [ "$XSAPPNAME.Display" ] }, { "name": "Manager", "description": "Maintain Users", "scope-references": [ "$XSAPPNAME.Display", "$XSAPPNAME.Update" ] } ] }
Two roles (
Viewer
andManager
) are introduced. These roles represent sets of OAuth 2.0 scopes or actions. The scopes are used later in the microservice’s code for authorization checks. -
Update the XSUAA service. To do that, in the
node-tutorial
directory execute:Bash/ShellCopycf update-service nodeuaa -c xs-security.json
-
In the
myapp
folder, create a new file calledusers.json
with the following content:JSONCopy[{ "id": 0, "name": "John" }, { "id": 1, "name": "Paula" }]
This will be the initial list of users for the REST service.
-
You need to add a dependency to
body-parser
that will be used for JSON parsing. To do that, in themyapp
folder, execute:Bash/ShellCopynpm install body-parser --save
-
Change the
start.js
file, adding GET and POST operations for theusers
REST endpoint. You can replace the initial code with the following one:JavaScriptCopyconst express = require('express'); const passport = require('passport'); const bodyParser = require('body-parser'); const xsenv = require('@sap/xsenv'); const JWTStrategy = require('@sap/xssec').JWTStrategy; const users = require('./users.json'); const app = express(); const services = xsenv.getServices({ uaa: 'nodeuaa' }); passport.use(new JWTStrategy(services.uaa)); app.use(bodyParser.json()); app.use(passport.initialize()); app.use(passport.authenticate('JWT', { session: false })); app.get('/users', function (req, res) { var isAuthorized = req.authInfo.checkScope('$XSAPPNAME.Display'); if (isAuthorized) { res.status(200).json(users); } else { res.status(403).send('Forbidden'); } }); app.post('/users', function (req, res) { const isAuthorized = req.authInfo.checkScope('$XSAPPNAME.Update'); if (!isAuthorized) { res.status(403).json('Forbidden'); return; } var newUser = req.body; newUser.id = users.length; users.push(newUser); res.status(201).json(newUser); }); const port = process.env.PORT || 3000; app.listen(port, function () { console.log('myapp listening on port ' + port); });
NOTE: Authorization checks are enforced by the
xssec
package in the@sap
directory. To every request object, usingpassport
andxssec.JWTStrategy
, a security context is attached as anauthInfo
object. The resulting request object is initialized with the incoming JWT token. To check the full list of methods and properties of the security context, see: Authentication for Node.js ApplicationsAs defined in the
start.js
file, for HTTP GET requests users need theDisplay
scope to be authorized. For HTTP POST requests, they need to have theUpdate
scope assigned. -
Update the UI to be able to send POST requests. To do that, go to
web>resources
and in theindex.html
file, replace the content with the following code:HTMLCopy<html> <head> <title>JavaScript Tutorial</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script> function fetchCsrfToken(callback) { jQuery.ajax({ url: '/myapp/users', type: 'HEAD', headers: { 'x-csrf-token': 'fetch' } }) .done(function(message, text, jqXHR) { callback(jqXHR.getResponseHeader('x-csrf-token')) }) .fail(function(jqXHR, textStatus, errorThrown) { alert('Error fetching CSRF token: ' + jqXHR.status + ' ' + errorThrown); }); } function addNewUser(token) { var name = jQuery('#name').val() || '--'; jQuery.ajax({ url: '/myapp/users', type: 'POST', headers: { 'x-csrf-token': token }, contentType: 'application/json', data: JSON.stringify({ name: name }) }) .done(function() { alert( 'success' ); window.location = '/myapp/users' }) .fail(function(jqXHR, textStatus, errorThrown) { alert('Error adding new user: ' + jqXHR.status + ' ' + errorThrown); }); } function addUser() { fetchCsrfToken(addNewUser); } </script> </head> <body> <h1>JavaScript Tutorial</h1> <a href="/myapp/users">Show users</a> <br/> <br/> <input type="text" id="name" placeholder="Type user name"></input> <input type="button" value="Add User" onClick="javascript: addUser()"></input> </body> </html>
The UI contains a link to get all users, an input box to enter a user name, and a button to send “create a new user” requests.
In the sample, the code seems more complicated than expected. Clicking the
Add User
button tries to fetch a CSRF token, and on SUCCESS it sends a POST request with the users’ data as a JSON body. As you are using the application router, you need to get a CSRF token before sending the POST request. This token is required for all requests that change the state. -
Go to the
node-tutorial
directory and execute:Bash/ShellCopycf push
This command will update both applications (
myapp
andweb
). -
Try to access
myapp
again (in a browser) in both ways – directly, and through theweb
application router.
RESULT
When you access the
web
application and click theShow users
link, it should result in a403 Forbidden
response due to missing permissions. The same error is thrown if you try to add a new user.To get permissions, you need to create a role collection containing the roles
Viewer
andManager
and assign these roles to your user. You can do this only from the SAP BTP cockpit. -
- Step 5
Open the SAP BTP cockpit and go to your subaccount.
From the left-side menu, navigate to
Security
>Role Collections
.Create a new role collection. For example,
MyNodeAppRC
.Click this role collection and then choose
Edit
.In the
Roles
tab, click theRole Name
field.Type Viewer. From the displayed results select the
Viewer
role that corresponds to your application, and chooseAdd
.Repeat the same steps for
Manager
.Now go to the
Users
tab, and in theID
field, enter your e-mail. Then enter the same e-mail in theE-Mail
field.Save your changes.
Your role collection is now assigned to your user and contains the roles you need to view and manage the content of your application.
Now you need to apply these changes to the
myapp
application by redeploying it again.Go back to the command line, and in the
node-tutorial
directory execute:Bash/ShellCopycf push
RESULT
Accessing the
myapp
application results in the following:If you try to access it directly, a response
403 Forbidden
is displayed due to lack or permissions (roles). This is a correct and expected behavior.If you try to access it through the
web
application router, theShow users
link will show the list of users - John and Paula. If you enter a new name, it will be successfully recorded in the user database.
Which of the following statements are correct?