implementing refresh-tokens with angular and express-jwt implementing refresh-tokens with angular and express-jwt express express

implementing refresh-tokens with angular and express-jwt


I managed to implement this scenario.

What I've done...

On the server:

-Enable an API endpoint for signin. This endpoint will respond with the Json Web Token in the header. The client side has to catch it (with $http interceptors) and save it (I use local storage). The client will also manage the refreshed tokens sent by the server.

-On every request to the server configure a middleware in express to validate the token. At first I tried express-jwt module but jsonwebtoken was the right one for me.

For specific routes you may want to disable the middleware. In this case signin and signout.

var jwtCheck = auth.verifyJWT;jwtCheck.unless = unless;app.use('/api', jwtCheck.unless({path: [    '/api/auth/signin',    '/api/auth/signout']}));

-The middleware verifyJWT always responds with a token in the header. If the token needs to be refreshed a refreshed function is called.

jwtLib is my own library where the code lives to create, refresh and fetch jwt tokens.

function(req, res, next) {    var newToken,        token = jwtLib.fetch(req.headers);    if(token) {        jwt.verify(token, config.jwt.secret, {            secret: config.jwt.secret        }, function(err, decoded) {            if(err) {                return res.status(401).send({                    message: 'User token is not valid'                });            }            //Refresh: If the token needs to be refreshed gets the new refreshed token            newToken = jwtLib.refreshToken(decoded);            if(newToken) {                // Set the JWT refreshed token in http header                res.set('Authorization', 'Bearer ' + newToken);                next();            } else {                res.set('Authorization', 'Bearer ' + token);                next();            }        });    } else {        return res.status(401).send({            message: 'User token is not present'        });    }};

-The refresh function (jwtLib). As argument needs a decoded token, see above that jsonwebtoken resolve a decoded when call to jwt.verify().

If you create during signin a token with an expiration of 4 hours and have a refresh expiration of 1 h (1 * 60 * 60 = 3600 secs) that means that the token will be refreshed if the user has been inactive for 3 hours or more, but not for more than 4 hours, because the verify process would fail in this case (1 hour window of refreshing). This avoids generating a new token on each request, only if the token will expire in this time window.

module.exports.refreshToken = function(decoded) {    var token_exp,        now,        newToken;    token_exp = decoded.exp;    now = moment().unix().valueOf();    if((token_exp - now) < config.jwt.TOKEN_REFRESH_EXPIRATION) {        newToken = this.createToken(decoded.user);        if(newToken) {            return newToken;        }    } else {       return null;    }};

On the client (Angularjs):

-Enable a client side for login. This calls the server endpoint. I use Http Basic Authentication encoded with base64.You can use base64 angular module to encode the email:passwordNote that on success I do not store the token on the localStorage or Cookie. This will be managed by the http Interceptor.

//Base64 encode Basic Authorization (email:password)$http.defaults.headers.common.Authorization = 'Basic ' + base64.encode(credentials.email + ':' + credentials.password);return $http.post('/api/auth/signin', {skipAuthorization: true});

-Configure the http interceptors to send the token to the server on every request and store the token on the response. If a refreshed token is received this one must be stored.

// Config HTTP Interceptorsangular.module('auth').config(['$httpProvider',    function($httpProvider) {        // Set the httpProvider interceptor        $httpProvider.interceptors.push(['$q', '$location', 'localStorageService', 'jwtHelper', '$injector',            function($q, $location, localStorageService, jwtHelper, $injector) {                return {                    request: function(config) {                        var token = localStorageService.get('authToken');                        config.headers = config.headers || {};                        if (token && !jwtHelper.isTokenExpired(token)) {                            config.headers.Authorization = 'Bearer ' + token;                        }                        return config;                    },                    requestError: function(rejection) {                        return $q.reject(rejection);                    },                    response: function(response) {                        //JWT Token: If the token is a valid JWT token, new or refreshed, save it in the localStorage                        var Authentication = $injector.get('Authentication'),                            storagedToken = localStorageService.get('authToken'),                            receivedToken = response.headers('Authorization');                        if(receivedToken) {                            receivedToken = Authentication.fetchJwt(receivedToken);                        }                        if(receivedToken && !jwtHelper.isTokenExpired(receivedToken) && (storagedToken !== receivedToken)) {                            //Save Auth token to local storage                            localStorageService.set('authToken', receivedToken);                        }                        return response;                    },                    responseError: function(rejection) {                        var Authentication = $injector.get('Authentication');                        switch (rejection.status) {                            case 401:                                // Deauthenticate the global user                                Authentication.signout();                                break;                            case 403:                                // Add unauthorized behaviour                                break;                        }                        return $q.reject(rejection);                    }                };            }        ]);    }]);