Skip to Content
Technical Articles

Usage of external JWT in CDS Services

Introduction

In most cases, a CDS service from a CAP model based app in Cloud Foundry is authenticated via a SAML identity provider configured in SAP Cloud Platform. You authenticate against an IdP and receive a JWT token from the XSUAA instance that is bound to the service.
In this blog I want to show how you can authenticate to a CDS service with an external JWT token instead of a trusted SAML IdP and how to use JWT attributes in this case.

Solution Architecture

  1.  First we need to validate the incoming JWT and read the required attributes.
  2.  To be able to use the attributes of the incoming JWT, you must create a new SAML assertion that forms the basis for the JWT provided by the XSUAA.
  3. After the SAML assertion has been sent to the XSUAA instance, it responds with a XSUAA JWT token.
  4. The XSUAA JWT token can then be used for the authentication against the CDS service.
We will be using SAP Cloud API Management for all these steps.

1. Read and Validate Incoming JWT

The first step is to read and validate the incoming JWT in API management.
You can use the already defined JWTTokenVerificationPolicy template from SAP:
After importing the template into the ProxyEndpoint Preflow, some adjustments must be made, which you can find in the template description.
(From the template description:)
4) Modify the following two policies by updating URL tag:
readJWTKeys : Enter your identity provider JWKS URL
verifyJWT : Enter your JWT Token Issuer URL
Instead of using the setResponse policy from the template, you can add an optional JS-policy to check for scopes. In my case the policy looks like this:
<Javascript async="false" continueOnError="false" enabled="true" timeLimit="200" xmlns='http://www.sap.com/apimgmt'>
 <IncludeURL>jsc://lib.js</IncludeURL>
 <ResourceURL>jsc://maincode.js</ResourceURL>
</Javascript>
with maincode.js
//function to check the string of scope "CONTAINS USER"
function checkScopeContains(){
  return checkScope(context.getVariable("jwt.DecodeJWT.claim.scope"), "USER");
}

context.setVariable("vars.checkedScope", checkScopeContains());

 

Your ProxyEndpoint Preflow should now look like this:

2. API Flow for Creating a SAML Assertion for SCP CF

