Skip to content

Instantly share code, notes, and snippets.

@JedWatson
Last active April 16, 2023 02:11
Show Gist options
  • Save JedWatson/f88b60e9d6b33b6c47c4 to your computer and use it in GitHub Desktop.
Save JedWatson/f88b60e9d6b33b6c47c4 to your computer and use it in GitHub Desktop.
API Auth with KeystoneJS

To implement API authentication in KeystoneJS, you need the following:

For key based authentication

  • Middleware that validates the key in the request body or a header

For session based authentication

  • An endpoint that handles signin
  • An endpoint that handles signout
  • Middleware that validates the session

Examples of both methods are below.

Note that the code in each file below would normally be spread across several files, depending on your project organisation, e.g. route handlers in a /routes/api/... folder, and route bindings in /routes/index.js.

// check that the key has been provided in the request body,
// could also be a header
function checkAPIKey(req, res, next) {
// you would have the key in an env variable or load it from
// your database or something.
if (req.body.apiKey === SECRET_API_KEY) return next();
return res.status(403).json({ 'error': 'no access' });
}
// then bind that middleware in your routes before any paths
// that should be protected
app.all('/api*', checkAPIKey);
// the rest of your api endpoints go below here, e.g.
app.get('/api/stuff', getStuff);
// create a route that handles signin
function signin(req, res) {
if (!req.body.username || !req.body.password) return res.json({ success: false });
keystone.list('User').model.findOne({ email: req.body.username }).exec(function(err, user) {
if (err || !user) {
return res.json({
success: false,
session: false,
message: (err && err.message ? err.message : false) || 'Sorry, there was an issue signing you in, please try again.'
});
}
keystone.session.signin({ email: user.email, password: req.body.password }, req, res, function(user) {
return res.json({
success: true,
session: true,
date: new Date().getTime(),
userId: user.id
});
}, function(err) {
return res.json({
success: true,
session: false,
message: (err && err.message ? err.message : false) || 'Sorry, there was an issue signing you in, please try again.'
});
});
});
}
// you'll want one for signout too
function signout(req, res) {
keystone.session.signout(req, res, function() {
res.json({ 'signedout': true });
});
// also create some middleware that checks the current user
// as long as you're using Keystone's session management, the user
// will already be loaded if there is a valid current session
function checkAuth(req, res, next) {
// you could check user permissions here too
if (req.user) return next();
return res.status(403).json({ 'error': 'no access' });
}
// add an API endpoint for signing in _before_ your protected routes
app.post('/api/signin', signin);
app.post('/api/signout', signout);
// then bind that middleware in your routes before any paths
// that should be protected
app.all('/api*', checkAuth);
// the rest of your api endpoints go below here, e.g.
app.get('/api/stuff', getStuff);
@intrnet
Copy link

intrnet commented Jul 16, 2018

Hi,

Below is my JWT authentication sample which I would like to discuss with you. It works well but maybe there is a way to do that better. I will show you how it works and let me know what you think about it.

routes/views/session/signinjwt.js

var keystone = require('keystone'),
    async = require('async'),
    jwt = require('jsonwebtoken'),
    _ = require('lodash'),
    crypto = require('crypto'),
    utils = require('keystone-utils');
 
exports = module.exports = function(req, res) {
    
    if (req.user && req.cookies.uToken) {
        return res.redirect(req.cookies.target || '/me');
    }
     
    var view = new keystone.View(req, res),
        locals = res.locals;
        locals.section = 'session';
        locals.form = req.body;

    view.on('post', { action: 'signinjwt' }, function(next) {
        
        if (!req.body.email || !req.body.password) {
            req.flash('error', 'Please enter your username and password.');
            return next();
        }
        
            var token = '',
                userdata = {};
                async.series([   
                    function (next) {             
                        var User = keystone.list(keystone.get('user model'));
                        if (typeof req.body.email === 'string' && typeof req.body.password === 'string') {
                            
                            if (!utils.isEmail(req.body.email)) {
                                return next({message:'Incorrect email or password'});
                            }
                            var emailRegExp = new RegExp('^' + utils.escapeRegExp(req.body.email) + '$', 'i');
                         
                            User.model.findOne({ email: emailRegExp }).exec(function (err, user) {
                                if (user) {
                                    user._.password.compare(req.body.password, function (err, isMatch) {
                                        if (!err && isMatch) {
                                            userdata = user;
                                            return next();
                                        } else {
                                            return next({ message: err || 'Incorrect email or password' });
                                        }
                                    });
                                } else {
                                    return next({ message: err });
                                }
                            });
                        } else {
                            return next({ message: 'Incorrect user or password'});
                        }
                    },
                    function(next) { 
                        token = jwt.sign({id: userdata._id, email:userdata.email}, keystone.get('jwt secret'), { expiresIn: '15s' });
                        keystone.app.locals.user = userdata;
                        
                        return next();
                    },
                    function(next) {
                        res.cookie('uToken', token,  { maxAge: 900000, httpOnly: true });
                        res.clearCookie('afterLoginUrl');   
                        return next();                     
                    },
                    function(next) {
                        if (req.body.target && !/join|signinjwt/.test(req.body.target)) {
                            return res.redirect(req.body.target); 
                            return next();        
                        } else {
                            var afterLoginUrl = req.cookies.afterLoginUrl || '/me'; 
                            
                            return res.redirect(afterLoginUrl);
                          
                            return next(); 
                        }
                    }       
                ], function(err) {
                    if (err) {
                        req.flash('error', err.message || 'Sorry, there was an issue signing you in, please try again.');
                        return res.redirect('/signinjwt');
                    }
                });
    });
    
    view.render('session/signinjwt');
    
}

routes/views/session/signoutjwt.js

var keystone = require('keystone');

exports = module.exports = function(req, res) {
    
    var view = new keystone.View(req, res),
        locals = res.locals;
    
    locals.section = 'session';
    res.clearCookie('uToken');
    keystone.app.locals.user = null;
    
    res.redirect('/');
};

routes/middleware.js

   ...

exports.initLocals = function (req, res, next) {
   ...

   locals.user = keystone.app.locals.user;

   ...

}

   ...

exports.jwtCheck = function (req, res, next) {
 
    if (!req.cookies.uToken) {
        req.user = null;
        res.cookie('afterLoginUrl', req.originalUrl, { maxAge: 900000, httpOnly: true });
        req.flash('error', 'Please sign in to access this page.');
        res.redirect('/signinjwt');
    } else {
        
        jwt.verify(req.cookies.uToken, keystone.get('jwt secret'), function(err, decoded) {
            if(err) {
                req.user = null;
                res.clearCookie('uToken');
                res.cookie('afterLoginUrl', req.originalUrl, { maxAge: 900000, httpOnly: true });
                req.flash('error', 'Please sign in to access this page.');
                res.redirect('/signinjwt'); 
            } else {
                 
                next();
            }
            
        });
    }
};
   ...

/routes/index.js


  ...

    app.all('/signinjwt', routes.views.session.signinjwt);
    app.get('/signoutjwt', routes.views.session.signoutjwt);
    app.all('/needauth*', middleware.jwtCheck);

  ...

/keystone.js

   ...

keystone.init({
...
'session': false,
...
});

   ...

@agmt5989
Copy link

I'm still wondering at #4880, how users fare who have more than one email saved. So far, they would need to supply both at login. Is there a way to make emails work as a relationship field, and still able to participate in authentication?

@alexander-mart
Copy link

@JedWatson This is will work for KeystoneJS 5?

@evextechnologiesinc
Copy link

@JedWatson This is will work for KeystoneJS 5?

Did you get the answer? I'm looking for ways to API Auth users in Keystone JS 5

@ioandev
Copy link

ioandev commented Oct 26, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment