Basic strategy for delegate refresh token to get new JWT - angularjs

I have been doing pretty well implementing Angular SPA and JWT, but I always have a hard time with delegating for a new token.
My basic strategy has been:
In auth interceptor get Auth Error = > Delegate with refresh token, replace JWT, else logout
Which did not work because multiple Async calls would fire and one would get the delegate function, but then the refresh token would have been used for the second one and that one will fail, then the user would get logged out.
Before anything else: Check token expiration, if expired => delegate with refresh token, replace jwt, else logout
Which had a similar issue where the first call would notice it was expired, and go get the new token, but since it is Async, the rest of the calls would fire and fail etc.
What is the basic strategy here. I feel like the very first thing the app should do is check the JWT and delegate for a new one if it's a bad token, but in that case it should no be asynchronous. Do I just not delete the refresh token on use?
Any help would be great, I feel like this is the last major hole in my understanding. Thanks!

Try using Witold Szczerba's "http interceptor".
In a nutshell the first failed http call triggers and event and subsequent calls get pushed into an array. Upon the event firing you have a chance to do some logic and then replay the failed calls.
That said you probably should just do a token rotation before needing to actually use the refresh token. For example consider this code which could be added to this function
.config(function($httpProvider) {
$httpProvider.interceptors.push(function(moment, $rootScope, $q, httpBuffer) {
return {
request: function (config) {
if ($rootScope.authToken) {
config.headers["Authentication"] = 'Bearer ' + $rootScope.authToken;
var payload = angular.fromJson(atob($rootScope.authToken.split('.')[1]));
var utcNow = moment.utc().valueOf();
// 3600000 ms = 1 hr
if(utcNow > payload.iat + 3600000){
$rootScope.$broadcast('auth:rotateToken', $rootScope.authToken);
}
}
return config;
},
//responseError: ...see Witold's code...
});
})

Related

How to refresh authentication token in Axios with parallel responses?

I want to implement JWT authentication in a react app using Axios.
There are many solutions using axios.interceptors that fetch the token again if the request failed due to the authentication error.
Example:
axios.interceptors.response.use(function(response) {
return response;
},function(error) {
const originalReq = error.config;
if ( error.response.status == 401 &&
!originalReq._retry &&
error.response.config.url != URL_USER_AUTHENTICATE ) {
originalReq._retry = true;
return axios.post(BASE_URL+URL_REFRESH_TOKEN,{})
.then((res) =>{
if ( res.data.status == "success") {
return axios(originalReq);
}
}).catch((error) => {window.location.href="/logout/nosession"});
}
return Promise.reject(error);
});
However, it doesn't work if we have parallel requests. Imagine that we have two parallel requests, so both will request the new authentication token.
Does anybody know any fix?
My example React SPA has some code that manages this, and may give you some ideas for your own solution. The third class below uses a technique of handling a list of promises but only making the remote call on one of them, then returning the same result to all callers.
API 401 logic
Refresh trigger
Concurrency handler
An alternatiive solution can be to do a silent refresh in a background timer when the token is close to expiry, based on the expires_in field returned with the access token. But that is not fully reliable since 401s can occur for multiple reasons, eg due to some types of infrastructure or server key changes, so I have always written 401 handling in clients, to ensure reliability.

BrowserAuthError: interaction_in_progress: Interaction is currently in progress with azure/msal-browser#2.11.2

I has this error when trying to loginRedirect in React app using #azure/msal-react#1.0.0-alpha.6 and #azure/msal-browser#2.11.2. The login data returns correctly but the exception is raised in the console.
Uncaught (in promise) BrowserAuthError: interaction_in_progress:
Interaction is currently in progress. Please ensure that this
interaction has been completed before calling an interactive API.
import * as msal from "#azure/msal-browser";
const msalConfig = {
auth: {
clientId: '995e81d0-',
authority: 'https://login.microsoftonline.com/3a0cf09b-',
redirectUri: 'http://localhost:3000/callback'
},
cache: {
cacheLocation: "sessionStorage", // This configures where your cache will be stored
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
}
};
const msalInstance = new msal.PublicClientApplication(msalConfig);
try {
msalInstance.handleRedirectPromise()
.then(res=>{
console.log(res)
})
.catch(err => {
console.error(err);
});
var loginRequest = {
scopes: ["api://58ca819e-/access_as_user"] // optional Array<string>
};
msalInstance.loginRedirect(loginRequest);
} catch (err) {
// handle error
console.log(err)
}
The exception
Uncaught (in promise) BrowserAuthError: interaction_in_progress: Interaction is currently in progress. Please ensure that this interaction has been completed before calling an interactive API.
at BrowserAuthError.AuthError [as constructor] (http://localhost:3000/static/js/vendors~main.chunk.js:852:20)
at new BrowserAuthError (http://localhost:3000/static/js/vendors~main.chunk.js:8943:24)
at Function.BrowserAuthError.createInteractionInProgressError (http://localhost:3000/static/js/vendors~main.chunk.js:9023:12)
at PublicClientApplication.ClientApplication.preflightInteractiveRequest (http://localhost:3000/static/js/vendors~main.chunk.js:13430:30)
at PublicClientApplication.<anonymous> (http://localhost:3000/static/js/vendors~main.chunk.js:12581:33)
at step (http://localhost:3000/static/js/vendors~main.chunk.js:215:17)
at Object.next (http://localhost:3000/static/js/vendors~main.chunk.js:146:14)
at http://localhost:3000/static/js/vendors~main.chunk.js:118:67
at new Promise (<anonymous>)
at __awaiter (http://localhost:3000/static/js/vendors~main.chunk.js:97:10)
at PublicClientApplication.ClientApplication.acquireTokenRedirect (http://localhost:3000/static/js/vendors~main.chunk.js:12565:12)
at PublicClientApplication.<anonymous> (http://localhost:3000/static/js/vendors~main.chunk.js:13760:16)
at step (http://localhost:3000/static/js/vendors~main.chunk.js:215:17)
at Object.next (http://localhost:3000/static/js/vendors~main.chunk.js:146:14)
at http://localhost:3000/static/js/vendors~main.chunk.js:118:67
at new Promise (<anonymous>)
at __awaiter (http://localhost:3000/static/js/vendors~main.chunk.js:97:10)
at PublicClientApplication.loginRedirect (http://localhost:3000/static/js/vendors~main.chunk.js:13755:12)
at Module.<anonymous> (http://localhost:3000/static/js/main.chunk.js:192:16)
at Module../src/App.tsx (http://localhost:3000/static/js/main.chunk.js:292:30)
at __webpack_require__ (http://localhost:3000/static/js/bundle.js:857:31)
at fn (http://localhost:3000/static/js/bundle.js:151:20)
at Module.<anonymous> (http://localhost:3000/static/js/main.chunk.js:2925:62)
at Module../src/index.tsx (http://localhost:3000/static/js/main.chunk.js:3028:30)
at __webpack_require__ (http://localhost:3000/static/js/bundle.js:857:31)
at fn (http://localhost:3000/static/js/bundle.js:151:20)
at Object.1 (http://localhost:3000/static/js/main.chunk.js:3570:18)
at __webpack_require__ (http://localhost:3000/static/js/bundle.js:857:31)
at checkDeferredModules (http://localhost:3000/static/js/bundle.js:46:23)
at Array.webpackJsonpCallback [as push] (http://localhost:3000/static/js/bundle.js:33:19)
at http://localhost:3000/static/js/main.chunk.js:1:67
msalInstance.loginRedirect(loginRequest);
The piece of code above does next:
Looks into session storage for key msal.[clientId].interaction.status and other temp values required for redirection process. If such key exist and its value equals 'interaction_in_progress' error will be thrown.
Creates entry in session storage msal.[clientId].interaction.status = interaction.status
Redirects user to auth-page.
In case of successful login user will be redirected to initial page with your code and go through 1-3 steps and will catch an error;
The piece of code below removes all temp values in session storage and completes auth redirection flow but it is async and never will be completed.
msalInstance.handleRedirectPromise()
.then(res=>{
console.log(res)
})
.catch(err => {
console.error(err);
});
The solution will be
// Account selection logic is app dependent. Adjust as needed for different use cases.
// Set active acccount on page load
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}
msalInstance.addEventCallback((event) => {
// set active account after redirect
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload.account) {
const account = event.payload.account;
msalInstance.setActiveAccount(account);
}
}, error=>{
console.log('error', error);
});
console.log('get active account', msalInstance.getActiveAccount());
// handle auth redired/do all initial setup for msal
msalInstance.handleRedirectPromise().then(authResult=>{
// Check if user signed in
const account = msalInstance.getActiveAccount();
if(!account){
// redirect anonymous user to login page
msalInstance.loginRedirect();
}
}).catch(err=>{
// TODO: Handle errors
console.log(err);
});
I believe this is the correct answer and way to set this up. Others here led me to clues to solve this.
TLDR; set your code up like this:
// authRedir.ts (or authRedir.vue inside mounted())
await msalInstance.handleRedirectPromise();
// mySignInPage.ts (or userprofile.vue, or whatever page invokes a sign-in)
await msalInstance.handleRedirectPromise();
async signIn(){
const loginRequest: msal.RedirectRequest = {
scopes: ["openid", "profile", "offline_access","your_other_scopes"]
redirectUri: "http://localhost:8080/authredirect"
};
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
await msalInstance.loginRedirect();
}
}
If you do this correctly, you wont need the code #shevchenko-vladislav shared, wherein setActiveAccount() has to be manually done by you. Remember to verify all async/await wherever you call this in your app! And notice how I did NOT use handleRedirectPromise().then() or anything, really, in my main authredirect.vue file. Just handleRedirectPromise() on load.
Other solutions on Stackoverflow suggest things like checking for and deleting the interaction state from the session. Um, no! If you have that state left over after a sign-in, it means the process wasn't done right! MSAL cleans itself up!
Full details:
It is super important to understand what MSAL is actually doing during it's entire lifecycle (especially the redir path as opposed to popup), and sadly the docs fail to do a good job. I found this little "side note" extremely, extremely important:
https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/errors.md#interaction_in_progress
"If you are calling loginRedirect or acquireTokenRedirect from a page
that is not your redirectUri you will need to ensure
handleRedirectPromise is called and awaited on both the redirectUri
page as well as the page that you initiated the redirect from. This is
because the redirectUri page will initiate a redirect back to the page
that originally invoked loginRedirect and that page will process the
token response."
In other words, BOTH your Redirect page AND the page that INVOKED the sign-in request MUST call handleRedirectPromise() on page load (or on mounted(), in my case, since I am using Vue)
In my case, I have this:
http://localhost:8080/authredirect *
http://localhost:8080/userprofile
*Only my AuthRedirect Uri needs to be registered as a RedirectUri with my app registration in Azure AD.
So here is the loginRedirect() lifecycle, which I had NO idea, and lost a days work sorting out:
/UserProfile (or some page) invokes a sign-in request
The request calls handleRedirectPromise() (which sets MSAL up with info about where the request was made AND the interaction state that will bite you later if you dont complete the process)
and THEN calls loginRedirect(loginRequest)
-> user is redirected, completes sign-in
Azure redir back to -> /AuthRedirect
/AuthRedirect invokes handleRedirectPromise(), which forwards along to -> /UserProfile
/UserProfile invokes handleRedirectPromise() which does the actual processing of tokens AND internally calls setActiveAccount() to save your user to session.
Dang. That was fun. And not explained in the docs AT ALL.
So, the reason you are getting the interaction-in-progress error is because you are thinking you're all done on step 6. NOPE! Step 7 is where that interaction_in_progress state gets settled and cleaned up so that subsequent calls wont trip up on it!!
Final thought:
If you have a designated sign-in page you want users to always start/finish from (and itself is the registered redirect Uri), I suppose these steps will be reduced (no forwarding like in step 6 here). In my case, I want the user redirected back to wherever they might have gotten bumped out of due to a session expiration. So I just found it easier to call handleRedirectPromise() on every single page load everywhere, in case said page it needs to finalize authentication. Or, I could build my own redirect logic into a dedicated sign-in page that can put the user back where they were prior to hitting it. It's just that as for MSAL, I had NO idea the process was finishing up on the requesting page rather than contained within my AuthRedirect page, and THAT is what bit me.
Now, if we could just get MS to provide far better docs on the delicate and critical nature of MSAL, and to provide a Vue plugin (why does only Angular and React get all the glory? :) ), that would be great!
During development, it is possible that you left the sign-in flow in a progress-state due to a coding issue that you will need to correct. You can clear the immediate problem by deleting the msal.interaction.status cookie from the browser. Of course, if this problem persists, then you need to correct the problem using one of the other solutions suggested on this page.
You can clear the browser storage before open the loginPopup:
let msalInstance: PublicClientApplication = this._msauthService.instance as PublicClientApplication;
msalInstance["browserStorage"].clear();
I have found that in msal.js v2 you can check interaction status in vanilla .js code to see if there is an interaction in progress, should you need to do this for some reason:
const publicClientApplication = new window.msal.PublicClientApplication(msalConfig);
var clientString = "msal." + msalConfig.clientId + ".interaction.status";
var interaction-status = publicClientApplication.browserStorage.temporaryCacheStorage.windowStorage[clientString]
Update #azure/msal-browser#2.21.0.
For folks with an Azure/Active Directory situation:
My issue wasn't with my code. It was with deactivating the "Access tokens (used for implicit flows)" setting found in the Active Directory > Authentication > Implicit grant and hybrid flows section.
After you put the proper Redirect URIs into the Web section:
ex: https://example.com/.auth/login/aad/callback
And after you put the proper Redirect URIs into the Single-page application section:
ex: https://example.com
ex: https://localhost:4200
The last step is to make sure you disable the Access tokens I mentioned in the beginning:
When I was migrating my apps from .NET5 to .NET6 and the prior Angular Authentication over to MSAL, this setting was already checked for me (both were checked). After unchecking this setting, everything ended up working.
This may not be a clean solution. But this does work at least in Vue.js.
Next to your acquireToken() logic, add this
// Check Local or Session storage which may have already contain key
// that partially matches your Azure AD Client ID
let haveKeys = Object.keys(localStorage).toString().includes('clientId')
// That error will just go away when you refrest just once
let justOnce = localStorage.getItem("justOnce");
if (haveKeys && !justOnce) {
localStorage.setItem("justOnce", "true");
window.location.reload();
} else {
localStorage.removeItem("justOnce")
}
I have faced the similar error in my project.I took reference of the below link. It takes hardly 10 minutes to go through it. It will surely resolve if you face the scenarios given in it.
Link:
https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/redirects.md

CompactToken validation failed 80049228

Some users are getting this error back when trying to sign in using Microsoft Sign In in order to access mail via MS Graph. I've had both corporate users and personal (Hotmail.com) users both showing this error number but it works fine for most users.
This is the call:
https://login.microsoftonline.com/common/oauth2/v2.0/token
This is the error returned:
Code: InvalidAuthenticationToken
Message: CompactToken validation failed with reason code: 80049228
Any pointers? Where can I find a reference to this error number?
This means the token expired and it need to be refreshed. If you want to refresh it without user interaction you'll need a refresh_token which is returned when you obtain a token initially.
Here is how you can refresh it:
function refreshTokenIfNeeded(tokenObj){
let accessToken = oauth2.accessToken.create(tokenObj);
const EXPIRATION_WINDOW_IN_SECONDS = 300;
const { token } = accessToken;
const expirationTimeInSeconds = token.expires_at.getTime() / 1000;
const expirationWindowStart = expirationTimeInSeconds - EXPIRATION_WINDOW_IN_SECONDS;
const nowInSeconds = (new Date()).getTime() / 1000;
const shouldRefresh = nowInSeconds >= expirationWindowStart;
let promise = Promise.resolve(accessToken)
if (shouldRefresh) {
console.log("outlook365: token expired, refreshing...")
promise = accessToken.refresh()
}
return promise
}
Where tokenObj is the token object you store in your database.
Make sure it also has expires_at or otherwise oauth2.accessToken.create() will create it and calculate from the current moment in time.
More details can be found in this tutorial and in this github repo (this is where the code above was taken from)
Found a Solution To This
In my case, I was refreshing the token before using the access_token with Microsoft Graph API even once.
Once you successfully call https://login.microsoftonline.com/common/oauth2/v2.0/token You will get a refresh_token and an access_token, my guess is that you have been refreshing the token before using the first access token from the URL mentioned above.
Steps to Fix:
Call https://login.microsoftonline.com/common/oauth2/v2.0/token as you did before
Copy the access_token from the response and use it at least once with your Microsoft Graph API
Now you can copy the refresh_token (or once the access_token is expired) and exchange for a new access token
Enjoy your API integration
Smile :)
Reference:
Microsoft Authentication (Tokens) Docs - Including Refresh Token
OneDrive Refresh Token Answer

Django Rest Framework JWT: How to change the token expiration time when logged in

I'm using Django REST framework JWT Auth for session creation and permissions, the only problem is: when I log in and after the token expires I can't continue doing the operation I want, unless I log in again. And I didn't fully understand the documentations provided for the additional settings.
So can any one explain a method for dynamically creating (and refreshing) my token (following best practices) so that I can keep doing operations when I'm logged in.
P.S: I'm using angular 2 for my front end, and I'm inserting the token in the Http requests headers. Thanks.
JWT token refresh is a little confusing, and i hope this explanation helps.
tokens have an issued at time (iat in the token)
tokens have an expiration date (now() + 1 hour, for example)
the token can't be changed. server can only issue a new one
iat never changes, but expires does change with each refresh
When you want to extend a token, this is what happens:
You send your token to the server endpoint /.../refresh/
Server checks its not expired: now() <= token.iat + JWT_REFRESH_EXPIRATION_DELTA
If not expired:
Issue a NEW token (returned in the json body, same as login)
New Token is valid for now() + JWT_EXPIRATION_DELTA
The issued at value in the token does not change
App now has 2 tokens (technically).
App discards the old token and starts sending the new one
If expired: return error message and 400 status
Example
You have EXPIRATION=1 hour, and a REFRESH_DELTA=2 days. When you login you get a token that says "created-at: Jun-02-6pm". You can refresh this token (or any created from it by refreshing) for 2 days. This means, for this login, the longest you can use a token without re-logging-in, is 2 days and 1 hour. You could refresh it every 1 second, but after 2 days exactly the server would stop allowing the refresh, leaving you with a final token valid for 1 hour. (head hurts).
Settings
You have to enable this feature in the backend in the JWT_AUTH settings in your django settings file. I believe that it is off by default. Here are the settings I use:
JWT_AUTH = {
# how long the original token is valid for
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=2),
# allow refreshing of tokens
'JWT_ALLOW_REFRESH': True,
# this is the maximum time AFTER the token was issued that
# it can be refreshed. exprired tokens can't be refreshed.
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
}
Then you can call the JWT refresh view, passing in your token in the body (as json) and getting back a new token. Details are in the docs at http://getblimp.github.io/django-rest-framework-jwt/#refresh-token
$ http post localhost:8000/auth/jwt/refresh/ --json token=$TOKEN
Which returns:
HTTP 200
{
"token": "new jwt token value"
}
I've had same problem in angularjs and I've solved it by writing a custom interceptor service for my authentication headers.
Here's my code:
function($http, $q, store, jwtHelper) {
let cache = {};
return {
getHeader() {
if (cache.access_token && !jwtHelper.isTokenExpired(cache.access_token)) {
return $q.when({ 'Authorization': 'Token ' + cache.access_token });
} else {
cache.access_token = store.get('token');
if (cache.access_token && !jwtHelper.isTokenExpired(cache.access_token)) {
return $q.when({ 'Authorization': 'Token ' + cache.access_token });
} else {
return $http.post(localhost + 'api-token-refresh/',{'token': cache.access_token})
.then(response => {
store.set('token', response.data.token);
cache.access_token = response.data.token;
console.log('access_token', cache.access_token);
return {'Authorization': 'Token ' + cache.access_token};
},
err => {
console.log('Error Refreshing token ',err);
}
);
}
}
}
};
}
Here, on every request I've had to send, the function checks whether the token is expired or not.
If its expired, then a post request is sent to the "api-token-refresh" in order to retrieve the new refreshed token, prior to the current request.
If not, the nothing's changed.
But, you have to explicitly call the function getHeader() prior to the request to avoid circular dependency problem.
This chain of requests can be written into a function like this,
someResource() {
return someService.getHeader().then(authHeader => {
return $http.get(someUrl, {headers: authHeader});
});
}
just add this line to your JWT_AUTH in settings.py file:
'JWT_VERIFY_EXPIRATION': False,
it worked for me.

AngularJS or SPA with JWT - expiry and refresh

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?
Renewing the token every 15 minutes (if it lives for 30) works if you don't have another restriction in your server in which you need to check for 1 hour inactivity to log the user out. If you just want this short lived JWT and keep on updating it, it'd work.
I think one of the big advantages of using JWT is to actually NOT need a server session and therefore not use the JTI. That way, you don't need syncing at all so that'd be the approach I'd recommend you following.
If you want to forcibly logout the user if he's inactive, just set a JWT with an expiration in one hour. Have a $interval which every ~50 minutes it automatically gets a new JWT based on the old one IF there was at least one operation done in the last 50 minutes (You could have a request interceptor that just counts requests to check if he's active) and that's it.
That way you don't have to save JTI in DB, you don't have to have a server session and it's not a much worse approach than the other one.
What do you think?
I think for my implementation I'm going to go with, after a bit of search, is...
Use case:
JWT is only valid for 15 minutes
User session will timeout after 1 hour of inactivity
Flow:
User logs in and is issued a JWT
JWT has a 15 minute expiration with claim 'exp'
JWT JTI is recorded in db has a session of 1 hour
After a JWT expires (after 15 min):
Current expired JWT will be used # a /refresh URI to exchange for a new one. The expired JWT will only work at the refresh endpoint. IE API calls will not accept an expired JWT. Also the refresh endpoint will not accept unexpired JWT's.
JTI will be checked to see if its been revoked
JTI will be checked to see if its still within 1 hour
JTI session will be deleted from DB
New JWT will be issued and new JTI entry will be added to the db
If a user logs out:
JWT is deleted from client
JTI is deleted from db so JWT cannot be refreshed
With that said, there will be database calls every 15 minutes to check a JTI is valid. The sliding session will be extended on the DB that tracks the JWT's JTI. If the JTI is expired then the entry is removed thus forcing the user to reauth.
This does expose a vulnerability that a token is active for 15 minutes. However, without tracking state every API request I'm not sure how else to do it.
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.

Resources