Dealing with axios interceptors when sending many ajax requests - ajax

I use Larave+JWT and vue2 + vuex2 + axios
So when user logins I store auth token in vuex store. When the token expires I need to refresh it. In order to refresh it I need to send the same token to /refresh route, and get a new token. At least that's how I got it and actually it works.
The problem is that interceptor catches 401 responses and tries to refresh token, but what if, say, in my component I send many requests with expired token? Since ajax requests are async, the interceptor code runs many times. So I got many refresh requests. Once the initial token is refreshed it is not considered valid. When interceptor tries to refresh invalid token server responds with error and I redirect to login page.
Here is the code:
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
axios.post('auth/refresh').then((response) => {
let token = response.data.token
store.dispatch('auth/setAuthToken', token)
let authorizationHeader = `Bearer ${token}`
axios.defaults.headers = { 'Authorization': authorizationHeader }
originalRequest.headers['Authorization'] = authorizationHeader
return axios(originalRequest)
}, (error) => {
store.dispatch('auth/clearAuthInfo')
router.push({ name: 'login' })
})
}
return Promise.reject(error);
});

I think you'll have to change your approach on how you refresh your token. Leaders like Auth0 recommends proactive periodic refresh to solve this problem.
Here is a SO answer where they talk about it.
Set the token expiration to one week and refresh the token every time the user open the web application and every one hour. If a user doesn't open the application for more than a week, they will have to login again and this is acceptable web application UX.

Related

Spring Keycloak Adapter sometimes returns with "Token is not active" when uploading files

lately we are facing an issue that our Spring Boot backend service (stateless REST service) SOMETIMES returns an HTTP 401 (Unauthorized) error when users try to upload files >70 MB (or in other words, when the request takes longer than just a couple of seconds). This does not occur consistently and only happens sometimes (~every second or third attempt).
The www-authenticate header contains the following in these cases:
Bearer realm="test", error "invalid_token", error_description="Token is not active"
Our Spring (Boot) configuration is simple:
keycloak.auth-server-url=${KEYCLOAK_URL:http://keycloak:8080/auth}
keycloak.realm=${KEYCLOAK_REALM:test}
keycloak.resource=${KEYCLOAK_CLIENT:test}
keycloak.cors=true
keycloak.bearer-only=true
Essentially, our frontend code uses keycloak-js and does the following to keep the access token fresh:
setInterval(() => {
// updates the token if it expires within the next 5s
this.keycloak.updateToken(5).then((refreshed) => {
console.log('Access token updated:', refreshed)
if (refreshed) {
store.commit(AuthMutationTypes.SET_TOKEN, this.keycloak.token);
}
}).catch(() => {
console.log('Failed to refresh token');
});
}, 300);
Further, we use Axios and a respective request filter to inject the current token:
axios.interceptors.request.use(
(request: AxiosRequestConfig) => {
if (store.getters.isAuthenticated) {
request.headers.Authorization = 'Bearer ' + store.getters.token;
}
return request;
}
);
This worked very well so far and we have never experienced such a thing for our usual GETs/POSTs/PUTs etc. This happens only when users try to upload files larger than (around) 70MBish.
Any hint or tip how to debug this any further? We appreciate any help...
Cheers

Laravel Sanctum with SPA: Best practice for setting the CSRF cookie

Currently, my SPA is simple and has only a contact form. The form data is sent to the backend. I have installed Laravel Sanctum for that.
axios.get('/sanctum/csrf-cookie').then((response) => {
axios.post('api/contact')
.then((response) => {
//doing stuff
});
});
This works perfectly fine. However, I was wondering. Where and which time in your SPA do you fire the initial request to get the CSRF cookie? (axios.get('/sanctum/csrf-cookie'))
My first thoughts were:
Every time the page is mounted/loaded
Only when you receive a 419 and you attempt to refresh the token and retry the previous request
You don't fire any API requests, only when the user tries to log in and only then you are requesting a cookie (in my case I don't have any user authentification)
For each API request, you wrap it with axios.get('/sanctum/csrf-cookie') around
So for future readers, I chose:
Only when you receive a 419 and you attempt to refresh the token and retry the previous request
For that, I'm using the package axios-auth-refresh.
My settings look like that
//bootstrap.js
import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
axios.defaults.withCredentials = true;
axios.defaults.baseURL = process.env.GRIDSOME_BACKEND_URL;
const refreshAuthLogic = (failedRequest) => axios.get('/sanctum/csrf-cookie').then((response) => Promise.resolve());
createAuthRefreshInterceptor(axios, refreshAuthLogic, { statusCodes: [419] });
window.axios = axios;

Axios : put on hold requests

I have the following axios interceptor.
It checks the validity of the stored token. If not valid, a request is fired to retrieve and store the refreshed token.
axios.interceptors.request.use(async config =>
if(checkValidity(localStorage.getItem('token')) === false) {
const response = await axios.get('http://foobar.com/auth/refresh');
localStorage.setItem('token', response.headers['token']);
config = getConfigFromResponse(response);
}
return config;
});
It works great. The problem is that if I have many requests with invalid token then many requests to http://foobar.com/auth/refresh are done to refresh it.
Is it possible to put all the requests in an array and fire them after when the refresh is done ?
The idea is to avoid catching 401 errors and replaying the request : this is why I want to "save" the requests while the token is being retrieved and then fire them when the token is ready.

