Skip to Content
Technical Articles
Author's profile photo Jhodel Cailan

CAP: Demystify User Authentication

Before the release of the CAP Model, I have been working on the XSJS framework both in XSA and Cloud Foundry environments. And because of that, I’ve gotten familiar with how the framework handles user authentication. Now, with the CAP Model taking over, I have to start over again and figure out how does the CDS framework handles the user authentication.

In this blog post, I will share my journey in understanding how CAP handles user authentication and what it does behind the scenes. I will show how to set up user authentication for a CAP-based service. Subsequently, I will deep dive into the inner workings of the CDS framework and unearth how user information is handled.

 

 

Prerequisites


  • SAP Cloud Platform for the Cloud Foundry Environment Account
  • SAP Business Application Studio / Visual Studio Code

 

CAP Base Project


The base project for this is the solution from my previous blog post about Using HANA DB Sequence in CAP — see below:

https://github.com/jcailan/cap-samples/tree/blog-db-sequence

Note:

The official documentation for User Authentication via Node.js can be found here — About user authentication while accessing CDS services

 

Set Up Mocked Authentication


  • 1. In the NorthWind.cds service model, annotate the service with — @requires : ‘authenticated-user’
using {Products as ProductsEntity} from '../db/schema';

@path     : '/NorthWind'
@requires : 'authenticated-user'
service northwind {
    entity Products as projection on ProductsEntity;
}
  • 2. Add mocked authentication user in the config file .cdsrc.json
{
	"auth": {
		"passport": {
			"strategy": "mock",
			"users": {
				"jhodel": {
					"password": "1234",
					"ID": "jhodel",
					"roles": [
						"authenticated-user"
					]
				}
			}
		}
	}
}
  • 3. Install the node module passport:
> npm install passport
  • 4. Set the DB config (in package.json) to sql
	"cds": {
		"requires": {
			"db": {
				"kind": "sql"
			}
		}
	}
  • 5. Test the mocked authentication by starting the service using cds watch. When the initial page of the service is loaded, click on the Products entity, and you will be asked to enter the user credentials — user name and password. Use the credentials configured from .cdsrc.json file.

At this point, we can say that the mocked authentication is running and it serves its purpose for local testing. But we are just getting started, we need to proceed with setting up the Token-Based Authentication next because this is the actual scenario that will happen once the application is deployed in SCP Cloud Foundry.

 

Set Up Token-Based Authentication


  • 1. Generate the xs-security.json configuration file
> cds compile srv/ --to xsuaa > xs-security.json
  • 2. Update the package.json with the CDS configuration for HANA DB and XSUAA
	"cds": {
		"requires": {
			"db": {
				"kind": "hana"
			},
			"uaa": {
				"kind": "xsuaa"
			}
		}
	}
  • 3. Install additional node modules needed by CDS framework for XSUAA authentication
> npm install @sap/xssec@^2 @sap/xsenv

Note:

Based on CAP documentation, version 3 of @sap/xssec node module is not supported yet, hence, make sure that you specify ^2.

  • 4. Create a new node module for the application router
    • 4a. Create package.json inside the app-router folder
{
	"name": "app-router",
	"description": "Node.js based application router service",
	"engines": {
		"node": "^8.0.0 || ^10.0.0"
	},
	"dependencies": {
		"@sap/approuter": "6.8.0"
	},
	"scripts": {
		"start": "node node_modules/@sap/approuter/approuter.js"
	}
}
    • 4b. Create the routing config file xs-app.json
{
	"authenticationMethod": "route",
	"routes": [
		{
			"source": "^/(.*)",
			"destination": "srv_api"
		}
	]
}

 

  • 5. Generate mta.yaml file by using the command:
> cds add mta

And update the configuration to include XSUAA configuration and binding. Also, add the module configuration for the application router. You should have a similar end result as shown below:

