Skip to Content
Personal Insights
Author's profile photo Piotr Tesny

Developing multi-tenant SaaS applications easy with SAP BTP, Kyma runtime.

SAP Business Technology Platform (BTP) offers a hundred of business services and a number of runtime environments, namely:

that customers and partners developing cloud native applications can choose from.

Furthermore, SAP BTP offers an intrinsic, both very comprehensive and affordable, multi-tenancy mechanism with its SaaS Provisioning Service

And the good news is this mechanism can be applied to any of these three runtime environments. In this brief, we shall see how to make the best of it with SAP BTP, Kyma runtime environment.

Putting it all together

Here goes the agenda for this brief. Part A is dedicated to BTP multi-tenancy model applied to Kyma. Part B is dedicated to single sign on.

A. Developing Multi-tenant SaaS applications with SAP BTP, Kyma runtime, SaaS Provisioning and Destination services B. Easy routing with Kyma API rules and OAuth2SAMLBearer destinations

Multi-tenant SaaS applications with SAP BTP, Kyma runtime

SAP BTP runtimes are great for developing extensions.

But when it comes to Kyma, we are putting the bar much higher and targeting Kyma as a comprehensive runtime environment to develop fully-fledged multi-tenant SaaS applications.

Multi-tenant architectures for SaaS applications.

If you are planning to offer the same service to multiple consumers featuring zero administration, principal user propagation and business data separation …then multi-tenancy is an obvious architectural choice.

SAP SaaS provisioning service with Kyma runtime environment. (saas-registry)

Nowadays, the kubernetes-managed, containerised workloads are becoming increasingly popular, almost ubiquitous.

These workloads can either act as standalone micro-services or otherwise be orchestrated as fully-fledged Software as a Service (SaaS) applications.

Thus, ultimately, we shall need a mechanism to interact with a Kyma runtime-hosted SaaS application, through a set of well-defined and secure connection endpoints, featuring both accessibility enforcement and observability.

  • SAP BTP, Kyma runtime’s API rule makes it fairly easy to securely expose micro-services and their endpoints to the external world. Please refer to the appendix for further details.
  • SAP SaaS Provisioning Service APIs make it fairly easy to expose a multi-tenant solution with workloads running on Kyma.

SAP BTP multi-tenancy with Kyma environment.

SAP SaaS Provisioning Service offers an easy way to promote or demote consumers access to SaaS applications services.

  • The multi-tenant approuter and frontend application are deployed as highly available workloads in the Kyma cluster labeled Multitenancy.
  • For higher resilience, SaaS application routes are implemented with both sapse/approuter intrinsic routing and in a separate [serverless] workload with the @sap/cloud-sdk destinations.
  • The SaaS application itself is hosted in a SaaS LOB Kyma runtime (either managed or open source) possibly without any SAP BTP tie-in.
  • How and what access consumers get is regulated through a default set of configurations (=destination definitions) available at a provider. These initial configurations can be superseded and/or customised by providing the same-named destination definitions separately at each consumer tenant…

SAP BTP multi-tenancy with Kyma environment

Hybrid designs with service instance sharing. Don’t fix what ain’t broken.

Q. What if you had an existing multi-tenant application with SAP BTP Cloud Foundry or ABAP environments ? It is already multi-tenant solution so why would you have to migrate it to for instance Kyma ? At the same time the next generation of product functionalities could greatly benefit from Kyma qualities?

A. The good news is the above architecture caters for a such a use case as well. Nothing really prevents you from expanding your CF/ABAP-based solution into Kyma/Kubernetes.

Nowadays, [for all BTP services that are cross-consumable and support instance sharing], SAP BTP service broker allows for service instances to be shared from one runtime environment to another…

Thus, all the roles, roles collections, other security settings, destination definitions become shareable between CF/Other and Kyma/Kubernetes environments.


Last but not least, there ain’t no good SaaS solution without a solid principal user propagation mechanism. Please continue reading…

Easy routing with OAuth2SAMLBearerAssertion destinations.

User Propagation (SSO) from SAP BTP Provider and Consumers sub-accounts
into Kyma with BTP destinations.

The below scenario is based on the following concept Option 2 – Setting up Trust between Subaccounts in Different Regions | SAP Help

The identity of a business user logged into a Consumer A tenant is validated by an identity provider (IDP) of the respective consumer subaccount 1/region 1.

Subsequently,  the same user may want to reach out to services available at a remote resource – SaaS LOB application – hosted in a Kyma cluster without the need to re-authentify…but with the relevant permissions.

