Skip to Content
Technical Articles
Author's profile photo Jorg Thuijls

Custom authentication in CAP with social logins

Custom authentication in CAP with social logins

Hey everyone, a little while ago I prepared a small demo for CAP that shows how to add a custom authentication handler, and how to use that to add a social login to your CAP project. In this case, I chose to integrate a Github oAuth app. In this post, I’ll describe exactly what you need to do to integrate an external authentication provider of your own. The following only applies to NodeJS. For the JAVA people, I apologise but you’re on your own.

If you’d like to skip ahead, there’s a live demo here that uses VueJS and Tailwind for the front end, and all the code is in our Github. Feel free to clone and dive right in.

This post will take you through the following 4 steps:

  1. Create the oAuth application on Github.
  2. Create your CAP application and add a custom server object so you can add Passport
  3. Add a custom authentication handler to your CAP configuration
  4. Add a custom login button to your front end

The reason I chose passport is that it’s old and proven technology, and there are at the time of posting 520 different authentication strategies available for you to integrate so you can choose whichever one suits you.

Result

Just to show the results of this exercise, this is our app:

Calling the service without being logged in and without cookies set, CAP is going to present a 401 on the service.

After logging in, cookies are present and CAP will let you through

oAuth flow in CAP / Express

Creating an oAuth application

Since I chose Github as the oAuth provider, I simply followed the steps in the documentation. The process is straight forward. This is the path to follow:

github%20oauth%20path

github oauth path

The most important parts to get right are through the homepage, especially the redirect URL:

 

Redirection%20settings

Redirection settings

 

For development purposes, it is fine to put your local link in there like http://localhost:4004/auth/github/callback. Just remember to have a development version and a production version, or to switch URL’s.

Create a CAP application, add a custom server and add Passport

The first thing to do after creating a new CAP app is to install the missing packages from NPM:

npm install --save passport passport-github2 cookie-session

The documentation and examples on how to create a custom server or extend the existing server so you can work with the default Express app is pretty clear. If you’d like to read up on that the documentation click here, you’ll find my version below.

File structure

Here are the relevant files in the project we’ll be discussing:

.env

First of all, we need a place to safely store some secrets. The best place to store secrets is in a .env file. Don’t forget to add it to your .gitignore therefore, your secrets are kept away from your Git repositories. In our example, we have the following:

GITHUB_CLIENT_ID=xxx
GITHUB_CLIENT_SECRET=xxx
GITHUB_CALLBACK_URL=http://localhost:4004/auth/github/callback

The callback URL is specific to your host, but the local one works fine when you’re testing this on your own machine.

srv/server.js

The custom server loads our server implementation. I separated those so I can reuse the implementation in Jest tests.

const cds = require("@sap/cds");
const implementation = require('./serverImplementation');

cds.on("bootstrap", async (app) => await implementation(app));

module.exports = cds.server;

srv/serverImplementation.js

/* eslint-disable no-unused-vars */
const passport = require('passport');
const cookieSession = require('cookie-session')
require('../auth/passport')

module.exports = async (app) => {
  //cookie-session converts the current session to an encrypted cookie using the 
  //keys below
  app.use(cookieSession({
    name: 'github-auth-session',
    keys: ['key1', 'key2']
  }))

  //initialise passport and set it up to use sessions
  app.use(passport.initialize());
  app.use(passport.session());

  /**
   * Added for the purpose of oAuth example
   */
  app.get('/auth/error', (_req, res) => res.send('Unknown Error'))
  app.get('/auth/logout', (req, res) => {
    req.session = null;
    req.logout();
    res.redirect('/');
  })
  app.get('/auth/my-user', (req, res) => {
    res.json(req?.user?._json)
  })
  app.get('/auth/github', passport.authenticate('github', { scope: ['user:email'] }));
  app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/auth/error' }),
    function (_req, res) {
      res.redirect('/');
    });
}

Routes we’re adding here

So we’re adding several routes:

  • auth/error, this will get called when an error occurs
  • auth/logout, this is the route that resets the session and removes the cookie
  • auth/my-user is the route that returns the current user as it is stored on the session
  • auth/github is the authenticating method, This will hand over to passport and tell passport to present the Github login screen and check the Github access token, if one is present. Passport in this case does all the heavy lifting, initiating the redirect to Github and asks for your username and password.
  • auth/github/callback is the method that’s configured on the Github oAuth client, these have to match. If the callback on the oAuth client is not identical to the method on your CAP server, the redirections are not completed and your app will fail. After the user authenticates with Github, Passport takes over to turn the response from Github into a user session for the Express application to use.

