Expired JWT token triggers 401 in Angular HttpInterceptor without automatic redirect or refresh
0 reputation · 24 Nov 2024, 19:43 UTC
0 reputation · 24 Nov 2024, 19:43 UTC
Goal: Ensure that Angular applications enforce least‑privilege access through route guards while gracefully handling expired JWT credentials.
Constraints: Route guards execute synchronously; any asynchronous token validation must return a Promise or Observable, which can affect navigation timing. An HttpInterceptor can detect 401 responses, but automatically refreshing the token inside the interceptor risks infinite loops if the refresh endpoint also returns 401, requiring additional guard conditions.
Uncertainty: Teams disagree on whether refresh logic should reside in the interceptor, a dedicated AuthService, or a third‑party library, and how to guarantee that guards evaluate the refreshed token without blocking the UI.
Should token refresh be performed inside the HttpInterceptor with loop protection, delegated to a dedicated AuthService, or handled by a third‑party library? How can teams ensure that route guards receive the refreshed token before evaluating least‑privilege claims without causing navigation delays? What verification steps confirm that a chosen refresh strategy avoids infinite loops and maintains responsive UI?
34775 reputation · 25 Nov 2024, 02:45 UTC
Place the refresh logic in a dedicated AuthService and let the HttpInterceptor call that service. The interceptor should not perform the refresh itself; it only delegates, retries the original request, and protects against infinite loops. Route guards stay synchronous, but they can return an Observable that waits for a pending refresh to finish before allowing navigation. This keeps the UI responsive while ensuring guards see the latest token.
AuthService owns token storage, refresh logic, and state, making unit‑testing straightforward.Observable pause navigation only until the refresh resolves (typically <200 ms), so the user sees a brief spinner if needed.@Injectable({providedIn: 'root'})
export class AuthService {
private token$ = new BehaviorSubject(null);
private refreshing = false;
refreshToken(): Observable {
if (this.refreshing) return this.token$;
this.refreshing = true;
return this.http.post<{token:string}>('/api/refresh', {})
.pipe(
tap(res => this.token$.next(res.token)),
map(res => res.token),
finalize(() => this.refreshing = false)
);
}
isAuthenticated(): Observable {
return this.token$.pipe(map(t => !!t));
}
}
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest, next: HttpHandler): Observable> {
const token = this.auth.token$.value;
const authReq = token ? req.clone({setHeaders: {'Authorization': `Bearer ${token}`}}) : req;
return next.handle(authReq).pipe(
catchError(err => {
if (err.status === 401 && !req.url.endsWith('/api/refresh')) {
return this.auth.refreshToken().pipe(
switchMap(newToken => {
const retryReq = req.clone({setHeaders: {'Authorization': `Bearer ${newToken}`}});
return next.handle(retryReq);
})
);
}
return throwError(() => err);
})
);
}
}
@Injectable({providedIn: 'root'})
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(): Observable {
return this.auth.isAuthenticated().pipe(
switchMap(isAuth => {
if (isAuth) return of(true);
this.router.navigate(['/login']);
return of(false);
})
);
}
}
AuthService.To fine‑tune loop protection, could you confirm whether your refresh endpoint returns a 401 on failure or a different status code?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.