The consumer user JWT token will be passed in the x-user-token header when calling the destination’s find api

SAP BTP subaccount 2/region 2 has a custom IDP for user authentication and has an xsuaa-based OAuth2 client to request a bearer access token by calling the token issuance endpoint,

The token issuance endpoint is the AssertionConsumerService URL of the subaccount 2 (recognisable by its suffix alias/quovadis.aws-live-eu10)

As depicted above, SAP BTP subaccount 2 is in a BTP region 2 (region == data center) different from Provider/Consumers BTP region 1.

This offers an elegant way to overcome the hard data center tie-in between Provider and Consumer sub-accounts. And eventually achieves a region-free SaaS multi-tenancy.

Configure User Propagation from SAP BTP consumer tenant into a Kyma-hosted SaaS applications

Configure user propagation (single sign-on), using OAuth2 communication from the SAP BTP consumer tenant to SAP BTP, Kyma runtime.

As OAuth2 mechanism, we shall be using SAML 2.0 Assertion flow with the  OAuth 2.0 SAML Bearer Assertion destinations

weather-anywhere API destination definition (Provider/Consumer) weather-anywhere API rule definition (kyma cluster)

Please refer to User Propagation between SAP BTP and third-party Applications. | SAP Blogs and to notes in the appendix below…

MS Teams integration

It is fairly easy to embed the frontend application into MS Teams (either web or desktop Teams) for provider and consumers tenants likewise, for instance:

There is no much magic behind this integration. Pick the URLs from the Subscription Management Dashboard and add them as websites in Teams:

Just make sure to add https://*.microsoft domain name to the trust settings in every provider/consumer BTP sub-account…

Conclusion

Q. How to build comprehensive and affordable software solutions?

  • The choice of an application runtime is always a bit opinionated per se. Budgets, skillsets and politics will have their say.
  • Regardless, what really matters is the power of the underlying business platform with its services.
  • That’s the ease of consumption and the degree of integration of the platform services that will likely determine the choice of a runtime environment.
  • Less is more. Whatever qualities can be kept away from the business logic is more. And whatever quality must be baked into it is less.

Q. Why choose Kyma runtime ?

Kyma runtime offers intrinsic auto-healing, auto-scaling and high availability (multi-zone worker nodes) cloud qualities. Kyma runtime main ingredients
SAP%20BTP%2C%20Kyma%20runtime.%20Main%20ingredients.

Top it with the built-in open telemetry support,  service mesh, serverless framework, eventing, stable egress IP addresses, enterprise grade backing services and provisioning location transparency, etc.

And that helps, as none of this needs to be baked into the business logic.

That combined with SAP BTP built-in multi-tenancy and destinations services offers a comprehensive framework geared towards building affordable SaaS solutions.

Last but not least, I hope you have enjoyed reading this blog post. Feel free to provide feedback in the comments section below.

 


Appendix

Accessibility enforcement with SAP BTP, Kyma runtime API rules

Easy routing with SAP BTP, Kyma runtime API rules

Let’s consider the below API rule definition with a GET/HEAD/POST access methods to a specific system endpoint.

The most important part of the below API rule definition is the accessStrategies config as follows:

apiVersion: gateway.kyma-project.io/v1beta1
kind: APIRule
metadata:
  name: "{{ .Values.services.srv.name }}-{{ .Release.Namespace }}"
spec:
  gateway: {{ .Values.gateway }}
  host: "{{ .Values.services.srv.name }}-{{ .Release.Namespace }}.{{ .Values.clusterDomain }}"
  rules:
    - accessStrategies:
        - config:
            jwks_urls:
              - https://<tenant>.authentication.<region>.hana.ondemand.com/token_keys
            trusted_issuers:
              - https://<tenant>.authentication.<region>.hana.ondemand.com/oauth/token
            required_scope:
              - quovadis-xsuaa-master!t119894.Read        
          handler: jwt
      methods:
        - GET
        - HEAD
        - POST
      path: /arcgis
    - accessStrategies:
        - config:
            jwks_urls:
              - >-
                https://<tenant>.authentication.<region>.hana.ondemand.com/token_keys
            required_scope:
              - openid
            trusted_issuers:
              - >-
                https://<tenant>.authentication.<region>.hana.ondemand.com/oauth/token
          handler: jwt
      methods:
        - GET
      path: /forecast
    - accessStrategies:
        - config:
            jwks_urls:
              - https://<tenant>.authentication.<region>.hana.ondemand.com/token_keys
            trusted_issuers:
              - https://<tenant>.authentication.<region>.hana.ondemand.com/oauth/token
            required_scope:
              - quovadis-xsuaa-master!t119894.Admin        
          handler: jwt
      methods:
        - GET
        - HEAD
        - POST
      path: /
  service:
    name: {{ .Values.services.srv.name }}
    port: {{ .Values.services.srv.port }}

The tenant and the region values are those of the custom IDP.

Likewise, in order to further restrict access to the API rule to specific user categories only, it is possible to include additional user authorisations (scopes) that are assigned to a user JWT token via Roles and Roles Collections assigned to business users at a BTP sub-account level.

Worth mentioning, the above API rule has three rules defined, each for a different path with a separate jwt access strategy…and with different scopes.

Assert the JWT access token

A JWT access token will be passed to the API rule in the Authorization header. Subsequently, the API rule will apply the accessor logic to assert the token and eventually grant or deny access to the resource.

JWT claim API rule config name Description
iss trusted_issuers Token issuance endpoint. A fully qualified URL with scheme, host, and sometimes with a port number and path components but with no query or fragment components. The URL definition must match one of the trusted_issuers provided in the API rule.

  • asserts that the JWT was issued by a trust worthy identity service.
scope required_scope (optional) list of scopes.

  • asserts that the requested scopes match the scopes from the JWT token
aud target_audience (optional)  list of audiences

  • asserts that the requested audiences match the aud claim from the JWT token

Signature Validation

Validates that the JWT signed with the public key of the trust-worthy identity service.

The digital signature is verified by trying an appropriate public key from the server JWK set. The used key is typically identified by the kid and the alg header parameter.

JWT header parameter API rule config name Description
kid The “kid” (key ID) parameter is a case-sensitive id used to match a specific public key.
alg allowed_algorithms (optional) The “alg” (algorithm) parameter identifies the algorithm intended for use with the key. Defaults to “RS256”.

Configure User Propagation from SAP BTP consumer tenant into a Kyma-hosted SaaS applications.

As aforementioned, SAP BTP sub-account 2/region 2 has a custom IDP for user authentication and an xsuaa-based OAuth2 client used to request a bearer access token by calling AssertionConsumerService URL (ACS) as a token issuance endpoint,

In a nutshell, the destination service is creating a signed SAMLAssertion and then passes it to the AssertionConsumerService URL of the BTP subaccount 2 performing an IDP-initiated login on behalf of the consumer subaccount 1 user(s).

Please note, in order to be able to retrieve the delegated user permissions (scopes) from the bearer access token we need to set the OAuth2 client security descriptor authorities property.

Here goes a working sample of the XSUAA OAuth2 client’s xs-security.json descriptor:

{
    "xsappname": "quovadis-xsuaa-master",
    "tenant-mode": "dedicated",
    "authorities": ["$ACCEPT_GRANTED_AUTHORITIES"],
    "scopes": [
    {
      "name": "$XSAPPNAME.Read",
      "description": "Read Permissions."
    },
    {
      "name": "$XSAPPNAME.Admin",
      "description": "Admin permissions."
    }
  ],
  "role-templates": [
    {
      "name": "Viewer",
      "description": "View Data",
      "scope-references": [
        "$XSAPPNAME.Read",
        "uaa.user"
      ]
    },
    {
      "name": "Administrator",
      "description": "View Sensitive Data",
      "scope-references": [
        "$XSAPPNAME.Read",
        "$XSAPPNAME.Admin"
      ]
    }
  ],
  "role-collections": [
    {
      "name": "Viewer",
      "description": "Viewer (read)",
      "role-template-references": [
        "$XSAPPNAME.Viewer"
      ]
    },
    {
      "name": "Administrator",
      "description": "Administrator (read all)",
      "role-template-references": [
        "$XSAPPNAME.Administrator"
      ]
    }
  ]    
}

Good to know:

  • quovadis-xsuaa-master is the sending application and the property authorities: ["$ACCEPT_GRANTED_AUTHORITIES"] qualifies all the scopes as grantable to a receiving application

Custom Identity Provider

It is defined on a BTP sub-account 2 level:

This IDP is used only for the IDP-initiated flow with the OAauth2SAMLBearerAssertion destination(s) (it is not meant for any interactive user login).

Its x509 certificate is used to validate the consumer user SAMLAssertion when calling the token issuance endpoint of the BTP subaccount 2.

