Skip to Content
Technical Articles

Using Job Scheduler in SAP Cloud Platform [4]: using App Router

This blog is part of a little series explaining usage scenarios for SAP Cloud Platform Job Scheduler

Quicklinks:
Intro Blog
Project Files

Recently I’ve received several questions regarding usage of Job Scheduler together with Application Router
Thanks for the questions, dear readers 😉
You motivated me to try it out and write this new blog

What… and Why?
Oh, sorry, 2 questions at once is too much for my single brain…

What?
Application Router is an existing application (node module) delivered by SAP
We can just configure it in a file and deploy it to SAP Cloud Platform
We can define routes to any existing URLs
It can act as single point of entry for any kind of application,
e.g.  web app, or REST service
App Router is widely used by applications running in SAP Cloud Platform

Why?
It has several advantages.
From my perspective, the biggest advantage:
Approuter can handle the user login
In our previous tutorials:
We’ve created a little sample app and protected it with OAuth
As a result, accessing our REST endpoint has become very tedious, because we need to send a JWT token
BUT now: we can use the approuter
So now: Approuter will take care of authentication and it will display a login screen
Finally now: calling our REST endpoint is much more comfortable for human users

How?
This blog explains how

When?
After the diagram

Overview

In this blog we will create, configure and deploy the Application Router
The router forwards the requests to our application which we created in previous blog
We don’t create a new application

We will use this app router in 3 variants:

  • As human user in browser
  • As human user with REST client tool
  • As machine user from JobScheduler

My apologies that we don’t cover many aspects of approuter. It is a powerful tool.
Chapters:

Prerequisites

  • Obviously, access to SAP Cloud Platform is required.
    This blog is based on a productive account, but app router can of course be used in Trial accounts as well.
  • We need an application to which the app router should forward the incoming calls.
    We re-use the application appauthandscope which we created in previous blog
  • Also, we need the instance of xsuaa with name xsuaawithscopeandgrant, created in previous blog
  • And, obviously, we need an instance of JobScheduler
    If you already deleted the app and instance (I fully understand, because you didn’t know that this blog would be written….), then you have to recreate it…
  • The app router is a Node.js application, so it is an advantage to have Node.js running on your machine
  • You should have basic understanding of app router, these three blogs are helpful for beginners: One two three

Create App Router

I have to confess that the title is misleading: we don’t create it, because it already exists
We only have to install it

Create Project

Create a project folder in your file system, e.g. tmp_approuter_for_jobs
Inside, create the following files with names:

manifest.yml
package.json
xs-app.json

See screenshot for project structure

Paste the following content:

manifest.yml

As mentioned, approuter is an existing application. But it is only a node module, it is not deployable as such.
But we can install it and we deploy it.
As such, we need a deployment descriptor for Cloud Foundry

---
applications:
- name: approuterForJobApp
  host: approuterForJobApp
  memory: 128M
  services:
  - xsuaawithscopeandgrant
  env:
    destinations: >
      [
          {
              "name": "env_destination_jobapp",
              "url": "https://appauthandscope.cfapps.eu10.hana.ondemand.com/doSomething",
              "forwardAuthToken": true
          }
      ]

Explanation

name
We can give any arbitrary name for our installed approuter application

services
Our approuter app must be bound to an instance of XSUAA service
Note:
This instance MUST be the same instance like the app to which we want to forward
Means, we use the xsuaa instance which we created in previous blog (name was xsuaawithscopeandgrant) and which should be (still) bound to the application we created before

env: destinations
The target, to which our approuter should forward the call, is defined in a destination.
In our example, we define the destination as environment variable

name
This can be any name of your choice, but you need to remember it, because it is referenced later

url
This is the URL of the target, to which our app router should forward the call.
It can be a final full URL
Alternatively, it can be just the host, such that the final calls can contain more segments or parameters.
We will try both variants below
To start simple, for the beginning, we paste the full URL of our endpoint here
You might need to adapt to your own endpoint URL
It is the endpoint which you want to be invoked by JobScheduler

