Skip to content

Instantly share code, notes, and snippets.

@umutyerebakmaz
Created August 9, 2023 15:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save umutyerebakmaz/43f0555aa1865f4e7753fcec16650ff1 to your computer and use it in GitHub Desktop.
Save umutyerebakmaz/43f0555aa1865f4e7753fcec16650ff1 to your computer and use it in GitHub Desktop.
Auth Interceptor
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { catchError, filter, switchMap, take } from 'rxjs/operators';
import { AuthService } from '@services/auth.service';
import { Router } from '@angular/router';
import { TokenType } from '@app/graphql.module';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private newAccessTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private authService: AuthService, private router: Router) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const accessToken = this.authService.getAccessToken();
if (accessToken === null) {
request = request.clone({
setHeaders: {
Accept: 'charset=utf-8',
},
});
}
if (accessToken) {
request = this.setRequest(request, accessToken, 'access');
}
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
const e = error.error.errors[0];
if (e.message === 'expired token') {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.newAccessTokenSubject.next(null);
const refreshToken = this.authService.getRefreshToken();
return this.authService.refreshAccessToken(refreshToken).pipe(
switchMap(res => {
this.isRefreshing = false;
this.newAccessTokenSubject.next(res.accessToken);
const clonedRequest = this.setRequest(request, res?.accessToken, 'refresh');
console.log(clonedRequest);
return next.handle(clonedRequest).pipe();
}),
catchError(refreshError => {
this.isRefreshing = false;
this.authService.removeTokens();
this.router.navigate(['login']);
throw new Error(refreshError);
})
);
} else {
return this.newAccessTokenSubject.pipe(
filter(token => token !== null),
take(1),
switchMap(subject => {
console.log(4, subject);
const cloneRequest2 = this.setRequest(request, subject.accessToken, 'access');
return next.handle(cloneRequest2);
})
);
}
}
throw new Error(error as any);
})
);
}
private setRequest(request: HttpRequest<any>, token: string, tokenType: TokenType): HttpRequest<any> {
return request.clone({
setHeaders: {
Accept: 'charset=utf-8',
Authorization: `Bearer ${token}`,
tokenType: tokenType,
},
});
}
}
@umutyerebakmaz
Copy link
Author

In this line, I want you to extend the duration of the access token by sending the refresh token and tokenType information to the server, but every time I try, the request is sent with an access token and an access token. Can anyone help me with what I missed?

@aliosmanyuksel
Copy link

It looks like the issue is arising because of how you're handling the refreshed token and token type when cloning the request.

You mentioned that on line 43 you want to extend the duration of the access token by sending the refresh token and tokenType information to the server. But in the current logic, you're using the new access token (res?.accessToken) and setting the tokenType to 'refresh' which contradicts your statement. If you want to send the refresh token and 'refresh' tokenType, you should do so.

Can you test it?

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { catchError, filter, switchMap, take } from 'rxjs/operators';
import { AuthService } from '@services/auth.service';
import { Router } from '@angular/router';
import { TokenType } from '@app/graphql.module';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    private isRefreshing = false;
    private newAccessTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);

    constructor(private authService: AuthService, private router: Router) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const accessToken = this.authService.getAccessToken();

        if (accessToken === null) {
            request = request.clone({
                setHeaders: {
                    Accept: 'charset=utf-8',
                },
            });
        }

        if (accessToken) {
            request = this.setRequest(request, accessToken, null, 'access');
        }

        return next.handle(request).pipe(
            catchError((error: HttpErrorResponse) => {
                const e = error.error.errors[0];
                if (e.message === 'expired token') {
                    if (!this.isRefreshing) {
                        this.isRefreshing = true;
                        this.newAccessTokenSubject.next(null);
                        const refreshToken = this.authService.getRefreshToken();
                        return this.authService.refreshAccessToken(refreshToken).pipe(
                            switchMap(res => {
                                this.isRefreshing = false;
                                this.newAccessTokenSubject.next(res.accessToken);
                                const clonedRequest = this.setRequest(request, res?.accessToken, refreshToken, 'refresh');
                                return next.handle(clonedRequest);
                            }),
                            catchError(refreshError => {
                                this.isRefreshing = false;
                                this.authService.removeTokens();
                                this.router.navigate(['login']);
                                throw new Error(refreshError);
                            })
                        );
                    } else {
                        return this.newAccessTokenSubject.pipe(
                            filter(token => token !== null),
                            take(1),
                            switchMap(subject => {
                                const cloneRequest2 = this.setRequest(request, subject, null, 'access');
                                return next.handle(cloneRequest2);
                            })
                        );
                    }
                }
                throw new Error(error as any);
            })
        );
    }

    private setRequest(request: HttpRequest<any>, accessToken: string, refreshToken: string, tokenType: TokenType): HttpRequest<any> {
        const token = tokenType === 'access' ? accessToken : refreshToken;
        return request.clone({
            setHeaders: {
                Accept: 'charset=utf-8',
                Authorization: `Bearer ${token}`,
                tokenType: tokenType,
            },
        });
    }
}

@umutyerebakmaz
Copy link
Author

@aliosmanyuksel Hello, I tested and the result did not change. By default, the interceptor goes as access token and tokenType = 'access' in the header, and if this request gives an error as expired token from the server, it should treat the refreshToken endpoint as refresh token and tokenType = 'refresh'. In the current situation, the mutable is behaving correctly or there is something wrong with my setup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment