Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
jhodel18
Active Contributor
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!~
11 Comments
martinkoch
Active Participant
Hi Jhodel,

thanks for the great blog post!

This is very valueable for me.

Regards from Austria,

Martin
jhodel18
Active Contributor
0 Kudos
Thanks for your comment martin.koch5 !

Glad to know this was helpful to you.
LeonardoGomez
Advisor
Advisor
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.
jhodel18
Active Contributor
0 Kudos

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.

 

0 Kudos
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​




LeonardoGomez
Advisor
Advisor
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.

 
spurkayastha
Explorer
0 Kudos
Hi jhodel18

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

 
jhodel18
Active Contributor
0 Kudos
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.
spurkayastha
Explorer
0 Kudos
Hi jhodel18,

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
I533303
Advisor
Advisor
0 Kudos
Hi jhodel18,

Thank you for the interesting Blog.

I followed the same steps to protect CAP Odata service that I have deployed on BTP successfully.

I could access the metaData  after authenticating with SAP Default IDP. But  when  I have tested to call bound action inside the service via Rest Client (Post man) using basic authentication (email and password on SAP BTP) I get 40 1Unauthorized error as follow 

Test on Postman :


test on BAAS


can you please guide me how to perform such call using authentication ?

Thank you and best regards,

Mariam
Dino
Explorer
0 Kudos
I think it is better to use something like this in the .cdsrc:

{

  "[localhost]": {

    "auth": {

      "passport": {

        "strategy": "mock",

        "users": {

          "dummy": {

            "password": "1234",

            "ID": "dummy@dummy.de",

            "roles": [

              "authenticated-user"

            ]

          }

        }

      }

    }

  }

}



If you don't use the "[localhost]" the service might not work if deployed to the CF-Subaccount.

That is what happend in my case 🙂


Best regards,

Dino
Labels in this area