<?xml version="1.0" encoding="utf-8"?><md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://auth.pingone.eu/<tenant>" ID="<ID>">
  <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <md:KeyDescriptor use="signing">
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:X509Data>
          <ds:X509Certificate>
            MIIFGzCCAwMCBGBb1dwwDQYJKoZIhvcNAQELBQAwUjELMAkGA1UEBhMCVVMxDDAK

            XT6c0QNVYg37DBU/qhSN
          </ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </md:KeyDescriptor>
    <md:SingleLogoutService Location="https://auth.pingone.eu/<tenant>/saml20/idp/slo" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"/>
    <md:SingleLogoutService Location="https://auth.pingone.eu/<tenant>/saml20/idp/slo" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"/>
    <md:SingleSignOnService Location="https://auth.pingone.eu/<tenant>/saml20/idp/sso" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"/>
    <md:SingleSignOnService Location="https://auth.pingone.eu/<tenant>/saml20/idp/sso" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"/>
    <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="user.preferredLanguage"/>
    <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="user.address.region"/>
    <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="user.id"/>
    <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="user.email"/>
  </md:IDPSSODescriptor>
</md:EntityDescriptor>

 

Easy routing with OAuth2SAMLBearerAssertion destinations

The consumer user (BTP sub-account 1) JWT token will be passed in the x-user-token header when calling the destination’s find api.

The destination service will internally create a signed SAMLAssertion and then will pass it to the AssertionConsumerService URL of the BTP subaccount 2 executing an IDP-initiated login on behalf of the consumer subaccount 1 user.

  {
    "Name": "weather-anywhere",
    "Type": "HTTP",
    "URL": "https://weather-anywhere.<custom domain name>.com",
    "Authentication": "OAuth2SAMLBearerAssertion",
    "ProxyType": "Internet",
    "KeyStorePassword": "<KeyStorePassword>",
    "tokenServiceURLType": "Dedicated",
    "audience": "https://<tenant>.authentication.<region>.hana.ondemand.com",
    "Description": "weather-anywhere",
    "authnContextClassRef": "urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession",
    "assertionIssuer": "https://auth.pingone.eu/<pingone tenant>",
    "tokenServiceUser": "sb-quovadis-xsuaa-master!t119894",
    "tokenServiceURL": "https://<tenant>.authentication.<region>.hana.ondemand.com/oauth/token/alias/quovadis.aws-live-eu10",
    "tokenServicePassword": "<tokenServicePassword>",
    "HTML5.DynamicDestination": "true",
    "clientKey": "sb-quovadis-xsuaa-master!t119894",
    "KeyStoreLocation": "quovadis_ateam-isveng.p12",
    "nameIdFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
    "scope": "openid quovadis-xsuaa-master!t119894.Read quovadis-xsuaa-master!t119894.Admin",
    "userIdSource": "email"
  }

In the aftermath, the destination will return a bearer access token in its payload.

Let’s show it once again using the decoded input and output user JWT tokens

As aforementioned, the above destination gets in the x-user-token header a consumerA’s JWT token from BTP sub-account 1 / region 1, for instance:

{
  "alg": "RS256",
  "jku": "https://<consumer tenant>.authentication.<consumer region>.hana.ondemand.com/token_keys",
  "kid": "default-jwt-key--1249532834",
  "typ": "JWT",
  "jid": "<jid>"
}.{
  "sub": "<sub>",
  "user_name": "foo.bar@acme.com",
  "iss": "https://<consumer tenant>.authentication.<consumer region>.hana.ondemand.com/oauth/token",
  "aud": [
    "uaa",
    "openid"
  ],
  "ext_attr": {
    "enhancer": "XS_APPLICATIONUSER",
    "zdn": "<consumer tenant>"
  },
  "zid": "<zid>",
  "user_id": "<user_id>",
  "azp": "<azp>",
  "scope": [
    "openid",
    "uaa.user"
  ],
  "exp": <exp>,
  "iat": <iat>,
  "jti": "<jti>",
  "email": "foo.bar@acme.com",
  "cid": "<cid>"
}.[Signature]

Next, it creates a SAMLAssertion, passes it to the token issuance endpoint, The saml assertion gets validates and eventually a bearer access token is returned with the scopes and audiences as required by a SaaS application endpoint, for instance:

