Skip to content

Instantly share code, notes, and snippets.

@JedWatson
Last active April 16, 2023 02:11
Show Gist options
  • Star 54 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • 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);
@jorisw
Copy link

jorisw commented May 27, 2015

There's a closing curly brace missing for the signout method.

@bmulyawan
Copy link

in term of security.. for session based isn't it better with token key as userId never get changed? and

 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.'
      });

just wondering why not success:false ?

@MikelArnaiz
Copy link

With a correct username and wrong password you get a 200 code. I changed the keystone.session.signin error callback to:

return res.status(401).json({ 'error': 'wrong credentials' });

@bradwbradw
Copy link

You can use a hack-ish method to determine if a user is logged in from the same origin, that doesn't require additional code.

This doesn't help with signing in or registering, but it is can determine if someone is logged in already.

Just send a GET request to the built-in keystone api: domain.com/keystone/api/listname

you can use any list for "listname". If it comes back with actual json data, then you're logged in. If it comes back with the html of the sign in page, then you aren't logged in!

@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