๐Ÿณ IntuitiveFE
Login
โ† All concepts

Angular Auth Patterns

โฑ๏ธ ~6-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

โœˆ๏ธ HTTP interceptor = passport control at the border. Every outbound request shows its passport (Bearer token). Border control (interceptor) stamps it before it departs. If the passport is expired, the traveler visits the visa office (refresh endpoint) and re-queues โ€” invisibly, from the app's perspective.

๐Ÿ”

AuthService

Issues and revokes tokens. Holds user$ Observable. The only place the token lives.

๐Ÿ›‚

HttpInterceptor

Stamps every request. Handles 401 silently. Clones requests โ€” never mutates.

๐Ÿšง

Route Guards

Blocks navigation without a valid session. Returns UrlTree to redirect cleanly.

RxJS operators used in auth flows

switchMapLogin form submit โ€” cancels previous in-flight request on re-submit
catchErrorInterceptor 401 โ€” catches error, triggers refresh, retries. Keeps stream alive.
filter + take(1)Queue: wait for refresh to complete, then replay the buffered request
finalizeForm submit โ€” always re-enable the submit button regardless of success/failure
BehaviorSubjectuser$ โ€” replays last value to new subscribers. Guards get current user synchronously.
๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Pattern

Interceptor pipeline

RequestComponent calls HttpClient.get(). Angular queues the request.
Interceptorintercept() fires. Reads the current token from AuthService.
Add Headerreq.clone({ setHeaders: { Authorization: 'Bearer ...' } }). Cloned โ€” immutable.
ServerRequest reaches the API. Server validates the JWT signature.
Response200 OK โ€” data flows back. catchError skips this path.
active done error
AuthService โ€” code example
ts
1import { Injectable, inject } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3import { BehaviorSubject, Observable, tap, map } from 'rxjs';
4import { Router } from '@angular/router';
5ย 
6export interface AuthUser { id: string; email: string; role: string }
7ย 
8@Injectable({ providedIn: 'root' })
9export class AuthService {
10 private readonly http = inject(HttpClient);
11 private readonly router = inject(Router);
12ย 
13 // Private: only this service touches the token
14 private accessToken: string | null = null;
15ย 
16 // Public stream: components subscribe to know who's logged in
17 private userSubject = new BehaviorSubject<AuthUser | null>(null);
18 readonly user$: Observable<AuthUser | null> = this.userSubject.asObservable();
19ย 
20 // Derived: synchronous snapshot for guards and interceptors
21 get isAuthenticated(): boolean { return !!this.accessToken; }
22 get token(): string | null { return this.accessToken; }
23ย 
24 login(email: string, password: string): Observable<void> {
25 return this.http
26 .post<{ user: AuthUser; accessToken: string }>('/api/auth/login', { email, password })
27 .pipe(
28 tap(({ user, accessToken }) => {
29 this.accessToken = accessToken; // in-memory only โ€” no localStorage
30 this.userSubject.next(user);
31 }),
32 map(() => void 0),
33 );
34 }
35ย 
36 logout(): void {
37 this.accessToken = null;
38 this.userSubject.next(null);
39 // Revoke refresh token on server (httpOnly cookie cleared by server)
40 this.http.post('/api/auth/logout', {}, { withCredentials: true }).subscribe();
41 this.router.navigate(['/login']);
42 }
43ย 
44 refreshToken(): Observable<string> {
45 return this.http
46 .post<{ accessToken: string }>('/api/auth/refresh', {}, { withCredentials: true })
47 .pipe(
48 tap(({ accessToken }) => {
49 this.accessToken = accessToken;
50 }),
51 map(({ accessToken }) => accessToken),
52 );
53 }
54}

When to use

Every Angular app with authentication. providedIn: 'root' makes it a singleton shared across the app. Never instantiate it with new โ€” always inject. The BehaviorSubject exposes the current user synchronously (getValue()) and reactively (subscribe).

Gotcha: BehaviorSubject replays the last emitted value to new subscribers. This is intentional โ€” a component mounted after login still gets the current user. But it also means logout must explicitly emit null, not just clear the token field.
Insight: Exposing user$ as an Observable (via asObservable()) instead of the raw BehaviorSubject prevents external code from calling next() directly and corrupting the auth state. The service is the only owner of auth truth.
Explored 1/5:๐Ÿ”๐Ÿ›‚๐Ÿšง๐Ÿ”๐Ÿ“
๐ŸŽฏ

Challenge

Explore all 5 Angular auth patterns. Run the interceptor pipeline to see how token refresh works end-to-end.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: Why inject via constructor instead of direct instantiation?

Angular's DI system manages object lifecycles. providedIn: 'root' creates exactly one instance shared across the whole app โ€” a singleton. Direct instantiation with new AuthService()creates a separate instance that doesn't share state with the rest of the app and breaks testability. In tests, you replace the injected dependency with a mock โ€” you can't do that if the class instantiates its own dependencies.

