Create an Application with Cloud Foundry Node.js Buildpack
- How to create a simple “Hello World” application in Node.js
- How to create an application router for it
- How to run authentication and authorization checks via the Authorization and Trust Management (XSUAA) service
Prerequisites
- You have a trial or 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 the SAP BTP, Cloud Foundry environment.
- cf CLI is installed locally.
- Node.js and npm are installed locally. Make sure you have the latest Node.js version. In this tutorial, we use Node version 20.x and npm version 10.x.
- 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 in 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 a web microservice (application router). Finally, you will set authentication checks and authorization roles to properly access and manage your web application.
- Step 1
First, you need to connect to the SAP BTP, Cloud Foundry environment with your trial or enterprise (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
as an example.-
Open a command-line console.
-
Set the Cloud Foundry API endpoint for your subaccount. Run the following command (using your actual region URL):
Bash/ShellCopycf api https://api.cf.eu20.hana.ondemand.com
- Log on to the SAP BTP, Cloud Foundry environment:
Bash/ShellCopy
cf login
-
When prompted, enter your user credentials. These are the email and password you have used to register your trial or productive SAP BTP account.
IMPORTANT: If the authentication fails, even though you’ve entered correct credentials, try logging in via single sign-on.
-
Choose the org name and space where you want to create your application.
If you’re using a trial account, you don’t need to choose anything. You can use only one org name, and your default space is
dev
.
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 random-route: true path: myapp memory: 128M buildpacks: - 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. -
In the
node-tutorial
folder, create a subfoldermyapp
. -
In the
myapp
directory, run:Bash/ShellCopynpm init
Press Enter on every step. This process will walk you through creating a
package.json
file in themyapp
folder. -
Then, still in the
myapp
directory, run: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.19.2" } }
-
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": "20.x.x" }, "scripts": { "start": "node start.js" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.19.2" } }
-
In the
myapp
folder, create a filestart.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 code 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, run:Bash/ShellCopycf push
NOTE: Make sure you always run
cf push
in the directory 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. -
Open a browser window and enter the generated URL of the
myapp
application (seeroutes
).For example:
https://myapp-purple-tiger.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 providing 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", "oauth2-configuration": { "redirect-uris": [ "https://*.cfapps.eu20.hana.ondemand.com/**" ] } }
> NOTE: Replace
eu20
with the technical key of your actual SAP BTP region.-
Create an
xsuaa
service instance namednodeuaa
with planapplication
. To do that, in thenode-tutorial
directory run: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 random-route: true path: myapp memory: 128M buildpacks: - nodejs_buildpack services: - nodeuaa
The
nodeuaa
service instance will be bound to themyapp
application during deployment. -
Now you need to create a microservice (the application router). To do that, 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. -
In the
web
folder, create a subfolderresources
. This folder will provide the business application’s static resources. -
In 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 Node.js Application</a> </body> </html>
This will be the start page of the
myapp
application. -
In the
web
directory, run:Bash/ShellCopynpm init
Press Enter on every step. This process will walk you through creating a
package.json
file in theweb
folder. -
Now you need to create a directory
web/node_modules/@sap
and install anapprouter
package in it. To do that, in theweb
directory run: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 random-route: true path: web memory: 128M env: destinations: > [ { "name":"myapp", "url":"https://myapp-purple-tiger.cfapps.eu20.hana.ondemand.com/", "forwardAuthToken": true } ] services: - nodeuaa
NOTE: For the
url
value, enter your actual generated URL for themyapp
application. -
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, run 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 { createSecurityContext, requests, constants, TokenInfo, JWTStrategy } = require("@sap/xssec").v3; 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); });
IMPORTANT: Make sure your
@sap/xssec
is version 4.1.3 or higher. You can check this by running:npm list @sap/xssec
.
If it’s a lower version, runnpm update
and then check again. -
Go to the
node-tutorial
directory and run:Bash/ShellCopycf push
This command will update the
myapp
application and deploy theweb
application.What’s going on?
At 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 generated URL of the
web
application.For example:
https://web-happy-ladybug.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 Node.js Application
link. The browser window displays Application user:<e-mail>
, showing the email you have used for your Cloud Foundry login. -
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?
- In the
- 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" ] } ], "oauth2-configuration": { "redirect-uris": [ "https://*.cfapps.eu20.hana.ondemand.com/**" ] } }
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 run:Bash/ShellCopycf update-service nodeuaa -c xs-security.json
-
In the
myapp
folder, create a fileusers.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
directory run: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 { createSecurityContext, requests, constants, TokenInfo, JWTStrategy } = require("@sap/xssec").v3; 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.checkLocalScope('Display'); if (isAuthorized) { res.status(200).json(users); } else { res.status(403).send('Forbidden'); } }); app.post('/users', function (req, res) { const isAuthorized = req.authInfo.checkLocalScope('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); });
IMPORTANT: 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>My Node.js 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 run: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 it directly, you should still get a response
401 Unauthorized
. This is a correct and expected behavior. - 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 run:Bash/ShellCopycf push
RESULT
Accessing the
myapp
application results in the following:-
If you try to access it directly, a response
401 Unauthorized
is still displayed due to lack of 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.
Tip: For the new result to take effect immediately, you might need to clear the cache of your browser. Or just open the
web
application URL in a private/incognito browser tab.Which of the following statements are correct?
-