EDUCAÇÃO E TECNOLOGIA

Hardening access to SAP Kyma Runtime APIs with JWT tokens.


Mission statement.

The mission is to harden the access to publicly exposed Kyma APIs.

  • With Kyma it is fairly easy to expose API endpoints (functionality) to the public internet.
  • The WWW is not a safe place, the security is paramount, the public API endpoints must be secured.
  • Good news! With Kyma (and istio) it is fairly easy to protect the API endpoints with either user JWT or OAuth2 bearer access tokens.

In order to do so, one needs to create an API Rule that maps a given [internal] Kyma cluster service and then add an appropriate access strategy to the rule.

Like, for instance, a user JWT token strategy (as depicted in the appendix below).

Then, in order to call public API endpoints exposed via the above API Rule, a digitally signed bearer token will need to be provided in the Authorization header every time some API endpoint is being called.

As aforementioned, this bearer JWT is digitally signed with a private key of the token issuer (=OIDC identity provider, the XSUAA service in this scenario).

Furthermore, in order to be accepted by the callee, the JWT token needs to be validated against the issuer (=IDP that generated the token, the XSUAA in this scenario) and the token signature be checked with the public x509 certificate (that the issuer exposes via the jwks_uri access point.)

Before we start.

Pre-requisites:

  • Access to SAP Kyma Runtime – aka SAP managed k8s cluster (with SAP BTP trial, SAP BTP free tier or any paid contract)
  • Access to XSUAA application service plan on the BTP sub-account. I have chosen to provision an instance of the XSUAA service directly on Kyma side, in the namespace of my backend function I am about to secure.
  • Optionally, SAP BTP sub-account with SAP Integration Suite subscription provisioned with access to SAP API Management portal (with SAP BTP trial or free account).

Disclaimer:

  • This is not a tutorial. Working knowledge of or at least exposure to security concepts with the OIDC providers and JWT tokens as well as being acquainted with SAP BTP XSUAA service and Kyma runtime are assumed across this blog.
  • Please note all the code snippets below are provided “as is”.
  • All the x509 certificates, bearer access and/or refresh tokens and the likes have been redacted.
  • Images/data in this blog post is from SAP internal sandbox, sample data, or demo systems. Any resemblance to real data is purely coincidental.
  • Access to some online resources referenced in this blog may be subject to a contractual relationship with SAP and a S-user login may be required.

As aforementioned, the client application (the caller) will need to fetch a JWT token (from an OIDC provider) and pass it in the Authorization header of the API call (the callee).

Good to know:

  • What a JWT token is? In a nutshell a JWT (Json Web Token) token represents a caller’s identity. Please refer to appendix section below for more details.
  • In order to implement either JWT or OAuth2 authentication with Kyma the running pod requires the istio proxy side-car.

Hardening access to Kyma APIs with a user JWT token.

Step1. Authorization and Trust Management Service (xsuaa)

XSUAA as an OIDC identity provider and a token issuer.

In a nutshell, xsuaa-oidc depicted below, will generate a bearer JWT token when instructed to do so by some consumer [OAuth2 client] application (the one that is calling the issuer endpoint).

  • It is worth mentioning that Kyma has its own intrinsic OIDC provider called DEX that can be used as a fallback OIDC provider in case the external one is down or not reachable.

From the xsuaa-oidc service secret above you will need to retrieve only the three following values: clientid, clientsecret and the url.

That’s it.

clientid: clientid
clientsecret: clientsecret
url: url
const credentials_xsuaa = { client: { id: '<clientid>', secret: '<clientsecret>' }, auth: { authorizeHost: '<url>', authorizePath: 'oauth/authorize', tokenHost: '<url>', tokenPath: 'oauth/token' }, options: { authorizationMethod: 'body' }
}

The url format is https://<identityzone>.authentication.<region>.<domain>, for instance https://trial.authentication.eu10.hana.ondemand.com.