/auth/passport.js

const passport = require('passport');
const GitHubStrategy = require('passport-github2').Strategy;
const { GITHUB_CALLBACK_URL, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env;

//methods to indicate how to serialise and deserialise the user object. 
passport.serializeUser(function (user, done) {
  done(null, user);
});

passport.deserializeUser(function (user, done) {
  done(null, user);
});

//adding the Github strategy
passport.use(new GitHubStrategy({
  clientID: GITHUB_CLIENT_ID,
  clientSecret: GITHUB_CLIENT_SECRET,
  callbackURL: GITHUB_CALLBACK_URL
},
  function (accessToken, refreshToken, profile, done) {
    return done(null, profile);
  }
));

The implementation of the passport strategy needs three things:

  1. Passport needs to know how to serialise and deserialise the user. In our case there’s really nothing to do but sometimes this user information could be encoded
  2. We need to initialise the Github strategy with the client ID, client secret and callback, so Passport can redirect us to the correct oAuth application
  3. We need a callback to tell passport what to do. Since we’re not doing anything flash we’re just telling Passport that we’re done and we’ll use the returning profile

In many cases this callback should be extended by checking if we have a local user in our Users table already, for instance, so that we can create a profile that is not just the GitHub profile.

Add a custom authentication handler to your configuration

After setting up passport in the custom server object, CAP has a default security middleware. In Express, middleware is a function that runs on a route before the default route implementation is executed. In order to specify your own and override the default, add the following to your .cdsrc.json:

{
  "requires": {
    "auth": {
      "impl": "./auth/handler.js"
    }
  }
}

The documentation on custom authentication methods is here.

Finally, we’ll have a look at the custom handler:

./auth/handler

const cds = require("@sap/cds");

module.exports = (req, res, next) => {
  if (req?.user) {
    req.user = new cds.User(req.user)
    next()
  } else {
    res.status(401).send();
  }
}

This snippet converts the user object on the request, fetched and deserialised by Passport into a CDS.User object. CAP uses this user to call its own methods, such as role check:

req.user.is('admin')

If the user object is not of type cds.User the app will probably crash, since methods like the above are used internally.

Add a custom login button to your front end

Front-end wise, there’s not much to do. You’ll only have to direct the user to route /auth/github and Passport will take over the redirections to our oAuth client, any button or link is fine. To log out, simply add a link to /auth/logout to remove the cookie and the session.

Conclusion

That’s it, the CAP app is now protected by the GitHub social login. To generalise the steps you need to take to replace the standard authentication with a social login or external oAuth are:

  1. Create the external oAuth application. There are many out there, and this demo uses Github
  2. Initialise the routes you’ll need to start the authentication process, the callbacks etcetera. Check with passport to select a strategy that fits your needs
  3. Replace the existing authentication middleware to avoid clashes
  4. Integrate the your new login into your front end

Thanks for making it this far. Here’s again a link to the demo and the Github repository. If you have any questions feel free to leave a comment.

Assigned Tags

      5 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Tobias Steckenborn
      Tobias Steckenborn

      Once again great work Jorge!

      Author's profile photo Jorg Thuijls
      Jorg Thuijls
      Blog Post Author

      Thanks Tobias 🙂

      Author's profile photo Andre Kuller
      Andre Kuller

      Hi Jorg,

      very good and interesting article.

      One question, did you try to save the data in auth/handler in a cds table?

      I had a small error in thinking.If the adjustment is made in the handler of the passport function everything works.

      Author's profile photo Jorg Thuijls
      Jorg Thuijls
      Blog Post Author

      Thanks Andre 🙂

      I have not tried saving or retrieving the profile. I'm thinking that's what the serialise and deserialise functions are for, but I suppose the callback on the passport initialisation would work as well. It's mentioned at the bottom of the /auth/passport.js bit.

      Author's profile photo Sreehari V Pillai
      Sreehari V Pillai

      kudos.

      As SAP provides a functionality to implement custom authenticator , should we not write the authentication related code in handler.js ( and its helpers ) ? . I was trying to keep it clean and write it here . But I couldn't, as I was not able to get the app object from req object.  Did you also try this way ? I ended up writing the logic in server.js as I get access to app object directly .
      I tried req.app , but failed.