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);
}
}
})
);
}
Related
I need help creating a function that gets the categories from my API and also checks for status of the call. I have written my function like the code bellow but it keep showing me the bellow error:
Argument of type '(token: GetResult) => Subscription' is not assignable to parameter of type '(value: GetResult, index: number) => ObservableInput<any>'
Here is the code of my function:
getCategories() {
return from(Preferences.get({ key: "TOKEN_KEY" })).pipe(
switchMap((token) => {
const headers = new HttpHeaders().set(
"Authorization",
`Bearer ${token.value}`
);
return this.httpClient
.get(`${environment.apiUrl}categories`, {
headers,
observe: "response",
})
.subscribe(
(res) => {
return res.body;
},
(error) => {
console.log(error.status);
if (error.status === 400) {
console.log(error.error.message);
}
if (error.status === 401) {
this.authService.logout();
this.router.navigateByUrl("/login", { replaceUrl: true });
}
}
);
})
);
}
You should not use .subscribe inside a subscription. .subscribe returns a Subscription which can't be assigned to the Observable that a switchMap should return.
Use catchError to to handle the error case and a map to handle the success case.
getCategories() {
return from(Preferences.get({ key: 'TOKEN_KEY' })).pipe(
switchMap(token => {
const headers = new HttpHeaders().set('Authorization', `Bearer ${token.value}`);
return this.httpClient.get(
`${environment.apiUrl}categories`,
{ headers, observe: 'response' }
)
}),
catchError(error => {
console.log(error.status);
if (error.status === 400) {
console.log(error.error.message);
}
if (error.status === 401) {
this.authService.logout();
this.router.navigateByUrl('/login', { replaceUrl: true });
}
return EMPTY
}),
map(res => res.body)
)
};
How to direct error response with Axios Interceptor. I used axios interceptor for all http requests!
the expectation is that when the server error 500, 501 etc, will be redirected to route "/500"
export const request = axios.create({ baseURL: BASE_URL_API });
request.interceptors.request.use(
async (config: AxiosRequestConfig) => {
try {
const token = GetCookie('token');
config.headers = {
Authorization: `Bearer ${token}`,
};
return config;
} catch (errorConfig) {
return Promise.reject(errorConfig);
}
},
error => {
return Promise.reject(error);
},
);
// Add a response interceptor
request.interceptors.response.use(
response => {
return response;
},
async (error: AxiosError) => {
const status = error.response?.status;
try {
if (status === 401 && error.response?.data.error.message === "Full authentication is required to access this resource" ) {
RemoveCookie('token')
RemoveCookie('_currentUser')
window.location.href = '/';
} else {
return Promise.reject(error);
}
return Promise.reject(error);
} catch (errorValue) {
return Promise.reject(errorValue);
}
},
);
The below angular code switchMap is not working, I'm not sure what error I made. Under the switchMap second API call not triggering '/upload/file'
zip(
this.service.submitForm(formValue),
this.service.upload(fData)
).subscribe(
([submitForm, upload]) => {
if (submitForm === 'success' && upload === 'Ok') {
//Redirect confirmation page
}
},
(err) => {
console.log(err, 'ERORORO');
}
)
//Service code
upload(formData): Observable <any> {
return this.sessionService.keepAlive().pipe(
switchMap(data => {
let token = data.jwtToken;
console.log(token, 'TOKEN SESSION');
// getting output as Bearer xyz
// with formData as req
const request_config = {
headers: {
"Authorization": token
}
};
console.log("REQUEST CONFIG", request_config); // getting output
return this.http.post < any > (
'/upload/file',
formData,
request_config
).pipe( // this is not working
map((res) => {
console.log(res, 'RESPONSE');
return res.status;
}),
catchError((error: HttpErrorResponse) => {
throw error;
})
)
})
)
}
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);
The problem is that, when the access token is expired and I do any click, then access token does not refresh. But when I do the second click, it refreshes. For the first click, I get 'GET 401 error'. The actual problem for me is to get access token before any HTTP request in the interceptor.
I tried switchMap for my code to wait for the access token, but it didn't work.
Do you have any ideas how could I fix this?
export class DevelopmentInterceptor implements HttpInterceptor {
constructor(
private authService: AuthService,
private router: Router,
private toastr: ToastrService,
private translate: TranslateService
) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const headers = new HttpHeaders({
'Content-Type': 'application/json; charset=utf-8',
'Authorization': `Bearer ${this.authService.getAccessToken()}`
});
let apiReq = null;
if (req.url.indexOf('i18n') >= 0) {
apiReq = req;
} else if (req.url.indexOf('token') >= 0) {
apiReq = req.clone({ url: environment.authServerUrl + `${req.url}` });
} else if (req.url.indexOf('sign-up') >= 0) {
apiReq = req.clone({ url: environment.signupUrl });
} else if (req.url.indexOf('api/users') >= 0 || req.url.indexOf('api/roles') >= 0 || req.url.indexOf('api/permissions') >= 0) {
apiReq = req.clone({ headers: headers, url: environment.authServerUrl + `${req.url}` });
} else {
apiReq = req.clone({ headers: headers, url: environment.backenUrl + `${req.url}` });
}
if (req.url.endsWith('token')) {
return next.handle(apiReq).catch((err: any) => { //<--if error use a catch
if (err instanceof HttpErrorResponse) {
return this.handleError(err);
}
});
} else {
return this.authService.checkExpiry().switchMap( (result) => {
if (result) {
return next.handle(apiReq)
.catch((err: any) => { // <--if error use a catch
if (err instanceof HttpErrorResponse) {
return this.handleError(err);
}
});
}
} )
}
}
private handleError(err: Response | any) {
...
}
}
public checkExpiry() : Observable<any> {
if (!this.cookieService.get('user_id')) {
this.removeTokens(); // not logged in
return Observable.of(true);
} else if (!this.cookieService.check('access_token')) {
if (this.cookieService.check('refresh_token')) {
if (this.secondsTillExpiry('refresh_token') > 0) {
return this.refreshAccessToken().switchMap((data:any) => {
if (data) {
this.saveTokenInCookies(data);
this.updateExpiration(data);
return Observable.of(true);
}
})
} else {
this.router.navigate(['/login']);
this.removeTokens();
return Observable.of(true);
}
}
} else if (this.cookieService.check('access_token') ) {
return Observable.of(true);
}
}
public secondsTillExpiry(tokenMode: string): any {
if (tokenMode == 'access_token') {
return ((new Date(1970, 0,
1).setSeconds(jwt_decode(this.getAccessToken()).exp)) -
(Math.round(Date.now()) / 1000));
} else if (tokenMode == 'refresh_token') {
return ((new Date(1970, 0,
1).setSeconds(jwt_decode(this.getRefreshToken()).exp)) -
(Math.round(Date.now()) / 1000));
}
}
private refreshAccessToken(): Observable<Object> {
const params = 'refresh_token=' + this.getRefreshToken() +
'&grant_type=refresh_token';
return this.http.post(this.authUrl, params, this.getOptions());
}
you can also check expire token before send any request in CanActivate and if it's valid token so user can is navigated to required route else it will be redirected to login
here is an example to handle this
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
let accesstoken: string = next.queryParams.accesstoken;
if (this.authService.IsAuthenticated) {
let user = this.authService.GetUser();
let CurrentDate = new Date();
let date = CurrentDate.getFullYear() + "-" + (CurrentDate.getMonth() + 1) + "-" + CurrentDate.getDate();
if (user.expire_date) {
if (Date.parse(date) <= Date.parse(user.expire_date)) {
if (accesstoken) {
// if token in url, redirect to url without token :)
if (user.uuid == accesstoken)
this.router.navigateByUrl(window.location.pathname);
// if new token is not the same of current one, Register with new token
else {
return this.authService.checkAccess(accesstoken).pipe(
map(data => {
if (data === true) {
if (accesstoken) {
this.router.navigateByUrl(window.location.pathname);
}
return true;
} else {
this.router.navigate(['/login']);
return false;
}
})
);
}
}
return true;
}
else if (Date.parse(date) > Date.parse(user.expire_date)) {
this.router.navigate(['/login']);
return false;
}
}
}
else {
this.router.navigate(['/login']);
return false;
}
}
you should process it according to your code