Today we explore how to use a different Identity and Access Management (IAM) Solution for a SAP CAP node.js based application. The default approach to handle Authentication and Authorization with CAP is to use XSUAA in conjunction with BTP or SAP CIAM. Of course it is easier to use the SAP default, out-of-the-box solution, but for different reasons you may have to use another IAM solution in conjunction with SAP CAP.
Thanks to the openness and usage of open standards it is possible to integrate and use another IAM solution which is not part of the SAP ecosystem with SAP CAP. For demonstration purposes we will explore how to use Auth0 as IAM solution, instead of the default, out-of-the-box SAP solution and have a closer look on Social Auth and Role Based Access Control (RBAC).
For in-depth articles on how to use the default SAP approach to authenticate user using SAP CAP I recommend to read the Blog Post
CAP: Demystify User Authentication by Jhodel Cailan.
Outline
What we will build in this article:
- Demo Project: Demo SAP CAP Project which provides a simple OData Service
- Authentication with Auth0: The OData Service can only be accessed by authenticated user.
- Role Base Secured Service: Data can only be read or written by users who have the necessary roles assigned.
This post will take you through the following 3 steps:
- Create an Auth0 project and configure it
- Create a CAP App with a custom server handler
- Add a custom authentication handler to CAP
- Bonus: Add and secure a simple Fiori App.
Setup Auth0 Project and Configuration
Register an Application
We first create a regular "Web Application" at Auth0. There we need to note down the following information:
- Domain, which we will use later as
ISSUER_BASE_URL
- Client ID, which we will use later as
CLIENT_ID
- Client Secret, which we will use later as
CLIENT_SECRET
For testing purpose we need to configure the following parameters:
Register an API
In our application we want to use roles. These can be defined in Auth0 using APIs with RBAC Policies activated.
We activate "Enable RBAC" and "Add Permissions in the Access Token".
Then we define the possible roles
read:books
and
write:books
which we can assign later to our users.
Setup Demo CAP Project
I assume that you have already setup all required SAP CAP development tools. If not, please follow the 3 steps described in the
local setup documentation.
Let's create a minimal CAP Project with
cds init
.
Then we create a simple model in the
db/schema.cds
:
namespace db;
entity Books {
isbn : String;
title : String;
}
Next we create a service in
srv/catalog-service.cds
:
using {db} from '../db/schema';
@(requires : 'authenticated-user')
service CatalogService {
entity Books as projection on db.Books;
@(restrict: [
{grant: 'READ', to: 'read:books'},
{grant: 'WRITE', to: 'write:books'},
])
entity ProtectedBooks as projection on db.Books;
}
We only want to allow authenticated users to access the Service. This can be achieved by annotating the service with
@(requires : 'authenticated-user')
(see
SAP CAP - Restricting Roles with @requires).
Further, a user should only be allowed to read data, if the role
read:books
is assigned to the user. Similar, we grant write access only to users who have the role
write:books
assigned. Both is achieved with the
restrict:grant:to
annotation (see
SAP CAP - Access Control with @restrict).
For testing, we add some dummy data by adding a
db.Books.csv
file into the folder
db/data
(see
SAP CAP - Providing Initial Data).
Testing the initial App
That's already enough to start and test the app with
cds watch
. Navigate to
http://localhost:4004/catalog/Books and we will receive a popup asking for username and password. This means, that the endpoint is secured. The default mocked admin user is
alice
with password
alice
. We will see a response with the books.
Let's try to navigate to
http://localhost:4004/catalog/ProtectedBooks. We will receive a 403 - forbidden message, as the user is missing the role
read:books
. We can add the role in
.cdsrc.json
and we will retrieve data:
{
"requires": {
"auth": {
"users": {
"alice": {
"roles": ["read:books"]
}
}
}
}
}
Note: Do the testing in an incognito window, otherwise you have to remove cookies per hand.
Custom Server Handler
Before we start, we need to add some dependencies for our implementation:
npm add express-openid-connect jsonwebtoken
First, we create our custom server handler in
srv/server.js
:
const cds = require("@sap/cds");
const { auth, requiresAuth } = require("express-openid-connect");
const config = {
authRequired: false, // deactivate auth for all routes
auth0Logout: true, // logout from IdP
authorizationParams: { // required to retrieve JWT including permissions (our roles)
response_type: "code",
scope: "openid",
audience: "<https://cap-auth0-demo-api.com>",
},
};
cds.on("bootstrap", (app) => {
// initialize openid-connect with auth0 configuration
app.use(auth(config));
});
module.exports = cds.server;
This implementation extends the standard CAP express implementation. We use the one-time event
bootstrap
to add the general auth configuration (see
SAP CAP - Bootstrap).
The package
express-openid-connect
configures also a
/callback
,
/login
and
/logout
route. The
/callback
route is used to process the JWT token which we receive after login using Auth0.
We set the parameter
authRequired
to
false
, to deactivate auth checks on every route. We want to have full control which route to protect.
To retrieve the JWT token including roles, we need to provide
authorizationParams
.
Environment Variables
The configuration requires more environment specific and security related information which we store in an
.env
file. There we add the Auth0 variables
ISSUER_BASE_URL
,
CLIENT_ID
, and
CLIENT_SECRET
. Additionally, we need to add a
BASE_URL
, which is the root URL of the app, e.g.,
http://localhost:4004 and a
SECRET
variable, which is used to crypt the cookie which holds the auth information for the user, e.g., an 80-character string.
ISSUER_BASE_URL=<AUTH0_ISSUER_BASE_URL>
CLIENT_ID=<AUTH0_CLIENT_ID>
CLIENT_SECRET=<AUTH0_CLIENT_SECRET>
SECRET=<LONG_RANDOM_STRING>
BASE_URL=<http://localhost:4004>
Note: Environment specific or security related configuration should always be stored outside of the code, e.g., in
.env
files. Security related information such as passwords or secret keys must never be checked into git.
Custom Auth Handler
Now, we need to add a custom Auth implementation to CAP. The general approach is described in the documentation:
SAP CAP Custom-Defined Authentication. The Auth handler is used as express middleware within CAP to protect the routes in
srv/auth.js
.
const cds = require("@sap/cds");
const { requiresAuth } = require("express-openid-connect");
const jsonwebtoken = require("jsonwebtoken");
// To debug this module set export DEBUG=cds-auth0
const DEBUG = cds.debug("cds-auth0");
// CAP user
const Auth0User = class extends cds.User {
is(role) {
DEBUG && DEBUG("Requested role: " + role);
return role === "any" || this._roles[role];
}
};
// the authentication function for CAP
function capAuth0(req, res, next) {
if (!req.oidc.user) {
DEBUG && DEBUG("No user");
return next(Error());
}
// map token attributes to CAP user
let capUser = {
id: req.oidc.user.sub,
_roles: ["authenticated-user"],
};
// retrieve permissions
let jwtDecoded = jsonwebtoken.decode(req.oidc.accessToken.access_token);
if (jwtDecoded.permissions) {
capUser._roles.push(...jwtDecoded.permissions);
}
req.user = new Auth0User(capUser);
DEBUG && DEBUG("capUser");
DEBUG && DEBUG(capUser);
next();
}
module.exports = [requiresAuth(), capAuth0];
The key in this implementation is to check if a user is logged-in and translate it to the
cds.User
object which is evaluated by CAP. The security handling (login, logout, etc.) is handled by the
requiresAuth()
handler provided by the
express-openid-connect
package and configured in the last step. We simply need to check the
oidc
request variable and extract the user information (including the roles) into an extended class of
cds.User
. This class requires the function
is()
which returns true, if the requested role is present and false otherwise.
Next, we need to tell CAP where to find the custom auth implementation by adding the following lines to
cdsrc.json
:
{
"requires": {
"auth": {
"impl": "srv/auth.js"
}
}
}
Testing the custom Auth Implementation
Let's navigate to
http://localhost:4004/catalog/Books. This time we are redirected to the Auth0 login page. If we have no user yet, we can sign up. Otherwise, we can use email/ password to login or any social provider we have configured. After successful login, we should see the books.
Let's navigate to
http://localhost:4004/catalog/ProtectedBooks and we should get a 403-Forbidden, as we have not yet configured the roles. We will do this in the next step.
User Management in Auth0
In Auth0 we can assign roles per hand to users or using a Management API. For simplicity of this article, we do it manually in the Auth0 console. First we create a role, e.g.,
book_viewer
. Then we assign the permissions, e.g.,
read:books
. Then we assign the role to users.
Note: In a productive scenario it is possible to use the Auth0 Management API to assign users to roles. See
Auth0 Management API.
Testing the roles
Let's navigate to
http://localhost:4004/catalog/ProtectedBooks and now we should see the books. We may need to logout and login again. We can do this by using the URLs
http://localhost:4004/login and
http://localhost:4004/logout.
Debugging
We can debug the application at anytime by setting a debug environment variable like so:
# windows powershell
$env:DEBUG="cds-auth0"
# windows cmd
set DEBUG=cds-auth0
# unix
export DEBUG=cds-auth0
Bonus: Protect an App
Let's create an app with the known generators in VSCode, e.g.,
app/bookshop
.
If we are not logged in and we navigate to
http://localhost:4004/bookshop/webapp/ we will receive an error page, but we are not redirected to a login screen. Within the SAP ecosystem the app router would handle this gracefully.
A simple solution is to add a protected route, e.g.,
/app
which serves the static assets within the
/app
folder. The protected route checks if a user is logged in. If not, the user is redirected to the login form. Therefore, we add the following line into the
server.js
after the line
app.use(auth(config));
app.use('/app', requiresAuth(), express.static(__dirname + '/../app'));
Note: Do not forget to import express with
const express = require('express');
With this set, we are redirected to the login page if we try to access
http://localhost:4004/app/bookshop/webapp/.
Related Articles from the SAP Network
I am not the first person who wanted to try out a different Auth approaches using CAP. Here are some inspirations with different implementation approaches which I stumbled across while writing this article:
Final Thoughts
You see, that it is not that hard to implement a custom authentication provider like Auth0 if you understand what needs to be done, thanks to the open standards which are used in SAP CAP.
Still, this is a custom implementation: It takes time and effort to implement and it may not cover all the features and integration a default, out-of-the-box SAP solution provides.