forwardAuthToken
This setting is very important
Why?
The endpoint is protected with OAuth and it requires a JWT token for authorization
As such, the approuter MUST forward the token, otherwise the (forwarded) call will always fail
Which token?
As I mentioned, the approuter takes care of user authentication, so it will obtain and then forward the JWT token

package.json

The existing approuter is a module, it isn’t a standalone deployable application.
So here, we’re creating an application which wraps the existing approuter node module
The only purpose of our approuter-app is to configure the approuter module

So first, we need a package.json file to declare the dependency to approuter
Second purpose is to declare a start command, which will be automatically invoked by Cloud Foundry on deployment

{
  "name": "myapprouter",
  "scripts": {
    "start": "node node_modules/@sap/approuter/approuter.js"
  },
  "dependencies": {
    "@sap/approuter": "latest"
  }
}

Install
To finally install approuter, run the command npm install on the command line, in the same folder where package.json is located

Configure App Router

This is the essential purpose of this blog
Configuring approuter means to define the routes
This is done in the file xs-app.json

Let’s start with a first route.

As I promised, the approuter should make our life easier when calling the token-protected endpoint of our sample application

xs-app.json

{
  "authenticationMethod": "route",
  "routes": [
    {
      "source": "^/humanuser",
      "target": "/",
      "destination": "env_destination_jobapp"
      "authenticationType": "xsuaa"
    }
  ]
}

Explanation

routes
The file can contain multiple routes

Source
A route defines a source
This is a name of our choice
It will form the final URL which will be invoked by our users
So in our example, the user calls a URL like this
https://approuter…../humanuser
Why this name?
We want to make life easier for human users
And approuter takes care of forwarding the incoming call (source) to our sample app
The sample app is then the target

Target
The target to which approuter forwards the call is defined in the property target
Usually, it is just a placeholder, because the actual URL is encapsulated in a destination

destination
Remember that we created a destination configuration as env variable in manifest?
sure
So the name of that destination has to be entered here, as value of the destination property
Make sure to not make a mistake here
So, when the approuter forwards an incoming call, it will use the destination which we defined

authenticationType
This is the type of authentication which we require for the route which we define here
There are 2 types: “none”, “basic” and “xsuaa”
We will cover them all, but let’s start with xsuaa, because this route is meant for human users using a browser
The type “xsuaa” is the default, so it can also be completely removed
This property is only relevant for this route, such that every route can define a different authenticationType

authenticationMethod
The property authenticationMethod is specified on top level of the xs-app.json
It can be “none” or “route”
In our example, “route”, we say that every route can have its own authentication requirement
If “none” is specified here, then all routes will have no authentication

Note
In our example, we’re creating and deploying a separate application.
I believe it makes things clearer
However, it is possible to combine the approuter with the target application in one project
If the target app is a node app, then even the package.json can be shared
Like that, only one deployment is required
Furthermore, if approuter is used for only one app, it makes sense to bundle them together
One more alternative could be to have them both in one MTA (multi target application)

Deploy App Router

Make sure that the required xsuaa is already there
The application to which we forward the call must also be deployed (sooner or later)

Use App Router with Browser

After deploy, we can call the approuter app.
To compose the URL, take the application URL from the cloud cockpit and concatenate it with the route which we defined in xs-app.json

In my example:

approuterForJobApp.cfapps.eu10.hana.ondemand.com
and
/humanuser
leads to
https://approuterforjobapp.cfapps.eu10.hana.ondemand.com/humanuser

When opening that URL in a browser, we’re presented a login screen

Here we can enter user/password of our cloud user and as a result, we see the response of the endpoint /doSomething as defined in our sample application in previous tutorial

Note:
In case you get an error message like “Forbidden” or “unauthorized”:
In previous blog we defined a scope in our app and in xs-security.json file
So to call the endpoint /doSomething, the user must have the required role
In previous blog, Appendix 1  we defined the role and added it to our user
That’s required for this login as well