Q: Why clone the request in the interceptor instead of mutating it?

HttpRequest is intentionally immutable in Angular. There is no setter for headers โ€” the design forces you to call .clone(), which returns a new request with your modifications applied. This prevents interceptors from accidentally interfering with each other โ€” each interceptor works on its own copy. Immutability also makes debugging easier: the original request object is always unchanged.

Q: Why switchMap for the login form submit?

switchMap cancels the previous Observable when a new one arrives. If a user clicks Submit, then clicks again before the first request completes, switchMap unsubscribes from the first HTTP call and subscribes to the second. Only one request completes. For the login flow this is correct โ€” the second submit supersedes the first. Use exhaustMap if you want to ignore subsequent clicks while the first is in flight (e.g. payment forms where you never want duplicate submissions).

๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

Full AuthInterceptor with RxJS refresh + retry

The complete pattern: request interceptor attaches the token, response interceptor catches 401, refreshes via httpOnly cookie, queues concurrent 401s, and replays them with the new token. The BehaviorSubject(null) acts as a lock โ€” other requests filter on non-null and take(1) before retrying.

js
1// Full refresh-queue pattern
2private isRefreshing = false;
3private refreshToken$ = new BehaviorSubject<string | null>(null);
4ย 
5private handle401(req, next) {
6 if (this.isRefreshing) {
7 return this.refreshToken$.pipe(
8 filter((t): t is string => t !== null),
9 take(1),
10 switchMap(token => next.handle(this.addToken(req, token)))
11 );
12 }
13 this.isRefreshing = true;
14 this.refreshToken$.next(null);
15 return this.auth.refreshToken().pipe(
16 switchMap(token => {
17 this.isRefreshing = false;
18 this.refreshToken$.next(token);
19 return next.handle(this.addToken(req, token));
20 }),
21 catchError(err => { this.auth.logout(); return throwError(() => err); })
22 );
23}

withCredentials: true for httpOnly cookie flows

When the refresh token is stored in an httpOnly cookie, the browser only sends it if the request includes withCredentials: true. Without it, cross-origin requests strip all cookies. Set it on the refresh call specifically โ€” or globally via a separate interceptor for your auth domain only.

ts
1// Refresh call โ€” must include credentials
2this.http.post<{ accessToken: string }>(
3 '/api/auth/refresh',
4 {},
5 { withCredentials: true } // sends the httpOnly refresh cookie
6)

providedIn: 'root' vs feature-level scoping

providedIn: 'root' creates a singleton available everywhere โ€” correct for AuthService since the whole app shares one auth state. Feature-level scoping (providers: [AuthService] in a route config) creates a fresh instance per route activation โ€” useful for services that should reset when a feature is destroyed, but wrong for auth. An AuthService scoped to a feature would lose the token when navigating away.

Signal-based auth state with toSignal()

Angular 17+ lets you bridge the Observable world into signals. toSignal(authService.user$) creates a read-only signal that stays in sync with the Observable. Components using OnPush can read it directly in the template โ€” no async pipe needed. The signal is computed synchronously, so there is no initial null flash.

js
1import { toSignal } from '@angular/core/rxjs-interop';
2import { computed } from '@angular/core';
3ย 
4@Component({
5 changeDetection: ChangeDetectionStrategy.OnPush,
6 template: `
7 <span *ngIf="isAdmin()">Admin ๐Ÿ›ก๏ธ</span>
8 <span>{{ user()?.email }}</span>
9 `,
10})
11export class NavComponent {
12 private auth = inject(AuthService);
13 // Bridge Observable โ†’ Signal
14 user = toSignal(this.auth.user$, { initialValue: null });
15 isAdmin = computed(() => this.user()?.role === 'admin');
16}

Functional guards vs class-based guards

Class-based guards (implementing CanActivate) are deprecated in Angular 15+. Functional guards are preferred: they're tree-shakable (unused guards are eliminated from the bundle), simpler to write (no class boilerplate), and easier to test (plain functions, no TestBed needed). Functional guards also compose better โ€” multiple guards can be combined in an array and Angular runs them in sequence, short-circuiting on the first false or redirect.

โœ“ canActivate: CanActivateFnโœ“ canActivateChild: CanActivateChildFnโœ“ canDeactivate: CanDeactivateFnโœ“ canMatch: CanMatchFnโœ— implements CanActivate (deprecated)
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

Prevents external code from calling next() and corrupting auth state โ€” the service is the sole owner.

HttpRequest is immutable by design โ€” clone() is the only way to modify it, preventing interceptor cross-contamination.

Each 401 independently calls /auth/refresh, causing rotation race conditions that revoke the entire token family.

Returning a UrlTree avoids double navigation and lets the router manage the redirect cleanly.

toSignal bridges Observable to Signal, enabling synchronous template reads and OnPush optimization without async pipe.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What does finalize() in an RxJS pipe guarantee about loading state?