Technical Articles
SAP Cloud Platform Backend service: Tutorial [26]: App Router (3): route to Backend service
Quicklinks:
Summary
Reference
Sample project files
This is the next round of our Application Router learning.
In the first part: the basics
In the second part: add user login
In this part: use one login and reuse it to call a protected API
Everything is done without one single line of code
Goal
Create an Application Router configuration app which routes to an API in Backend service
The advantage:
When using App Router, the user doesn’t need to do manual complex token handling
Remember: When directly calling a Backend service API, the user needs to manually follow the OAuth flow requirements
Prerequisites
Coding skills: not required at all
App Router Tutorial part 1: sure, required
App Router Tutorial part 2: recommended
Backend service tutorials: obviously, one is required, all others are just good
Configuration in cloud: access role for API is required
We want to call an API in Backend service. (see here)
In my examples, I’m using a service called PRODUCTSERVICE
As such, when I say “Backend service API”, I mean an OData service like this:
https://backend-service-api.cfapps.eu10.hana.ondemand.com/odatav2/DEFAULT/PRODUCTSERVICE/Products
It was created with SAP Cloud Platform Backend service
If you’ve created an API, you must have configured your user roles as required (see here)
Preparation
You can reuse the existing app from previous tutorials, because we’re just adding another route (and destination)
Anyways, you can also create the same project structure again, then deploy with different name
XSUAA service instance
In previous learning 5, we’ve already created an instance of XSUAA, which was required to enable authentication/authorization
Now, for calling our API in Backend service, we need an XSUAA instance which contains the required scope.
Background: an API in Backend service is protected via OAuth and to call an API, the caller needs to have a certain role.
This required role is defined by Backend service. We need to add a reference to that role in our XSUAA instance
For more Background, see here – and for configuring the xsuaa instance, see here
Summary:
Create an instance of XSUAA with name “XsuaaForAppRouterWithBs” and these parameters:
{
"xsappname": "XsuaaForAppRouterWithBs",
"tenant-mode" : "dedicated",
"foreign-scope-references": [
"$XSAPPNAME(application,4bf2d51c-1973-470e-a2bd-9053b761c69c,Backend-service).AllAccess"
]
}
Note:
tenant-mode dedicated is required, otherwise host pattern need to be specified
After creation, add the dependency to the manifest.yml file:
. . .
path: appfolder
services:
- XsuaaForAppRouterWithBs
. . .
Note:
If you already have a dependency to XSUAA service, make sure to replace it
Note:
If you’ve already deployed this app, while following the tutorial, it is better to undeploy the app from Cloud Foundry, then redeploy from scratch
It doesn’t take more time
Delete either with delete command (cf d <myApp> -r -f) or pressing the “delete”-icon in the “Applications” overview in Cloud Cockpit
Learning 8: Route to Backend service API
Why another chapter? We’ve already defined lots of routes…
This title is a bit understatement
It should be: Route to an OAuth-protected API
If you’ve read the blog about calling a Backend service API from remote application, then…
No, I haven’t
You should
Only should? No must?
You should read it, it is a must-read
Ehmm
OK, the blog about calling a Backend service API from remote application shows that calling the Backend service API is not simple, because it is protected by OAuth 2.0
The security-mechanism of Backend service APIs require a JWT token. It has to be obtained from “Authorization Service”, etc
For explanation see here and here
Apart from marketing…
I know the question: what does it have to do with App Router?
Yes, that was the question
So how do the great AppRouter blogs relate to the fantastic OAuth blogs?
That was not really the question…
The answer:
In the wonderful previous tutorial, we’ve learned that we can use App Router for authentication and authorization. So, App Router can display user login screen
In such scenarios, App Router is bound to an instance of XSUAA
In such scenarios, the XSUAA is used as “Authorization Server” for App Router.
It issues a JWT token for access to App Router
The good question: Why not just reuse the JWT token for authentication to Backend service?
And, the good answer: It is possible
Another good question: how can the magic be done?
Another good answer: in the App Router config: in the configured destination
Short question: how?
Short answer: forwardAuthToken property
See the diagram for simplified flow:
Before my (always impatient) alter ego starts complaining, let’s do it:
It is short: just add a property to the destination
“forwardAuthToken”: true
manifest.yml
Add a destination pointing to your Backend service API with entity set
It contains the forwardAuthToken property:
services:
- XsuaaForAppRouterWithBs
env:
destinations: >
[
{
"name": "env_destination_bs",
"url": "https://backend-service-api.cfapps.eu10.hana.ondemand.com/odatav2/DEFAULT/PRODUCTSERVICE;v=1/Products/",
"forwardAuthToken": true
}
xs-app.json
Specify a new route:
{
"authenticationType": "xsuaa",
"source": "^/backendservice/(.*)$",
"target": "$1",
"destination": "env_destination_bs"
}
Deploy & Test
After deploy, open the App Router route.
For example:
https://myapprouterconfappbs.cfapps.eu10.hana.ondemand.com/backendservice/
In the login screen, enter the user credentials of a valid Cloud Platform user, e.g. your Trial Account user
Note:
The user must have the required role for accessing Backend service API (as mentioned above)
Afterwards, the expected payload of the OData service should be displayed.
That’s it
Cool, it was easy!
Yes, it was so easy: only by adding the little forward-property to the destination, we were able to overcome the OAuth protection of Backend service
“Overcome”??? Does that mean that we’ve hacked the API???
No, no, don’t panic
Everything is fine: there was the login-screen displayed by XSUAA server
So why do you say “overcome”?
Because for an end-user it is tedious to handle the OAuth flow from external application
Now by using App Router, it is much easier: end-user only needs to enter his credentials
But…. There’s a prerequisite
The correct App Router-configuration
Troubleshooting:
If you get an error message saying that you don’t have “sufficient authorization”, then there’s something wrong with the authorization
Obviously…
I see 2 possible error causes:
XSUAA:
Either the Backend-service-scope is missing, or the App Router is not bound to the correct XSUAA
User:
Check if the end-user has the Backend-service-role assigned
And one more possible error cause:
Check if the App Router configuration is correct
If you get an error message during deploy “No UAA …”:
Check if there’s anything wrong with the XSUAA instance
Check if the binding of your app to the service instance has been done
If everything looks fine, you can probably solve the error by deleting the app and re-deploying
Learning 9: Calling Backend service via App Router from external REST client tool
We can still improve the learning of above chapter
Still not reached the end?
2 aspects to improve
– We cannot use REST client, because the App Router itself is OAuth protected
– the destination URL is hardcoded, pointing to one entity set
Route protection
The interesting point to learn here:
We can change the type of authentication for our route from “xsuaa” to “basic”
Like that, our entry to App Router is protected with “Basic authentication” instead of OAuth
Like that, we can easily use a REST client tool for executing e.g. POST requests
{
"authenticationType": "basic",
"source": "^/srv/(.*)$",
"target": "$1",
"destination": "env_destination_odataservice"
}
Route destination
Until now, we could call the App Router endpoint and we received the response of the OData entity set. But we weren’t able to perform e.g. a single READ (details of one entry)
See below for possible OData URLs
We want to use the App Router endpoint similar like the final OData service. As such, we need more flexibility
So now, we change the destination URL: it should point to the root URL of the OData service:
{
"name": "env_destination_odataservice",
"url": "https://backend-service-api.cfapps.eu10.hana.ondemand.com/odatav2/DEFAULT/PRODUCTSERVICE;v=1/",
"forwardAuthToken": true
}
Deploy
As usual, after modifying the configuration, delete the deployed app in Cloud Foundry and redeploy the application
Test in browser
After deployment, open a browser and enter the app URL with appended route (/srv/):
https://myapprouterconfappbs.cfapps.eu10.hana.ondemand.com/srv/
Result:
This time we don’t get the user login page. This time we get a user credentials popup
What’s the difference?
This time no redirect to XSUAA (XSUAA acting as OAuth “Authorization Server”)
Just “basic authentication” is required
If we check the URL of the browser, we can see that the URL hasn’t changed
If we check the dev tools of the browser, we can see that no redirect has happened
The good thing:
With our App Router configuration, we can call the OData URIs as we’re used to
Examples:
https://myapprouterconfappbs.cfapps.eu10.hana.ondemand.com/srv/$metadata
https://myapprouterconfappbs.cfapps.eu10.hana.ondemand.com/srv/Products
https://myapprouterconfappbs.cfapps.eu10.hana.ondemand.com/srv/Products(‘1’)
https://myapprouterconfappbs.cfapps.eu10.hana.ondemand.com/srv/Products(‘1’)/categoryLink
Note:
They look like OData service URLs, but in fact they are APP Router URLs
Test in REST client
To fire requests against the App Router app, just proceed as for normal OData service
Enter the URL .e.g https://myapprouterconfappbs.cfapps.eu10.hana.ondemand.com/srv/Products
Configure the Authorization type as “Basic” and enter your (Trial) user and password
Then just hit send
Cool.
No tedious token-fetching anymore
Now try with POST/PUT/DELETE
You’ll see that everything just works like you’re used to
Stop! Help!
What’s the problem?
I’m getting “Forbidden”
What have you done?
Nothing
Which steps were required for nothing?
Just POST request – like I’m used to
Aha, and the x-csrf-token?
Aha, the x-what-token?
I see.
I don’t see
Listen:
As you know, for security reasons, modifying requests are secured by x-csrf-token
No
No what?
I didn’t know
OK, you need to send that token whenever you fire a POST/PUT/PATCH/DELETE request
How does it look like?
e.g.: x-csrf-token: 13f7f69080d0f6fd-MGG3fnoL21k5cse1hQ86wA0R4s=
How do I get it?
GET it with x-csrf-token: fetch (See appendix)
Which URL?
Can be any valid URL
E.g. www.google.com?
No, a URL of your service, e.g. the $metadata document
https://myapprouterconfappbs.cfapps.eu10.hana.ondemand.com/srv/$metadata
Disable the CSRF protection
The very last section.
Finally
If circumstances permit, we can disable the csrf protection (e.g. for testing phase)
This is an App Router setting, defined on route level
As per default, the csrf protection is enabled
We can disable it by adding the following property to our route:
“csrfProtection”: false
For trying it, we can define an additional route, e.g.
{
"authenticationType": "basic",
"csrfProtection": false,
"source": "^/srvnotoken/(.*)$",
"target": "$1",
"destination": "env_destination_bs_api"
}
This has been a nice learning:
No access token
No x-csrf-token
Wonderful….
Troubleshooting
If your request in REST client doesn’t work, it returns status 200 but the response body contains the javascript for displaying a login form, although you’ve entered the correct credentials
-> then the reason might not be wrong credentials, it might be that you’ve entered the wrong route in the URL of your request
Summary
We want to call an API defined in SAP Cloud Platform Backend service
That API is protected with OAuth 2.0
This means, that for calling it, an access token must be sent
Instead of fetching the token ourselves, we can use App Router
At beginning, the App Router displays user login screen to get access token
The App Router supports forwarding that access token to target service
We can define a route to Backend service API, incl forwarded token
A nice benefit:
It is possible to configure the route with authentication type as “basic”, but still forwardAuthToken
Basic authentication is very useful when using a REST client tool
Even the csrf protection can be disabled, to make life even easier
Links
SAP Help Portal: documentation to configure csrf protection in app router: here
Interesting readings:
App Router tutorial 1
App Router tutorial 2
OAuth explanation 1
OAuth explanation 2
Manually calling an API usind REST client
All the other blogs about SAP Cloud Platform Backend service
Appendix: Reference
The App Router configuration:
xs-app.json:
Enable authentication on top-level:
“authenticationMethod”: “route”,
and specify on route-level
“authenticationType”: “xsuaa”,
or
“authenticationType”: “basic”,
manifest.yml:
enable forwarding
“forwardAuthToken”: true
Bind app to XSUAA instance
XSUAA instance must contain the scope which is required by Backend service API
To disable the csrf protection: specify on route-level
“csrfProtection”: false
Appendix: x-csrf-Token
If you fire requests which modify the data of a service, you have to be aware that it might require an x-csrf-token header.
As such, if your POST, PATCH, PUT, DELETE request fails, you can check the response if there’s a header:
x-csrf-token : required
If yes, proceed as follows:
Execute a valid GET request and add a header
x-csrf-token : fetch
The response contains the valid token in a header
x-csrf-token : 123412324134
Copy the value and send your intended request (e.g. POST) with header:
x-csrf-token : 123412324134
Appendix: sample project files (Learning 9)
xsuaa-params
{
"xsappname": "XsuaaForAppRouterWithBs",
"tenant-mode" : "dedicated",
"foreign-scope-references": [
"$XSAPPNAME(application,4bf2d51c-1973-470e-a2bd-9053b761c69c,Backend-service).AllAccess"
]
}
The full command:
(adapt myInstanceName and myXsappname)
cf create-service xsuaa application myInstanceName -c “{\”xsappname\”:\”myXsappname\”,\”tenant-mode\”:\”dedicated\”,\”foreign-scope-references\”:[\”$XSAPPNAME(application,4bf2d51c-1973-470e-a2bd-9053b761c69c,Backend-service).AllAccess\”]}”
Note:
The id in foreign scope is valid for Trial account
manifest.yml
---
applications:
- name: ApprouterToBackendservice
host: tobs
path: appfolder
memory: 128M
services:
- XsuaaForAppRouterWithBs
env:
destinations: >
[
{
"name": "env_destination_bs_api",
"url": "https://backend-service-api.cfapps.eu10.hana.ondemand.com/odatav2/DEFAULT/PRODUCTSERVICE;v=1/",
"forwardAuthToken": true
}
]
xs-app.json
{
"authenticationMethod": "route",
"routes": [
{
"authenticationType": "basic",
"source": "^/srv/(.*)$",
"target": "$1",
"destination": "env_destination_bs_api"
}
]
}
package.json
{
"name": "approuterconfigapp",
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
},
"dependencies": {
"@sap/approuter": "^6.0.1"
}
}
Hi Carlos Roggan ,
It's very helpful.
One more question is, for example, I have a UI5 application and need to query data from the Backend service API. How can I integrate UI5 application with the app router?
Regards,
Tri
Hi Minh Tri Le ,
Sorry for the late reply, and thanks for your question.
Now I have tried it and please refer to below blogs for my findings:
https://blogs.sap.com/2019/07/15/sap-cloud-platform-backend-service-tutorial-30-api-called-from-html/
https://blogs.sap.com/2019/07/16/sap-cloud-platform-backend-service-tutorial-31-api-called-from-ui5/
And thanks for your feedback !!
Kind Regards,
Carlos
They're very helpful.
Thanks a lot Carlos Roggan .
In case anybody encounters the 403 while working wit SuccessFactors OAuthSaml2BearerAssertions disblae csrf in your route, approuter is handling that internaly, everything else does not work
super, thanks for the contribution !!
Hello,
In case if the service has to be accessible from ui5 and as an API both, how do we set the authenticationType in xs-app.json?
Will it accept both "xsuaa" and "basic"?
Thanks,
Anand
I don't think that's supported, but what you can do would be to define 2 routes with different auth and pointing to the same destination.
KInd Regards,
Carlos
Thanks Carlos for your reply!
I tried with another route in xs-app.json going to same destination. But it gives me an error -
Hello Anand Kolte ,
I think the $1 and the target property are missing
Thanks Carlos Roggan , that worked!
Hello Carlos Roggan
Thanks for the blog post.
I am facing an issue in our application. when the browser language is set to 'Japanese', the application shows blank page. On investigating it is found that sap-ui-core.js library is not loading properly. And after checking with SAP UI5 teams, they observed that it is because of the content type header value modified to "text/html;charset=Shift_JIS", which is causing the issue.
It should have been "text/html;charset=utf-8".
Is there any way I can add content type in the approuter while returning the html file ?
Thanks
Pradeep
Hello Pradeep T N ,
sorry for the late reply - I was on vacation.
I remember from the approuter docu the support for custom headers - that could be used to serve your needs, maybe?
See here:
https://www.npmjs.com/package/@sap/approuter
Kind Regards,
Carlos
Hello Carlos Roggan,
Thanks for the blog post!
I have a scenario that I am holding a JWT (issued by SAP) and I can use Postman to call directly to my backend app as expected (without using the app router).
Now, a new requirement coming, I need to use the app router to forward the JWT to the backend app.
Here is my configuration for the app router
package.json
Create xs-app.js and use "authenticationMethod": "none"
manifest.yml
In this step I use "forwardAuthToken": true (I am expecting the app router can forward the JWT to the backend API app)
Using Postman and calling directly to my backend app with the Bearer JWT (issued by SAP), I get the response as expected.
Then, call the app router and pass the same Bearer JWT in the request header, but it returns a Not Found error.
I am looking for a way to configure the app router to forward the Bearer JWT to my backend app. But, I think my configuration is incorrect, or the app router cannot work in this way.
Why do I have this scenario?
In my system, there are some non-GUI apps (i.e., web services, OS services,...) and they cannot display the login form to enter credentials. So, I have to use JWT issued by SAP mentioned above to call my backend app hosted in SAP BTP. And it works without using the app router.
Why do I need to use the app router in this scenario?
There is some business logic I need to put into the app router (i.e., license validation,...). So, the app router is added in this scenario.
I am expecting the app router will only forward JWT to my backend app.
Does the app router support this scenario? Or, is there an alternative way to solve this challenge?
Hello Devin Bui
as far as I understand, your scenario is similar to what I had investigated some time ago:
https://blogs.sap.com/2020/02/27/using-job-scheduler-in-sap-cloud-platform-4-using-app-router/
I assume, you'll find the solution there.
As far as I remember, I did few simple config in approuter, such that it fust forwards the token.
I really hope this helps you.
You've very well described your scenario, top !
Kind Regards,
Carlos
Thank you for your help, Carlos Roggan!
After updating xs-app.js as below, it works.
Thanks for the update Devin Bui !
Good to know that your scenario is working and also thanks for the description, good to know about this use case.
Kind Regards,
Carlos