EDUCAÇÃO E TECNOLOGIA

SAP Application Router

In this blog post I will try to help you to understand what is AppRouter, how to easily configure and consume it. I will also provide answers for frequently asked questions and solutions for possible issues.

AppRouter – is Node JS library that is available in Public SAM NPM. It represents a single entry point to your application.

  • SAP CF – Cloud Foundry
  • SAP XSA – XS Advanced (On Premise)
  • Local environment
  • Dispatching of requests to other microservices
  • Authentication
  • Authorization check
  • Complete integration with Destination service
  • Complete integration with HTML5 Application repository
  • Complete integration with Business Services
  • Inside you application’s package.json add the dependency on Approuter and configure its start point:
{ "dependencies": { "@sap/approuter": "<APPROUTER_VERSION>" }, "scripts": { "start": "node node_modules/@sap/approuter/approuter.js" } }
  • Configure your NPM to point to Public SAP Registry:
npm config set @sap:registry https://npm.sap.com/
  • Run npm install
  • Now you have approuter code inside your node_modules, you can start work

The main configuration for Approuter is done by file xs-app.json that needs to be created in the level of your AppRouter application. Next sections in this blog will help you to configure your Approuter correctly with various modeling options in your xs-app.json file.

Main properties on root level:

authenticationMethod

This property indicates which authentication will be applied for this xs-app.json. Can be none (means that all routes are not protected) and route (authentication type will be chosen according to definition in a particular route). The default value is route .

logout

By using this property, you can define two important thing about your business application central logout handling.

logoutEndpoint – contains some internal path. When accessing this path your application will trigger central logout procedure. Triggering central logout will destroy user session data in Approuter, call XSUAA in order to remove user session on their side and also will call logout paths of destinations that are defined for this specific application (please refer to property destinations).

logoutPage – can be internal path or absolute external URL. Value of this field describes so called “landing page” page address, that user will be redirected in the browser after central logout.

"logout": { "logoutEndpoint": "/my/logout", "logoutPage": "/logout-page.html" }

Tip: In order to change browser location and trigger central logout from client-side Javascript code you can run:

window.location.replace('/my/logout');

For more information and tips please refer to section Logout flow .

destinations

This property indicates destinations endpoints that need to be called during session/central logout in order to destroy sessions on their side.

services

The same as destinations. Services can implement their specific logout logic and approuter will trigger these endpoints during central logout / session timeout scenario.

Routing:

One of important capabilities of AppRouter is to be a reverse proxy for your application. In order to achieve that you need to model correctly property routes . That property is an array of Objects. Each object represents one particular route.

Let’s see how your routes should be modelled.

source

First main property inside the route is “source”. The value can be string or regular expression that should match the incoming request URL. Example:

routes": [ { "source": "/rest/.*"

This definition means that if the Approuter is called with request https:<approuter_host>/rest/… , this particular route configuration will be applied.