{
  "alg": "RS256",
  "jku": "https://<tenant>.authentication.<region>.hana.ondemand.com/token_keys",
  "kid": "default-jwt-key--623944614",
  "typ": "JWT",
  "jid": "<jid>"
}.{
  "jti": "<jti>",
  "ext_attr": {
    "enhancer": "XSUAA",
    "subaccountid": "<subaccountid>",
    "zdn": "quovadis"
  },
  "xs.system.attributes": {
    "xs.rolecollections": [
      "Administrator",
      "Viewer"
    ]
  },
  "given_name": "foo.bar",
  "xs.user.attributes": {},
  "family_name": "acme.com",
  "sub": "<sub>",
  "scope": [
    "quovadis-xsuaa-master!t119894.Read",
    "openid",
    "quovadis-xsuaa-master!t119894.Admin"
  ],
  "client_id": "sb-quovadis-xsuaa-master!t119894",
  "cid": "sb-quovadis-xsuaa-master!t119894",
  "azp": "sb-quovadis-xsuaa-master!t119894",
  "grant_type": "urn:ietf:params:oauth:grant-type:saml2-bearer",
  "user_id": "<ser_id>",
  "origin": "httpsauth.pingone.eu5f3341ef-3cf9-4c",
  "user_name": "foo.bar@acme.com",
  "email": "foo.bar@acme.com",
  "rev_sig": "b4df0449",
  "iat": <iat>,
  "exp": <exo>,
  "iss": "https://<tenant>.authentication.<region>.hana.ondemand.com/oauth/token",
  "zid": "<zid>",
  "aud": [
    "sb-quovadis-xsuaa-master!t119894",
    "quovadis-xsuaa-master!t119894",
    "openid"
  ]
}.[Signature]

Worth mentioning, the above exchange mechanism allows to separate the SaaS provisioning aspects with a provider and consumers from the provisioning of the SaaS application itself.

 


Additional resources

Before you get started.

Multitenancy

Sample code repositories, gists and blogs.

Public code generators.

 


SAP Community: https://community.sap.com/

SAP Community Topic Page link: https://community.sap.com/topics/kyma

SAP Community Q&A Tags:
Kyma Open Source: https://answers.sap.com/tags/2936b97d-6a90-4cd8-b635-0e51441611eb
SAP BTP, Kyma runtime: https://answers.sap.com/tags/73554900100800003012

Follow me in SAP Community: https://people.sap.com/piotr.tesny

 

 

Assigned Tags

      6 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo RAJESH SHARMA
      RAJESH SHARMA

      Thanks for sharing very nice explanation.

      Author's profile photo Mustafa Bensan
      Mustafa Bensan

      Hi https://people.sap.com/piotr.tesny,

      Thanks for the thorough reference.  I would also highly recommend to readers the post Multitenant SaaS applications on SAP BTP using CAP? SAP BTP, Kyma Runtime! by Martin Frick and Alper Dedeoglu.

      Regards,

      Mustafa.

      Author's profile photo Piotr Tesny
      Piotr Tesny
      Blog Post Author

      Hi Mustafa Bensan ,

      Thanks for the heads up; Indeed this is an excellent blog post with a very comprehensive code sample;

      On a side note, having several blog posts talking about SAP BTP multi-tenancy in the context of SAP BTP, Kyma runtime is a sign of time.

      I had an existing SaaS application running on a kyma cluster and wanted an easy enough (affordable) solution to make it a multi-tenant SaaS application. That's how I had come up with the solution described above. In this journey I learnt how to leverage a couple of BTP services, how to share them and most importantly designed how to implement a user SSO.

      kind regards; Piotr

      PS. Furthermore this approach can cater for other SaaS solutions. I have extended the initial design beyond my kyma-hosted SaaS and integrated with a number of SAP LOBs like SAP S/4HANA Public/Private Cloud, SAP SuccessFactors and SAP Analytics Cloud. I'm still in the process of targeting SAP DataSphere thus I will publish a code sample in due course of time.

      Author's profile photo Mustafa Bensan
      Mustafa Bensan

      Thanks for the additional context https://people.sap.com/piotr.tesny.  I look forward to your future post about integration with SAP Datasphere.

       

      Author's profile photo Martin Frick
      Martin Frick

      Mustafa Bensan - Once again, thank you for sharing our additional Kyma multitenancy blog post Multitenant SaaS applications on SAP BTP using CAP? SAP BTP, Kyma Runtime! and the respective sample application! We really appreciate it! I totally agree with https://people.sap.com/piotr.tesny that it's fantastic to see Kyma's adoption increasing, especially in use-cases like SaaS and multitenancy.

      By sharing a powerful runtime among multiple customers, Kubernetes-based scenarios become much more affordable, while also allowing you to leverage all of the additional capabilities that Kyma offers.

      Author's profile photo Martin Frick
      Martin Frick

      https://people.sap.com/piotr.tesny - Thanks for sharing this impressive summary! As always, your blog posts and content are very extensive and whet the appetite for more! I can't await to see your sample application in action! Especially with the announced integration patterns like Microsoft Teams! Thanks for all your efforts!