Technology Blog Posts by SAP
cancel
Showing results for 
Search instead for 
Did you mean: 
CarlosRoggan
Product and Topic Expert
Product and Topic Expert
18,756

This tutorial is part of a little series about the SAP Job Scheduling service.

Quick links:
Intro Blog
Tutorial 1
Tutorial 2
Appendix 5: All Project Files 


In the previous tutorial, we created a Node.js application which was protected with OAuth 2.0,
and we used the Job Scheduling service to call our app.
That app required authentication, but not authorization.

In this tutorial, we‘re going to add an authorization requirement to the app.
It is again a simple tutorial. There are only a few steps which require special attention.

Note that it requires a productive account, it cannot be executed in a Trial account.

 

Overview

Create instances of the Job Scheduling service and the Authorization and Trust Management service (security settings: xs-security)
Create Node.js application
Optional: Test with human user
Configure Job Scheduling service
Appendix 1: Assign a role to a user
Appendix 2: List of useful CLI commands
Appendix 3: Logging the JWT content
Appendix 4: More apps with more scopes
Appendix 5: All project files

 

Prerequisites

  • Access to a productive SAP BTP account
  • The previous blog is not a prerequisite as we’re going to re-create the app from scratch. However, we’re not going to re-explain the same stuff.
  • So, let’s make it a prerequisite: the previous blog

 

Create Job Scheduling Service Instance

The Job Scheduling service instance needs to be created first (before xsuaa) and with the parameter given below.
Reason 1: The Job Scheduling service instance must exist before proceeding with the next step.
Reason 2: The instance name will be referenced by the xsuaa security descriptor.

The parameter below is required, because we’re going to configure the Job Scheduling service to call an OAuth-protected REST endpoint of our app.
This parameter is not required when the Job Scheduling service is used to call Cloud Foundry tasks.

See Using Job Scheduling Service in SAP BTP [0]: Intro and Prep to create the service instance using the cockpit.

Details:

Service Plan: Standard

Instance Name: jobschedulerinstance

Command for creating the service instance using the command line:

 

cf create-service jobscheduler standard jobschedulerinstance

 

Takeaway: Create JOBs first!

 

Create XSUAA Service Instance

In this tutorial, we want to add an authorization requirement to our application.
This means that users who want to call our REST endpoint need to be authenticated AND must have a special role.
This role is defined by us, the app developers. We require it. Why?
Because our endpoint might be sensitive, so we want to ensure that e.g. only administrators can invoke it.


How to define a role?

When creating an instance of XSUAA, we pass JSON parameters which contain the scope (role) that we require.
We can specify any arbitrary name of our choice.
Let’s give it a stupid name: scopeformyapp
To make that name a bit more unique, we concatenate our chosen name to the property xsappname, which has to be unique anyways.
This is a best practice and we can use a variable to avoid typos:

 

