implementing refresh-tokens with angular and express-jwt
Asked Answered
D

1

8

I want to implement the Sliding expiration concept with json web tokens using angular, nodejs and express-jwt. I'm a little confused on how to do this, and am struggling to find any example of refresh tokens or and other material relating to sessions with these technologies/frameworks.

A few options I was thinking of were

  • Generating a new token with each request after the initial login
  • Keeping track of issued token on the server side along

But I'm honestly not sure, please help

Desecrate answered 27/10, 2014 at 2:13 Comment(2)
I have basically the same question, and posted my first-pass approach at: #27409262Shoreline
you can use gist.github.com/Mirodil/952e5932c284a2d205dbInterleaf
C
14

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:password Note 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 Interceptors
angular.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);
                    }
                };
            }
        ]);
    }
]);
Charleen answered 17/12, 2014 at 18:14 Comment(4)
In this solution it seems like you're just issuing a new JWT if the old one has expired. This breaks the whole security of a JWT not being able to be used once expired, not even to refresh and issue a new token. The server really should have issued another token called the refresh token, which it would save into a database and send that to the client. The refresh token would be the one used to obtain a new JWT access token once the previous one has expired.Hegumen
refreshToken() is only called if the user is authenticated, so there is no a security issue. refreshToken() generates a new token if the expiration is in the time window of TOKEN_REFRESH_EXPIRATION, otherwise returns null. Anyway this piece of code was written some time ago. It's still valid but maybe some other approach could be better.Charleen
@Charleen when somebody stole ur token, hacker will be authenticated and can refresh w/o limitPyuria
@Pyuria a refresh token would be just as easy to steal if it's handled by the client therefore provides no additional protection. My understanding of refresh tokens is that they give you the ability to log someone out after a period of time (say 1 day, while the jwt is refreshed more regularly) and the ability to force someone to log back in. i.e. you mark the refresh token as invalid in the db. stephen is right though. We should go back to the db to check the user is still allowed access each time the JWT expires. Otherwise might as well just have one long-lived JWT which is dangerousSogdiana

© 2022 - 2024 — McMap. All rights reserved.