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.

auth.service.ts
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.

auth.interceptor.ts
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);
    })
  );
};
app.config.ts — registration
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")].

auth.guard.ts
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;
};
app.routes.ts
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.