The issuer endpoint is the URL that will be called [by the OAuth2 client] to generate the JWT token and JWKS URI URL is a pointer to a json array that will be used to validate the JWT digital signature by the callee.

Sample Issuer URL
  • https://trial.authentication.eu10.hana.ondemand.com/oauth/token
Sample JWKS URI URL
  • https://trial.authentication.eu10.hana.ondemand.com/token_keys
OIDC metadata
  • https://trial.authentication.eu10.hana.ondemand.com/.well-known/openid-configuration

Step2. Create an API Rule protected with a JWT token.

Let’s create a new API Rule with a JWT access strategy for our  backend Kyma function…

The issuer and jwks_uri urls must match your XSUAA service instance values as explained in the previous section above.

When trying to call this API Rule we are getting the Unauthorized (401) error. This is expected and by design.

{"error":{"code":401,"status":"Unauthorized","request":"<request>","message":"The request could not be authorized"}}

Good to know:

  • With Kyma one can define several API rules for a single internal service.
  • Furthermore, a list of OIDC identity providers can be maintained per single API Rule and per JWT access strategy.
  • That means one could add any of the SAP IAS, Keycloak, AzureAD or Okta as other/additional OIDC providers as well.  Like, for instance, additional XSUAA issuer(s) for increased resilience.

Step3. Fetch a bearer JWT Token

As explained in the mission statement, in order to be able to securely call a publicly exposed API endpoint, a JWT token needs to be passed it in the Authorization header of each API call.

The following code snippet demonstrates how to execute a client credentials grant flow with XSUAA authorization server using a standard simple-oauth2 library, namely: https://www.npmjs.com/package/simple-oauth2

const clientCredentials = require('simple-oauth2'); async function xsuaa_get_jwt_access_token() { const client = new clientCredentials.ClientCredentials(credentials_xsuaa); const tokenParams = {}; let logonToken = {}; try { const token = await client.getToken(tokenParams); logonToken = token.token.access_token; console.log("logonToken: '%s'", logonToken); } catch (error) { console.log('xsuaa_get_jwt_access_token: Access Token error', error.message); return JSON.stringify(error, null, 2); // throw error.message; } return logonToken;
}

Step4. Bootstrap the API

Let’s have a look at the following code snippet.

That’s the “bootstrapping” code needed to call an API endpoint secured with a digitally signed JWT token.

const axios = require('axios') //The url is the JWT-protected API Rule enpoint
//The logonToken is the bearer JWT token obtained from xsuaa try { let logonToken = await xsuaa_get_jwt_access_token(); let configGet = { method: 'get', url: url, withCredentials: true, headers: { "Authorization": 'Bearer ' + logonToken, } }; const response = await axios(configGet); console.log(response.status); return response.data; } catch(error) { console.log(error); return error; }; 

or you can test it directly with curl as follows:

curl -ik https://<hostname>.<domain>/here_location?location=tokyo -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vb2VtLWF6dXJlLmF1dGhlbnRpY2F0aW9uLmFwMjEuaGFuYS5vbmRlbWFuZrsKGSwfr6KoREMSrJ7wxp80NgoFFqD8CS2lK4OSoXmIQqWJsRZzCHfa-rKA' HTTP/2 200 { "items": [ { "title": "Japan東京", "id": "here:cm:namedplace:25853290", "resultType": "locality", "localityType": "city", "address": { "label": "Japan東京", "countryCode": "JPN", "countryName": "日本", "county": "Japan", "city": "東京" } } ]
}

