Technical Articles
Using Job Scheduler in SAP Cloud Platform [2]: simple OAuth scenario
This tutorial is part of a little series about SAP Job Scheduler
Quicklinks:
Intro Blog
Previous Blog
Project Files
In the present tutorial we’re going to learn how to create a Node app that exposes a REST endpoint that is protected with OAuth 2.0
And it shows that Jobscheduler is able to trigger that endpoint
It is a simple tutorial that focuses on small portions:
Do the minimum steps in our app, to enforce authentication based on OAuth 2.0
We don’t require authorization
Configure JobScheduler to call the protected application
Overview
** Create instances of Jobscheduler service and XSUAA service **
** Create and deploy application **
** Create Job to call that application **
** Background information **
Prerequisites
Access to SAP Cloud Platform, Trial or productive landscape
To create the simple application, Node.js is required
For deploying the application, it makes sense to use the Cloud Foundry Command Line Client.
The previous blog of our series is not required, but of course recommended
Create instance of JobScheduler service
First, create an instance of JobScheduler service.
Both service plans, lite or standard, can be used
If you’ve already created an instance, e.g. because you’ve followed my great previous blog, you can just use it
If you really don’t want to follow the first tutorial, see here to create an instance with name, e.g. jobschedulerinstance
Create instance of xsuaa service
Create simple xsuaa instance:
no params
service plan: broker
name e.g. xsuaa_default
Create Application
In this tutorial, we’re again using a very simple app, which allows us to focus on the essential part:
enabling authentication
With other words: protect our endpoint with OAuth 2.0
Following steps are required
- Bind app to XSUAA
- Add dependencies to package.json
- Add some lines of code
- Add trust ACL to app environment
Note:
See appendix for full content of all files
1. XSUAA binding
As usual, create manifest.yml file in the project root folder
Add a name for the app, I’ve chosen a name to identify our app: appauthnoscope
(the app requires authentication, but no specific scope/no authorization)
applications:
- name: appauthnoscope
host: appauthnoscope
Note:
you might need to change the host name to something unique
Now to add authentication, we need to specify the name of the XSUAA service instance which we created above:
services:
- xsuaa_default
- jobschedulerinstance
2. Dependencies
Additional dependencies required for OAuth authentication, to be added to package.json
"passport": "latest",
"@sap/xssec": "latest",
"@sap/xsenv": "latest"
* We use passport, a standard middleware for express server, used for authentication
* We need the @sap/xssec module provided by SAP. It is used to configure passport with a specific JWT strategy. This is required to validate OAuth tokens issued by SAP XSUAA
* And the @sap/xsenv module to parse the app environment variables. A convenience library, to avoid doing it manually.
Don’t forget to execute npm install
3. Code
The essential line of code is this one:
app.use(passport.authenticate('JWT', { session: false }));
Here we’re adding a middleware to our express server.
This middleware will be called before our endpoint gets invoked
The middleware ensures that authentication is done, otherwise our endpoint doesn’t get invoked
We specify ‘JWT’ as authentication strategy
This means that a JWT token is required
With other words: basic authentication is not possible, you cannot authenticate with your username and password
In addition, the passport middleware itself needs to be configured
It needs an implementation of the authentication strategy
So the following line is required previously:
const jwtStrategy = new JWTStrategy(xsuaaCredentials)
passport.use(jwtStrategy);
Here, an instance of JWTStrategy (module @sap/xssec) is passed to passport
This implementation takes care of validating the JWT token, which is sent by the user who calls the endpoint of our app
One last (first) step:
The JWTStrategy has to be configured as well
It has to know how to validate, it needs the “sensitive” information.
I mean, in order to validate a user, it would need the user and password.
As such, in order to validate a JWT token, it needs the OAuth info
So before creating the JWTStrategy, we need to find this OAuth info
Remember:
When our app is bound to XSUAA, it receives an environment variable for XSUAA (see next chapter)
This variable is a json object and it contains OAuth info (Remember that XSUAA acts as OAuth-Resource-Server)
That json object contains a nested object called “credentials” which contains all info required for following the OAuth flow
We can manually parse the environment variables of our service, but it is more convenient to use @sap/xsenv module
The method call shown below will return the “credentials” object (see below for screenshot), which we store as “xsuaaCredentials”
const xsuaaService = xsenv.getServices({ myXsuaa: { tag: 'xsuaa' }});
const xsuaaCredentials = xsuaaService.myXsuaa;
That’s all.
Now we can again have a look at all the relevant code, this time in the correct order, from top to bottom:
const xsuaaService = xsenv.getServices({ myXsuaa: { tag: 'xsuaa' }});
const xsuaaCredentials = xsuaaService.myXsuaa;
const jwtStrategy = new JWTStrategy(xsuaaCredentials)
passport.use(jwtStrategy);
const app = express();
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));
4. Configure ACL
For some reasons (explained below), we need to open our app for foreign users.
It is like adding a “White List” to our app
Remember: ACL stands for “Access Control List”
The XSUAA will look at the environment of our app for a variable containing the ACL
As such, we need to add the following snippet to our manifest.yml
applications:
- name: appauthnoscope
. . .
env:
SAP_JWT_TRUST_ACL: >
[
{"clientid":"*", "identityzone":"*"}
]
For the sake of simplicity, we just allow all client IDs and all identity zones.
We’ll enhance it later
Now we’re done with changes to our app, to enforce protection with OAuth 2.0
Small recap:
In manifest, we’ve bound our app to xsuaa + added ACL to env
In code, we’ve added and configured passport with JWT
Of course, we’ve also added the required dependencies to package.json
As you know, you can find the complete content of all files in the appendix section
Now we can deploy our app to the SAP Cloud Platform
Deploy sample app
Yep, deploy it
See here or here for help
Optional: Test the endpoint of our app
After deployment, you can find the app url in the cloud cockpit, in the app “overview” section
After appending the REST endpoint (as defined in our app code), the final URL will look similar like this:
https://appauthnoscope.cfapps.eu10.hana.ondemand.com/doSomething
Since we defined the endpoint to be invoked with GET request, we can go ahead and call it, as usual, with our favorite browser.
However, the response is an error: Unauthorized
Second, we can try to use a REST client (e.g. Postman), such that we can pass our user credentials (those which we use when logging into the SAP Cloud Platform)
However, for basic authentication, the response is the same error: Unauthorized with status code 401
At this point you would usually start to panic
BUT: you can safely follow my tutorial – and all will be good 😉
Last attempt: use REST client with authentication type OAuth 2.0
See here for detailed description.
Short description:
In postman, enter the endpoint URL, switch to the “Authorization” tab and specify OAuth 2.0
Then press “Get New Access Token” and fill the subsequent dialog with information which you get from the app environment (your xsuaa credentials. See section below for more information)
Press “Request Token”, then scroll down and click “Use Token”
Back in Postman screen, you can see that the token is entered and you can press “Send”
The result should be success
Create job in Job Scheduler Dashboard
Last step of our little simple tutorial is to call our endpoint with Job Scheduler
Note:
this section is NOT optional.
Users who skip this chapter won’t be rewarded
(users who continue till the very end – won’t get a reward either, I’m afraid)
Creating jobs is described in previous blog
For your convenience and my entertainment (note that I won’t get a reward either…) I’m adding few clickable screenshots
Open Job Scheduler dashboard with little action icon at the right side:
Create Job
Specify action name, e.g.
https://appauthnoscope.cfapps.eu10.hana.ondemand.com/doSomething
Create Schedule
After job finishes, check result
The screenshot has proven:
We’ve finished our simple tutorial with a green success message, so it wasn’t totally useless (even without rewards)
What is so amazing about it???
We’ve protected our app with OAuth – but Jobscheduler was still able to successfully invoke the endpoint
We didn’t have to specify clientsecret and similar tedious stuff in Jobscheduler –
Remember how tedious it was in postman? (just in case that you went through that section…)
How is that possible?
Optional: Understanding the authentication mechanism
If you’re familiar with security and need deeper understanding:
-> this chapter will be useless for you
If you’ve no clue about security and need basic understanding of OAuth:
-> you can follow this blog
If you’d like to waste some time:
-> you can continue reading
About Job Scheduler:
Job Scheduler contacts XSUAA and receives a JWT token which is sent to our app endpoint
About our application:
We’ve created an application and since we’re proud of it, we want everybody to use it
On the other side, since we’re also little selfish, we want only registered users to use it
The SAP Cloud Platform offers the service Authorization & Trust Management (XSUAA) to support such scenarios
We’ve bound our app to that service (xsuaa) and in the implementation code, we’ve added authentication middleware, to ensure (or enforce) the authentication
When binding our app to XSUAA, then the XSUAA provides our app with OAuth information which is required to call our app, following the so-called OAuth flow
This OAuth information, provided as environment variables, contains e.g.
A client ID: it represents a user who wants to call the app (endpoint)
A client secret: that’s like a password
URL: this URL is called with clientid/clientsecret and it responds with a JWT token
The content of environment variables can be viewed
on command line (cf env <appname>)
or in the cloud cockpit (application details screen)
So, from now on, the flow to call our endpoint is as follows:
1. The user calls the <URL> and uses <clientid> and <clientsecret>
That <URL> has to be composed as follows:
url + /oauth/token
where url is the value of the property url from the credentials section (see above)
e.g. https://subaccount.authentication.eu10.hana.ondemand.com/oauth/token
2. The response is a JWT token (that’s just a long string with silly characters, e.g. abcxyz)
3. The users calls the endpoint of our app and uses the token in a header:
Authorization: Bearer abcxyz
This is better than using: Authorization = user/password e.g. with a technical user
Our app receives that Bearer token in the “Authorization” header of the req object which is passed into our callback
app.get('/doSomething', function(req, res){
The authentication is done as follows:
Our app had configured the authentication middleware with the same “credentials” section which we can see in the screenshot above
The bearer token contains exactly the same information, encoded in the silly string (JWT token)
BTW, this is only the case if the user has properly called the xsuaa server and received a valid JWT token
If this is the case, then the JWT Strategy will allow access to our app endpoint
Now, in our case, the <user> is the Jobscheduler
That makes things a bit more complicated
When Jobscheduler calls our app-endpoint, it sends a JWT token which is valid, but not valid for our app.
The reason is that the JWT token contains a client ID which is not the one we’ve seen in the screenshot above.
Why?
JobScheduler is an existing application, it has its own binding to xsuaa, so it receives its own clientid.
We can see it:
It is the one from different VCAP_SERVICE, like shown in screenshot below:
This is rejected in our app code, by our JWT Strategy, because we configured it with the previous xsuaa-credentials, not the jobscheduler-credentials. Which is correct
Remember:
const xsuaaService = xsenv.getServices({ myXsuaa: { tag: 'xsuaa' }});
const xsuaaCredentials = xsuaaService.myXsuaa;
const jwtStrategy = new JWTStrategy(xsuaaCredentials)
In my example, the xsuaaCredentials contains this clientid:
As such, the clientid starting with sb-na-cff … is a good one
But the Jobscheduler sends the clientid starting with sb-20db… which is a bad one
Again:
As per default, the xssec lib accepts only tokens containing the same client running in the same subaccount.
So the jobscheduler-clientid is rejected.
How can this be fixed?
We need to explicitly allow that bad clientid
That’s done with an environment variable called SAP_JWT_TRUST_ACL
In our sample application we’ve added this variable in the manifest.yml
Remember:
env:
SAP_JWT_TRUST_ACL: >
[
{"clientid":"*", "identityzone":"*"}
]
Here we simply allow all bad clients
We can see this setting in the cloud cockpit:
App details -> User-Provided Variables:
If you were bad and didn’t follow my tutorial, then you can enter the ACL here in the cockpit as well
Third way of adding an environment variable is using the command line
You can use this command:
cf set-env <application-name> SAP_JWT_TRUST_ACL "[{"clientid":"*","identityzone":"*"}]"
Note:
This command doesn’t work under windows.
Windows users should try with escape:
cf set-env <application-name> SAP_JWT_TRUST_ACL "[{\"clientid\":\"*\",\"identityzone\":\"*\"}]"
Optional: fine-tuning the ACL
For those of you who feel good with opening your app to all clients and identity zones:
You can skip this chapter and still feel good:
because you save time
And because you’re still quite safe because any client still will be issued by an XSUAA, so not completely coming from wilderness
Nevertheless, if you continue reading, you are free to stop at any time.
Good.
So if we want to replace the asterisk, as seen above:
With anything concrete, that at the end would look similar like this:
Then you need to know how to get the client id and the indentity zone
1. client id: You already have it, you take it from the environment variables, jobscheduler section
Note: don’t forget to make sure you’re copying from “jobscheduler” section
2. the identity zone is also taken from the screenshot but there’s a little trick:
Don’t use the identityzone….
The value of the ACL-property “identityzone” has to be the identityzoneID….
See screenshot above
Note:
if you’re unsure about which client id is being sent by JobScheduler, you can check it programmatically.
See appendix for code snippet
Now you have 2 possibilities:
Restrict the identity zone or restrict the client id or both (actually 3 possibilities)
1. Identity zone
This means that you trust all JobScheduler instances which are living in the same subaccount like your app.
Doesn’t sound wrong
1. clientid
Only trust one client which has foreign client id.
Can be our jobscheduler.
Can also be another consuming application which is bound to different xsuaa and which you want to add to whitelist
This option is more restricted thus more safe
OK, you can try both options, change the property values of SAP_JWT_TRUST_ACL
e.g. in the cockpit by using the “Edit” button
Or use the command line
Or change the manifest.yml and redeploy the application
Note: in first 2 cases, it is required to restart or restage the application
If any value is invalid, the JobScheduler run will terminate with error “Forbidden”
Summary
In this little tutorial we’ve learned how to write a simple Node.js application which is protected with Oath 2.0
We only need to add few lines of code to use libraries which do all the work for us.
Not complicated
And we’ve learned that JobScheduler supports OAuth 2.0 authentication flow without further configuration
Next Steps
The next blog will explain how to enhance the OAuth protection by adding authorization requirement to our application
That will require special handling for JobScheduler
Previous Steps
Links
SAP Help Portal: Job Scheduler documentation
SAP Help Portal Job Scheduler: Security chapter
SAP Help Portal XSUAA documentation
OAuth
Specification: https://oauth.net/2
And rfc: https://tools.ietf.org/html/rfc6749
JWT
Specification https://jwt.io
And rfc: https://tools.ietf.org/html/rfc7519
A tool to view token content: https://devtoolzone.com/decoder/jwt
Node
Passport module and documentation http://www.passportjs.org
Advertising my own blogs
How to call OAuth protected endpoint with REST client
Some basic simple introduction to OAuth protection and OAuth grant type
Sample to call protected API from Node.js application, and one more sample in cloud
Using command line client for cloud
About reading JWT token and more
Appendix 1: All Project Files
In this appendix you can find all files required to run the described sample application.
For your convenience, see here again the folder structure:
Find below the content of all 3 requried files
manifest.yml
---
applications:
- name: appauthnoscope
host: appauthnoscope
path: myapp
memory: 128M
buildpacks:
- nodejs_buildpack
services:
- xsuaa_default
- jobschedulerinstance
env:
SAP_JWT_TRUST_ACL: >
[
{"clientid":"*", "identityzone":"*"}
]
package.json
{
"main": "server.js",
"dependencies": {
"@sap/xsenv": "latest",
"@sap/xssec": "latest",
"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();
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));
// app endpoint which will be called by jobscheduler
app.get('/doSomething', function(req, res){
res.send('App endpoing finished doing something');
});
const port = process.env.PORT || 3000;
app.listen(port, function(){})
Appendix 2: Retrieve client id programmatically
We want to retrieve the JWT token which is sent to our app in the authorization header.
The token contains sensitive information about client id etc
In case of our sample app, we need to add an own middleware, which has to be invoked before passport-middleware.
Because if passport would reject the incoming call, we wouldn’t be invoked, thus no chance to read the JWT token
// Middleware to read JWT sent by JobScheduler
function jwtLogger(req, res, next) {
console.log('===> [JWT-LOGGER] decoding auth header' )
let authHeader = req.headers.authorization;
if (authHeader){
var theJwtToken = authHeader.substring(7);
if(theJwtToken){
let jwtBase64Encoded = theJwtToken.split('.')[1];
if(jwtBase64Encoded){
let jwtDecoded = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii');
let jwtDecodedJson = JSON.parse(jwtDecoded);
console.log('===> [JWT-LOGGER]: JWT contains scopes: ' + jwtDecodedJson.scope);
console.log('===> [JWT-LOGGER]: JWT contains client_id sent by Jobscheduler: ' + jwtDecodedJson.client_id);
}
}
}
next()
}
app.use(jwtLogger)
app.use(passport.initialize())
.use(passport.authenticate('JWT', { session: false }));
// . . . etc
Thanks for the information. Do you have the setup instructions suppose i have approuter thats responsible for the service to generate authtoken for the application. How do i configure job scheduler to work with approuter
Hi Sampath Ramanujam , I've created a Blog to answer your question regarding approuter
https://blogs.sap.com/2020/02/27/using-job-scheduler-in-sap-cloud-platform-4-using-app-router/
Have fun 😉
Cheers,
Carlos
Is it possible to obtain the identity zone from the deployer, e.g. with a placeholder? We'll be deploying our application into multiple sub-accounts / identify zones.
Hi Carlos Roggan ,
as I read in your Blog post about the outdated JWT ACL environment parameter,
I'm wondering how I can extend the scope of the job scheduler to add the clientID of the job-scheduler to the audience to set trust to my job application.
Speaking for the community ... can you do us catlovers a favor and update this?
I really appreciate your job ... thank you!!!
Hi Carlos Roggan , your blog is very informative. Thank you so much for sharing this great information. Could you provide your inputs on issues here I am facing given below?
1: Getting error -401 while scheduling the job. Here I have not setup authorization just setup authentication(Using Job Scheduler in SAP Cloud Platform [2]: simple OAuth scenario | SAP Blogs)
2: Also I tried to create xsuaa instance given on next tutorial(Setup authorization) with "grant-as-authority-to-apps": ["$XSSERVICENAME(jobschedulerinstance)"] then getting error given below.
Service broker error: Service broker xsuaa failed with: org.springframework.cloud.servicebroker.exception.ServiceBrokerException: Error creating application null (Error parsing xs-security.json data: Inconsistent xs-security.json: grant-as-authority-to-apps or granted-apps not supported on null)
FAILED
xs-security.json
Hello Anuj Kumar ,
Thanks for your nice feedback.
The error message sounds like the "jobschedulerinstance" could not be resolved.
Reason could be a typo?
Or maybe you didn't follow the necessary order: the jobscheduler instance must be created before you create the xsuaa.
Remember: Create JOBS first...;-)
Cheers,
Carlos
Hi Carlos Roggan , Thank you for your response.
I used same name in both place(xs-security.json& manifest.yml). I think Shreyas had similar issue mentioned here Using Job Scheduler in SAP Cloud Platform [3]: enable OAuth Authorization | SAP Blogs. Also he had 401 error same as mine.
manifest.yml
Hi Carlos Roggan , this issue has been resolved when I moved jobscheduler service from plan lite to standard ). Thanks
Hi Anuj,
Is this Oath Authorization can't work in "lite" plan? as i understand
Hi @Carlos Roggan ,
First of all, great blog series.. Thanks 🙂
Now, the second thing is the issue I need your help with , I am getting `401 UnAuthorised` error when running schedules, but it works okay when using xsuaa client id and secret.
I have tried re-creating the services in mentioned order and also tried using the productive environment with standard plan for jobscheduler service since someone told me the lite version is a bit different from the standard version.So, Can you kindly have a look and let me know if I am doing something wrong?
Here the app logs:
Thanks,
Raheel
Hello Raheel Ahmad ,
As far as I can see, the log shows that Jobscheduler sends its own client_id.
Also, it sends only the default scope (uaa.resource) which is added by Cloud Foundry in case of client-credentials.
Your XSUAA instance has different client_id.
As such, it rejects the request of Jobscheduler.
One question: have you defined a scope in your application, and have you granted it to Jobscheduler?
This is required.
During the grant mechanism, the client_id of YOUR own xsuaa will be added to the "aud" claim.
As such, when Jobscheduler sends the JWT token along with its request, the token will contain YOUR client_id and that will be accepted by your xsuaa.
Please refer to my other blog posts, I described it in detail there.
BTW, one more info:
Whenever you do a change related to xsuaa or xs-security, or JWT token, you will need to wait 12 hours before the change gets effective. Reason is that Jobscheduler caches the token during 12 hours, to improve performance:
Hehe, if your manager doesn't like to see you "waiting" 12 hours, you can try to delete and recreate a job, as a workaround. 😉
And thanks for your feedback 😉
Hi @Carlos Roggan,
Thanks for your answer.
So, I investigated this further with help from my team and the issue turned out to be with "xssec" package version. So, now the latest versions take into account the 'aud' in the token as well which wasn't the case in older versions.
so, if someone does exactly what is explained in this blog then it will work properly if you use an older version of @sap/xssec and not the latest one. In my case, I tried version 2.2.5 and it worked.
Also, your suggestion regarding adding scopes in application for jobscheduler should also work but I haven't tried that yet on my side.
and yes, I have already hit the 12 hours milestone once but now too many people know about that to correct you. 😉
Thanks and i appreciate having this blog series.
BR,
Raheel
Thank you for the feedback, Raheel Ahmad , and thanks for sharing your finding with the community!
Cheers,
Carlos 😉
Hi Carlos,
Thank you for your detailed Blog Post and all the explanation in every step .
For others who work with SAP Job Scheduling service with cloud application programing (CAP) and XSUAA there is another great blog post of Vinh Phat Tu about this use case which was very useful for me as well.
Best regards,
Mariam