JWT Auth (Interceptor & Guard)
The Angular equivalent of JWT Auth State (Blazor): an AuthService holding the current user as observable state, a functional HttpInterceptorFn that attaches the token to every request (the Angular counterpart to Blazor's AuthTokenHandler), and a functional CanActivateFn guard (the counterpart to [Authorize]).
AuthService — token storage + reactive current-user state
A BehaviorSubject holds the decoded user so any component can subscribe and re-render reactively when login/logout happens, instead of polling storage.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs';
import { jwtDecode } from 'jwt-decode';
interface JwtClaims {
sub: string;
name: string;
role: string;
exp: number;
}
interface LoginResponse {
succeeded: boolean;
message?: string;
value?: { jwt: string; expiresOn: string };
}
const TOKEN_KEY = 'auth_token';
@Injectable({ providedIn: 'root' })
export class AuthService {
private currentUserSubject = new BehaviorSubject<JwtClaims | null>(this.readStoredUser());
currentUser$: Observable<JwtClaims | null> = this.currentUserSubject.asObservable();
constructor(private http: HttpClient) {}
login(email: string, password: string): Observable<LoginResponse> {
return this.http.post<LoginResponse>('/auth/login', { email, password }).pipe(
tap((response) => {
if (response.succeeded && response.value) {
localStorage.setItem(TOKEN_KEY, response.value.jwt);
this.currentUserSubject.next(jwtDecode<JwtClaims>(response.value.jwt));
}
})
);
}
logout(): void {
localStorage.removeItem(TOKEN_KEY);
this.currentUserSubject.next(null);
}
getToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
get currentUser(): JwtClaims | null {
return this.currentUserSubject.value;
}
isAuthenticated(): boolean {
const user = this.currentUserSubject.value;
if (!user) return false;
return user.exp * 1000 > Date.now();
}
private readStoredUser(): JwtClaims | null {
const token = this.getToken();
if (!token) return null;
try {
const claims = jwtDecode<JwtClaims>(token);
return claims.exp * 1000 > Date.now() ? claims : null;
} catch {
return null;
}
}
}
Functional interceptor — attaches the token to every request
Modern Angular (16+) interceptors are plain functions, not classes — same role as Blazor's AuthTokenHandler : DelegatingHandler: every outgoing request passes through it, and it adds the header without any call site having to remember to.
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { AuthService } from './auth.service';
import { Router } from '@angular/router';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const router = inject(Router);
const token = auth.getToken();
const authReq = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next(authReq).pipe(
catchError((error) => {
if (error.status === 401) {
auth.logout();
router.navigate(['/login']);
}
return throwError(() => error);
})
);
};
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([authInterceptor])),
// ...
],
};
Functional route guard — the Angular [Authorize]
A CanActivateFn gates a route the same way [Authorize] gates a controller action. A second guard variant checks a role claim, mirroring [Authorize(Roles = "Admin")].
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) return true;
router.navigate(['/login']);
return false;
};
export const roleGuard = (allowedRoles: string[]): CanActivateFn => () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated() && allowedRoles.includes(auth.currentUser?.role ?? '')) {
return true;
}
router.navigate(['/unauthorized']);
return false;
};
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard],
},
{
path: 'admin',
component: AdminPanelComponent,
canActivate: [authGuard, roleGuard(['Admin'])],
},
];
Side-by-side with Blazor:AuthStateProvider↔AuthService(both decode the JWT client-side and expose reactive current-user state),AuthTokenHandler↔authInterceptor(both attach the header to every outgoing request),[Authorize]↔canActivate: [authGuard]. Same three-part shape, different framework's idioms for wiring it in.