Skip to Content
Technical Articles
Author's profile photo Kai Niklas

SAP CAP with Social Login and RBAC using Auth0

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:

  1. Create an Auth0 project and configure it
  2. Create a CAP App with a custom server handler
  3. Add a custom authentication handler to CAP
  4. 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.

Assigned tags

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