Tymon JWT Laravel Vue refresh token stored in localstorage

So I started using Tymon JWT package for Laravel for my SPA. Its all going good ( adding user, signing in, getting Auth user) but when I make an API request it only works for an hour. I understand that the token I stored on on login expires so when I make a request to the API after 60 minutes it doesnt work. My question is how do I refresh the token and restore it in my local storage? On a new request? Every 59 minutes?
AuthController.php
public function authenticate(Request $request)
{
// grab credentials from the request
$credentials = $request->only('email', 'password');
try {
// attempt to verify the credentials and create a token for the user
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'Sorry, we cant find you.']);
}
} catch (JWTException $e) {
// something went wrong whilst attempting to encode the token
return response()->json(['error' => 'could_not_create_token'], 500);
}
// all good so return the token
$user = JWTAuth::toUser($token);
//Fire off the login event
event( new LoginSuccessful($user) );
return response()->json( compact('token', 'user') );
}
Login.vue
axios.post('/api/authenticate',{
email: this.email,
password: this.password
})
.then(response => {
if(response.data.error){
//Show the error
this.error = response.data.error
this.loading = false;
} else {
this.loading = false;
//Store the items in storage
localStorage.setItem('jwt_token', response.data.token);
localStorage.setItem('user', JSON.stringify(response.data.user));
//Mutate the state
this.$store.commit('login');
//Now go to the dashboard
this.$router.push('dashboard');
}
})
.catch(error => {
});
in my head tag
<script>
window.hopbak = {
'jwt_token': localStorage.getItem('jwt_token'),
'csrfToken': {!! json_encode( csrf_token() ) !!},
};
</script>
in my bootstrap.js
let token = window.hopbak.jwt_token;
if (token) {
window.axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
} else {
console.log('no token');
}
I think you need to register the RefreshToken middleware in app/Http/Kernel.php:
protected $routeMiddleware = [
'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken'
];
And then assign it to the routes you want to refresh the token.
Looking at the source code, I can tell that this middleware will handle the refreshing of your token in every request by adding a new token to the response header.
JWT_TTL, JWT_REFRESH_TTL and JWT_BLACKLIST_GRACE_PERIOD values are setted in config/jwt.php file.
How does it work:
The client sends the credentials (email and password) to Laravel and
receives a token (JWT) in response. This token is valid for JWT_TTL
minutes. During this time all requests with the header Authorization
= "Bearer token" will be successful.
For a request made after JWT_TTL minutes, that is, with the token expired, two situations will occur: (1) If there is less than JWT_REFRESH_TTL minutes since the creation of the token (the token carries within it the date of creation on claim IAT), then this token will be invalidated (blacklist) and a new token will be generated and sent as a response to the client. JWT_REFRESH_TTL defines how many minutes after creating the first token the new tokens can be created. For example, for JWT_REFRESH_TTL = 21600, new tokens will be generated for 15 days, after which time the user should reauthenticate. (2) The request occurs after JWT_REFRESH_TTL minutes after the first token was created. In this case, it will not be possible to generate a new token for the client and it must authenticate again. A 401 error will be sent to the client.
When multiple concurrent requests are made with the same JWT,
it is possible that some of them fail, due to token regeneration on
every request. Set grace period in seconds to prevent parallel
request failure, because the JWT will consider to be valid for
JWT_BLACKLIST_GRACE_PERIOD seconds, even if it's on the blacklist.
For more details see this my explanation.

Google authorization code flow - no refresh token

I'm using Google API to obtain access and refresh tokens for offline access, however, the refresh token is always null.
The authorizationCode comes from the client, so I exchange it with the tokens on the server:
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
HTTP_TRANSPORT,
JSON_FACTORY,
clientId,
clientSecret,
scopes)
.setAccessType("offline")
.build();
GoogleTokenResponse response = flow
.newTokenRequest(authorizationCode)
.setRedirectUri(redirectUri)
.execute();
// response has access_token and id_token. don't see any refresh token here
return flow.createAndStoreCredential(response, null);
// this returns Credential with refreshToken = null
Client code:
$(document).ready(function() {
gapi.load('auth2', function() {
auth2 = gapi.auth2.init({
client_id: '<my client id>',
// Scopes to request in addition to 'profile' and 'email'
scope: 'https://www.google.com/m8/feeds https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.appfolder'
});
});
$('#signinButton').click(function() {
auth2.grantOfflineAccess({'redirect_uri': 'postmessage'}).then(onSignIn);
});
});
// the onSignIn function sends the one time code (i.e. authResult['code']) to the server
Am I doing anything wrong here?
I saw this question: Google OAuth: can't get refresh token with authorization code but it didn't really answer my question and I'm not aware of refresh token only being created the first time a user logs in.
Edit:
I believe this is actually a client side problem, because the consent window doesn't ask for offline access. Will update this question based on the results of a new question here: Google javascript sign-in api: no offline access

Resources