Reputation: 21
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
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