In this step a new API TargetEndpoint PreFlow is defined. It creates a SAML assertion using a trust relationship between API Management and CF subaccount.
You can follow this blog (https://blogs.sap.com/2018/01/19/part-2-single-sign-on-from-fiori-application-to-sap-gateway-via-sap-cloud-platform-api-management) to generate certificates, a JAR file that contains the certificates and IdP metadata that can be used for the trust configuration.
You need to upload the JAR File into the API Portal in subsection Configure -> Certificates as a key store:
Create%20Key%20Store
Now we can use a template again:
After importing the template, you can apply it to the PreFlow of the TargetEndpoint and enter some missing values.
(From the template description)
In the samlHelper.js file, enter values for the following:
* sapapim.issuer : Value of identity Provided Name on SAP Cloud Foundry Account
* sapapim.audience : Service Provider name of the SAP Cloud Foundry tenant UAA, value of ‘entityID’ from generated SAML metadata
* sapapim.recipient : Value of entity ‘AssertionConsumerService'(url with oauth/token/alias..) from the generated SAML metadata
* sapapim.username : User name used to login to microservice. (For example a technical User, in this example “TECH_USER”)
* sapapim.storename : Certificate Key store name created in the API Management certificate tab
* sapapim.keyname : Certificate Key name created in the API Management certificate tab
* sapapim.clientId : Value is in the XSUAA binding to your cloud foundry application
* sapapim.secret : Value is in the XSUAA binding to your cloud foundry application
* In the policy getOAuthToken enter ‘HTTPTargetConnection URL’, value is same as of ‘sapapim.recipient’
In samlHelper.js, you can also map some JWT attributes to SAML assertion attributes and define a group that is assigned to a SCP role collection:
var saml2groupattributes = "<saml2:AttributeStatement>"+
      "<saml2:Attribute Name=\"groups\" NameFormat=\"urn:oasis:names:tc:saml2:2.0:attrname-format:basic\">"+
        "<saml2:AttributeValue xsi:type=\"xs:string\">Tech_User_Group</saml2:AttributeValue>"+
  "</saml2:Attribute>"+
 "<saml2:Attribute Name=\"customerId\" NameFormat=\"urn:oasis:names:tc:saml2:2.0:attrname-format:basic\">"+
        "<saml2:AttributeValue xsi:type=\"xs:string\">" + context.getVariable("jwt.DecodeJWT.claim.customerId") + "</saml2:AttributeValue>"+
  "</saml2:Attribute>"+
 "<saml2:Attribute Name=\"vg\" NameFormat=\"urn:oasis:names:tc:saml2:2.0:attrname-format:basic\">"+
        "<saml2:AttributeValue xsi:type=\"xs:string\">" + context.getVariable("jwt.DecodeJWT.claim.vg") + "</saml2:AttributeValue>"+
  "</saml2:Attribute>"+
    "</saml2:AttributeStatement>";

 

As an optional policy you can add a Key Value Map Operations policy at the start to grab the values for sapapim.clientId and sapapim.secret from an API Management Key-Value-Map:
<KeyValueMapOperations mapIdentifier="stg-stock-logic-uaa" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt">
  <Get assignTo="private.client_id" index='1'>
    <Key><Parameter>client_id</Parameter></Key>
  </Get>
  <Get assignTo="private.client_secret" index='1'>
    <Key><Parameter>client_secret</Parameter></Key>
  </Get>
  <Scope>environment</Scope>
</KeyValueMapOperations>

 

In the end the PreFlow looks like this:
API%20Management%20Preflow

3.1 Set up Trust Configuration

To set up a trust configuration between API Management and SCP CF Subaccount you need to upload the metadata generated in step 2 to SCP CF. The security options of your sub-account can be found in the Security section -> Trust Configuration.
Trust%20Configuration
Note: You may need to set the validation date in the metadata xml file to a higher value.

3.2 Roles and Attributes

The goal is for the JWT issued by the XSUAA to have user attributes and scopes that can be used by the service. To achieve this, the relevant properties of the incoming JWT are mapped to assertion attributes.
First you need to create a XSUAA instance. I used the following xs-security.json configuration with attributes
  • customerId: this property already exists in the incoming JWT and is forwarded as a SAML assertion attribute (see samlHelper.js)
  • vg: already exists in the incoming JWT as well
  • groups: will be used for role collection mapping
{
    "xsappname": "example-app",
    "tenant-mode": "dedicated",
    "description": "Security profile",
    "scopes": [
      {
        "name": "$XSAPPNAME.User",
        "description": "User"
      }
    ],
    "attributes": [
      {
        "name": "customerId",
        "description": "Customer ID",
        "valueType": "s"
      },
      {
        "name": "vg",
        "description": "VG",
        "valueType": "s"
      },
      {
        "name": "groups",
        "description": "Groups",
        "valueType": "s"
      }
    ],
    "role-templates": [
      {
        "name": "User",
        "description": "Basic Business Partner Role",
        "scope-references": ["$XSAPPNAME.User"],
        "attribute-references": ["customerId", "groups", "vg"]
      }
    ]
  }

 

You can create the service directly in SCP CF or with the CF CLI:
cf create-service xsuaa application {instance_name} -c xs-security.json
Based on the role-template “User” you can use step 5 and 6 of this blog (https://blogs.sap.com/2020/07/24/mapping-of-saml-attributes-with-xsuaa-jwt-in-cloud-foundry/) to map the assertion attributes to attributes defined in the xs-security.json and to map the SAML group “Tech_User_Group” from samlHelper.js to a role collection.
  The next step is to go to “Trust Configurations” and click on the newly created configuration. Here you can create the technical user with the same name as sapapim.username from step 2. You also need to add this user to the role collection:
Role%20Collection

4. Results

You can check your XSUAA JWT by starting a debug session in the API Management and calling your service endpoint. The decoded XSUAA JWT should look something like this:

 

{
  "jti": "******",
  "ext_attr": {
    "enhancer": "XSUAA"
  },
  "xs.system.attributes": {
    "xs.saml.groups": [
      "Tech_User_Group"
    ],
    "xs.rolecollections": [
      "TECH_USER_RC"
    ]
  },
  "given_name": "TECH_USER",
  "xs.user.attributes": {
    "customerId": [
      "0000000001"
    ],
    "vg": [
      "gb"
    ],
    "groups": [
      "Tech_User_Group"
    ]
  },
  "family_name": "this-default-was-not-configured.invalid",
  "sub": "******",
  "scope": [
    "openid",
    "example-app!t48928.User"
  ],
  "client_id": "******",
  "cid": "******",
  "azp": "******",
  "grant_type": "urn:ietf:params:oauth:grant-type:saml2-bearer",
  "user_id": "******",
  "origin": "saml.api.proxy",
  "user_name": "TECH_USER",
  "email": "*****",
  "rev_sig": "******",
  "iat": "******",
  "exp": "******",
  "zid": "******",
  "aud": [
    "uaa",
    "openid",
    "example-app!t48928",
    "sb-example-app!t48928"
  ]
}

 

Attributes such as customerId and vg can now be used directly in the CDS service. For example:
entity Stocks @(restrict: [
   { grant: ['READ', 'WRITE'], to: 'authenticated-user', where: 'customerId = $user.customerId'}
 ]) as projection on stock.Stocks;

 

Used Resources

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