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
eu20as 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-tutorialfolder. -
In this folder, create a file
manifest.ymlwith the following content:YAMLCopy--- applications: - name: myapp random-route: true path: myapp memory: 128M buildpacks: - nodejs_buildpackThe
manifest.ymlfile 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
myappin your space! If you do, use a different name and adjust the whole tutorial according to it. -
In the
node-tutorialfolder, create a subfoldermyapp. -
In the
myappdirectory, run:Bash/ShellCopynpm initPress Enter on every step. This process will walk you through creating a
package.jsonfile in themyappfolder. -
Then, still in the
myappdirectory, run:Bash/ShellCopynpm install express --saveThis operation adds the
expresspackage as a dependency in thepackage.jsonfile.After the installation is completed, the content of
package.jsonshould 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.jsonfile and update thescriptssection. Yourpackage.jsonfile 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
myappfolder, create a filestart.jswith 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
expressmodule represents the web server part of this application. Once these steps are completed, you can see that theexpresspackage has been installed in thenode_modulesfolder. -
Deploy the application on Cloud Foundry. To do that, in the
node-tutorialdirectory, run:Bash/ShellCopycf pushNOTE: Make sure you always run
cf pushin the directory where themanifest.ymlfile is located! In this case, that’snode-tutorial. -
When the staging and deployment steps are completed, the
myappapplication should be successfully started and its details displayed in the command console. -
Open a browser window and enter the generated URL of the
myappapplication (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/approuterpackage. To do that, a separate Node.js micro-service will be created, acting as an entry point for the application.- In the
node-tutorialfolder, create anxs-security.jsonfile 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
eu20with the technical key of your actual SAP BTP region.-
Create an
xsuaaservice instance namednodeuaawith planapplication. To do that, in thenode-tutorialdirectory run:Bash/ShellCopycf create-service xsuaa application nodeuaa -c xs-security.json -
Add the
nodeuaaservice inmanifest.ymlso the file looks like this:YAMLCopy--- applications: - name: myapp random-route: true path: myapp memory: 128M buildpacks: - nodejs_buildpack services: - nodeuaaThe
nodeuaaservice instance will be bound to themyappapplication during deployment. -
Now you need to create a microservice (the application router). To do that, go to the
node-tutorialfolder and create a subfolderweb.IMPORTANT: Make sure you don’t have another application with the name
webin your space! If you do, use a different name and adjust the rest of the tutorial according to it. -
In the
webfolder, create a subfolderresources. This folder will provide the business application’s static resources. -
In the
resourcesfolder, create anindex.htmlfile 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
myappapplication. -
In the
webdirectory, run:Bash/ShellCopynpm initPress Enter on every step. This process will walk you through creating a
package.jsonfile in thewebfolder. -
Now you need to create a directory
web/node_modules/@sapand install anapprouterpackage in it. To do that, in thewebdirectory run:Bash/ShellCopynpm install @sap/approuter --save -
In the
webfolder, open thepackage.jsonfile and replace the scripts section with the following:JSONCopy"scripts": { "start": "node node_modules/@sap/approuter/approuter.js" }, -
Now you need to add the
webapplication to your project and bind the XSUAA service instance (nodeuaa) to it. To do that, insert the following content at the end of yourmanifest.ymlfile.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: - nodeuaaNOTE: For the
urlvalue, enter your actual generated URL for themyappapplication. -
In the
webfolder, create anxs-app.jsonfile with the following content:JSONCopy{ "routes": [ { "source": "^/myapp/(.*)$", "target": "$1", "destination": "myapp" } ] }With this configuration, the incoming request is forwarded to the
myappapplication, configured as a destination. By default, every route requires OAuth authentication, so the requests to this path will require an authenticated user. -
In the
myappdirectory, 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
JWTStrategyprovided by the@sap/xssecpackage. To do that, go to themyappdirectory, and replace the content of thestart.jsfile 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/xssecis version 4.1.3 or higher. You can check this by running:npm list @sap/xssec.
If it’s a lower version, runnpm updateand then check again. -
Go to the
node-tutorialdirectory and run:Bash/ShellCopycf pushThis command will update the
myappapplication and deploy thewebapplication.What’s going on?
At this point of the tutorial, the URL of the
webapplication will be requested instead of themyappURL. It will then forward the requests to themyappapplication. -
When the staging and deployment steps are completed, the
webapplication 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
webapplication.For example:
https://web-happy-ladybug.cfapps.eu20.hana.ondemand.com -
Enter the credentials for your SAP BTP user.
Both the
myappandwebapplications 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 Applicationlink. The browser window displays Application user:<e-mail>, showing the email you have used for your Cloud Foundry login. -
Check that the
myappapplication 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/approuterpackage 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 ausersREST 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.jsonin thenode-tutorialfolder, 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 (
ViewerandManager) 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-tutorialdirectory run:Bash/ShellCopycf update-service nodeuaa -c xs-security.json -
In the
myappfolder, create a fileusers.jsonwith 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-parserthat will be used for JSON parsing. To do that, in themyappdirectory run:Bash/ShellCopynpm install body-parser --save -
Change the
start.jsfile, adding GET and POST operations for theusersREST 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
xssecpackage in the@sapdirectory. To every request object usingpassportandxssec.JWTStrategy, a security context is attached as anauthInfoobject. 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.jsfile, for HTTP GET requests users need theDisplayscope to be authorized. For HTTP POST requests, they need to have theUpdatescope assigned. -
Update the UI to be able to send POST requests. To do that, go to
web>resourcesand in theindex.htmlfile, 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 Userbutton 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-tutorialdirectory and run:Bash/ShellCopycf pushThis command will update both applications (
myappandweb). -
Try to access
myappagain (in a browser) in both ways – directly and through thewebapplication 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
webapplication and click theShow userslink, it should result in a403 Forbiddenresponse 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
ViewerandManagerand 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
Rolestab, click theRole Namefield. -
Type Viewer. From the displayed results, select the
Viewerrole that corresponds to your application, and chooseAdd. -
Repeat the same steps for Manager.
-
Now go to the
Userstab, and in theIDfield, enter your e-mail. Then enter the same e-mail in theE-Mailfield. -
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
myappapplication by redeploying it again. -
Go back to the command line, and in the
node-tutorialdirectory run:Bash/ShellCopycf push
RESULT
Accessing the
myappapplication results in the following:-
If you try to access it directly, a response
401 Unauthorizedis still displayed due to lack of permissions (roles). This is a correct and expected behavior. -
If you try to access it through the
webapplication router, theShow userslink 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
webapplication URL in a private/incognito browser tab.Which of the following statements are correct?
-