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);`
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);`
Share Improve this question edited Nov 19, 2024 at 13:18 JSON Derulo 18k11 gold badges57 silver badges75 bronze badges asked Nov 19, 2024 at 13:12 ErenbErenb 211 bronze badge 2- i think you could try to add a flag in auth service and setting it in refresh token and you can in guard to check this flag and based on it you can make your redirections – Hager Khaled Commented Nov 19, 2024 at 14:49
- If the token exploded while checking authService.isAuthenticated(), I will recreate it with refresh token – Erenb Commented Nov 19, 2024 at 17:46
1 Answer
Reset to default 0Unless 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.