I understand the flow of JWT and a single page application in terms of login and JWT issuance. However, if the JWT has a baked in expiry, AND the server isn't issuing a new JWT on each request, what is the best way for renewing? There is a concept of refresh tokens, but storing such a thing in a web browser sounds like a golden ticket.
IE I could easily go into a browsers local storage and steal a refresh token. Then I could go to another computer and issue myself a new token. I feel like there would need to be a server session in a db that's referenced in the JWT. Therefore the server could see if the session ID is still active or invalidated by a refresh token.
What are the secure ways to implement JWT in a SPA and handling new token issuance whilst the user is active?
– A legal JWT must be added to HTTP Header if Angular 11 Client accesses protected resources. – With the help of Http Interceptor, Angular App can check if the accessToken (JWT) is expired ( 401 ), sends /refreshToken request to receive new accessToken and use it for new resource request.
Let’s see how the Angular JWT Refresh Token example works with demo UI. – User signs in with a legal account first. – Now the user can access resources with provided Access Token. – When the Access Token is expired, Angular automatically sends Refresh Token request, receives new Access Token and uses it for new request.
To make that possible, the Angular frontend needs to refresh the token before it expires automatically. To make that possible, Angular and RxJs features are used in the frontend and a Spring Boot REST endpoint checks and updates the JWT in the backend. The frontend design is based on the concepts explained in this article.
– When the Access Token is expired, Angular automatically sends Refresh Token request, receives new Access Token and uses it for new request. So the server still accepts resource access from the user. – After a period of time, the new Access Token is expired again, and the Refresh Token too. Our App forces logout the user.
I can offer a different approach for refreshing the jwt token. I am using Angular with Satellizer and Spring Boot for the server side.
This is the code for the client side:
var app = angular.module('MyApp',[....]);
app.factory('jwtRefreshTokenInterceptor', ['$rootScope', '$q', '$timeout', '$injector', function($rootScope, $q, $timeout, $injector) {
const REQUEST_BUFFER_TIME = 10 * 1000; // 10 seconds
const SESSION_EXPIRY_TIME = 3600 * 1000; // 60 minutes
const REFRESH_TOKEN_URL = '/auth/refresh/';
var global_request_identifier = 0;
var requestInterceptor = {
request: function(config) {
var authService = $injector.get('$auth');
// No need to call the refresh_token api if we don't have a token.
if(config.url.indexOf(REFRESH_TOKEN_URL) == -1 && authService.isAuthenticated()) {
config.global_request_identifier = $rootScope.global_request_identifier = global_request_identifier;
var deferred = $q.defer();
if(!$rootScope.lastTokenUpdateTime) {
$rootScope.lastTokenUpdateTime = new Date();
}
if((new Date() - $rootScope.lastTokenUpdateTime) >= SESSION_EXPIRY_TIME - REQUEST_BUFFER_TIME) {
// We resolve immediately with 0, because the token is close to expiration.
// That's why we cannot afford a timer with REQUEST_BUFFER_TIME seconds delay.
deferred.resolve(0);
} else {
$timeout(function() {
// We update the token if we get to the last buffered request.
if($rootScope.global_request_identifier == config.global_request_identifier) {
deferred.resolve(REQUEST_BUFFER_TIME);
} else {
deferred.reject('This is not the last request in the queue!');
}
}, REQUEST_BUFFER_TIME);
}
var promise = deferred.promise;
promise.then(function(result){
$rootScope.lastTokenUpdateTime = new Date();
// we use $injector, because the $http creates a circular dependency.
var httpService = $injector.get('$http');
httpService.get(REFRESH_TOKEN_URL + result).success(function(data, status, headers, config) {
authService.setToken(data.token);
});
});
}
return config;
}
};
return requestInterceptor;
}]);
app.config(function($stateProvider, $urlRouterProvider, $httpProvider, $authProvider) {
.............
.............
$httpProvider.interceptors.push('jwtRefreshTokenInterceptor');
});
Let me explain what it does.
Let's say we want the "session timeout" (token expiry) to be 1 hour. The server creates the token with 1 hour expiration date. The code above creates a http inteceptor, that intercepts each request and sets a request identifier. Then we create a future promise that will be resolved in 2 cases:
1) If we create for example a 3 requests and in 10 seconds no other request are made, only the last request will trigger an token refresh GET request.
2) If we are "bombarded" with request so that there is no "last request", we check if we are close to the SESSION_EXPIRY_TIME in which case we start an immediate token refresh.
Last but not least, we resolve the promise with a parameter. This is the delta in seconds, so that when we create a new token in the server side, we should create it with the expiration time (60 minutes - 10 seconds). We subtract 10 seconds, because of the $timeout with 10 seconds delay.
The server side code looks something like this:
@RequestMapping(value = "auth/refresh/{delta}", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<?> refreshAuthenticationToken(HttpServletRequest request, @PathVariable("delta") Long delta, Device device) {
String authToken = request.getHeader(tokenHeader);
if(authToken != null && authToken.startsWith("Bearer ")) {
authToken = authToken.substring(7);
}
String username = jwtTokenUtil.getUsernameFromToken(authToken);
boolean isOk = true;
if(username == null) {
isOk = false;
} else {
final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
isOk = jwtTokenUtil.validateToken(authToken, userDetails);
}
if(!isOk) {
Map<String, String> errorMap = new HashMap<>();
errorMap.put("message", "You are not authorized");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorMap);
}
// renew the token
final String token = jwtTokenUtil.generateToken(username, device, delta);
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}
Hope that helps someone.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With