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: 
jthuijls
Participant

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 oauth path


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

 


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.
5 Comments
tobias_steckenborn
Active Participant
Once again great work Jorge!
jthuijls
Participant
Thanks Tobias 🙂
former_member610335
Participant
0 Kudos

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.

jthuijls
Participant
0 Kudos

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.

sreehari_vpillai
Active Contributor
0 Kudos
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.
Labels in this area