How to refresh authentication token in Axios with parallel responses? - reactjs

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.

Related

How to tell if a user is logged in with http only cookies and JWT in react (client-side)

So I'm trying to follow the security best practices and I'm sending my JWT token over my React app in a only-secure http-only cookie.
This works fine for requests but the major issue I find with this approach is, how can I tell if the user is logged-in on client-side if I can't check if the token exists? The only way I can think of is to make a simple http to a protected endpoint that just returns 200.
Any ideas? (not looking for code implementations)
The approach I would follow is to just assume the user is logged in, and make the desired request, which will send the httpOnly token automatically in the request headers.
The server side should then respond with 401 if the token is not present in the request, and you can then react in the client side accordingly.
Using an endpoint like /api/users/me
Server-side
Probably you don't only need to know if a user is already logged in but also who that user is. Therefore many APIs implement an endpoint like /api/users/me which authenticates the request via the sent cookie or authorization header (or however you've implemented your server to authenticate requests).
Then, if the request is successfully authenticated, it returns the current user. If the authentication fails, return a 401 Not Authorized (see Wikipedia for status codes).
The implementation could look like this:
// UsersController.ts
// [...]
initializeRoutes() {
this.router.get('users/me', verifyAuthorization(UserRole.User), this.getMe);
}
async getMe(req: Request, res: Response) {
// an AuthorizedRequest has the already verified JWT token added to it
const { id } = (req as AuthorizedRequest).token;
const user = await UserService.getUserById(id);
if (!user) {
throw new HttpError(404, 'user not found');
}
logger.info(`found user <${user.email}>`);
res.json(user);
}
// [...]
// AuthorizationMiddleware.ts
export function verifyAuthorization(expectedRole: UserRole) {
// the authorization middleware throws a 401 in case the JWT is invalid
return async function (req: Request, res: Response, next: NextFunction) {
const authorization = req.headers.authorization;
if (!authorization?.startsWith('Bearer ')) {
logger.error(`no authorization header found`);
throw new HttpError(401, 'unauthorized');
}
const token = authorization.split(' ')[1];
const decoded = AuthenticationService.verifyLoginToken(token);
if (!decoded) {
logger.warn(`token not verified`);
throw new HttpError(401, 'unauthorized');
}
(req as AuthorizedRequest).token = decoded;
const currentRole = UserRole[decoded.role] ?? 0;
if (currentRole < expectedRole) {
logger.warn(`user not authorized: ${UserRole[currentRole]} < ${UserRole[expectedRole]}`);
throw new HttpError(403, 'unauthorized');
}
logger.debug(`user authorized: ${UserRole[currentRole]} >= ${UserRole[expectedRole]}`);
next();
};
}
Client-side
If the response code is 200 OK and contains the user data, store this data in-memory (or as alternative in the local storage, if it doesn't include sensitive information).
If the request fails, redirect to the login page (or however you want your application to behave in that case).

Firebase - Best Practice For Server Firestore Reads For Server-Side Rendering

I have a server-side-rendered reactjs app using firebase firestore.
I have an area of my site that server-side-renders content that needs to be retrieved from firestore.
Currently, I am using firestore rules to allow anyone to read data from these particular docs
What worries me is that some bad person could setup a script to just continuously hit my database with reads and rack up my bills (since we are charged on a per-read basis, it seems that it's never wise to allow anyone to perform reads.)
Current Rule
// Allow anonymous users to read feeds
match /landingPageFeeds/{pageId}/feeds/newsFeed {
allow read: if true;
}
Best Way Forward?
How do I allow my server-side script to read from firestore, but not allow anyone else to do so?
Keep in mind, this is an initial action that runs server-side before hydrating the client-side with the pre-loaded state. This function / action is also shared with client-side for page-to-page navigation.
I considered anonymous login - which worked, however, this generated a new anonymous user with every page load - and Firebase does throttle new email/password and anonymous user accounts. It did not seem practical.
Solution
Per Doug's comment, I thought about the admin SDK more. I ended up creating a separate API in firebase functions for anonymous requests requiring secure firestore reads that can be cached.
Goals
Continue to deny public reads of my firestore database
Allow anonymous users to trigger firestore reads for server-side-rendered reactjs pages that require data from Firestore database (like first-time visitors, search engines).
Prevent "read spam" where a third party could hit my database with millions of reads to drive up my cloud costs by using server-side CDN cache for the responses. (by invoking unnessary reads in a loop, I once racked up a huge bill on accident - I want to make sure strangers can't do this maliciously)
Admin SDK & Firebase Function Caching
The admin SDK allows me to securely read from firestore. My firestore security rules can deny access to non-authenticated users.
Firebase functions that are handling GET requests support server caching the response. This means that subsequent hits from identical queries will not re-run all of my functions (firebase reads, other function invocations) - it will just instantly respond with the same data again.
Process
Anonymous client visits a server-side rendered reactjs page
Initial load rendering on server triggers a firebase function (https trigger)
Firebase function uses Admin SDK to read from secured firestore database
Function caches the response for 3 hours res.set('Cache-Control', 'public, max-age=600, s-maxage=10800');
Subsequent requests from any client anywhere for the next 3 hours are served from the cache - avoiding unnecessary reads or additional computation / resource usage
Note - caching does not work on local - must deploy to firebase to test caching effect.
Example Function
const functions = require("firebase-functions");
const cors = require('cors')({origin: true});
const { sendResponse } = require("./includes/sendResponse");
const { getFirestoreDataWithAdminSDK } = require("./includes/getFirestoreDataWithAdminSDK");
const cachedApi = functions.https.onRequest((req, res) => {
cors(req, res, async () => {
// Set a cache for the response to limit the impact of identical request on expensive resources
res.set('Cache-Control', 'public, max-age=600, s-maxage=10800');
// If POST - response with bad request code - POST requests are not cached
if(req.method === "POST") {
return sendResponse(res, 400);
} else {
// Get GET request action from query
let action = (req.query.action) ? req.query.action : null;
console.log("Action: ", action);
try {
// Handle Actions Appropriately
switch(true) {
// Get Feed Data
case(action === "feed"): {
console.log("Getting feed...");
// Get feed id
let feedId = (req.query.feedId) ? req.query.feedId : null;
// Get feed data
let feedData = await getFirestoreDataWithAdminSDK(feedId);
return sendResponse(res, 200, feedData);
}
// No valid action specified
default: {
return sendResponse(res, 400);
}
}
} catch(err) {
console.log("Cached API Error: ", err);
return sendResponse(res, 500);
}
}
});
});
module.exports = {
cachedApi
}

Adal js Library - this.adalService.acquireToken method giving "Token renewal operation failed due to timeout" on first time login

Though there are some link related to this questions but I did't find any relevant answer, So hoping someone will answer this time.
Here is the scenario, In my Angular Application I am using adal-angular4 which is wrapper over Adal.js
Issue : this.adalService.acquireToken method during only first time login. I am getting timeout error but after login if i will do page refresh then this.adalService.acquireToken method working properly and the interesting part are following.
Issue is only coming in deployed environment not in the localhost.
Error "Token renewal operation failed due to timeout" coming only sometimes when network is slow or random times.
Here is my request interceptor service
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> | Observable<HttpSentEvent | HttpHeaderResponse
| HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
if (req && req.params instanceof CustomAuthParams && req.params.AuthNotRequired) {
return this.handleAuthentication(req, next, null);
} else {
if (!this.adalService.userInfo.authenticated) {
console.log(req, 'Cannot send request to registered endpoint if the user is not authenticated.');
}
var cachedToken = this.adalService.getCachedToken(environment.authSettings.clientId);
console.log('cachedToken', cachedToken);
if (cachedToken) {
return this.adalService.acquireToken(resourceURL).timeout(this.API_TIMEOUT).pipe(
mergeMap((token: string) => {
return this.handleAuthentication(req, next, token);
})
).catch(err => { console.log('acquire token error', err); return throwError(err) })
} else {
this.adalService.login();
}
}
}
Well, after struggling for 1 to 2 days I have found the root cause. So posting this answer so that it will help others.
adal-angular4 library is using 1.0.15 version of adal-angular which is old version where default timeout for loadFrameTimeout is 6 seconds and in this version there is no configuration to increase the loadFrameTimeout. please see below link
Adal configurations
Now during first time login there are many steps happens.
After authentication, application redirect to configured URI by azure AD, By appending ID and Access token in the reply URL.
Then Library set all these token in the local storage or session storage depends on the configuration.
Then your applications loads and start making calls to webapi. Now here is the interesting things was happening, for each request I am calling acquireToken method against webapi application, So if network is slow acquireToken calls will give timeout error since 6 second is not enough sometimes. But for some of the API it will able to get the token.
Now on first call acquireToken method takes time but for subsequent request it takes token from the cache if it is available, so timeout error was coming only for first time not after that.
So, In this library for now there is no way to increase the loadFrameTimeout so I used
Angular5 warpper which is using 1.0.17 version of adal-angular where we can increase loadFrameTimeout which solved my issue.

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.

Basic strategy for delegate refresh token to get new JWT

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...
});
})

Resources