prawin-s / LEARNings

0 stars 0 forks source link

Code Snippet : AuthInterceptor #7

Open prawin-s opened 3 years ago

prawin-s commented 3 years ago
// - Get accessToken and accessTokenExpiry
// - Attempt to refresh if accessToken expired
// - Attempt to refresh once if a request returns 401
// - Handle 401 when all have failed -> force logout ->redirect to login
// - Handle 403 -> redirect to not authorized

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    private allowed = ['/token/refresh', '/assets'];

    constructor(
        private readonly authStateService: AuthStateService,
        private readonly authService: AuthService,
        private readonly redirectService: RedirectService,
    ) { }

    private static addToken(req: HttpRequest<unknown>, token: unknown) {
        return req.clone({
            headers: req.headers.set('Authorization', `Bearer ${token}`),
        });
    }

    private refreshToClonedRequest(req: HttpRequest<unknown>, next: HttpHandler) {
        return concat(
            this.authService.refreshToken(),
            this.newTokenToClonedRequest(req, next),
        );
    }

    private newTokenToClonedRequest(
        req: HttpRequest<unknown>,
        next: HttpHandler,
    ) {
        return this.authStateService.token$.pipe(
            mergeMap((newToken) =>
                next.handle(AuthInterceptor.addToken(req, newToken)),
            ),
        );
    }

    intercept(
        request: HttpRequest<unknown>,
        next: HttpHandler,
    ): Observable<HttpEvent<unknown>> {
        if (this.allowed.some((url) => request.url.includes(url))) {
            // Allowed URLs do not need bearer token
            return next.handle(request);
        }

        let hasRetried = false;
        return combineLatest([
            this.authStateService.token$,
            this.authStateService.tokenExpiry$,
        ]).pipe(
            take(1),
            mergeMap(([token, expiry]) => {
                if (!token) {
                    // No token, just forward the request
                    return next.handle(request);
                }

                const cloned = AuthInterceptor.addToken(request, token);
                return defer(() => {
                    if (expiry && moment().isAfter(expiry)) {
                        return this.refreshToClonedRequest(request, next) as Observable<
                            HttpEvent<unknown>
                        >;
                    }

                    return next.handle(cloned).pipe(
                        retryWhen((errObs) =>
                            errObs.pipe(
                                mergeMap((err: SwaggerException) => {
                                    if (err.status === 401 && !hasRetried) {
                                        hasRetried = true;
                                        return this.refreshToClonedRequest(
                                            request,
                                            next,
                                        ) as Observable<HttpEvent<unknown>>;
                                    }
                                    return throwError(err);
                                }),
                            ),
                        ),
                    );
                }).pipe(
                    catchError((err) => {
                        if (SwaggerException.isSwaggerException(err)) {
                            if (err.status === 401) {
                                this.authService.logout().subscribe({
                                    complete: () => {
                                        this.redirectService.redirectToLogin();
                                    },
                                });
                            } else if (err.status === 403) {
                                this.redirectService.redirectToNotAuthorized();
                            }
                        }

                        return throwError(err);
                    }),
                );
            }),
        );
    }
}