{

  "xsappname" : "xsappwithscopeandgrant",

  "scopes": [{

      "name": "$XSAPPNAME.scopeformyapp",

 

This means that at runtime our role name will be resolved to something like:
xsappwithscopeandgrant!t22273.scopeformyapp

You can see the value of the property xsappname:

  • In the env of your app
  • VCAP for xsuaa
  • Credentials section

Note: It makes sense to create an endpoint in a productive application which is only used by the Job Scheduling service for recurring tasks, like database clean-up.
So it would make sense to name the scope accordingly.
Furthermore, if the scope is not referenced by a role-template, then it cannot be assigned to a human user, then it cannot be found in the list of available roles. See Appendix 1: Assign Role to User.

OK, we’ve learned to define roles when creating an instance of XSUAA.
That role must be assigned to any user who wants to call an application, which is bound to the XSUAA instance which knows about that role.

Problem: The Job Scheduling service is not a user, so we cannot assign a role to it.

Solution: SAP BTP provides a mechanism to assign a role to an application:
grant-as-authority-to-apps

We only have to add the following line to our scope definition:

 

"grant-as-authority-to-apps": ["$XSSERVICENAME(jobschedulerinstance)"]

 

Note: Make sure to adapt to the name of your instance name of the Job Scheduling service created above.

Note: Now you can see the reason why the Job Scheduling service instance has to be created first.
(remember: create JOBs first)

Finally, the JSON parameters which are passed to the instance of XSUAA look as follows:

 

{

  "xsappname" : "xsappwithscopeandgrant",

  "tenant-mode" : "dedicated",

  "scopes": [{

      "name": "$XSAPPNAME.scopeformyapp",

      "description": "Users of my great app need this special role",

      "grant-as-authority-to-apps": ["$XSSERVICENAME(jobschedulerinstance)"]

  }]

}

 

The snippet above can be copied and pasted during the instance creation in the cockpit.
However, it makes sense to store these parameters in a file.
Such file is usually called xs-security.json, but the name can be any arbitrary name.
Also, that file is not required at runtime, so it doesn't need to be deployed along with the app.


Details for creating an XSUAA instance:

Service Plan: application or broker
Parameters: as given above
Instance Name: xsuaawithscopeandgrant

Assuming that the above parameters are stored in a file with the name xs-security.json in the same directory, command line users can use the following command:

 

cf create-service xsuaa broker xsuaawithscopeandgrant -c xs-security.json

 

Note for users who already have an app and a XSUAA instance:
In our tutorial, we’re going to create a new application.
However, if you already have an app which is bound to an XSUAA instance, you can just update the existing XSUAA instance to add the above JSON parameters (e.g. the grant for JobScheduler).
The command for updating the XSUAA instance looks as follows:

 

cf update-service xsuaawithscopeandgrant -c xs-security.json

 

However, after running the update-service command, make sure to re-bind the service to your app.
But always:

FIRST bind XSUAA to app
THEN bind Jobscheduler to your app

Background:
When the Job Scheduling service is bound to our app, the service reads the grant-as-authority-to-apps.
Thus, our app MUST be bound to an XSUAA instance (with grant) BEFORE binding the app to the Job Scheduling service. Otherwise, the invocation of our endpoint will fail, because the Job Scheduling service won't have the required scope.

Note:
You can always unbind and bind the Job Scheduling service instance after having updated the XSUAA service (after bind: restage or restart the app).

These commands are useful:

cf unbind-service <yourappname> jobschedulerinstance
cf bind-service <yourappname> jobschedulerinstance
cf restage <yourappname>

Takeaway: Create JOBs first
Takeaway: Bind xsUSA first again

 

Create Application

We’re going to reuse the application from the previous blog.
Here, we’re only explaining the differences.

1. manifest.yml

The first step for enhancing our app with authorization was done in the previous step when creating the instance of XSUAA.

In the manifest.yml file, we just bind our app to the service instances and define the ACL.

 

applications:

- name: appauthandscope

  . . .

  services:

    - xsuaawithscopeandgrant

    - jobschedulerinstance

 

Note: The services section in the snippet above shows that we bind the app to XSUAA first.
This order of binding will be kept during deployment. At least in most cases. However, there’s no guarantee.
So in case of trouble, remember:
Bind xsUSA first again
And after binding XSUAA, don't forget to unbind and bind the Job Scheduling service, and restage the app.

2. package.json

<No changes>
Don’t forget to run npm install.

3. Code

In the previous app, we’ve enforced the authentication with passport and the JWT strategy of xssec library.
Now we only need to enforce the authorization.
We’re going to do it manually.

We have to understand:
Whenever a user authenticates against XSUAA, they receive a JWT token.
Remember: XSUAA acts as "Authorization Server" in the OAuth flow.
That token contains info about clientid etc and also info about the scopes (roles) which the user has.

So our task seems to be clear.
When our endpoint is invoked:
1. We have to extract the scopes out of the token.
2. We have to check if our required scope is available.

And we have to react accordingly:
If the scope $XSAPPNAME.scopeformyapp is not contained in the JWT token, we have to fail with the corresponding HTTP status code.

For the application developer, it doesn’t make a difference if the user is a human, or if it is the Job Scheduling service. If the instance creation and binding were done properly, the Job Scheduling service will send the required scope in the JWT token.

And one more good news:
There’s a helper method available which does the validation of the token.
We don’t really need to manually decode the token to get the list of available scopes to check if our required scope can be found.
We only need to use the helper method and pass our required scope name to it.

But first we have to figure out, which is the correct name of the scope (role) that a user needs to have to call our endpoint successfully. Remember that the full xsappname is generated during deployment.

In our xs-security.json file, we defined the xsappname as follows:

 

xsuaaCredentials.xsappname

 

 

  "scopes": [{

      "name": "$XSAPPNAME.scopeformyapp",

 

We’ve learned that during deployment it will be generated and look somehow like this:
xsappwithscopeandgrant!t22273.scopeformyapp

So we cannot hard-code the required name.
We have to obtain the $XSAPPNAME from our app environment.
It is easy.
We already have parsed the environment in the previous tutorial:

 

const xsuaaService = xsenv.getServices({ myXsuaa: { tag: 'xsuaa' }});

const xsuaaCredentials = xsuaaService.myXsuaa; 

 

Now we can access the credentials to get the value of the property “xsappname”:

 

xsuaaCredentials.xsappname

 

We could store it in a constant.
But no, in this simple tutorial we make it the simple way…
See here how the implementation of our endpoint looks like:

 

app.get('/doSomething', function(req, res){      

   const MY_SCOPE = xsuaaCredentials.xsappname + '.scopeformyapp'

   if(req.authInfo.checkScope(MY_SCOPE)){

      res.send('The endpoint was properly called, the required scope has been found in JWT token. Finished doing something successfully');

   }else{

      return res.status(403).json({

         error: 'Unauthorized',

         message: 'The endpoint was called by user who does not have the required scope: <scopeformyapp> ',

     });

   }

});

 

The promised helper is this one:

 

req.authInfo.checkScope('theScope'))

 

Actually, there are 2 helpers.

First helper:

 

req.authInfo

 

This is an object that is filled by the framework, it is a convenience object.
We don't need to know how to access the "Authorization" header.
And best: it contains info extracted from parsed JWT token.

BTW, see Appendix 3: App with JWT Logger for a simple example for manually extracting info from JWT token.

Second helper:

 

authInfo.checkScope('theScope')

 

The authInfo object provides not only properties, but also a helper method.
It doesn’t do sophisticated stuff, but is handy.
It checks if the JWT token contains the scope.
If the check fails, then our app responds with a proper status code.
As per specification, the code 403 is the right one for our error (required scope not available).

OK, that’s all!

As I promised, also in this tutorial, there’s not much work to do and the work is simple:
1. Compose the scope name that our app requires.
2. Validate the scope of incoming call.

 

Deploy Application

We've already covered this part in Using SAP Job Scheduling Service in SAP BTP [1]: First Simple Scenario.

For more information about deploying apps to the SAP BTP, Cloud Foundry environment, see Deploying to the Cloud Foundry Environment.

 

Optional: Test the Endpoint of Our App

In the previous tutorial, I gave brief description about how to use a REST client for an OAuth flow.
You can repeat the steps for the current scenario.

However, it will fail.
Why?
As a human user, I proceed as follows:
- Open the cockpit, go to the app details page, open the Environment Variables and view the VCAP_SERVICES variable for the xsuaa-binding of my app
- With the help of Postman, ask the authentication endpoint of XSUAA (the Authorization server in terms of OAuth) to provide a JWT token for my user.
- The identity provider is contacted to verify my user.
- And here comes the weak point:
My user has roles assigned. But obviously not the silly role $XSAPPNAME.scopeformyapp required by the simple app.

How to fix it?
See Appendix 1: Assign Role to User.

 

Create Job

Create a job with an endpoint URL such as this one:
https://appauthandscope.cfapps.eu10.hana.ondemand.com/doSomething

job_result_succ.jpg

 

Summary

Here comes a summary of 2 blogs:

  • We’ve learned how to write a Node.js app which is protected by OAuth.
  • We’ve learned a bit about the protection mechanism using passport and xssec JWT strategy.
  • We’ve learned how to define a scope.
  • We’ve learned how to enforce a scope.

I almost forgot that this tutorial series is about the Job Scheduling service.
We almost don’t need to do anything to make the Job Scheduling service call our protected app:
1. Add jobscheduler to ACL.
2. Assign (grant) the role to the Job Scheduling service.

Remember:
The order of creation and order of binding is important.
Never forget the mnemonics for correct Order :

Takeaway: Create JOBs first
Takeaway: Bind xsUSA first again

Maybe a diagram helps to remember?

 

Links

SAP Help Portal: Security in Cloud Foundry
SAP Help Portal: xs-security.json docu
SAP Help Portal: xs-security.json reference
SAP Help Portal: little docu about grant

HTTP status codes: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

Security Glossary

More links: see previous blog

Series:
0 Intro
1 Simple
2 Authentication
3 This blog
4 App Router

 

Appendix 1: Assign Role to User


My apologies, I didn’t expect that you would be interested in calling that endpoint with a REST client.
That’s why I described above a rather simple security descriptor.
It contained only the parameters needed by the Job Scheduling service.
Now, you want to call your silly app endpoint with your (smart) human user.
OK.
I can describe that.
(sigh)

We have to enhance the security descriptor.

Copy the following content into the xs-security.json file or create a second file with some silly name of your choice, e.g. xs-humanSecurity.json.

This time, the parameters contain an additional role-template:

 

{

  "xsappname" : "xsappwithscopeandgrant",

  "tenant-mode" : "dedicated",

  "scopes": [{

      "name": "$XSAPPNAME.scopeformyapp",

      "description": "Users of my great app need this special role",

      "grant-as-authority-to-apps": ["$XSSERVICENAME(jobschedulerinstance)"]

  }],

  "role-templates": [ { 

      "name"                : "RoleTemplateForMyGreatApp", 

      "description"         : "Users of my great app need this special role", 

      "default-role-name"   : "My App My Role",

      "scope-references"    : ["$XSAPPNAME.scopeformyapp"]

  }]

}

 

See SAP Help Portal for more information about the parameters.

Now update the XSUAA service instance with the following command:

 

cf update-service xsuaawithscopeandgrant -c xs-humanSecurity.json

 

Now you have to ask the admin (probably yourself) to assign that silly role to your user.
2 steps are required:

1. Create a role collection and add our role to it.

In the SAP BTP cockpit:

a. Go to your global account and subaccount. For more information, see Navigate in the Cockpit.

b. Choose Security and then Role Collections.

c. To create a new role collection, choose + (Create New Role Collection). Create the role collection with any name of your choice, e.g. rc_app_with_scope.

For more information about creating role collections in the SAP BTP cockpit, see Define a Role Collection.

d. Choose Save.

e. Choose the name of the new role collection.

f. Go to the Roles section and choose Edit.

e. Use our our xsappname (xsappwithscopeandgrant) to add the role according to the instructions in Add Roles to a Role Collection, and see the role-template and the default role as defined in the security descriptor above.

2. Assign a user to the role collection:

In the SAP BTP cockpit:

a. Go to your global account and subaccount. For more information, see Navigate in the Cockpit.

b. Choose Security and then Role Collections.

c. Choose the role collection to which you want to assign the user.

d. Go to the Users section and choose Edit.

e. Enter the user ID, identity provider (e.g. SAP ID Service), and e-mail address of the user that you want to assign to the role collection.

f. Save your changes.

For more information on assigning users to a role collections, see Assign Users to Role Collections.

And now you can proceed with chapter Optional: Test the Endpoint of Our App.

And I promise: it will work fine now!

 

Appendix 2: Useful Commands

For your convenience, I’m listing all commands for Cloud Foundry command line client, ready to copy & paste for this tutorial.

Creating the services:

 

cf create-service jobscheduler standard jobschedulerinstance

cf create-service xsuaa broker xsuaawithscopeandgrant -c xs-security.json

 

Update:

 

cf update-service xsuaawithscopeandgrant -c xs-security.json

 

Re-binding xsuaa:

 

cf unbind-service appauthandscope xsuaawithscopeandgrant

cf bind-service appauthandscope xsuaawithscopeandgrant

cf restage appauthandscope

 

Re-binding jobscheduler:

 

cf unbind-service appauthandscope jobschedulerinstance

cf bind-service appauthandscope jobschedulerinstance

cf restage appauthandscope

 

 

Appendix 3: App with JWT Logger

If you’re interested in adding logs to your app to view the JWT token which is sent, and to verify the scopes with your eyes, use the following code snippet in your Node.js app.

Hint: The property user_name will be filled only when you call the endpoint with your user and REST client. The Job Scheduling service doesn’t have a user_name.

 

const express = require('express');

const passport = require('passport');

const xsenv = require('@sap/xsenv');

const JWTStrategy = require('@sap/xssec').JWTStrategy;



//configure passport

const xsuaaService = xsenv.getServices({ myXsuaa: { tag: 'xsuaa' }});

const xsuaaCredentials = xsuaaService.myXsuaa; 

const jwtStrategy = new JWTStrategy(xsuaaCredentials)

passport.use(jwtStrategy);



// scope name copied from xs-security.json

const SCOPE_NAME = 'scopeformyapp'

const MY_SCOPE = xsuaaCredentials.xsappname + '.' + SCOPE_NAME;



// configure express server with authentication middleware

const app = express();



// Middleware to read JWT sent by JobScheduler

function jwtLogger(req, res, next) {

   console.log('===> [MIDDLEWARE]  decoding auth header' )

   let authHeader = req.headers.authorization;

   if (authHeader){

      var theJwtToken = authHeader.substring(7);

      if(theJwtToken){

         console.log('===> [MIDDLEWARE] the received JWT token: ' + theJwtToken )

         let jwtBase64Encoded = theJwtToken.split('.')[1];

         if(jwtBase64Encoded){

            let jwtDecoded = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii');

            let jwtDecodedJson = JSON.parse(jwtDecoded);

            //console.log('User: ' + jwtDecodedJson.user_name);

            console.log('===> [MIDDLEWARE]: JWT: scopes: ' + jwtDecodedJson.scope);

            console.log('===> [MIDDLEWARE]: JWT: client_id: ' + jwtDecodedJson.client_id);

            console.log('===> [MIDDLEWARE]: JWT: user: ' + jwtDecodedJson.user_name);

         }

      }

   }

   next()

}

app.use(jwtLogger)



app.use(passport.initialize());

app.use(passport.authenticate('JWT', { session: false }));



// app endpoint with authorization check

app.get('/doSomething', function(req, res){      

   if(req.authInfo.checkScope(MY_SCOPE)){

      res.send('The endpoint was properly called, the required scope has been found in JWT token. Finished doing something successfully');

   }else{

      return res.status(403).json({

         error: 'Unauthorized',

         message: 'The endpoint was called by user who does not have the required scope: <scopeformyapp> ',

     });

   }

});



const port = process.env.PORT || 3000;

app.listen(port, function(){})

 

Note: Better not write scopes to logs when running in productive mode.

 

Appendix 4: More Apps with More Scopes

Curious about how the Job Scheduling service behaves when there are many apps bound to it?
ONE instance of the Job Scheduling service and MANY apps bound to it.
As a consequence, MANY jobs created for many apps.

You can try, here's a little experiment:

1. Create a second XSUAA service instance, add multiple scopes in JSON parameters.
2. Then create a second (silly) app, bind it to this XSUAA and to the Job Scheduling service.
3. Deploy it.
4. Then run the job of the first app again and check the log output (previous Appendix 3: App with JWT Logger) for the scopes.

You’ll see: The JWT token sent by the Job Scheduling service to the first app contains all scopes: the scope of the first app and those of the second app.

You can use this xs-security.json:

 

{

  "xsappname" : "xsappwithscopeandgrant2",

  "tenant-mode" : "dedicated",

  "scopes": [

    {

      "name": "$XSAPPNAME.scopeformyapp2",

      "description": "scope2",

      "grant-as-authority-to-apps": ["$XSSERVICENAME(jobschedulerinstance)"]

    },

    {

      "name": "$XSAPPNAME.scopeformyapp3",

      "description": "scope3",

      "grant-as-authority-to-apps": ["$XSSERVICENAME(jobschedulerinstance)"]

    }

  ]

}

 

 

Appendix 5: All Project Files

manifest.yml

 

---

applications:

- name: appauthandscope

  path: app

  memory: 128M

  buildpacks:

    - nodejs_buildpack

  services:

    - xsuaawithscopeandgrant

    - jobschedulerinstance

 

xs-security.json

 

{

  "xsappname" : "xsappwithscopeandgrant",

  "tenant-mode" : "dedicated",

  "scopes": [{

      "name": "$XSAPPNAME.scopeformyapp",

      "description": "Users of my great app need this special role",

      "grant-as-authority-to-apps": ["$XSSERVICENAME(jobschedulerinstance)"]

  }]

}

 

package.json

 

{

  "main": "server.js",

  "dependencies": {

    "@sap/xsenv": "latest",

    "@sap/xssec": "3.6.1",

    "express": "^4.16.3",

    "passport": "^0.4.1"

  }

}

 

server.js

 

const express = require('express');

const passport = require('passport');

const xsenv = require('@sap/xsenv');

const JWTStrategy = require('@sap/xssec').JWTStrategy;



//configure passport

const xsuaaService = xsenv.getServices({ myXsuaa: { tag: 'xsuaa' }});

const xsuaaCredentials = xsuaaService.myXsuaa; 

const jwtStrategy = new JWTStrategy(xsuaaCredentials)

passport.use(jwtStrategy);



// configure express server with authentication middleware

const app = express();



// Middleware to read JWT sent by JobScheduler

function jwtLogger(req, res, next) {

   console.log('===> [MIDDLEWARE]  decoding auth header' )

   let authHeader = req.headers.authorization;

   if (authHeader){

      var theJwtToken = authHeader.substring(7);

      if(theJwtToken){

         console.log('===> [MIDDLEWARE] the received JWT token: ' + theJwtToken )

         let jwtBase64Encoded = theJwtToken.split('.')[1];

         if(jwtBase64Encoded){

            let jwtDecoded = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii');

            let jwtDecodedJson = JSON.parse(jwtDecoded);

            console.log('===> [MIDDLEWARE]: JWT: scopes: ' + jwtDecodedJson.scope);

            console.log('===> [MIDDLEWARE]: JWT: client_id: ' + jwtDecodedJson.client_id);

            console.log('===> [MIDDLEWARE]: JWT: user: ' + jwtDecodedJson.user_name);

         }

      }

   }

   next()

}

app.use(jwtLogger)



app.use(passport.initialize());

app.use(passport.authenticate('JWT', { session: false }));



// app endpoint with authorization check

app.get('/doSomething', function(req, res){      

   const MY_SCOPE = xsuaaCredentials.xsappname + '.scopeformyapp'

   if(req.authInfo.checkScope(MY_SCOPE)){

      res.send('The endpoint was properly called, the required scope has been found in JWT token. Finished doing something successfully');

   }else{

      return res.status(403).json({

         error: 'Unauthorized',

         message: 'The endpoint was called by user who does not have the required scope: <scopeformyapp> ',

     });

   }

});



const port = process.env.PORT || 3000;

app.listen(port, function(){})

 

 

34 Comments