Last but not least:

  • The bootstrapping nodejs code snippet above, with axios as the http client: https://www.npmjs.com/package/axios, is for illustration purposes only.
  • The bootstrapping sequence is fairly simple and may need to be adapted to the caller’s native environment itself: GO, ABAP, JAVA, C++, etc..
  • For instance, with SAP API Management, a suitable security policy could be used to generate a JWT token without the need of writing any code…or like I did using a highly portable CURL command.
  • Means of getting the JWT token may very as well. This time, in order to make it caller-agnostic and suitable for unattended/unmanned design, I have chosen to generate the JWT token with the client credentials of the XSUAA service. Another way would be to self-generate a JWT token
  • The XSUAA client credentials grant type is like having a technical user. That serves well the purpose of this exercise because there is no need to propagate the caller’s user identity, as the callee is a technical backend service.

As stated in the mission statement, the WWW is not a very safe place.

Thus this is paramount to protect the public API endpoints from unauthorised access.

JWTs are not caveats free though. As tokens they can be decoded by any 3rd party tool/application. Thus are not suitable to carry any secrets.

JWTs are a good fit for purpose when there is no need to propagate any user context or scopes (authorizations) to the resource server.

For instance, if an on premise application needed to call into a protected Kyma API, a self-generated JWT might be a better approach….

Next time I will show how to leverage the Kyma built-in OAuth2 access strategy and eventually the use of the ubiquitous OAuth 2.0 SAML Assertion flow to make the whole scenario enterprise-grade ready.


adStep1:

OIDC metadata definition: https://trial.authentication.eu10.hana.ondemand.com/.well-known/openid-configuration

{ "issuer": "https://trial.authentication.eu10.hana.ondemand.com/oauth/token", "authorization_endpoint": "https://trial.authentication.eu10.hana.ondemand.com/oauth/authorize", "token_endpoint": "https://trial.authentication.eu10.hana.ondemand.com/oauth/token", "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post", "tls_client_auth" ], "token_endpoint_auth_signing_alg_values_supported": [ "RS256", "HS256" ], "userinfo_endpoint": "https://trial.authentication.eu10.hana.ondemand.com/userinfo", "jwks_uri": "https://trial.authentication.eu10.hana.ondemand.com/token_keys", ........................(truncated)...............................
}

JWT authentication strategy

adStep2:

Create an API Rule for a kyma cluster service: in your namespace goto services and locate the service you want to expose via an API rule.

Click on the Expose Service + button and proceed with the API rule creation. Give the rule a name, pick a hostname and select JWT access strategy.

Within the JWT access strategy you can decide which HTTP methods will be allowed and you can maintain a list of token issuers.

Actually this is very convenient feature as it means you can decide to have an issuer that fits a specific integration scenario like for instance using a technical user JWT token or some named user identity, etc…

Decoded JWT token.

It’s been generated by the xsuaa authorization service with the client credentials grant type.

{ "alg": "RS256", "jku": "<url>/token_keys", "kid": "default-jwt-key--xxxxxxx", "typ": "JWT"
}.{ "jti": "<jti>", "ext_attr": { "enhancer": "XSUAA", "subaccountid": "<subaccountid>", "zdn": "<zdn>" }, "sub": "sb-xsuaa-oidc!xxxx", "authorities": [ "uaa.resource" ], "scope": [ "uaa.resource" ], "client_id": "sb-xsuaa-oidc!xxxx", "cid": "sb-xsuaa-oidc!xxxx", "azp": "sb-xsuaa-oidc!xxxx", "grant_type": "client_credentials", "rev_sig": "<rev_sig>", "iat": 1637363459, "exp": 1637406659, "iss": "<url>/oauth/token", "zid": "<zid>", "aud": [ "uaa", "sb-xsuaa-oidc!xxxx" ]
}.[Signature]

DEX service – Kyma built-in OIDC service

DEX Issuer URL
  • https://dex.<cluster hostname>.<cluster domain>
DEX JWKS URI URL
  • https://dex.<cluster hostname>.<cluster domain>/keys
DEX OIDC metadata
  • https://dex.<cluster hostname>.<cluster domain>/.well-known/openid-configuration


https://github.com/kyma-project/examples/tree/main/gateway

https://github.com/ory

XSUAA on GitHub

@sap/approuter on npm