Erenb
Erenb

Reputation: 21

Angular auth bug guard and interceptor

I want to do token-based authorization with angular, but I have a bug like this I can protect pages with auth guard if the token has not exploded, but I cannot use the interceptor because auth guard protects pages on a per page basis while the interceptor protects http requests so I can't create a new token with refresh token and it logins when the acces token explodes how can I solve this bug

Auth.guard.js

export const AuthGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  if (!authService.isAuthenticated()) {
    console.log("User not authenticated, redirecting to login");
    router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
    return false;
  }
  return true;
};

Auth.interceptor.js

import { HttpInterceptorFn, HttpRequest, HttpErrorResponse, HttpContextToken } from '@angular/common/http';
import { inject } from "@angular/core";
import { AuthService } from "./auth.service";
import { catchError, of, switchMap } from "rxjs";


export const AuthInterceptor: HttpInterceptorFn = (req, next) => {
  const authSvc = inject(AuthService);



  if (req.context.get(IS_PUBLIC)) {
    return next(req);
  }

  const accessToken = localStorage.getItem('accessToken');
  if (accessToken) {
    const authRequest = addAuthorizationHeader(req, accessToken);
    return next(authRequest).pipe(
      catchError((error) => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          return authSvc.refreshToken().pipe(
            switchMap((newAccessToken) => {
              if (newAccessToken) {
                const authRequestWithNewToken = addAuthorizationHeader(req, newAccessToken.data.accessToken);
                return next(authRequestWithNewToken);
              } else {
                authSvc.logout();
                return of();
              }
            })
          );
        }
        authSvc.logout();
        return of();
      })
    );
  } else {
    authSvc.logout();
    return of();
  }
};

const addAuthorizationHeader = (req: HttpRequest<any>, token: string) => {
  return req.clone({
    headers: req.headers.set('Authorization', `Bearer ${token}`)
  });
};

export const IS_PUBLIC = new HttpContextToken(() => false);`

Upvotes: 2

Views: 44

Answers (1)

Carlos Bensant
Carlos Bensant

Reputation: 297

Unless it is strictly necessary, do not make unnecessary HTTP requests for every route change, use instead an Angular HTTP Interceptor(s) to validate if the incoming response was (un)authorized/forbidden.

What you're trying to achieve is as easy as combining 2 HTTP Interceptors (Auth and Token Interceptor(s)) and one Guard (Auth Guard).

Add canActivate to authenticated routes, excepting the sign-in, register page:

const routes: Routes = [
  {
    path: 'sign-in',
    component: LoginComponent,
  },
  {
    path: '',
    component: MainComponent,
    children: [
      {
        path: 'home',
        component: HomeComponent,
        canActivate: [AuthGuard],
      },
    ]
  },
]

Use an Angular Guard to validate the user access & permissions, or if some kind of data isn't available.

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(
    private store: Store<IStore>,
    private router: Router,
  ) {}

  canActivate(_: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
    return this.store.select(selectCurrentUser).pipe(switchMap((currentUser) => {
      // @NOTE: We can also check for user role and permissions here.
      if (currentUser) {
        return of(true);
      }

      this.redirectToLogin(state.url);
      return of(false);
    }))
  }

  redirectToLogin(returnUrl: string): void {
    this.router.navigate(['/sign-in'], { queryParams: { returnUrl } });
  }
}

Remember, do not use Angular Guard to validate if the user is still logged-in, use instead an (Auth) Http Interceptor, and whenever the user gets a 401 or 403 Http Error Response, you can redirect them to the login page since the local (storage) app can be holding stale user data.

@Injectable()
export class AuthHttpInterceptor implements HttpInterceptor {
  constructor(private modalController: ModalController) {}

  isQuickLoginModalOpen = false;
  async handleUnauthorized(req: HttpRequest<any>, next: HttpHandler) {
    try {
      if (!this.isQuickLoginModalOpen) {
        this.isQuickLoginModalOpen = true;
        const modal = await this.modalController.create({
          component: QuickLoginComponent,
          componentProps: {
            showCurrentUser: true,
          },
          initialBreakpoint: 1,
          breakpoints: [0, 0.5, 1, 0.5],
        });
        await modal.present();
        await modal.onWillDismiss();
        this.isQuickLoginModalOpen = false;
      }
    } catch (error) {
      this.isQuickLoginModalOpen = false;
    }

    return lastValueFrom(next.handle(req));
  }

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        if (req.headers.has('Auth')) {
          if (
            error instanceof HttpErrorResponse &&
            [401, 403].includes(error.status) &&
            !req.url.includes('/authenticate')
          ) {
            // I'm showing a quick login modal but you can easily call `this.router.navigate(['sign-in']);`
            from(this.handleUnauthorized(req, next));
          }
        }

        return next.handle(req);
      })
    );
  }
}
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
  constructor(
    private store: Store<IStore>,
    private router: Router
  ) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    return this.store.select(selectCurrentUser).pipe(
      first(),
      flatMap(currentUser => {
        let authReq = req;
        if (currentUser) {
          return next.handle(req.clone({
            setHeaders: {
              Authorization: `Bearer ${currentUser.accessToken}`,
            },
          }));
        }
        
        this.router.navigate(['sign-in']);
        return next.handle(authReq);
      })
    );
  }
}

Once you've learned all the beauty Angular has (Http Interceptor, Effects, etc), you will update most of the implementation of your current project.

I have designed complex Angular architecture with support for offline-first, user accounts switching (similar to Instagram), real-time, optimistic updates, queues (with retries and rollbacks), etc.

Upvotes: 0

Related Questions