
In this post, you can get a step-by-step tutorial which helps you develop a NodeJS application from the scratch to show how to use the SaaS registry to build a multi-tenant application on BTP Cloud Foundry Runtime.
For more general descriptions on how many steps it takes to do from a normal application to a multitenant application, you can read this blog.
More information on this Series - Multitenancy:
npm install express --save
npm install -g express-generator
For more information on how to develop and run business applications on SAP Business Technology Platform (SAP BTP) using our cloud application programming model, APIs, services, tools, and capabilities, see Development on BTP.
Here are some channels prepared for you to get BTP accounts:
Let's assume you are a SaaS Application provider, for example: Provider: TIA. Provider: TIA would like to provide an application which displays the logged in user’s name and customer's tenant related information, shown as below:
Final project with multitenancy can be found: here.
A consumer can subscribe to the application through the SAP BTP Account Cockpit.
Each multitenant application has to deploy its own application router, and the application router handles requests of all tenants to the application. The application router is able to determine the tenant identifier out of the URL and then forwards the authentication request to the tenant User Account and Authentication (UAA) service and the related identity zone.
For general instructions, see Application Router.
Create folder cf_multitenant_approuter_app under the root directory.
mkdir cf_multitenant_approuter_app
cd cf_multitenant_approuter_app
Under the folder cf_multitenant_approuter_app, create a file package.json with the following content:
{
"name": "cf_multitenant_approuter_app",
"dependencies": {
"@sap/xsenv": "^3",
"@sap/approuter": "^8"
},
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
}
}
Then we should configure the routes in the application router security descriptor file (xs-app.json) so that application requests are forwarded to the multitenant application destination. Under the folder cf_multitenant_approuter_app, create a file xs-app.json with the following content:
{
"authenticationMethod": "none",
"routes": [{
"source": "/",
"target": "/",
"destination": "dest_cf_multitenant_node_app"
}]
}
Update the backend NodeJS app in the mta.yaml file to provide a destination to approuter app:
provides:
- name: dest_cf_multitenant_node_app
properties:
node_app_url: '${default-url}'
Add the approuter app into the mta.yaml file:
- name: cf_multitenant_approuter_app
type: nodejs
path: cf_multitenant_approuter_app
parameters:
disk-quota: 256M
memory: 256M
host: ${org}-cf-multitenant-approuter-app
provides:
- name: Router_api
properties:
url: ${default-url}
application: ${app-name}
requires:
- name: dest_cf_multitenant_node_app
group: destinations
properties:
name: dest_cf_multitenant_node_app
url: '~{node_app_url}'
forwardAuthToken: true
To use a multitenant application router, you must have a shared UAA service and the version of the application router has to be greater than 2.3.1.
For general instructions, see SAP Authorization and Trust Management Service in the Cloud Foundry Environment.
Create and configure an XSUAA instance in the mta.yaml (Also, we can create a security descriptor file xs-security.json in JSON format that specifies the functional authorization scopes for the application instead):
resources:
- name: uaa_cf_multitenant
type: org.cloudfoundry.managed-service
requires:
- name: Router_api
properties:
XSAPPNAME: ${xsuaa-app}
parameters:
#path: ./xs-security.json
service-plan: application
service: xsuaa
shared: true
xsuaa-app: ${space}-~{Router_api/application}
config:
xsappname: ${xsuaa-app}
### tenant-mode
tenant-mode: shared
description: Security profile of called application
scopes:
- name: "$XSAPPNAME.Callback"
description: With this scope set, the callbacks for tenant onboarding, offboarding and getDependencies can be called.
grant-as-authority-to-apps:
- "$XSAPPNAME(application,sap-provisioning,tenant-onboarding)"
oauth2-configuration:
redirect-uris:
- "http*://*.${default-domain}/**"
Add this instance into both the apps in the mta.yaml:
requires:
- name: uaa_cf_multitenant
Update the xs-app.json file:
{
"authenticationMethod": "route",
"routes": [{
"source": "/",
"target": "/",
"destination": "dest_cf_multitenant_node_app",
"authenticationType": "xsuaa"
}]
}
Add libraries for enabling authentication in the cf_multitenant_node_app/app.js file:
//**************************** Libraries for enabling authentication *****************************
var passport = require('passport');
var xsenv = require('@sap/xsenv');
var JWTStrategy = require('@sap/xssec').JWTStrategy;
//************************************************************************************************
Enabling authorization in the cf_multitenant_node_app/app.js file:
//*********************************** Enabling authorization ***********************************
var services = xsenv.getServices({ uaa: { tag: 'xsuaa' } }); //Get the XSUAA service
passport.use(new JWTStrategy(services.uaa));
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false })); //Authenticate using JWT strategy
//************************************************************************************************
The application router must determine the tenant-specific subdomain for the UAA that in turn determines the identity zone, used for authentication. This determination is done by using a regular expression defined in the environment variable TENANT_HOST_PATTERN.
More details: https://help.sap.com/products/BTP/65de2977205c403bbc107264b8eccf4b/5310fc31caad4707be9126377e144627....
- name: cf_multitenant_approuter_app
...
properties:
TENANT_HOST_PATTERN: "^(.*)-cf-multitenant-approuter-app.${default-domain}"
Now, if you try to deploy the MTA application, the backend application would return your tenant information accordingly.
Under the routes/index.js file implements two APIs:
//******************************** API Callbacks for multitenancy ********************************
/**
* Request Method Type - PUT
* When a consumer subscribes to this application, SaaS Provisioning invokes this API.
* We return the SaaS application url for the subscribing tenant.
* This URL is unique per tenant and each tenant can access the application only through it's URL.
*/
router.put('/callback/v1.0/tenants/*', function(req, res) {
var consumerSubdomain = req.body.subscribedSubdomain;
var tenantAppURL = "https:\/\/" + consumerSubdomain + "-cf-multitenant-approuter-app." + "<custom-domain>";
res.status(200).send(tenantAppURL);
});
/**
* Request Method Type - DELETE
* When a consumer unsubscribes this application, SaaS Provisioning invokes this API.
* We delete the consumer entry in the SaaS Provisioning service.
*/
router.delete('/callback/v1.0/tenants/*', function(req, res) {
console.log(req.body);
res.status(200).send("deleted");
});
//************************************************************************************************
Replace the <custom-domain> with your custom domain for your multitenant application.
resources:
...
- name: reg_cf_multitenant
type: org.cloudfoundry.managed-service
requires:
- name: uaa_cf_multitenant
parameters:
service: saas-registry
service-plan: application
config:
xsappname: ~{uaa_cf_multitenant/XSAPPNAME}
appName: cloud-cf-multitenant-saas-provisioning-sample-hands-on
displayName: Multitenancy Sample in Cloud Foundry
description: 'A NodeJS application to show how to use the SaaS registry to build a multi-tenant application on BTP Cloud Foundry Runtime'
category: 'Provider: TIA'
appUrls:
onSubscription: https://${org}-cf-multitenant-node-app.${default-domain}/callback/v1.0/tenants/{tenantId}
onSubscriptionAsync: false
onUnSubscriptionAsync: false
# callbackTimeoutMillis: 1200000
Specify the following parameters:
ParametersDescription
xsappname | The xsappname configured in the security descriptor file used to create the XSUAA instance (see Develop the Multitenant Application). |
getDependencies | (Optional) Any URL that the application exposes for GET dependencies. If the application doesn’t have dependencies and the callback isn’t implemented, it shouldn’t be declared.NoteThe JSON response of the callback must be encoded as either UTF8, UTF16, or UTF32, otherwise an error is returned. |
onSubscription | Any URL that the application exposes via PUT and DELETE subscription. It must end with /{tenantId}. The tenant for the subscription is passed to this callback as a path parameter. You must keep {tenantId} as a parameter in the URL so that it’s replaced at real time with the tenant calling the subscription. This callback URL is called when a subscription between a multitenant application and a consumer tenant is created (PUT) and when the subscription is removed (DELETE). |
displayName | (Optional) The display name of the application when viewed in the cockpit. For example, in the application's tile. If left empty, takes the application's technical name. |
description | (Optional) The description of the application when viewed in the cockpit. For example, in the application's tile. If left empty, takes the application's display name. |
category | (Optional) The category to which the application is grouped in the Subscriptions page in the cockpit. If left empty, gets assigned to the default category. |
onSubscriptionAsync | Whether the subscription callback is asynchronous.If set to true, callbackTimeoutMillis is mandatory. |
callbackTimeoutMillis | The number of milliseconds the SAP SaaS Provisioning service waits for the application's subscription asynchronous callback to execute, before it changes the subscription status to FAILED. |
allowContextUpdates | Whether to send updates about the changes in contextual data for the service instance.For example, when a subaccount with which the instance is associated is moved to a different global account.Defaut value is false. |
Bind this instance to your NodeJS application, update your mta.yaml file:
modules:
- name: cf_multitenant_node_app
...
requires:
- name: uaa_cf_multitenant
- name: reg_cf_multitenant
...
You can build with the Cloud MTA Build Tool and Cloud Foundry CLI.
Download the Cloud MTA Build Tool: https://sap.github.io/cloud-mta-build-tool/download/
Install the Cloud MTA Build Tool:
npm install -g mbt
Navigate to the project root folder, and build your MTA project:
mbt build -p=cf
Install CF plugin to deploy MTA applications:
cf install-plugin multiapps
Make sure you are logged into a CF org/space:
cf login -a <api-endpoint> -o <org> -s <space>
In China you probably need to export an environment variable before you run "cf deploy" command.
export DEPLOY_SERVICE_URL=deploy-service.cfapps.<landscape-domain>
New variable name: MULTIAPPS_CONTROLLER_URL
For instance:
export DEPLOY_SERVICE_URL=deploy-service.cfapps.cn40.platform.sapcloud.cn
Deploy with mtar file:
cf deploy mta_archives/cloud-cf-multitenant-saas-provisioning-sample-hands-on_1.0.0.mtar -f
More details: MultiApps CF CLI Plugin, Build and Deploy Multitarget Applications using Cloud MTA Build Tool and Cloud Foundry CLI
Now, a consumer can subscribe to the application through the SAP BTP Account Cockpit.
Switch to another subaccount under the same Global Account with the multitenant application provider subaccount, you can see and subscribe to the multitenant application.
Create an instance for the SaaS Application:
Click on Create button:
Once it is subscribed, you can try to access it by clicking on the Go to Application button:
You will get a 404 error that says requested route does not exist:
In the last step, the SaaS Application Provider needs to add a new route to ensure that a consumer’s request goes to the app router.
cf map-route cf_multitenant_approuter_app exercise.sap-samples.cn40.apps.platform.sapcloud.cn --hostname trial3-tia-cf-multitenant-approuter-app
Try to clean cache and access it again:
Deploying the application again would remove all the routes required by your customers. To keep all the routes as it is, adds one parameter into your approuter application:
modules:
- name: cf_multitenant_approuter_app
...
parameters:
...
keep-existing:
routes: true
Try to deploy it again.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
10 | |
10 | |
9 | |
7 | |
7 | |
6 | |
6 | |
6 | |
6 | |
5 |