Tip: A request matches a particular route if its path contains the given pattern. To ensure the regular expression matches the complete path, use the following form: ^$`. Example:

"source": "^/rest/addressbook/testdataDestructor$"

Here we expect to have complete URL matching.

Tip: Be aware that the regular expression is applied to on the full URL including query parameters.

target

Second important property is “target” – it is an optional property that defines how the incoming request path will be rewritten for the corresponding destination. You can use capture groups inside regular expressions that are defined in “source” . Example:

"routes": [ { "source": "/sap/com/(.*)", "target": "$1"

This definition means that if the Approuter is called with request https:<approuter_host>/sap/com/server.js, it will be overwritten by AppRouter to  { "source": "^/web-pages/(.*)$", "target": "$1", "scope": ["$XSAPPNAME.viewer", "$XSAPPNAME.reader", "$XSAPPNAME.writer"] }

cacheControl

This String property represents the content of “Cache-Control” header in Approuter response and is relevant only for static resources Approuter will serve that are stored in your project. By default, this header is not set.

Tip: Model this property for your main route in order to prevent caching it by browser. Otherwise, after logout your page will be retrieved from the Browser Cache without reaching Approuter and login flow will not take place.

{ "routes": [ { "source": "^/ui/index.html", "target": "index.html", "localDir": "web", "cacheControl": "no-cache, no-store, must-revalidate" } ] }

Important note: The order of routes is very important – AppRouter tries to find matching route one-by-one until it finds one appropriate.

You can simply run Approuter configured by you by pushing your application to CF/XSA

or by running npm start in your local environment.

One of main capabilities of AppRouter is authentication with SAP XSUAA. The most common authentication type is OAuth2 authentication. Whenever users types URL of his application that uses AppRouter, login flow is taking place. It contains redirection to XSUAA where he can specify user and password and then return to the page that was requested in the beginning.

Here are the steps Approuter executes during Login flow:

  • User types address in url, for example https://my_approuter.cfapps.sap.hana.ondemand.com/index.html
  • Request comes to Approuter, where we check whether authentication is required and if yes – check whether session in Approuter also exists
  • In case there is no session – Approuter returns 200 to the Browser a tiny HTML page that is rendered in the Browser in case it is not an AJAX request. This HTML will contains several tags including location of XSUAA for client side redirect and cookie with information about the page that was primarily requested for example /index.html
  • XSUAA redirects to IDP where login page is displayed
  • User specifies user and password in login page and is redirected back to XSUAA
  • XSUAA redirects back to Approuter and attaches authorization code to the URL
  • Request arrives to Approuter and Approuter does the following:
    • Sends authorization code it got to XSUAA Server-to-Server
    • XSUAA responds with JWT token in case authorization code was valid
    • Approuter creates session on its sides and stores JWT token in it
    • Reads the cookie information about the page that was requested in the beginning
    • Redirects to the primarily requested page for example /index.html

Fragment handling:

In case the main page that was requested has client-side navigation suffix (# signs),for example https://my_approuter.cfapps.sap.hana.ondemand.com/index.html#MyApp , Approuter will redirect to exact page after login, i.e. including specified page place (#MyApp).

Approuter logout can take place in two main scenarios:

  • Approuter session was timed out (Session timeout)
  • User explicitly triggered logout flow (Client initiated)

Session timeout:

Session timeout can be modeled by configuring environment variable SESSION_TIMEOUT , the default value is 15 minutes.

Client initiated:

User can model logout section in xs-app.json and provide following properties:

  • logoutEndpoint – user by going to this endpoint will trigger the central logout
  • logoutPage – landing page that UAA will redirect to it after logout
, "logout": { "logoutEndpoint": "/my/logout", "logoutPage": "http://employees.portal" },

Here are the steps Approuter executes during Session timeout:

  • Session timeout in memory store fires event when the session time has been passed
  • Approuter deletes user session from session store
  • Approuter requests all backend services logout paths
  • Approuter requests all business services logout paths

Here are the steps Approuter executes during Client initiated logout:

  • Approuter deletes user session from session store
  • Approuter requests all backend services logout paths
  • Approuter requests all business services logout paths
  • Approuter redirects to logout endpoint of UAA and provides a modeled logoutPage as a redirect parameter
  • UAA executes logout and removes the session on their level
  • UAA verifies that logoutPage is in its modeled whitelist
  • UAA redirects to logoutPage

Tip: 

UAA will execute redirect only in case redirect URL is a valid redirect-uri in xs-security.json – redirect-uris are maintained as part of the oauth2-configuration section in the UAA application security descriptor JSON file given at the creation of the service instance. For example:

UAA application security descriptor xs-security.json:

"oauth2-configuration": { "redirect-uris": [ "http://employees.portal" ] }

Tip: In case your logoutPage is internal (accessed via Approuter host) you should have a dedicated route for that. This route must be public route (authenticationType: none) because user lands on this page after logout process. Otherwise you will remain stuck with logout-login loop.

{ "source": "^/logout-page.html$", "localDir": "openResources", "authenticationType": "none" }

Tip: In case your logoutPage is internal (accessed via Approuter host) and you want user can login again from it after, it is recommended to build your landing page with a link for example “Sign in again” and point it to your main page that must be protected path (authenticationType: xsuaa) or redirect to your main page after several seconds. In that way login flow will be triggered and a user  will see in the browser login form of UAA. More details can be found in Login flow section.

{ "source": "^/logout-page.html$", "localDir": "openResources", "authenticationType": "none" }, { "source": "^/ui/index.html", "target": "index.html", "localDir": "web", "authenticationType": "xsuaa" }

Tip: Be sure that your main route in your xs-app.json resource that matches the path is not cached by browser. Therefore, the best practice here would be to model “cacheControl” accordingly

{ "routes": [ { "source": "^/ui/index.html", "target": "index.html", "localDir": "web", "cacheControl": "no-cache, no-store, must-revalidate" } ] }

Generally, Application should start connection with Approuter with a GET call. Approuter checks the request and in case the route is protected (requires authentication), there is no session for it and the HTTP method is not GET – Approuter will return status code 401. The same happens if there is no session and AJAX request comes to AppRouter. AppRouter treats a request as AJAX if it contains

the following header ‘x-requested-with’: ‘xmlhttprequest’ .

This use-case is very useful for applications.

There are cases in which session timeout takes place in the middle of user’s work. For example a user is filling some form in UI with several fields. Let’s say he made a break and at this time session was timed out. When user returns – he continues filling the form and presses “Save”. Approuter will send status 401 and it helps the application to understand that it needs to store the state. After state was saved the application can refresh the page (for example location.reload() ), that will trigger Approuter login flow, and then to fetch the stored state.

Application router exposes functionality of CSRF protection. You can achieve that by setting property “csrfProtection” with boolean value. The property itself is optional, default value is true. If you want to disable it – specify explicit “csrfProtection”:false on one particular route.

{ "source": "/employeeData/(.*)", "target": "/services/employeeService/$1", "destination": "employeeServices", "csrfProtection": false }

CSRF protection is relevant only for HTTP methods that make a side-effect on server (not HEAD and not GET).

It should works as following:

  • Application gets status 403 Forbidden with response header x-csrf-token: Required from Approuter
  • Application  sends request with HTTP method HEAD or GET with header x-csrf-token: fetch
  • Approuter generates x-csrf token and send it back in response header x-csrf-token: <token>
  • Application sends again a request to Approuter but now with the header x-csrf-token: <token>

Tip: In case your backend application implement CSRF protection by itself, please make sure to switch it off in Approuter. In such case x-csrf token exchange will be executed directly between the client and the backend, without Approuter and it will work fine.

Tip: In case CSRF is enabled on Approuter side but it does not work – please verify you use it for  protected route (“authenticationType”: “xsuaa”).

Cross-origin resource sharing (CORS) permits Web pages from other domains to make HTTP requests to your application domain, where normally such requests would automatically be refused by the Web browser’s security policy. Cross-origin resource sharing(CORS) is a mechanism that allows restricted resources on a webpage to be requested from another domain (/protocol/port) outside the domain (/protocol/port) from which the first resource was served. CORS configuration enables you to define details to control access to your application resource from other Web browsers.

Approuter has full support for Cross Origin and aligned with Apache Tomcat standard.

Approuter CORS flow works according to the flowchart below:

The Cross-Origin configuration is provided in the CORS environment variable.

The CORS configuration is an array of objects. Here are the properties that a CORS object can have: uriPatternallowedOriginallowedMethodsmaxAgeallowedHeadersexposeHeadersallowedCredentials.

Example: 

[ { "uriPattern": "^/route1$", "allowedMethods": [ "GET" ], "allowedOrigin": [ { "host": "my_example.my_domain", "protocol": "https", "port": 345 } ], "maxAge": 3600, "allowedHeaders": [ "Authorization", "Content-Type" ], "exposeHeaders": [ "customHeader1", "customHeader2" ], "allowedCredentials": true } ]

Tip: In case your backend application has CORS configuration on its side, please make sure not to configure CORS in Approuter. In such case CORS flow will be validated directly between the client and the backend, without Approuter and it will work fine.

There are many timeout configurations in AppRouter and it might be a little bit confusing. In this section I will go over them and will try make it more clear:

  • AppRouter session timeout (environment variable SESSION_TIMEOUT) – positive integer number representing the AppRouter session timeout in minutes. This value represents number of minutes that  “idle” AppRouter (that does not get any request) stores user session. Each time request arrives during this time – AppRouter extends the session again. The default value of this property is 15 minutes.
  • XSUAA session timeout (cannot be configured) – limited in CF for 30 minutes. This the time that session on XSUAA is alive from the User made an authentication. After that time he will be redirected to login screen there he will need to authenticate again.
  • Token validity (access_token_validity property in xs-security.json) – positive integer number representing the time in seconds after which issued JWT token will be expired.
  • Incoming connection timeout (environment variable INCOMING_CONNECTION_TIMEOUT) – positive integer number in milliseconds representing the AppRouter session timeout in minutes. Maximum time in milliseconds for a client connection. After that time the connection is closed. If set to 0, the timeout is disabled. The default value 120000 (2 minutes).
  • Destination timeout (property timeout and HTML5.Timeout in destination configuration) – positive integer number that represents maximum wait time for a response in milliseconds. It is relevant also for destinations logout requests. Keep in mind that requests from AppRouter to target destinations and services are synchronous. Also pay attention that maximum timeout in CF is limited to 2 minutes, therefore it does not make sense to define a value more than two minutes for this property. The default value is 30000 (30 seconds).
  • Service timeout (property timeout in business service binding VCAP object configuration, inside endpoint section) – positive integer number that represents maximum wait time for a response in milliseconds. It is relevant also for business service logout requests. Keep in mind that requests from AppRouter to target destinations and services are synchronous. Also pay attention that maximum timeout in CF is limited to 2 minutes, therefore it does not make sense to define a value more than two minutes for this property. The default value is 30000 (30 seconds).

AppRouter has a support for work multi-tenant mode. In order to achieve that, you need to create XSUAA service instance with mode “shared” (the configuration is in xs-security.json).

This XSUAA service instance should be bound to your AppRouter.

After that, create an environment variable TENANT_HOST_PATTERN for your AppRouter application. The value of this environment variable is Regular Expression that describes how tenant name should be retrieved from the host.

Example: ^(.*)-my_approuter_app.cfapps.sap.hana.ondemand.com

In the example above tenant name comes before approuter host and separated with “-“.

AppRouter validates during the start that whenever it is bound to xsuaa with mode “shared” – TENANT_HOST_PATTERN environment variable needs to be set.

Once AppRouter gets a request and recognizes that it does not have session for this particular uri – it retrieves tenant name by applying the tenant host pattern regex on incoming url.

After that when AppRouter calls XSUAA – it calls it with specific tenant url. For example,

my_tenant.<xsuaa_url>.

Then, XSUAA undertstands in scope of which tenant it it called and authentication flow works

according to this.

Note: tenant specific cf route should be mapped to AppRouter in order to make the flow work.

For productive usage “*” route should be configured and mapped.

For more information about multitenancy – please refer to https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/5e8a2b74e4f2442b8257c850ed912f48.html

Approuter can work with destination of two types:

  • Destination defined as as environment variable on Approuter application
  • Destination defined in Destination service

The first type is more basic and support simple authentication type. It does not require service destination . It is useful when the backend (destination) itself is in the same space and bound to the same XSUAA instance as the Approuter. This type of destination is configured with environment variable destinations . Changing one of the destinations requires restart of Approuter application.

Example:

[ { "name" : "ui5", "url" : "https://sapui5.netweaver.ondemand.com", "proxyHost" : "proxy", "proxyPort" : "8080", "forwardAuthToken" : false, "timeout" : 1200 } ]

This type of destinations as every environment variable can be provided in the manifest.yml .

Example:

- name: node-hello-world memory: 100M path: web env: destinations: > [ {"name":"ui5", "url":"https://sapui5.netweaver.ondemand.com"} ]

Second type of destination requires binding of Approuter to service destination .

In such case one can specify destination details inside destination service, to create service instance of service destination and bound it to Approuter.

After that, in Runtime Approuter can dynamically call Destination service and get destination details from it.

Within such flexible integration with Destination service, user can benefit from:

  • Consuming destinations that are defined on both Sub Account and Service Instance levels
  • Consuming destination data relevant for this particular Consumer tenant
  • Supporting all Authentication Types that Destination service exposes
  • Ability to be redirected to changed destination data without restarting the Approuter

The application router supports seamless integration with the HTML5 Application Repository service. When the application router interacts with HTML5 Application Repository to serve HTML5 Applications, all static content and routes (xs-app.json) are retrieved from HTML5 Application Repository. In case application router needs to route to non HTML5 Applications, it is possible to model that in the xs-app.json of the application router.

To integrate HTML5 Application Repository to an application router based it is required to create an instance of html5-apps-repo service of plan app-runtime and bind it to the application. xs-app.json routes that are used to retrieve static content from HTML5 Application Repository may be modeled in the following format:

 { "source": "^(/.*)", "target": "$1", "service": "html5-apps-repo-rt", "authenticationType": "xsuaa" }

A valid request to application router that uses HTML5 Application Repository must have the following format:

https://<tenantId>.<appRouterHost>.<domain>/<bsPrefix>.<appName-appVersion>/<resourcePath>

bsPrefix (business service prefix) – Optional

  • It should be used in case the application is provided by a business service bound to this approuter

appName (application name) – Mandatory

  • Used to uniquely identify the application in HTML5 Application Repository persistence
  • Must not contain dots or special characters

appVersion (application version) – Optional

  • Used to uniquely identify a specific application version in HTML5 Application Repository persistence
  • If no version provided, default application version will be used

resourcePath (path to file)

  • The path to the file as it was stored in HTML5 Application Repository persistence

For more details please refer to the following Blog:

Programming applications in Sap Cloud Platform

Beside the backends of type destinations, Approuter is fully integrated with services.

Service can be authenticated with two types: client-credentials (technical user) and user authentication. In order to use service, the name of the service to which the incoming request should be forwarded should be modeled in route:

{ "source": "^/odata/v2/(.*)$", "target": "$1", "service": "com.sap.appbasic.country", "endpoint": "countryservice" }

Business Services expose specific information in VCAP_SERVICES credentials block that enable application router to serve Business Service UI and/or data.

Business Service data can be served using two grant types:

  • User Token Grant: Application router performs a token exchange between login JWT token and Business Service token and uses it to trigger a request to the Business Service endpoint
  • Client Credentials Grant: Application router generates a client_credentials token and uses it to trigger a request to the Business Service endpoint

While binding a Business Service instance to application router the following information should be provided in VCAP_SERVICES credentials:

  • sap.cloud.service: Service name as referenced from xs-app.json route and business service prefix (if Business Service UI provided) – Mandatory
  • sap.cloud.service.alias: Short service name alias for user friendly URL business service prefix- Optional
  • endpoints: One or more endpoints that can be used to access Business Service data. If not provided url or uri attributes will be used – Optional
  • html5-apps-repo: html5-apps-repo.app_host_id contains one or more html5-apps-repo service instance GUIDs that can be used to retrieve Business Service UIs – Optional
  • saasregistryenabled: flag that indicates that this Business Service supports SaaS Registry subscription. If provided, application router will return this Business Service xsappname in SaaS Registry getDependencies callback – Optional
  • grant_type: the grant type that should be used to trigger requests to the Business Service. Allowed values: user_token or client_credentials. Default value, in case this attribute is not provided, user_token – Optional
"country": [ { ... "credentials": { "sap.cloud.service": "com.sap.appbasic.country", "sap.cloud.service.alias": "country", "endpoints": { "countryservice": "https://icf-countriesapp-test-service.cfapps.sap.hana.ondemand.com/odata/v2/countryservice", "countryconfig": "https://icf-countriesapp-test-service.cfapps.sap.hana.ondemand.com/rest/v1/countryconfig" }, "html5-apps-repo": { "app_host_id": "1bd7c044-6cf4-4c5a-b904-2d3f44cd5569, 1cd7c044-6cf4-4c5a-b904-2d3f44cd54445" }, "saasregistryenabled": true, "grant_type": "user_token" ....

In order to support JWT token exchange, the login JWT token should contain the uaa.user scope. For that the xs-security configuration must contain a role template that references the uaa.user scope. For example:

{ "xsappname" : "simple-approuter", "tenant-mode" : "shared", "scopes": [ { "name": "uaa.user", "description": "UAA" }, { "name": "$XSAPPNAME.simple-approuter.admin", "description": "Simple approuter administrator" } ], "role-templates": [ { "name": "Token_Exchange", "description": "UAA", "scope-references": [ "uaa.user" ] }, { "name": "simple-approuter-admin", "description": "Simple approuter administrator", "scope-references": [ "$XSAPPNAME.simple-approuter.admin" ] } ] }

In this blog post I have shared with you main capabilities and features of SAP Application Router. After reading this you should be able to install, configure and start using it. However, it might be that you will have questions so in order to help you – I’ve already added some frequently asked questions and  the answers for some of them as well. Please feel free to share and comment.

Q: Is session cookie name JSESSIONID constant?

A: Yes, this name is hard-coded in Cloud Foundry and is required for enabling session stickiness. Using this cookie GoRouter in CF knows to dispatch request to the same application instance.

Q: How to prevent collision of session cookies, for example if there are two Approuter applications with the same host but different ports (XSA) and the browser ignores ports

A: You can set environment variable USE_JSESSIONID_COOKIE_SUFFIX: 1 on Approuter. In such case Approuter will generate uniques session cookie name for each Approuter application. Be aware that session stickiness will not work in CF in case there is more than one CF instance, because CF Go Router knows expects hard coded name for it – JSESSIONID .

Q: How to make Approuter to return in http response additional headers, for example X-Frame-Option, Vary, Content-Type etc

A: You can achieve it by configuring environment variable httpHeaders . Please be aware that in case your backend already returns such headers, approuter cannot override its value.

For example:

httpHeaders : [{"X-Frame-Options":"ALLOW-FROM http://localhost"},{"Test-Additional-Header":"1"}] 

Q: Can one run Approuter locally?

A: Yes. In order to do so, you should use two files in Approuter folder: default-env.json (that will contain environment variables you want to set to your Approuter project) and default-services.json (for CF and XSA, should contain VCAP information about bound services. Currently supports only xsuaa).

Q: Is it possible to use some service for authentication that does not have tag xsuaa?

A: Yes, you can define this service name with environment variable UAA_SERVICE_NAME 

Q: Is it possible to check scopes inside backend destination?

A: Yes. You can set property forwardAuthToken with value true in destination definition (both in environment variable destinations and destination definition in destination service).

Q: How can we know which AppRouter version is running?

A: During AppRouter start – the version is printed in Log

Q: How can we see more detailed logs of AppRouter

A: You can execute following commands in CLI:

cf set-env <your_app> XS_APP_LOG_LEVEL DEBUG
cf set-env <your_app> REQUEST_TRACE true
cf restage

Q: What can we do if Approuter gets status 502 Connection Timeout during calling a destination

A: Basically, you can configure timeout property on destination (can be done in both destination that is defined in environment variable destinations and destination defined in destination service).

But keep in mind that AppRouter connection is synchronous, and CF limit for connection is 2 minutes.

Therefore, such configuration might be helpful only it takes less than 2 minutes.

For “slow destinations” – solution might be to have another design, asynchronous-like.

That means that you will have 2 endpoints:

– First one POST request for your heavy operation triggering, that will return 200 immediately.

– Second  GET request that will make a polling each several seconds to check if the operation job was finished.

For that you will need to configure two different routes in xs-app.json.