Optional: Explanation

I recommend to open the developer tools of the browser, to get an idea of what’s happening
If you already logged in, you might need to clean the cache of browser, otherwise you don’t get the login screen again

When calling the approuter URL for a route which has authenticationType “xsuaa”, then the approuter delegates to XSUAA to follow the OAuth flow (grant type “Authorization Code” see here)
It can be seen in my screenshot:

1) Open approuter
https://approuterforjobapp…./humanuser

2) The approuter calls the XSUAA with long URL:
https://<subacc>.authentication.eu10.hana.ondemand.com/oauth/authorize?
response_type=code
&client_id=xxx
&redirect_uri=https://approuterforjobapp…./login/callback

We can see that the “authorization” endpoint of XSUAA has been invoked
The required params have been passed:
The resoponse_type=code identifies the desired OAuth flow.
Means that xsuaa will send code used for authorization afterwards
The approuter identifies itself with clientid etc taken from the xsuaa binding
The approuter has to provide a redirect-URL to which the xsuaa will send the code

3) Then XSUAA presents the login screen: Request URL: https://<subacc>.authentication.eu10.hana.ondemand.com/login

We enter our credentials


4) Then the XSUAA calls the redirect endpoint with the code as param: https://approuterforjobapp…./login/callback?code=A1B2C3D

5) Finally we see the response of
https://approuterforjobapp…..com/humanuser
with the response body containing the response of our sample app

What we don’t see between step 4 and 5:
Approuter uses the code to get a JWT token from XSUAA
Next, approuter uses the token to call our sample app
Then our sample app validates the token and sends response text to back approuter

Optional: Troubleshooting

To get more information in the logs:

  • Add my logger middleware to the sample app,
    as described in appendix 3 of previous tutorial
  • Check the logs of sample app
    cf logs appauthandscope –recent
  • Configure the approuter to write more logs
    add environment variable XS_APP_LOG_LEVEL for approuter with value DEBUG
    can be done in cockpit or on command line
    cf set-env approuterForJobApp XS_APP_LOG_LEVEL DEBUG
    The restage with cf restage approuterForJobApp
  • Check the logs of approuter
    cf logs approuterForJobApp –recent
    Search the logs for “msg” and find useful info
    For your convenience, I’ve parsed the logs of a call through approuter and filtered the interesting output:“msg”:
    “Incoming request to AppRouter.
    Path: /humanuser
    “referer”: “https://<acc>.authentication.eu10.hana.ondemand.com/login”
    “msg”:
    “sending page with client-side redirect to
    https://….authentication…/oauth/authorize?response_type=code
    &client_id=xxx
    &redirect_uri=approuter…/login/callback”
    “msg”:
    “Incoming request to AppRouter.
    Path: /login/callback?code=BnaKv7VrGg
    “referer”: “https://approuterforjobapp.cfapps.eu10.hana.ondemand.com/humanuser”
    “msg”:”requesting UAA at https://<acc>.authentication.eu10.hana.ondemand.com/oauth/token:
    parameters
    “grant_type”: “authorization_code”, “redirect_uri”:
    “https://approuterforjobapp…/login/callback”
    “code”: “***”
    “layer”:
    “/node_modules/@sap/approuter/lib/passport/oauth2.js”“msg”:
    “Incoming request to AppRouter. Path: /humanuser

    “msg”:”Entering refreshToken”

    “msg”:
    “Request to backend. Rewritten URL: https://appauthandscope.cfapps.eu10.hana.ondemand.com/doSomething

    “msg”:
    “Response from backend. Status: 200

    “msg”:
    “Response from AppRouter. Status: 200
    “layer”:
    “/node_modules/@sap/approuter/lib/middleware/request-handler.js”

Finally:
As we can see: approuter is a big help

Flexible Configuration

In above example we’ve defined the destination as full final URL

"url": "https://appauthandscope.cfapps.eu10.hana.ondemand.com/doSomething"

