I'm working on an Ionic app and trying to cash in the refresh token when a user gets a 401 response on an HTTP request. I found a few examples floating around online and was able to get this one (https://www.intertech.com/Blog/angular-4-tutorial-handling-refresh-token-with-new-httpinterceptor/) working with the exception of multiple requests coming in at once.
The problem I'm having is the first call in the series of calls invokes the refresh token and retries successfully, while the other ones never get retried. If I take the .filter and .take off the subject return for requests where a refresh is already in progress, the calls do get retried but without the new token. I'm pretty new when it comes to observables and subjects so I'm not really sure what the problem could be.
requests
this.myService.getData().subscribe(response => {this.data = response.data;});
this.myService.getMoreData().subscribe(response => {this.moreData = response.data;});
this.myService.getEvenMoreData().subscribe(response => {this.evenMoreData = response.data;});
interceptor
#Injectable()
export class HttpInterceptor implements HttpInterceptor {
isRefreshingToken: boolean = false;
tokenSubject = new BehaviorSubject<string>(null);
tokenService: tokenService;
constructor(private authService: AuthService, private injector: Injector) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
return this.authService.getUser().flatMap(user => {
request = this.addToken(request, next, user.accessToken);
return next
.handle(request)
.catch(error => {
if (error instanceof HttpErrorResponse) {
switch ((<HttpErrorResponse>error).status) {
case 401:
return this.handle401(request, next, user);
}
} else {
return Observable.throw(error);
};
})
});
}
addToken(request: HttpRequest<any>, next: HttpHandler, accessToken: string): HttpRequest<any> {
return request.clone({ setHeaders: { Authorization: 'Bearer ' + accessToken }})
}
handle401(request: HttpRequest<any>, next: HttpHandler, user: any) {
if (!this.isRefreshingToken) {
this.isRefreshingToken = true;
this.tokenSubject.next(null);
this.tokenService = this.injector.get(tokenService);
return this.tokenService.refresh(user.refreshToken)
.switchMap(refreshResponse => {
if (refreshResponse) {
this.authService.setUser(refreshResponse.id_token, refreshResponse.access_token, refreshResponse.refresh_token);
this.tokenSubject.next(refreshResponse.accessToken);
return next.handle(this.addToken(request, next, refreshResponse.access_token));
}
else {
//no token came back. probably should just log user out.
}
})
.finally(() => {
this.isRefreshingToken = false;
});
}
else {
return this.tokenSubject
.filter(token => token != null)
.take(1)
.switchMap(token => {
return next.handle(this.addToken(request, next, token));
});
}
}
}
It looks to me like you didn't have the right token:
You had:
this.tokenSubject.next(refreshResponse.accessToken);
Should be:
this.tokenSubject.next(refreshResponse.access_token);
I actually ended up solving this by moving the subject to my auth service and doing a next in the setUser method. Then in the else statement in my 401 method, I returned the subject from a new method on my auth service and that fixed it. I still needed the take(1) but was able to get rid of the filter since I ended up not using a BehaviorSubject.
I faced a similar issue in the past. For some unknown reason (at least to me), when I intercept the 401, I make the refresh and retry, but retry operation goes cancelled.
Nevertheless, I realised that I can read the JWT expiration on client-side, so I tricked the system by saving the token expiration time. I then made routing events (say onViewWillEnter) check the expiration and, if token expired, refresh it.
This mechanism is totally transparent to the user, ensures that auth token nor refresh token expire if the user stays too long without performing HTTP requests and, most importantly, reduces latencies as you never get a 401 response (which, in your scenario, translates to three requests).
One simple way to achieve this is by means of a guard:
canActivate(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot) {
if (this.refreshTokenService.isExpired) {
this.tokenEvent_.next();
return false;
} else {
this.refreshTokenService.refresh();
}
where refreshTokenService is a utility service that has the tokens and a method for performing refresh via HTTP. tokenEvent is a rxjs/Subject: it is subscribed in guard constructor and each time a new event comes, it redirects to login page.
Adding this guard on every route ensures that the token is always non-expired.
Related
I am implementing JWT refresh token in my angular project. I am following the below guide for that.
https://angular-academy.com/angular-jwt/
Here is my code:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const user: any = this.storage.user;
const addToken = !req.urlWithParams.includes('token');
const token = user ? user.token : null;
if (token && !req.url.includes('token=') && addToken) {
req = this.addToken(req, user.token);
}
return next.handle(req).pipe(switchMap((event) => {
if (event instanceof HttpResponse && event.body.code === 401 && token) {
return this.handle401Error(req, next);
}
return next.handle(req);
}));
}
private addToken(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
setParams: {
token
}
});
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.getRefreshedJWT().pipe(
switchMap((res: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(res.token);
return next.handle(this.addToken(request, res.token));
}));
} else {
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap(jwt => {
return next.handle(this.addToken(request, jwt));
}));
}
}
getRefreshedJWT() {
const jwt_refresh_url = 'api/v3/token/refresh?token=' + this.storage.user.token;
return this.http.getFromAccountsApi(jwt_refresh_url)
.pipe(tap((token) => {
this.storeJwtToken(token);
}));
}
private storeJwtToken(jwt: string) {
const user = this.storage.user;
user.token = jwt;
this.storage.user = user;
}
Btw. the reason I am not doing this inside catchError is because our backend is structured like it will always send HTTP status code 200 and inside that response they will send custom http code based on error such as 401, 500 or success such as 200 and etc. So it won't go inside catchError since it looks for HTTP status codes other than 200.
Now my issue is after implementing the inceptor now my API's getting called multiple times. See screenshot below:
Been stuck since yesterday and haven't found any proper solution yet. Would be great if anyone could point what I am doing here and how do I solve it?
If you have any further query, do let me know. Thank you..
A tip for:
Btw. the reason I am not doing this inside catchError is because our backend is structured like it will always send HTTP status code 200 and inside that response they will send custom http code based on error such as 401, 500 or success such as 200 and etc. So it won't go inside catchError since it looks for HTTP status codes other than 200.
You can do a map in the response from the server and check if theres an error, and then throw an error from there, then catchError should work on sequent pipes.
The error is because you are returning the handle in the switchMap making the request being called again.
return next.handle(req);
Change that line to:
return of(event)
And it should work
We are currently implemented ADAL.js using Angular, but unfortunately the token does not renew automatically, the code is the following to handle the logic to renew the token
We added some logic to check if the token is expired and renew it automatically, but this is not working for me. The application kick you out to white page, and it is required to refresh the browser in order sign in again.
#Injectable({
providedIn: 'root',
})
export class AuthenticationService {
checkAuthInterval$: Observable<number>;
constructor(
private jwtHelperService: JwtHelperService,
private adal: MsAdalAngular6Service
) {
this.checkAuthInterval$ = interval(6000).pipe(
tap(() => {
this.updateSession();
})
);
this.checkAuthInterval$.subscribe();
}
public isAuthorized(allowedRoles: string[]): boolean {
if (allowedRoles == null || allowedRoles.length === 0) {
return true;
}
const token = localStorage.getItem(
authenticationSettings.authenticationToken
);
const status = this.jwtHelperService.isTokenExpired(token);
const decodeToken = this.jwtHelperService.decodeToken(token);
if (!decodeToken) {
console.log('Invalid token');
return false;
}
decodeToken.roles = decodeToken.roles || [];
const allow = allowedRoles.some(r => decodeToken.roles.includes(r));
return allow;
}
get profileInfo(): User {
return this.adal.userInfo;
}
get expirationDate() {
const expiration = this.profileInfo.profile.exp * 1000;
return moment(expiration);
}
get issueDate() {
const expiration = this.profileInfo.profile.iat * 1000;
return moment(expiration);
}
get hasExpired(): boolean {
const today = moment();
return this.expirationDate.isSameOrBefore(today);
}
get shouldRenew(): boolean {
const today = moment().subtract(5, 'minutes');
return this.expirationDate.isSameOrBefore(today);
}
get isAuthenticated() {
const authenticated = this.adal.isAuthenticated;
return authenticated;
}
updateSession() {
if (this.hasExpired) {
this.adal.login();
} else if (this.shouldRenew) {
this.adal.RenewToken(environment.ApiBaseUrl);
}
}
}
Generally speaking, we don't recommend trying to renew the token yourself, as you should let the library handle that as needed. Our recommendation for acquiring tokens is as follows:
Check if the user has signed in, and if not, send them to the login screen: https://github.com/AzureAD/azure-activedirectory-library-for-js#2-login-the-user
When your app is in the process of making a network request to a resource (e.g. Microsoft Graph) that requires an access token, call acquireToken with the scopes needed for that request.
If there is already a valid token in the cache, that token should be returned to you.
If there is not a valid token in the cache (e.g. an expired token) and your user still has an active session with AAD, the library will make a network request to acquire a new token, and return that new token to you.
If there is not a valid token in the cache, and your user does not have an active session with AAD or there is some other scenario that requires interaction (e.g. consenting to new scopes), the library will return an error and you will need to invoke one of the interactive methods (acquireTokenPopup or acquireTokenRedirect) to resolve those issues, and then the new token will be returned to your application: https://github.com/AzureAD/azure-activedirectory-library-for-js#3-get-an-access-token
Automatically trying to renew the token yourself may cause other issues, which is why we don't recommend it. Instead, call acquireToken lazily right before you need to the token, and then have error handling in place (as described above) to invoke interactive methods if needed.
Have you tried the process described above, and if so, what were the specific issues/errors that you encountered? Are there errors in the browser console or the network tab?
I have Angular Service which make some http requests, but I need to get headers for those requests from Promise. Here how it works right now, I convert my promise to Observable:
export class SomeService {
constructor(private http: HttpClient, private auth: AuthenticationService) {}
getInfo(id: string): Observable<any> {
return this.auth.getToken().pipe(mergeMap((token: any) => {
let httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
})
}
return this.http.get(`${this.serverURL}/info`, httpOptions);
}))
}
}
getToken() {
return from(this.storage.get(TOKEN_KEY));
}
But obviously I have like 20-50 requests and it's not too good to fetch auth token with every request.
I want to fetch my token once and use it for all request. Also I have other header which comes from Promise I need to use in my request. So, how can I get my async headers once (probably in constructor) in this case?
First off consider if optimizing this code is actually needed. Optimizing for performance is often only useful in parts of code which are run very frequently. When you say you do some 20 to 50 requests it does not sound like it's used a lot (other parts of your app are probably a lot more cpu intensive).
That being said: if you still want to solve this you could indeed fetch the token in your constructor.
export class SomeService {
// We store the observable here for later use
private getTokenObservable: Observable<string>;
constructor(private http: HttpClient, private auth: AuthenticationService) {
// Retrieve the token now and store the observable
this.getTokenObservable = getToken();
}
getInfo(id: string): Observable<any> {
// Zip the two observables together
return zip(
// Re use the previously stored observable
this.getTokenObservable,
// Also request the last login
this.auth.getLastLogin()
).pipe(mergeMap((result) => {
const token = result[0];
const lastLogin = result[1];
let httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
})
}
return this.http.get(`${this.serverURL}/info`, httpOptions);
}))
}
}
getToken() {
return from(this.storage.get(TOKEN_KEY));
}
This works because you can subscribe multiple times to the same observable. So we request and store the getToken observable only once and then re use it for each request.
Also note how we use the zip operator provided by rxjs. This allows us to merge two observables together so we can handle the result of both observables in a single function.
You can write separate service for getting the token and call the service only once. For next time when you need token check if already you have token value in variable of service, so can skip http request and simply return token to requester.
private _tokenObsevable = new BehaviorSubject<string>(null);
constructor(...) {
this.auth.getToken().subscribe(token => this._tokenObservable.next(token))
}
getInfo(...) {
this._tokenObservable.pipe(
switchMap(token => // rest the same
)
optional you can create getter like
get tokenObservable() {
return this._tokenObservable.pipe(filter(val => !!val))
}
in this case you get only non null values, but also if token won't appeare you'll get stuck
In this case you can create a utility function in a file and can import it everywhere you need this token, or can create a service if this token is coming form a server call and then can inject it in all the places you need this token.
This token can also be saved in a const files and imported in the place wherever required.
To authenticate users in my Angular app i use access tokens with expiry time X seconds and refresh tokens that can be used to prolong the auth for another X seconds.
So the flow is this:
A user signs in. Both the access and refresh tokens are stored in local storage
A timer is set (5% shorter than X seconds).
When the timer is done, a refresh token request is sent to the server and the local storage is updated with the resulting (new) access and refresh tokens.
My problem is this:
If I have multiple tabs open, I will inevitably end up in situations where the refresh is triggered from multiple tabs at the same time. The server will accept the first request, but throw a 400 Bad Request - Invalid refresh token for the subsequent requests, since it considers them used.
Does anyone have a good idea how this could be solved? How does one synchronize things across tabs/windows? I have a couple of ideas but they all seem a bit far fetched:
If the response is 400 Bad Request, then retry in a little while (or check if there is a valid updated token already).
Try to synchronize the server requests across tabs by posting messages between them.
dont set timer , add interceptor and catch error and if you got 401 error , do your refresh token flow and then repeat failed request with new token
intercept(request: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status == 401) {
return this.refreshToken(request, next);
}
}
return throwError(error);
})
);
private refreshingInProgress: boolean = false;
private accessTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
private refreshToken(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.refreshingInProgress) {
this.refreshingInProgress = true;
this.accessTokenSubject.next(null);
return this.authenticationService.refreshToken().pipe(
switchMap((res: any) => {
this.refreshingInProgress = false;
this.accessTokenSubject.next(res);
// repeat failed request with new token
return next.handle(this.addToken(request, res));
})
);
} else {
// wait while getting new token
return this.accessTokenSubject.pipe(
filter((token) => token !== null),
take(1),
switchMap((token) => {
// repeat failed request with new token
return next.handle(this.addToken(request, token));
})
);
}
}
private addToken(request: HttpRequest<any>, token: Credentials) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token.access_token}`,
},
});
}
Ok this has been bugging me for a while, wondering if any one could show me a good way of chaining Observables between multiple services.
In the example below in the Auth class what would be a good way of creating an Observable from the this.api.postSignIn() so the signInSubmit() can subscribe to it back in the component? It is worth noting that this.api.postSignIn() is subscribing to an Angular2 http request.
Is this a bit of an anti pattern and is there better ways of doing this?
Basically the functionality I would like to achieve is:
Component - responsible for collecting the sign in data and sending it to the auth service in the correct format. Then once the Auth sign in is complete navigate to the admin page.
Service - Make api call to get token, set token via the token service and set isSignedIn bool then defer control back to the calling component.
#Component({...})
export class SignIn {
private signIn:SignInModel = new SignInModel();
constructor(private auth:Auth, private router:Router) {
}
ngOnInit() {
}
signInSubmit() {
this.auth.signIn(this.signIn)
.subscribe(
() => {
this.router.navigate(['/admin']);
}
)
}
}
#Injectable()
export class Auth {
private isSignedIn:boolean = false;
constructor(private api:Api, private tokenService:TokenService) {
}
public signIn(signIn:SignInModel) {
return this.api.postSignIn(signIn)
.subscribe(
response => {
this.tokenService.set(new TokenModel(response.token));
this.isSignedIn = true;
},
error => {
console.log(error);
}
);
}
public signOut() {
}
}
I would leverage the do and catch operators instead of subscribing within the signIn method.
Here is the refactored signIn method:
public signIn(signIn:SignInModel) {
return this.api.postSignIn(signIn)
.do(
response => {
this.tokenService.set(new TokenModel(response.token));
this.isSignedIn = true;
})
.catch(
error => {
console.log(error);
}
);
}
In your case, you can't subscribe on the returned object of this method since the subscribe method returns a subscription and not an observable. So you can't subscribe on it...