I am trying to call the MSAL silentTokenrefresh method from Angular authInterceptor whenever the 401 hits. And then i am trying to recall the failed request again with a new token so the service won't be interrupted. I have followed this stackoverflow link (answered by Andrei Ostrovski) and implemented the same in my application.
There are two problem with refreshToken() method.
the catchError expects return variable where i put EMPTY. The code is happy, but it is NOT triggering back the failed request where the total purpose is not achieved. It means that whenever it encounters 401 and it is allowing to acquire the new token and then it is not triggering the failed requests.
To resolve the above one, i just took another approach (instead of pipe, i used subscribed but there the return is not applicable) and then it became completely invalid.
Could you please suggest me here to make it work?
My complete authInterceptor file:
addAuthHeader(request) {
const tokenType = "Bearer";
const authHeader = this.sessionService.getAccessToken();
if (authHeader) {
return request.clone({
setHeaders: {
"Authorization": tokenType + " "+ authHeader
}
});
}
return request;
}
refreshToken(): Observable<any> {
if (this.refreshTokenInProgress) {
return new Observable(observer => {
this.tokenRefreshed$.subscribe(() => {
observer.next();
observer.complete();
});
});
} else {
this.refreshTokenInProgress = true;
// Getting the scope.
const loginRequest: { scopes: any } = {
scopes: ['openId','profile'],
};
return this.msalAuthService.acquireTokenSilent(loginRequest).pipe(
tap((payloadInfo)=>{
this.authServices.setSessions(payloadInfo);
this.refreshTokenInProgress = false;
this.tokenRefreshedSource.next();
}),
catchError(() => {
this.refreshTokenInProgress = false;
this.authServices.logout();
return EMPTY;
})
)
}
}
handleResponseError(error, request?, next?) {
// Invalid token error
if (error.status === 401) {
return this.refreshToken().pipe(
switchMap(() => {
request = this.addAuthHeader(request);
return next.handle(request);
}),
catchError(e => {
if (e.status !== 401) {
return this.handleResponseError(e);
} else {
this.authentiCationService.logout();
this.router.navigate(["logout"], {
replaceUrl: true
});
}
}));
}
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.totalRequests++;
req = req.clone({ headers: req.headers.set("Accept", "*/*") });
req = req.clone({
headers: req.headers.set("Content-Type", "application/json")
});
req = this.addAuthHeader(req);
const started = Date.now();
return next.handle(req).pipe(
tap(
(event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
// some internal logic for the success scenario
}
},
(err: any) => {
this.handleResponseError(err, req, next);
}
)
);
}
The compile time error you are getting is because you are returning the Subscription instead of the Observable. Please take a look at the difference between them: https://angular.io/guide/observables
You have to return the Observable from your code. In catchError return Observable.throw(error.statusText);
Related
I try to explain the problem.in App.js I have Function getUser .when call this function.in first request get 401 error . For this reason in axios.interceptors.response I receive error 401.At this time, I receive a token and repeat my request again.And it is done successfully.But not return response in Function getUser.
I have hook for authentication and send request.
import React from "react";
import axios from "axios";
const API_URL = "http://127.0.0.1:4000/api/";
function useJWT() {
axios.interceptors.request.use(
(request) => {
request.headers.common["Accept"] = "application/json";
console.log("request Send ");
return request;
},
(error) => {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
(response) => {
console.log("answer = ", response);
return response;
},
(error) => {
if (error?.response?.status) {
switch (error.response.status) {
case 401:
refreshToken().then((responseTwo) => {
return
sendPostRequest(
error.response.config.url
.split("/")
.findLast((item) => true)
.toString(),
error.response.config.data
);
});
break;
case 500:
// Actions for Error 500
throw error;
default:
console.error("from hook interceptor => ", error);
throw error;
}
} else {
// Occurs for axios error.message = 'Network Error'
throw error;
}
}
);
const refreshToken = () => {
const token = localStorage.getItem("refresh");
return axios
.post(API_URL + "token", {
token,
})
.then((response) => {
if (response.data.access) {
localStorage.setItem("access", response.data.access);
}
if (response.data.refresh) {
localStorage.setItem("refresh", response.data.refresh);
}
return response.data;
});
};
function login(email, password) {
return axios
.post(API_URL + "login", {
email,
password,
})
.then((response) => {
if (response.data.access) {
localStorage.setItem("access", response.data.access);
}
if (response.data.refresh) {
localStorage.setItem("refresh", response.data.refresh);
}
return response.data;
});
}
const sendPostRequest = (url, data) => {
console.log(300);
const token = localStorage.getItem("access");
axios.defaults.headers.common["jwt"] = token;
return axios.post(API_URL + url, {
data,
});
};
const logout = () => {
const token = localStorage.getItem("refresh");
return axios
.delete(API_URL + "logout", {
token,
})
.then((response) => {
localStorage.removeItem("access");
localStorage.removeItem("refresh");
});
};
return {
login,
logout,
refreshToken,
sendPostRequest,
};
}
export default useJWT;
In App.js ,I want to repeat the same request again if a 401 error is issued when I read the user information.
The request is successfully repeated but does not return the value.
When first request fail response is return equals null . and in catch when receive 401 error i am send second request but not return response.
I send request below code .
const getUser = () => {
console.log(12);
return sendPostRequest("user");
};
useEffect(() => {
let token = localStorage.getItem("access");
console.log("token = ", token);
if (token != null) {
//Here I have done simulation for 401 error
localStorage.setItem("access", "");
getUser()
.then((response) => {
console.log("response 1= ", response);
})
.catch((exception) => {
console.log("exception = ", exception.toString());
})
.then((response) => {
console.log("response 2= ", response);
});
} else {
navigate("/login");
}
}, []);
Best regards.
I didn't fully understand what exactly you want to do here.
But if you are looking to retry when 401 happens, you could use axios-retry to do it for you.
I'll pass the basics, but you can look more into what this does.
// First you need to create an axios instance
const axiosClient = axios.create({
baseURL: 'API_URL',
// not needed
timeout: 30000
});
// Then you need to add this to the axiosRetry lib
axiosRetry(axiosClient, {
retries: 3,
// Doesn't need to be this, it can be a number in ms
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
// You could do this way or try to implement your own
return error.response.status > 400
// something like this works too.
// error.response.status === 401 || error.response.status >= 500;
}
});
Just like in your code, we need to use interceptors if you want to avoid breaking your page, otherwise you can use try catch to catch any errors that may happen in a request.
// It could be something like this, like I said, it's not really needed.
axiosClient.interceptors.response.use(
(success) => success,
(err) => err
);
And finally, you could use the axiosClient directly since it now has your API_URL, calling it like this axiosClient.post('/user').
More or less that's it, you should just debug this code and see what is causing the return value to be null.
I would change these then/catch to be an async/await function, it would be more readable making your debugging easier.
axios-retry example if you didn't understand my explanation.
I find anwser for this question.
When error 401 occurs then create new Promise
I Wrote this code.
case 401:
return new Promise((resolve, reject) => {
refreshToken().then((responseTwo) => {
resolve(
sendPostRequest(
error.response.config.url
.split("/")
.findLast((item) => true)
.toString(),
error.response.config.data
)
);
});
});
I am new to Nest.JS and apparently don't understand how to use observables so hopefully ya'll can help.
Basically I have a method that needs to:
first: login to hashicorp vault and return a client_token via an http call.
second: if we got a token back from vault, we then check that the request contained a certification-id, if not we have to request a new certification to be generated. Which requires the client_token from vault.
The problem I am having is that when I call vault to get the client_token, it does not get returned in time for me to be able to use it to generate a new cert via a second api call.
What can I do in order to be able to use the client_token in the next step?
Here is the code for my latest attempt:
Controller:
#Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
#Post('getUserCert')
async getUserCert(#Body() loginDto: vaultLoginReqDto) {
return this.userService.getCertificate(loginDto);
}
}
Controller calls the getCertificate method:
getCertificate(loginDto: vaultLoginReqDto) {
this.loginToVault(loginDto);
if (this.vault_token) {
if (loginDto.cert_id) {
this.checkForExistingCert(loginDto);
} else {
this.generateNewCert(this.vault_token);
}
} else {
throw new Error('User is not authorized to access Vault.');
}
}
The logon method:
loginToVault(loginDto: vaultLoginReqDto) {
const url = 'http://vault:8200/v1/auth/jwt/login';
const payload: vaultLoginReqDto = {
jwt: loginDto.jwt,
role: loginDto.role,
};
try {
this.httpService
.post(url, payload)
.subscribe((res: AxiosResponse<vaultLoginResDto>) => {
this.vault_token = res.data.auth.client_token;
});
} catch (e) {
this.throwError(e, url, 'Unable to login to vault');
}
}
the problem method is the generateNewCert method. It is not getting the vault_token in time.
generateNewCert(vault_token: string): Observable<string> {
const url = `http://127.0.0.1:8200/v1/xxxx/xxxx/issue/reader`;
const payload = {
common_name: 'id.xxxx.com',
};
const headers = {
'X-Vault-Token': vault_token,
};
try {
return this.httpService.post(url, payload, { headers: headers }).pipe(
map((res: AxiosResponse<vaultGetCertResDto>) => {
return res.data.data.certificate;
}),
);
} catch (e) {
this.throwError(e, url);
}
}
I appreciate the help!
The easiest way to make it work is the convert to a Promise so you can wait for the result.
loginToVault(loginDto: vaultLoginReqDto) {
const url = 'http://vault:8200/v1/auth/jwt/login';
const payload = {
jwt: loginDto.jwt,
role: loginDto.role,
};
return this.httpService
.post(url, payload)
.pipe(
catchError(() => {/** ...handleError **/}),
map((res) => {
this.vault_token = res.data.auth.client_token;
return this.vault_token;
}),
)
.toPromise()
}
Now, you can use async / await at getCertificate
async getCertificate(loginDto: vaultLoginReqDto) {
await this.loginToVault(loginDto);
// or const vault_token = await this.loginToVault(loginDto)
if (this.vault_token) {
if (loginDto.cert_id) {
this.checkForExistingCert(loginDto);
} else {
this.generateNewCert(this.vault_token);
}
} else {
throw new Error('User is not authorized to access Vault.');
}
}
If you decide to stick with the observables, you can return an observable from the loginToVault method as opposed to subscribing to it
loginToVault(loginDto: vaultLoginReqDto): Observable<string> {
const url = 'http://vault:8200/v1/auth/jwt/login';
const payload = {
jwt: loginDto.jwt,
role: loginDto.role,
};
return this.httpService
.post(url, payload)
.pipe(
catchError(() => { /* handle errors */ }),
map((res) => res.data.auth.client_token)
)
}
Then in getCertificate method, you subscribe to loginToVault and handle the logic
getCertificate(loginDto: vaultLoginReqDto) {
this.loginToVault(loginDto)
.pipe(
tap(vault_token => {
if (!vault_token) {
throw new Error('User is not authorized to access Vault.');
}
})
)
.subscribe(vault_token => loginDto.cert_id ?
this.checkForExistingCert(loginDto) :
this.generateNewCert(vault_token)
)
}
The vault_token is passed from one service to another and thus will be accessible in the generateNewCert method. You do not need to declare it globally
I am setting custom headers in nodejs (express) application within the response:
res.header('Vme','true')
next()
When i get response back to client I can see headers correctly in the browser:
browserHeaders
The problem is that i am not able to access headers inside angular hhtp interceptors:
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.loaderService.show()
return next.handle(request)
.pipe(
map((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
const message = event?.body?.message
if (message?.length) {
this.toastService.show(event?.body?.message, SeveritiesEmun.Success)
}
}
return event
}),
catchError(error => {
let errorMessage = error?.error || 'Error'
if (typeof errorMessage === 'object') {
errorMessage = error?.statusText || 'Error'
}
this.toastService.show(errorMessage, SeveritiesEmun.Fail)
return throwError(errorMessage)
}),
finalize(() => {
this.loaderService.hide()
})
)
}
In both the error (error) and the successful instance ((event: HttpEvent ))
the "headers" property contains no value.
You have to process the send data like this:
import { tap } from "rxjs/operators";
next.handle(req).pipe(
tap((event: HttpEvent<any>): void => {
if (event instanceof HttpResponse) {
// do whatever with event
}
})
);
Then you can check if your header is applied by doing event.headers.has("Vme") and access it by event.headers.get("Vme")
The thing is if I have a 401 error (token expired) I am making a request for the server to refresh the token. It handles all the requests been sent. It refreshes the token but interrupts the previous request. How to continue request after cathing error?
private handleAuthError(err: HttpErrorResponse): Observable<any> {
if (err.status === 401 || err.status === 403) {
this.authService.refreshToken(this.userData.accessToken, this.userData.refreshToken, this.userData.tokenType).subscribe((resp: any) => {
this.userData.accessToken = resp.access_token;
this.authService.currentUserSubject.next(this.userData);
const userData: User = new User(resp);
localStorage.setItem('currentUser', JSON.stringify(this.authService.currentUserSubject));
localStorage.setItem(`${JWT_TOKEN_KEY}`, JSON.stringify(userData.accessToken));
})
return of(err.message);
}
return throwError(err);
}
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token: string = JSON.parse(localStorage.getItem(`${JWT_TOKEN_KEY}`));
const isLoggedIn = token;
const isApiUrl = request.url.startsWith(environment.apiUrl);
if (isLoggedIn && isApiUrl) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
console.log(request);
return next.handle(request)
.pipe(
catchError((x: any) => this.handleAuthError(x)));
}
}
Just make a new request, you can't use the same one since it already ended
You can try this function to catch any request while error occurs and rewind that request again.
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
map((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
return event;
}
}),
catchError(error => {
if (error instanceof HttpErrorResponse) {
if (error.status === 401) {
if (request.url !== Constants.AUTH_ENDPOINT) {
this.auth.collectFailedRequest(request);
return this.auth.refreshAuthToken().pipe(
switchMap((data: any) => {
if (data && data.access_token && data.refresh_token) {
localStorage.setItem(Constants.ACCESS_TOKEN, JSON.stringify(data.access_token));
localStorage.setItem(Constants.REFRESH_TOKEN, JSON.stringify(data.refresh_token));
this.auth.cachedRequests = this.auth.cachedRequests.clone({
setHeaders: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${data.access_token}`
}
});
return next.handle(this.auth.cachedRequests); // rewinds previously requested http call
} else {
toastr.error(error.error.toUpperCase(), error.status);
this.auth.removeAllTokens();
setTimeout(() => {
window.location.reload();
}, 1000);
}
})
);
} else {
if (request.body.grant_type === 'refresh') {
this.auth.removeAllTokens();
window.location.reload();
} else {
console.log('no refresh grant type');
}
}
} else {
return throwError(error);
}
}
})
);
}
Here is AuthInterceptor:
#Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const Token = this.authService.getToken();
if (!Token) {
return next.handle(req);
}
// Refresh Token first
if (Token.expiresRefreshToken && Number(Token.expiresRefreshToken) < Date.now()) {
this.authService.refreshTokenRefresh(Token.tokenref)
.subscribe((response) => {
localStorage.setItem('tokenref', response.tokenref);
localStorage.setItem('tokenrefexp', response.tokenrefexp);
});
}
// Then next Access Token
if (Token.expiresToken && Number(Token.expiresToken) < Date.now()) {
this.authService.refreshToken(Token.tokenref)
.subscribe((response) => {
localStorage.setItem('token', response.token);
localStorage.setItem('tokenexp', response.tokenexp);
});
}
// Original request with updated custom headers
return next.handle(req.clone({
headers: req.headers
.set('Authorization', 'Bearer ' + localStorage.getItem('token'))
.set('X-Auth-Provider', localStorage.getItem('provider'))
}));
}
}
I need to evaluate those conditions before sending the request because some custom headers may change after methods refreshToken and refreshTokenRefresh. Is there a way to evaluate everything inside a RxJS operator? First condition (refreshTokenRefresh), then second (refreshToken) and finally the req.
Update: I'm getting this error: RangeError: Maximum call stack size exceeded. How to fix this?
We want to wait until some requests will be completed (evaluate order does not matter?) than do another request.
const queue = this.handleRefreshToke(this.handleRefreshTokenRefresh([])); - place there all request that should be done before we call next.handle.
Use the forkJoin to wait until all request (placed in queue) will be completed than map to another Obervable ( mergeMap ).
PS We could also move handleRefreshTokenRefresh and handleRefreshToke to separated HttpInterceptor.
EDITED To prevent recursive call of interceptors we should skip interceptors for refreshTokens call.
export const InterceptorSkipHeader = 'X-Skip-Interceptor';
#Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) { }
handleRefreshTokenRefresh(queue: Observable<void>[]) {
const Token = this.authService.getToken();
if (Token.expiresRefreshToken &&
const req = this.authService.refreshTokenRefresh(Token.tokenref)
.pipe(tap((response) => {
localStorage.setItem('tokenref', response.tokenref);
localStorage.setItem('tokenrefexp', response.tokenrefexp);
}));
return [...queue, req];
}
return queue;
}
handleRefreshToke(queue: Observable<void>[]) {
const Token = this.authService.getToken();
if (Token.expiresToken && Number(Token.expiresToken) < Date.now()) {
const req = this.authService.refreshToken(Token.tokenref)
.subscribe((response) => {
localStorage.setItem('token', response.token);
localStorage.setItem('tokenexp', response.tokenexp);
});
return [...queue, req];
}
return queue;
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.headers.has(InterceptorSkipHeader)) {
const headers = req.headers.delete(InterceptorSkipHeader);
return next.handle(req.clone({ headers }));
}
const Token = this.authService.getToken();
if (!Token) {
return next.handle(req);
}
const queue = this.handleRefreshToke(this.handleRefreshTokenRefresh([]));
return forkJoin(queue).pipe(
mergeMap(()=>{
return next.handle(req.clone({
headers: req.headers
.set('Authorization', 'Bearer ' + localStorage.getItem('token'))
.set('X-Auth-Provider', localStorage.getItem('provider')),
}));
})
);
}
}
Add InterceptorSkipHeader to refreshTokens to skip interceptors.
// AuthService
refreshTokenRefresh(token){
const headers = new HttpHeaders().set(InterceptorSkipHeader, '');
return this.httpClient
.get(someUrl, { headers })
}
refreshToken(token){
const headers = new HttpHeaders().set(InterceptorSkipHeader, '');
return this.httpClient
.get(someUrl, { headers })
}