Tokens, Sessions and Users, oh my!

A strategy for unifying the Loopback token scheme to support access to the loopback generated REST API as well as site content.

  • We want to use site wide BasicAuth to protect the development environment from unauthorized access. Unfortunately the Loopback token scheme conflicts with http basicAuth http headers.
  • We need to have a generalized scheme for determining the current user context throughout the app. It's not good idea to install heavy weight middleware globally because many requests don't need it — only use on it routes that require the functionality so you don't burden every request.
  • Generalize a scheme for enforcing login for certain routes in middleware.

If you are unfamiliar with middleware you can get an overview here from the horse's mouth.


Initialize Loopback Token Middleware to use a cookie

Set up the loopback token gear overriding the default token names to eliminate conflict with BasicAuth. BasicAuth protocol uses the "Authorization" http header which loopback also uses by default so but thankfully this can be overridden by specifying the cookie name.

in server/server.js

// use loopback.context on all routes
app.use(loopback.context());

// use loopback.token middleware on all routes
// setup gear for authentication using cookie (access_token)
// Note: requires cookie-parser (defined in middleware.json)
app.use(loopback.token({  
  model: app.models.accessToken,
  currentUserLiteral: 'me',
  searchDefaultTokenKeys: false,
  cookies: ['access_token'],
  headers: ['access_token', 'X-Access-Token'],
  params: ['access_token']
}));

Documentation for loopback.token can be found here.


Set the signed access_token cookie after login

This cookie will be used by the loopback.token middleware to determine if the request is authenticated for /api/ endpoints and all routes requiring a valid user session.

Attach a remote hook to the user model which is called upon successful call to /api/users/login.

common/models/MyUser.js

// on login set access_token cookie with same ttl as loopback's accessToken
MyUser.afterRemote('login', function setLoginCookie(context, accessToken, next) {  
    var res = context.res;
    var req = context.req;
    if (accessToken != null) {
        if (accessToken.id != null) {
            res.cookie('access_token', accessToken.id, {
                signed: req.signedCookies ? true : false,
                maxAge: 1000 * accessToken.ttl
            });
            return res.redirect('/');
        }
    }
    return next();
});

Middleware for routes that need to know the logged in user.

Set up a place to store the context on the request (strongloop has deprecated loopbackContext so we roll our own utility for this and attach it to the request object)

middleware/context-myContext.js

Install the request context middleware in the app in server/server.js

var myContext = require('./middleware/context-myContext')();  
app.use(myContext);  

This middleware takes over where the Loopback token middleware leaves off finding the current user and saving it on the request so it can be accessed elsewhere within the app.

middleware/context-currentUser.js

var loopback = require('loopback');

module.exports = function () {  
    return function contextCurrentUser(req, res, next) {
        if (!req.accessToken) {
            return next();
        }

        req.app.models.MyUser.findById(req.accessToken.userId, function (err, user) {

            if (err) {
                return next(err);
            }

            if (!user) {
                //user not found for accessToken, which is odd.
                return next();
            }

            req.app.models.Role.getRoles({
                principalType: req.app.models.RoleMapping.USER,
                principalId: user.id
            }, function (err, roles) {

                var reqContext = req.getCurrentContext();
                reqContext.set('currentUser', user);
                reqContext.set('ip', req.ip);
                reqContext.set('currentUserRoles', roles);
                next();
            });
        });
    };
};

What about the loopback API routes?

Because we are going to use contextCurrentUser selectively on needed routes we need to Install middleware that is conditional for all routes under /api/

middleware/context-currentUserApi.js

var getCurrentUser = require('./context-currentUser')();

module.exports = function () {  
    return function contextCurrentUserApi(req, res, next) {
        if (req.path.match(/^\/api\//)) {
            getCurrentUser(req, res, next);
        }
        else {
            next();
        }
    };
};

We then install this middleware globally.

server/server.js

// put currentUser in loopback.context on /api routes
var getCurrentUserApi = require('./middleware/context-currentUserApi')();  
app.use(getCurrentUserApi);  

Now all the endpoints in /api/ have access to the currentUser through reqContext.


Enforcing login on routes

If the user is not logged in redirect the request to home or some other page.

middleware/context-ensureLoggedIn.js

var loopback = require('loopback');

module.exports = function () {  
    return function ensureLoggedIn(req, res, next) {
        var reqContext = req.getCurrentContext();
        if (!reqContext.get('currentUser')) {
            res.redirect('/');
        }
        else {
            next();
        }
    };
};

Defining a route that needs a logged in user

Use our middleware to get the current user and enforce login.

var setCurrentUser = require('../middleware/context-currentUser')();  
var ensureLoggedIn = require('../middleware/ensureLoggedIn')();

module.exports = function (server) {  
    var router = server.loopback.Router();

    router.get('/is-logged-in',setCurrentUser, ensureLoggedIn, function (req, res, next) {
        var ctx = server.req.getCurrentContext();
        var currentUser = ctx.get('currentUser');

        // we now have the current user and can process the request
        req.send({"currentUser":currentUser});
    });

    server.use(router);
};

If the user is optional for the route you can leave out the ensureLoggedIn middleware.


Protecting your development environment with basicAuth

Install middleware to perform http authorization for pages in the development environment but not the loopback API. The username and password are defined in the environment.

middleware/basicAuth.js

var basicAuth = require('basic-auth');

module.exports = function () {  
    return function basicAuthMiddleware(req, res, next) {

        if (!req.url.match(/\/(api|explorer)\//)) {
            var user = basicAuth(req);

            if (!user || user.name !== process.env.BASIC_AUTH_USER_NAME || user.pass !== process.env.BASIC_AUTH_PASSWORD) {
                res.set('WWW-Authenticate', 'Basic realm=Authorization Required');
                return res.sendStatus(401);
            }

            return next();
        }

        next();
    };
};

Include the middleware based on the running environment.

server/server.js

if (app.get('env') === 'development') {  
  var basicAuth = require('./middleware/basicAuth')();
  app.use(basicAuth);
}

Photo: West village piers NYC late '80s (Keith Haring?)
Document version 1.1