I am trying to achieve the following in my HTTP calls
If an API request returns 401 then call the refresh token endpoint to get the token.
Retry the same HTTP call with the updated token
Here is the relevant code
// this method invoke when the HTTP interceptor returns 401 status code
handle401(request: HttpRequest<any>, next: HttpHandler) {
if (!this.refreshTokenInProgress) {
this.refreshTokenInProgress = true;
this.refreshTokenSubject.next(null);
return this.getToken((data: any) => {
this.refreshTokenInProgress = false;
this.refreshTokenSubject.next(data);
request = request.clone({ headers: request.headers.set('Authorization', `Bearer ${data}`) });
return next.handle(request);
})
} else {
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap((accessToken) => {
request = request.clone({ headers: request.headers.set('Authorization', `Bearer ${accessToken}`) });
return next.handle(request);
})
);
}
}
Obtain the refresh token
getToken(cb: any) {
let poolData = {
UserPoolId: environment.cognitoUserPoolId, // Your user pool id here
ClientId: environment.cognitoAppClientId // Your client id here
};
let userPool = new CognitoUserPool(poolData);
let cognitoUser = userPool.getCurrentUser();
cognitoUser?.getSession((err: any, session: any) => {
const refresh_token = session.getRefreshToken();
cognitoUser?.refreshSession(refresh_token, (refErr, refSession) => {
const userToken = localStorage.getItem('token');
cb(userToken);
});
})
}
While executing I am getting the new token from the getToken method, but the retry of the same HTTP call is not happening.
The execution of HTTP request stops after obtaining the refresh token from the getToken method.
Can somebody please help on this issue
if you use catchError should return Observable
your getToken function has not return
maybe you can watch this
https://stackoverflow.com/a/73364684/19768317
Assuming you want to get userToken from getToken(). getToken() should return something, right now it's not returning anything. If some methods like getSession or refreshSession are async methods then those should be waited too.
async handle401(request: HttpRequest<any>, next: HttpHandler) {
if (!this.refreshTokenInProgress) {
this.refreshTokenInProgress = true;
this.refreshTokenSubject.next(null);
const token = await this.getToken(); // put "cb" param here, wait for returned token, then continiue
this.refreshTokenInProgress = false;
this.refreshTokenSubject.next(token);
request = request.clone({ headers: request.headers.set('Authorization', `Bearer ${token}`) });
return next.handle(request);
} else {
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap((accessToken) => {
request = request.clone({ headers: request.headers.set('Authorization', `Bearer ${accessToken}`) });
return next.handle(request);
})
);
}
}
getToken(cb: any) {
let poolData = {
UserPoolId: environment.cognitoUserPoolId, // Your user pool id here
ClientId: environment.cognitoAppClientId // Your client id here
};
let userPool = new CognitoUserPool(poolData);
let cognitoUser = userPool.getCurrentUser();
cognitoUser?.getSession((err: any, session: any) => {
const refresh_token = session.getRefreshToken();
cognitoUser?.refreshSession(refresh_token, (refErr, refSession) => {
const userToken = localStorage.getItem('token');
cb(userToken);
return userToken;
});
})
}
Related
My question is related to customAxios.interceptors.response.use . My purpose here is; if the token expired and I got a 401 error, make a request again where I got a 401 error and write the new token to the headers. On the other hand, if I get an error except for the 401 error, show me the error.response.data . Do you think this logic is set up correctly? I tried to test but I wasn't sure especially 401 error cases
import axios from "axios";
import { LoginAPI } from "../playwright/tests/login/login.api";
import { test } from "#playwright/test"
import {configEnv} from "../config/config"
test.beforeAll(async () => {
await LoginAPI.API.Signin.run()
});
const customAxios = axios.create({
baseURL: configEnv.apiBaseURL
});
customAxios.interceptors.request.use(
async (config) => {
if (config.headers) {
config.headers['Authorization'] = `Bearer ${LoginAPI.States.token}`;
return config;
}
return config;
},
(error) => {
Promise.reject(error);
}
);
customAxios.interceptors.response.use(
function(response) {
return response;
},
async function(error) {
if (401 === error.response.status) {
await LoginAPI.API.Signin.run()
customAxios.defaults.headers.common['Authorization'] = `Bearer ${LoginAPI.States.token}`
} else {
return Promise.reject(error.response.data);
}
}
);
export default customAxios
I would recommend you to store your token in a localStorage and then replace it after refresh. This way you can set a token in your API class in one place.
import axios from "axios";
export const ApiClient = () => {
// Create a new axios instance
const api = axios.create({
baseURL: "URL",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
// Add a request interceptor to add the JWT token to the authorization header
api.interceptors.request.use(
(config) => {
const token = sessionStorage.getItem("jwtToken");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Add a response interceptor to refresh the JWT token if it's expired
api.interceptors.response.use(
(response) => response,
(error) => {
const originalRequest = error.config;
// If the error is a 401 and we have a refresh token, refresh the JWT token
if (
error.response.status === 401 &&
sessionStorage.getItem("refreshToken")
) {
const refreshToken = sessionStorage.getItem("refreshToken");
let data = JSON.stringify({
refresh_token: refreshToken,
});
post("/refreshToken", data)
.then((response) => {
sessionStorage.setItem("jwtToken", response.token);
sessionStorage.setItem("refreshToken", response.refresh_token);
// Re-run the original request that was intercepted
originalRequest.headers.Authorization = `Bearer ${response.token}`;
api(originalRequest)
.then((response) => {
return response.data;
})
.catch((error) => {
console.log(error);
});
// return api(originalRequest)
})
.catch((err) => {
// If there is an error refreshing the token, log out the user
console.log(err);
});
}
// Return the original error if we can't handle it
return Promise.reject(error);
}
);
const login = (email, password) => {
return api
.post("/authentication_token", { email, password })
.then(({ data }) => {
// Store the JWT and refresh tokens in session storage
sessionStorage.setItem("jwtToken", data.token);
sessionStorage.setItem("refreshToken", data.refresh_token);
})
.catch((err) => {
// Return the error if the request fails
return err;
});
};
const get = (path) => {
return api.get(path).then((response) => response.data);
};
const post = (path, data) => {
return api.post(path, data).then((response) => response.data);
};
const put = (path, data) => {
return api.put(path, data).then((response) => response.data);
};
const del = (path) => {
return api.delete(path).then((response) => response);
};
return {
login,
get,
post,
put,
del,
};
};
Best,
Chris
I'm working on an auth system in angular with a django backend with jwt. I have an angular interceptor which checks within every request if the access token is still valid and if not, it calls a refreshtoken function and refreshes the access token.
Here is the code of the interceptor:
constructor(private authService:AuthService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
let access_token = localStorage.getItem("access_token");
if(access_token){
// Check if access token is no longer valid, if so, refresh it with the refresh token
if(this.authService.isLoggedOut()){
this.authService.refreshToken();
access_token = localStorage.getItem("access_token");
}
const cloned = request.clone({
headers: request.headers.set("Authorization", "Bearer " + access_token)
});
return next.handle(cloned);
}
else{
return next.handle(request);
}
It gets the access_token and checks the validity with the help of the auth service.
My problem is that if its unvalid its calls the refreshToken() function which looks like this,
refreshToken(){
let refresh_token = localStorage.getItem("refresh_token");
console.log("BEFORE REFRESH: " + localStorage.getItem("access_token"));
return this.http.post(`${apiUrl}token/refresh/`, {"refresh" : refresh_token}).subscribe(res => {
let access_token = res["access"]
const expiresAt = this.tokenExpiresAt(access_token);
localStorage.setItem("access_token", access_token);
localStorage.setItem("expires_at", JSON.stringify(expiresAt.valueOf()));
console.log("AFTER REFRESH: " + localStorage.getItem("access_token"));
});
}
but it doesn't wait for the refresh token function to finish and returns the handle.
So the first request with an invalid token throws an error and only the next requests are fine.
How can I revise this to wait for refreshToken() to finish?
Thanks for your time!
you can use rxjs operators such as switchMap
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
let access_token = localStorage.getItem("access_token");
if (access_token) {
// Check if access token is no longer valid, if so, refresh it with the refresh token
if (this.authService.isLoggedOut()) {
return this.authService.refreshToken().pipe(
switchMap(token => {
const cloned = request.clone({
headers: request.headers.set("Authorization", "Bearer " + access_token)
});
return next.handle(cloned);
})
)
}
} else {
return next.handle(request);
}
}
and in refreshToken function - make it return Observsble and set the localStorage inside pipe
refreshToken(){
let refresh_token = localStorage.getItem("refresh_token");
console.log("BEFORE REFRESH: " + localStorage.getItem("access_token"));
return this.http.post(`${apiUrl}token/refresh/`, {"refresh" : refresh_token}).pipe(tap(res => {
let access_token = res["access"]
const expiresAt = this.tokenExpiresAt(access_token);
localStorage.setItem("access_token", access_token);
localStorage.setItem("expires_at", JSON.stringify(expiresAt.valueOf()));
console.log("AFTER REFRESH: " + localStorage.getItem("access_token"));
});
)
}
I combined the answers from multiple questions and finally came up with this solution:
Interceptor:
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
let access_token = localStorage.getItem("access_token");
if (access_token) {
// Check if access token is no longer valid, if so, refresh it with the refresh token
if (this.authService.isLoggedOut()) {
return from(this.authService.refreshToken()).pipe(
mergeMap((access_token) => {
localStorage.setItem("access_token", access_token);
const cloned = request.clone({
headers: request.headers.set("Authorization", "Bearer " + access_token),
});
return next.handle(cloned);
})
);
}
const cloned = request.clone({
headers: request.headers.set("Authorization", "Bearer " + access_token),
});
return next.handle(cloned);
} else {
return next.handle(request);
}
}
refreshToken:
async refreshToken(): Promise<string> {
let refresh_token = localStorage.getItem("refresh_token");
const res$ = this.http
.post(`${apiUrl}token/refresh/`, { refresh: refresh_token })
.pipe(map((res) => res["access"]))
.pipe(first());
const res = await lastValueFrom(res$);
const expiresAt = this.tokenExpiresAt(res);
localStorage.setItem("access_token", res);
localStorage.setItem("expires_at", JSON.stringify(expiresAt.valueOf()));
console.log("Refreshed Access Token");
return res;
}
Info
I am creating an interceptor to use my refresh token to update my access token if I get a 401. The workflow looks like this now:
Sends request > gets 401 > sends refresh request > updates access token > sends new request
I am currently working with promises instead of observables.
Question
How do I logout if the last request fails?
Sends request > gets 401 > sends refresh request > updates access token > sends new request > fails > log out
I have a simple method for logging out, but I cannot find where to put it within the interceptor.
Code
export class RefreshInterceptor implements HttpInterceptor {
currentUser: User | null = null;
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(
null
);
constructor(private authenticationService: AuthenticationService) {
this.authenticationService.currentUser.subscribe(
user => (this.currentUser = user)
);
}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError(error => {
// check if user is signed in
if (!this.currentUser) {
return throwError(error);
}
// handle only 401 error
if (error instanceof HttpErrorResponse && error.status === 401) {
return from(this.handle401Error(request, next));
} else {
return throwError(error);
}
})
);
}
/**
* Adds the new access token as a bearer header to the request
* #param request - the request
* #param token - the new access token
*/
private async addToken(request: HttpRequest<any>, token: string) {
const currentUser = this.authenticationService.currentUserValue;
if (currentUser && currentUser.accessToken) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
return request;
}
private async handle401Error(request: HttpRequest<any>, next: HttpHandler) {
// check if it is currently refreshing or not
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
// send refresh request
const token = await this.authenticationService.getRefresh();
// update bearer token
const newRequest = await this.addToken(request, token);
// update values for next request
this.isRefreshing = false;
this.refreshTokenSubject.next(token);
return next.handle(newRequest).toPromise();
} else {
const token = this.refreshTokenSubject.value();
const newRequest = await this.addToken(request, token);
return next.handle(newRequest).toPromise();
}
}
}
I solved it with the following approach:
Modified the header of the outgoing, changed request (added a retry header so that I could identify it later).
Created a new interceptor for logout
Looked for a request with the retry header. Signed that request out.
Refresh token interceptor
if (currentUser && currentUser.accessToken) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
Retry: "true"
}
});
}
Logout interceptor
#Injectable()
export class LogoutInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError(error => {
// handle only 401 error
if (error instanceof HttpErrorResponse && error.status === 401) {
from(this.handleRequest(request));
return throwError(error);
}
return next.handle(request);
})
);
}
private async handleRequest(request: HttpRequest<any>) {
const isRetriedRequest = request.headers.get("retry");
if (isRetriedRequest) {
await this.authenticationService.logout();
}
}
}
I'm using the library axios in my react app.
I'm having a problem with the interceptor.
My question is let say I have three requests happening concurrently and I don't have the token, the interceptor calling the getUserRandomToken three time, I want the interceptor will wait until I'm getting the token from the first request and then continue to the others.
P.S. the token he is with an expiration date so I also checking for it and if the expiration date is not valid I need to create a new token.
This is the interceptor:
axios.interceptors.request.use(
config => {
/*I'm getting the token from the local storage
If there is any add it to the header for each request*/
if (tokenExist()) {
config.headers.common["token"] = "...";
return config;
}
/*If there is no token i need to generate it
every time create a random token, this is a axios get request*/
getUserRandomToken()
.then(res => {
/*add the token to the header*/
config.headers.common["token"] = res;
return config;
})
.catch(err => {
console.log(err);
});
},
function(error) {
// Do something with request error
return Promise.reject(error);
}
);
How about singleton object that will handle the token generations? something similar to this:
const tokenGenerator ={
getTokenPromise: null,
token: null,
getToken(){
if (!this.getTokenPromise){
this.getTokenPromise = new Promise(resolve=>{
/*supposed to be a http request*/
if (!this.token){
setTimeout(()=>{
this.token = 'generated';
resolve(this.token);
},0)
}else{
resolve(this.token);
}
})
}
return this.getTokenPromise;
}
you can reference this same object from the interceptors.
see example: JS FIddle
reference: reference
You can return a Promise from interceptor callback to "wait" until promise fullfiles (this will fit your case). Check out this example:
function axiosCall () {
return new Promise((resolve, reject) => {
Axios.post(URL, {apiKey}).then((response) => {
resolve(response.data.message);
}).catch((error) => {
reject(error);
});
});
}
instance.interceptors.request.use((config) => {
return axiosCall().then((tokenResponse) => {
setWebCreds(tokenResponse);
config.headers.Authorization = `Bearer ${tokenResponse}`;
return Promise.resolve(config)
}).catch(error => {
// decide what to do if you can't get your token
})
}, (error) => {
return Promise.reject(error);
});
More details here: https://github.com/axios/axios/issues/754
Following code doing certain tasks:
Update Token on 401
Make a queue of failed requests while the token is refreshing.
Restore the original request after token refreshing.
Once the peculiar request is given 200, remove it from the queue.
Config.js
import axios from 'axios';
import { AsyncStorage } from 'react-native';
import { stateFunctions } from '../../src/sharedcomponent/static';
const APIKit = axios.create({
baseURL: '',
timeout: 10000,
withCredentials: true,
});
const requestArray = [];
// Interceptor for Request
export const setClientToken = token => {
APIKit.interceptors.request.use(
async config => {
console.log('Interceptor calling');
let userToken = await AsyncStorage.getItem('userToken');
userToken = JSON.parse(userToken);
config.headers = {
'Authorization': `Bearer ${userToken}`,
'Accept': 'application/json',
"Content-Type": "application/json",
"Cache-Control": "no-cache",
}
// console.log('caling ' , config)
return config;
},
error => {
Promise.reject(error)
});
};
// Interceptor for Response
APIKit.interceptors.response.use(
function (response) {
if (requestArray.length != 0) {
requestArray.forEach(function (x, i) {
if (response.config.url == x.url) {
requestArray.splice(i, 1);
}
});
}
return response;
},
function (error) {
const originalRequest = error.config;
requestArray.push(originalRequest);
let reqData = "username=" + number + "&password=" + pin + "&grant_type=password" + "&AppType=2" + "&FcmToken=null";
// console.log('error ' , error);
if (error.message === "Request failed with status code 401" || error.statuscode === 401) {
if (!originalRequest._retry) {
originalRequest._retry = true;
return axios({
method: 'post',
url: '/api/login',
data: reqData,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Cache-Control": "no-cache",
}
})
.then(res => {
let response = res.data;
console.log('successfull Login', response)
if (res.data.StatusCode == 200) {
AsyncStorage.setItem('userToken', JSON.stringify(response.access_token));
stateFunctions.UserId = response.UserId;
stateFunctions.CustomerContactID = response.CustomerContactID;
let obj = {
access_token: response.access_token,
token_type: response.token_type,
expires_in: response.expires_in,
UserId: response.UserId,
CustomerContactID: response.CustomerContactID,
Mobile: response.Mobile,
StatusCode: response.StatusCode
}
AsyncStorage.setItem('logindetail', JSON.stringify(obj));
if (requestArray.length != 0) {
requestArray.forEach(x => {
try {
console.log(x, "request Url");
x.headers.Authorization = `Bearer ${response.access_token}`;
x.headers["Content-Type"] = "application/x-www-form-urlencoded";
APIKit.defaults.headers.common["Authorization"] = `Bearer${response.access_token}`;
APIKit(x)
} catch (e) {
console.log(e)
}
});
}
return APIKit(originalRequest);
}
})
.catch(err => {
console.log(err);
});
}
}
return Promise.reject(error);
}
);
export default APIKit;
Home.js
gettingToken = async () => {
let userToken = await AsyncStorage.getItem('userToken');
userToken = JSON.parse(userToken);
await setClientToken(userToken);
}
I want to use axios interceptor before every axios call to pass idToken as authorization header with all the axios calls and I want to refresh the idToken if it has expired before any call.
I am using the following code:
axios.interceptors.request.use(function(config) {
var idToken = getIdToken()
var refreshToken = {
"refreshToken" : getRefreshToken()
}
if(isTokenExpired(idToken)){
console.log("==============Reloading")
refresh(refreshToken).then(response=>{
setIdToken(response.idToken)
setAccessToken(response.accessToken)
})
idToken = getIdToken()
config.headers.Authorization = `${idToken}`;
}
else{
config.headers.Authorization = `${idToken}`;
}
return config;
}, function(err) {
return Promise.reject(err);
});
It works fine till the time idToken is valid. When the idToken expires it gets in an infinite loop and the page hangs. Please help me with this. The refresh() which call the refresh API looks like this:
function refresh(refreshToken) {
const url = `${BASE_URL}/user/refresh`;
return axios.post(url,JSON.stringify(refreshToken))
.then(response =>response.data.data)
.catch(e => {
console.log(e);
});
}
I had some similar problem and creating new axios instance to perform refresh token api call resolved the problem (new AXIOS instance is not resolved by defined axios.interceptors.request.use) (of course below code is just a simple example).
Remember to save original request and process it after token has been refreshed:
F.ex my http-common.js
import axios from 'axios'
const AXIOS = axios.create()
export default AXIOS
...
in App.vue:
axios.interceptors.request.use((config) => {
let originalRequest = config
if (helper.isTokenExpired(this.$store.getters.tokenInfo)) {
return this.refreshToken(this.$store.getters.jwt).then((response) => {
localStorage.setItem('token', response.data.token)
originalRequest.headers.Authorization = response.data.token
return Promise.resolve(originalRequest)
})
}
return config
}, (err) => {
return Promise.reject(err)
})
and the refresh token method:
refreshToken (token) {
const payload = {
token: token
}
const headers = {
'Content-Type': 'application/json'
}
return new Promise((resolve, reject) => {
return AXIOS.post('/api/auth/token/refresh/', payload, { headers: headers }).then((response) => {
resolve(response)
}).catch((error) => {
reject(error)
})
})
}
}