Monday 20 July 2020

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.

SAP HANA Study Material, SAP HAN Exam Prep, SAP HANA Learning, SAP HANA Certification, SAP HANA Prep

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

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.

SAP HANA Study Material, SAP HAN Exam Prep, SAP HANA Learning, SAP HANA Certification, SAP HANA Prep

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:

SAP HANA Study Material, SAP HAN Exam Prep, SAP HANA Learning, SAP HANA Certification, SAP HANA Prep

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.

SAP HANA Study Material, SAP HAN Exam Prep, SAP HANA Learning, SAP HANA Certification, SAP HANA Prep

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.

SAP HANA Study Material, SAP HAN Exam Prep, SAP HANA Learning, SAP HANA Certification, SAP HANA Prep

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.

No comments:

Post a Comment