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
But I'm honestly not sure, please help
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);
}
};
}
]);
}
]);