← Retour au blog
jwtangularauthenticationguardssecurity

JWT and Angular Guards: Implementing Authentication Without Security Pitfalls

JWT authentication in Angular has become standard practice, yet many developers implement it fragilly. The classic pattern—storing the token in localStorage, injecting it via an interceptor—works, sure, but leaves gaps. Between handling expired tokens, syncing across tabs, and XSS leaks, there's plenty to lose sleep over in production. This article offers a pragmatic approach: a robust authentication system with guards that truly reflect user state, without over-engineering.

Storing the JWT: localStorage, sessionStorage, or Memory?

Your token storage choice determines your attack surface. localStorage persists across sessions but remains vulnerable to XSS attacks; a malicious script can steal it in one line: localStorage.getItem('token') . sessionStorage is safer (destroyed when the tab closes) but inconvenient for multi-tab scenarios. The real solution? Use httpOnly cookies on the server side, which the browser sends automatically without JavaScript access. Unfortunately, many public APIs only support Bearer tokens in the Authorization header. In that case, combine localStorage for the refresh token (with a short access token lifetime) with an Angular service that centralizes token access. Never expose the token globally: never store it in a global variable or public service property. Encapsulate it in a private property with a secure getter.

Authentication Service Architecture

A good authentication service isolates business logic from storage. Create an AuthService class that handles token retrieval, renewal, and destruction. The service should expose an Observable or Signal of authentication state, not the token itself. Use a BehaviorSubject so all subscribed components react immediately to state changes. Here's a minimal structure: define a login(credentials) method that calls the API, stores the token securely, and updates internal state. Add a refreshToken() method that silently renews the token before expiration. Implement logout() which destroys the token and resets state. Crucially, the service must distinguish between an authenticated user (valid token in memory) and a recognized user (valid cookie on the server after a page refresh). This distinction is key to avoiding unnecessary redirects.

export class AuthService {
  private readonly tokenKey = 'access_token';
  private authState$ = new BehaviorSubject<AuthState>({ isAuthenticated: false });
  
  constructor(private http: HttpClient) {
    this.checkAuth();
  }
  
  login(credentials: { email: string; password: string }) {
    return this.http.post<{ token: string }>('/api/auth/login', credentials)
      .pipe(
        tap(res => {
          this.storeToken(res.token);
          this.authState$.next({ isAuthenticated: true, user: this.decodeToken(res.token) });
        })
      );
  }
  
  refreshToken() {
    return this.http.post<{ token: string }>('/api/auth/refresh', {})
      .pipe(
        tap(res => this.storeToken(res.token)),
        catchError(() => {
          this.logout();
          return throwError(() => new Error('Refresh failed'));
        })
      );
  }
  
  private storeToken(token: string) {
    localStorage.setItem(this.tokenKey, token);
  }
  
  getToken(): string | null {
    return localStorage.getItem(this.tokenKey);
  }
  
  logout() {
    localStorage.removeItem(this.tokenKey);
    this.authState$.next({ isAuthenticated: false });
  }
  
  isAuthenticated$() {
    return this.authState$.asObservable().pipe(map(state => state.isAuthenticated));
  }
}

Guards and Intelligent Redirection

Angular guards (CanActivate, CanActivateChild, CanDeactivate) are your first line of defense against unauthorized access. A guard should be synchronous if possible—a quick in-memory check of authentication state—to avoid visual flicker. Return a boolean or UrlTree to redirect. If you must validate the token on the server (e.g., to verify dynamic permissions), do it once at login and store roles/permissions locally. Create two separate guards: an AuthGuard that simply checks if the user is logged in, and a RoleGuard that controls access based on permissions. The common pitfall is adding asynchronous logic to the guard (HTTP calls) without handling timeouts—the guard can block access indefinitely if the server doesn't respond.

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivateFn {
  constructor(private auth: AuthService, private router: Router) {}
  
  canActivate: CanActivateFn = (route, state) => {
    if (this.auth.getToken()) {
      return true;
    }
    this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
    return false;
  };
}

@Injectable({ providedIn: 'root' })
export class RoleGuard implements CanActivateFn {
  constructor(private auth: AuthService, private router: Router) {}
  
  canActivate: CanActivateFn = (route, state) => {
    const requiredRole = route.data['role'];
    const userRole = this.auth.getCurrentUserRole();
    
    if (userRole === requiredRole) {
      return true;
    }
    this.router.navigate(['/403']);
    return false;
  };
}

Integrate the guard into routing config: { path: 'admin', component: AdminComponent, canActivate: [AuthGuard, RoleGuard], data: { role: 'admin' } } . Use function guards (CanActivateFn) rather than classes—they're lighter and easier to test.

Expiration Management and Silent Renewal

A JWT has a limited lifespan (typically 15 minutes). Waiting for the user to be redirected to login after expiration creates poor UX. Better to renew the token silently before it expires. Implement an interceptor that detects 401 responses, calls the refresh endpoint, and replays the original request. But beware: if multiple requests arrive simultaneously after expiration, you risk calling refresh multiple times in parallel. Use a Subject to centralize this logic—only one refresh request in progress, others wait for the result.

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  private refreshInProgress = false;
  private refreshSubject = new Subject<string>();
  
  constructor(private auth: AuthService) {}
  
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = this.auth.getToken();
    if (token) {
      req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
    }
    
    return next.handle(req).pipe(
      catchError(error => {
        if (error.status === 401 && !this.refreshInProgress) {
          this.refreshInProgress = true;
          return this.auth.refreshToken().pipe(
            tap(() => {
              this.refreshInProgress = false;
              this.refreshSubject.next(this.auth.getToken()!);
            }),
            switchMap(() => {
              const newToken = this.auth.getToken();
              const clonedReq = req.clone({
                setHeaders: { Authorization: `Bearer ${newToken}` }
              });
              return next.handle(clonedReq);
            }),
            catchError(() => {
              this.refreshInProgress = false;
              return throwError(() => error);
            })
          );
        }
        return throwError(() => error);
      })
    );
  }
}

Common Pitfalls and Anti-Patterns

The most frequent pitfall is storing the token in localStorage without checking its expiration before use. An expired but present token will be sent to the server, causing repeated 401 errors. Decode the JWT client-side (without signature verification—it's just base64) and compare the expiration ( exp claim) with Date.now() before each use. Another trap: forgetting that JWT offers no protection against client-side modification. The token can be read and decoded by any script. Never store sensitive data (passwords, card numbers) in the token. Finally, many projects create an asynchronous guard that calls the API on every navigation—this kills performance. Prefer a quick in-memory check, and sync the user state once at app startup.

Operational Takeaway

Implementing robust JWT + guards isn't rocket science, but it demands rigor. Summary: centralize your authentication logic in a dedicated service, use synchronous guards based on in-memory state, manage expiration with a smart interceptor, and test edge cases (slow network, simultaneous expiration, multi-tab). If you use httpOnly cookies on the server, you gain security but lose cross-domain flexibility. Evaluate your context: an internal app can tolerate localStorage + short tokens; a public app should prefer secure cookies. In any case, never store tokens in global variables, never put sensitive data in the JWT, and always have a logout endpoint that invalidates the token server-side (especially if you manage sessions).

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️