_schema-version: "3.1"
ID: cap-samples
version: 1.0.0
description: "A simple CAP project."
parameters:
  enable-parallel-deployments: true

build-parameters:
  before-all:
    - builder: custom
      commands:
        - npm install
        - npx cds build

modules:
  - name: cap-samples-app-router
    type: approuter.nodejs
    path: app-router
    parameters:
      disk-quota: 256M
      memory: 256M
    requires:
      - name: cap-samples-uaa
      - name: srv_api
        group: destinations
        properties:
          name: srv_api
          url: "~{url}"
          forwardAuthToken: true

  - name: cap-samples-srv
    type: nodejs
    path: gen/srv
    parameters:
      disk-quota: 1024M
      memory: 256M
    properties:
      EXIT: 1
    requires:
      - name: cap-samples-db
      - name: cap-samples-uaa
    provides:
      - name: srv_api
        properties:
          url: ${default-url}

  - name: db
    type: hdb
    path: gen/db
    parameters:
      app-name: cap-samples-db
    requires:
      - name: cap-samples-db
      - name: cap-samples-uaa

resources:
  - name: cap-samples-db
    type: com.sap.xs.hdi-container
    parameters:
      service: hana
      service-plan: hdi-shared
    properties:
      hdi-service-name: ${service-name}

  - name: cap-samples-uaa
    type: org.cloudfoundry.managed-service
    parameters:
      service: xsuaa
      service-plan: application
      path: ./xs-security.json
  • 6. That’s it! The next thing to do is Build > Deploy > and Test.

By this point, you should be able to see the Product data after you have entered your SCP credentials. And by now we have fully activated User Authentication for our CAP-based service. So our next task is to investigate how it is handled behind the scene by the CDS framework.

 

Investigate the OData Context Object


The OData Context Object is the object that is provided in almost all OData event handlers in CAP. I already did the debugging to find out the object that is related to Authorization Information and User Information, therefore, for simplicity of showcasing this information, I will be using the console.log function to display the information in the logs.

  • 1. Update the NorthWind.js custom handler by handling the before read event of Products entity.
	service.before("READ", Products, (context) => {
		console.log(context.user);
		console.log(context.req.authInfo);
		console.log(context.user.is('authenticated-user'));
	});

Let’s investigate the value of user and authInfo object, as well as check if the user is an authenticated-user.

  • 2. Next, let’s Build > Deploy > and Test. But before starting the test, make sure that you execute below commands on your terminal to expose the logs generated by the cap service.
> cf logs cap-samples-srv
  • 3. Analyze the logs that were generated after we triggered the GET Products operation.

Extracted from the logs, here’s the user information:

{ id: 'jhodel.cailan@sample.com',
  name: { givenName: 'Jhodel', familyName: 'Cailan' },
  emails: [ { value: 'jhodel.cailan@sample.com' } ],
  valueOf: [Function],
  toString: [Function],
  is: [Function],
  has: [Function],
  locale: 'en' }

Here’s the portion of authorization information (Security Context):

SecurityContext {
  token: '**this is my token**',
  config:
   { tenantmode: 'dedicated',
     sburl: 'https://internal-xsuaa.authentication.us10.hana.ondemand.com',
     clientid: '...',
     xsappname: 'cap-samples!t4150',
     clientsecret: '...',
     url: 'https://sample.authentication.us10.hana.ondemand.com',
     uaadomain: 'authentication.us10.hana.ondemand.com',
     verificationkey: '**key credentials**',
     apiurl: 'https://api.authentication.us10.hana.ondemand.com',
     identityzone: 'sample',
     identityzoneid: '...',
     tenantid: '...' }

Here’s the result of context.user.is(‘authenticated-user’):

true

There you have it! All the information about the user including the roles assigned to the user is inside the context object. All of this information is processed by the framework and it is used all throughout the lifecycle of a particular request.

Now, let’s take this understanding further into an OData Create operation.

 

User Context on OData Create Operation


Let’s say we want to keep track of who and when a product was created, in this scenario we can make use of the User Context during an OData Create Operation. Luckily, this is already supported by the CDS framework. We can make use of the @cds.on.insert annotation.

  • 1. We need to update the Products entity in the schema.cds file with two new fields — CreatedAt and CreatedBy.
entity Products {
    key ID               : Integer;
        Name             : String;
        Description      : String;
        ReleaseDate      : DateTime;
        DiscontinuedDate : DateTime;
        Rating           : Integer;
        Price            : Decimal(13, 2);
        CreatedAt        : Timestamp  @cds.on.insert : $now;
        CreatedBy        : String(255)@cds.on.insert : $user;
}

Note:

We have used the @cds.on.insert to annotate the properties of its default value during a database insert operation. CreatedAt will be populated by current date and time denoted by $now, while CreatedBy will be populated by the current user ID denoted by $user.

Also, note that we could have used the managed aspect provided by the cds module. However, in this example, I would like to show the usage of User Context in its simplest form, hence, I opted not to use the managed aspect.

  • 2. Next is to Build > Deploy > and Test. For this testing, I will be using the Mocked Authentication while still connected to the HANA DB in the SCP. Test by triggering a POST operation to create a new Product. See below the results:

See how the User ID was used by the framework to automatically populate the CreatedBy field. Isn’t that cool?!

Now let’s try to understand further the relevance of OData Context when overriding the default handling of OData operations.

 

OData Context on OData Create Operation


This time let’s try to analyze the importance of transaction (tx) and OData Context.

  • 1. Overwrite the default handling of the create operation for Products entity by adding the code logic below to the custom handler:
	service.on("CREATE", Products, async (context) => {
		console.log(context.data);
		await db.run(INSERT.into(Products).entries(context.data));
		return await db.run(SELECT.one(Products).where({ ID: context.data.ID }));
	});

In the above custom logic, we are writing the data/payload into the console so that we can see the data provided by the user (together with the auto-generated ID).

Next is that the data is inserted into the Products entity. Then lastly, there’s a query to the created record to be returned back to the service consumer.

  • 2. Test the service and analyze the results. In the screenshot below, you will see that the record that was saved into the DB has a CreatedBy = ANONYMOUS.

Perhaps there are a few questions in your mind: why am I getting ANONYMOUS?? why is the @cds.on.insert : $user not working??

Well, the answer to these questions is the subject of this topic: transaction (tx) and OData Context.

  • 3. Modify the implementation of the CREATE event handler by passing the context to the transaction (tx) function for the INSERT to DB operation.
	service.on("CREATE", Products, async (context) => {
		console.log(context.data);
		const tx = db.tx(context);
		await tx.run(INSERT.into(Products).entries(context.data));
		return await tx.run(SELECT.one(Products).where({ ID: context.data.ID }));
	});
  • 4. Test again the service and you should be able to see that this time, the user ID is now properly saved in the DB.

Based on the result of this investigation, we can conclude that whenever we use the DB connection in our custom handler, the context object should always be passed to the transaction (tx) function in order for the DB to use it in the subsequent operation.

Note:

In the context of using HDI Containers, the User that is used to connect to the HANA DB is not the authenticated user of the application, but instead, it is the technical user that was generated during the creation of the HDI container.

 

Closing


In this blog post, we have seen how easy it is to set up the authentication of a CAP-based service. We know that CAP handles a lot of stuff with regards to authentication and user information handling, however, it is still good to know how does the framework handle this information in the event that there’s a need to override the default implementation to cater for additional logic that you need to add.

This little exploration that I did with CAP’s handling of authentication was a good learning experience, and I hope you’ve learned something from this too.

If you know something about this topic that was not mentioned, especially the use of tx() function and OData Context, please do share by commenting below.

 

 

~~~~~~~~~~~~~~~~

Appreciate it if you have any comments, suggestions, or questions. Cheers!~

Assigned Tags

      9 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Martin Koch
      Martin Koch

      Hi Jhodel,

      thanks for the great blog post!

      This is very valueable for me.

      Regards from Austria,

      Martin

      Author's profile photo Jhodel Cailan
      Jhodel Cailan
      Blog Post Author

      Thanks for your comment Martin Koch !

      Glad to know this was helpful to you.

      Author's profile photo Leonardo Gomez
      Leonardo Gomez

      Hi Jhodel,

      I'm following your blog to apply it to a project of mine.

      Inside the section called Set Up Token-Based Authentication, on point 4a you ask to create a new module. Is that done by a command? I tried just creating a folder and the files package.json and xs-app.json inside. I also run the command cds add mta from within the folder. Then the problem is that when I do mbt build I get these errors:

       

      [2020-11-27 16:16:40] ERROR the "mta.yaml" file is not valid:
      line 85: the "srv_api" property set required by the "cap-samples-app-router" module is not defined
      line 89: the "url" property of the "cap-samples-app-router" module is unresolved; the "srv_api/url" property is not provided

       

      Do you know what's the problem?

       

      Thanks!

      Leonardo.

      Author's profile photo Jhodel Cailan
      Jhodel Cailan
      Blog Post Author

      Hi Leonardo Gomez

      About the 4a question, the answer is no, it’s the exact package.json you need to use. Or if you really want to start from scratch, you could use the command

      > npm init

      Then add the needed dependencies to the generated package.json.

      From the error you have, it seems like there’s something wrong with your MTA configuration. These errors should be descriptive enough:

      line 85: the “srv_api” property set required by the “cap-samples-app-router” module is not defined
      line 89: the “url” property of the “cap-samples-app-router” module is unresolved; the “srv_api/url” property is not provided

      My guess is that you are trying to incorporate my steps directly into your project, hence, there were mismatches in your MTA config. If that’s the case, then I would suggest following through exactly the steps to get the basic concepts. And once that is established, try applying the concepts to your own project.

       

      Author's profile photo ELI NAIM
      ELI NAIM

      You need to replace the url to srv url key "~{srv-url}"

      get it from the srv module like here:

         provides:
          - name: srv-api      # required by consumers of CAP services (e.g. approuter)
            properties:
              srv-url: ${default-url}
      and place it in the approuter:

             properties:
               name: srv-api
               url: "~{srv-url}"
               forwardAuthToken: true​
      Author's profile photo Leonardo Gomez
      Leonardo Gomez

      I appreciate both of you for the help. The problem was a typo where I was writing srv_api on one place and srv-api on the other.
      Unfortunately my problems don't stop there, it's still not working and I assume that the issue has to do with the fact that the identity provider is Success Factors.

       

      Author's profile photo Sangita Purkayastha
      Sangita Purkayastha

      Hi Jhodel Cailan,

      I tried the steps given in your blog for one of our projects and things seem to be working fine locally. But when I deployed the application to BTP and when I try to access the entity set exposed via the srv module, I get the error as 401 Unauthorized. But when I access the app-router module and try to access the entity sets from there, it gives the correct output. Is this an expected behavior? I am not able to connect the dots here. Can you please help here?

      Regards,

      Sangita Purkayastha

       

      Author's profile photo Jhodel Cailan
      Jhodel Cailan
      Blog Post Author

      Hi Sangita,

      Yes, that’s the expected behavior. When you activated the authentication for your service, it requires the JWT token, calling the service the directly without passing the JWT token will cause the unauthorized error. Using app-router, it will help to authenticate you and retrieve the JWT. Behind the scenes, app router will pass the JWT token while calling the service end point.

      Author's profile photo Sangita Purkayastha
      Sangita Purkayastha

      Hi Jhodel Cailan,,

      Thanks for the reply. As the next step how can I get the JWT token and how can I pass it to the srv module in order to correct the 401 Unauthorized error?

      Regards,

      Sangita