This way, any route can only forward to this fix URL
If our sample app has a second endpoint, we need to create a new destination
It is not flexible
Also, the route is not flexible:

"source": "^/humanuser",
"target": "/"

Also the URL called by end-user is not flexible
https://approuterforjobapp.cfapps.eu10.hana.ondemand.com/humanuser

I mean, e.g. adding parameters wouldn’t be possible:
https://approuterforjobapp…./humanuser?&performance=log

Solution:
In the following example, we define only the host in the destination

"url": "https://appauthandscope.cfapps.eu10.hana.ondemand.com

And the route contains a placeholder

"source": "^/douaa/(.*)$",
"target": "$1",

The desired endpoint name is now added to the URL called by end-user
https://approuterforjobapp.cfapps.eu10.hana.ondemand.com/humanuser

Try it

Let’s keep the existing configuration, we can just add more additional entries
Add the following destination to manifest.yml

    destinations: >
      [
          {
              "name": "env_destination_jobapp_host",
              "url": "https://appauthandscope.cfapps.eu10.hana.ondemand.com",
              "forwardAuthToken": true
          },

And add the following route to xs-app.json

  "routes": [
    {
      "source": "^/humanflex/(.*)$",
      "target": "$1",
      "destination": "env_destination_jobapp_host",
      "authenticationType": "xsuaa"
    },

Deploy

After deployment, try the following URL:
https://approuterforjobapp.cfapps.eu10.hana.ondemand.com/humanflex

Result is an error: not found
Reason: this route is not defined. We need a slash at the end

Try this:
https://approuterforjobapp.cfapps.eu10.hana.ondemand.com/humanflex/

Result is a new error: Cannot GET /
Reason can be seen in the approuter logs:

“msg”:
“Request to backend.
Rewritten URL: https://appauthandscope.cfapps.eu10.hana.ondemand.com/

And

“msg”:”Response from backend. Status: 404

So we can know that the reason is:
Our endpoint /doSomething has not been invoked
And our app doesn’t react to the root URL: /

As such, we know what we have to do:
We wanted intentionally to remove the /doSomething from the route, to be more flexible
As such, we have to write it in the URL

So now try this URL:
https://approuterforjobapp.cfapps.eu10.hana.ondemand.com/humanflex/doSomething

Result is success, we get the response from endpoint in browser

Note:
The URL is an approuter-URL, not our sample-app-URL

Use App Router with Browser and REST client

Above configuration has used the authentication type “xsuaa”
We’ve learned that approuter handles communication with XSUAA, and  XSUAA presents login screen

Now we want to call our approuter-URL from REST client
This is necessary in case that endpoints need Http Method POST
For testing, we need a REST client (e.g. postman).
If it is an extension of the same browser, we need to clean the cache, such that new fresh login is required

Ok.

Try to call
https://approuterforjobapp…./humanflex/doSomething
with GET request from REST client

Result is success, Status code 200

BUT… we shouldn’t be too happy…

If we look at the response body, we don’t see the expected text from our endpoint
Instead, we see some <html … > code
From our experience above, when looking at the html content, we can guess that here a login-screen has been sent as response
And the reason for the status code 200 is:  a response has been successfully sent

Ahhhhh – we think we know the reason:
We forgot to specify user credentials

So we go ahead and send the request with REST client again, this time we add our user credentials
However, the result is exactly the same

Now we get little panic.

We remember something with OAuth
But this won’t solve the problem either, the approuter WANTS a login screen.
Why?
Because we specified “xsuaa” as “authenticationType”

Ahhhh – we think we know the reason now!!!

In REST client we’re specifying “Basic Authentication” with our user credentials
AND we know the solution:
In approuter config, we need a route which has “authenticationType” as “basic” !!!

So we go ahead and in xs-app.json file, we define another additional route:

    {
      "source": "^/restflex/(.*)$",
      "target": "$1",
      "destination": "env_destination_jobapp_host",
      "authenticationType": "basic"
    },

We don’t need different destination here

After redeploy, we can call the following URL in REST client
https://approuterforjobapp.cfapps.eu10.hana.ondemand.com/restflex/doSomething

And this time we really need to specify our user credentials (our human cloud user) in REST client
Depending on the chosen REST client tool, a popup might be displayed
And this time we get not only success status, we also get the expected response text

Use App Router with Job Scheduler

Finally we’re reaching the only important chapter of this tutorial:
How to call approuter URL from Jobscheduler

We have now 3 URLs defined in our app router
We can try them with Jobscheduler….
But this chapter wouldn’t make sense if we could just call them without any problem
Let’s guess: It doesn’t work
Correct

Reason 1 :
When using the “xsuaa”-route, Job Scheduler will get the same <html…> code along with a success status
But it is an error anyways, as we know

Reason 2:
When using the “basic”-route, Job Scheduler will fail because it isn’t possible to send the user credentials along with the request
Yes, or better: no, Jobscheduler doesn’t support calling endpoint with basic authentication
It is currently not possible to send user-credentials, nor authorization header

Reason 3?
None

Solution?
Continue reading

We know that Job Scheduler supports OAuth flow out of the box.
As such, no more authentication support by approuter is required
Job Scheduler will already send the JWT token, this is the authentication
Approuter ONLY should forward the token
Approuter should NOT present ANY login screen

For such cases, we can use the “authenticationType: none”

Note:
Nevertheless, we require that approuter takes the JWT token and forwards it to our target sample application
We have the setting “forwardAuthToken” in the destination set to “true”
And we expect that approuter forwards it. Not only if approuter has handled the authentication, but also if the incoming call contains a token

So now we can go ahead and define a new route

    {
      "source": "^/jobflex/(.*)$",
      "target": "$1",
      "destination": "env_destination_jobapp_host",
      "authenticationType": "none"
    }

Deploy

Afterwards, we can try calling that route in browser
https://approuterforjobapp.cfapps.eu10.hana.ondemand.com/jobflex/doSomething
In browser?
Yes, we’re happy if router doesn’t require authentication…
However, we will be disappointed:
The call will fail
Approuter doesn’t require authentication, but our target sample app does

We could try with REST client, using OAuth authorization dialog, as explained here
This should work, but we’re too impatient, we want to try Jobscheduler, so we can finally CLOSE this tutorial…

OK.
We open Job Scheduler Dashboard and create a Job using the following action URL
https://approuterforjobapp.cfapps.eu10.hana.ondemand.com/jobflex/doSomething


The result is green, status code 200 AND the response is as expected
Great, isn’t it?

Super. Can we finish the tutorial?

However, I have an idea for another chapter

When going through the documentation for xs-app.json configuration syntax for routes,
I found the word “scope”
This is interesting, so let’s dedicate a chapter to it

Scopes

We can specify a “scope” in a route
As a consequence, approuter will check if the incoming request contains the required scope or the role (in case of user)

Quick test:
Add these 2 routes to xs-app.json

    {
      "source": "^/humanscope/(.*)$",
      "target": "$1",
      "destination": "env_destination_jobapp_host",
      "authenticationType": "xsuaa",
      "scope": "$XSAPPNAME.scopeformyapp"
    },
    {
      "source": "^/humanscopeerr/(.*)$",
      "target": "$1",
      "destination": "env_destination_jobapp_host",
      "authenticationType": "xsuaa",
      "scope": "not_existing_scope"
    }

After redeploy, try to call these 2 new routes with browser

https://approuterforjobapp…./humanscope/doSomething

https://approuterforjobapp…./humanscopeerr/doSomething  

The first call will be successful, while the second one will fail with error “Unauthorized”
A quick look into the approuter logs shows nice information:

‘User not authorized,
source of route: /^\\/humanscopeerr\\/(.*)$/,
required scopes: not_existing_scope,
user scopes: openid,xsappwithscopeandgrant!t22273.scopeformyapp’

“msg”:
“GET request to /humanscopeerr/doSomething completed with status 403 –
You do not have the required scopes to access this resource.”

This is really useful error message and it shows that app router has a built-in convenience feature to validate the configured scopes
Nice.

Note:
Approuter does not execute the scope-validation if “authenticationType” is set to “none”

Final Remark

Approuter is very flexible and there are many many use cases
You can use it as reverse proxy
You can use it to add login screen
You can use it to map basic auth to oauth
You can use it to add csrf-protection
You can use it to add scope protection
You can use it to add auth to non protected endpoint (if nobody knows the open endpoint)

And many more

However:
In our examples given above, the app router is not required for Job Scheduler
It only forwards the call to the endpoint
So Job Scheduler can as well directly call the endpoint
Since Job Scheduler is only a machine, it doesn’t care how a URL looks like

See below what documentation says: 

A calling component accesses a target service by means of the application router only if there is no JSON Web Token (JWT) available, for example, if a user invokes the application from a Web browser. If a JWT token is already available, for example, because the user has already been authenticated, or the calling component uses a JWT token for its own OAuth client, the calling component calls the target service directly; it does not need to use the application router.

And

The application router does not “hide” the back-end microservices in any way; they remain directly accessible when bypassing the application router.

OK, now that we’ve recognized that my blog was totally useless I’d like to say that I’ve written the blog for those of you who want or need the app router and I hope it has provided some help in basic understanding

Summary

In this blog we’ve learned how to use app router
We used app router as a component which is placed in between user and sample app
App router helps to do authentication for users
We’ve learned how to configure app router such that it can be called by Job Scheduler

We’ve noticed that for Job Scheduler, the app router is not really required

Links

SAP Help Portal App Router Docu
SAP Help Portal Configuration in xs-app.json

App Router Blog 1
App Router Blog 2
App Router Blog 3

Appendix: All Project Files

Below files contain all options discussed above

manifest.yml

---
applications:
- name: approuterForJobApp
  host: approuterForJobApp
  memory: 128M
  services:
  - xsuaawithscopeandgrant
  env:
    destinations: >
      [
          {
              "name": "env_destination_jobapp_host",
              "url": "https://appauthandscope.cfapps.eu10.hana.ondemand.com",
              "forwardAuthToken": true
          },
          {
              "name": "env_destination_jobapp",
              "url": "https://appauthandscope.cfapps.eu10.hana.ondemand.com/doSomething",
              "forwardAuthToken": true
          }
      ]

package.json

{
  "name": "myapprouter",
  "scripts": {
    "start": "node node_modules/@sap/approuter/approuter.js"
  },
  "dependencies": {
    "@sap/approuter": "latest"
  }
}

xs-app.json

{
  "authenticationMethod": "route",
  "routes": [
    {
      "source": "^/jobflex/(.*)$",
      "target": "$1",
      "destination": "env_destination_jobapp_host",
      "authenticationType": "none"
    },
    {
      "source": "^/humanflex/(.*)$",
      "target": "$1",
      "destination": "env_destination_jobapp_host",
      "authenticationType": "xsuaa"
    },
    {
      "source": "^/restflex/(.*)$",
      "target": "$1",
      "destination": "env_destination_jobapp_host",
      "authenticationType": "basic"
    },
    {
      "source": "^/humanuser",
      "target": "/",
      "destination": "env_destination_jobapp",
      "authenticationType": "xsuaa"
    },
    {
      "source": "^/humanscope/(.*)$",
      "target": "$1",
      "destination": "env_destination_jobapp_host",
      "authenticationType": "xsuaa",
      "scope": "$XSAPPNAME.scopeformyapp"
    },
    {
      "source": "^/humanscopeerr/(.*)$",
      "target": "$1",
      "destination": "env_destination_jobapp_host",
      "authenticationType": "xsuaa",
      "scope": "not_existing_scope"
    }
  ]
}

 

 

Be the first to leave a comment
You must be Logged on to comment or reply to a post.