Angular · Architecture

Authentication

Authentication ties several Angular pieces together: a service that knows who is signed in, an interceptor that attaches credentials, and guards that protect routes. The API does the real checking.

Advanced 7 min Lesson 31 of 38Examples target Angular 20 with standalone components. Each lesson states the version it was verified against.
Before you start:GuardsInterceptors
0/38 in Angular
On this page · 7 min · Advanced

Learning objectives#

  • Build an AuthService that logs in through HttpClient
  • Store the session and expose user and isLoggedIn signals
  • Handle token expiry and logout
  • Connect the service to an interceptor and a guard

Explanation#

Login flow: a form posts credentials to the API (e.g. ASP.NET Core POST /api/auth/login). The API returns a JWT (or sets an httpOnly cookie). The SPA never checks passwords itself.

AuthService keeps the token and user in signals; isLoggedIn and roles are computed. Components, guards and interceptors all inject the same service.

Where to keep the session: httpOnly cookies set by the server are safest against XSS (send requests with withCredentials). If you use bearer tokens, keep them in memory and, if needed, sessionStorage — and accept the XSS risk; never in plain cookies readable by JS.

A JWT’s payload (the middle part) contains claims like name, role and exp. You can read it to show the user’s name and to log out before expiry — but only the server can trust it.

Log out by clearing state and storage, then navigate to the login page. The interceptor logs out automatically on 401.

Syntax#

TypeScript Illustrative
login(email: string, password: string) {
  return this.http.post<{ token: string }>('/api/auth/login', { email, password })
    .pipe(tap(res => this.setSession(res.token)));
}
readonly isLoggedIn = computed(() => this.session() !== null);

Code example: An AuthService with JWT session handling#

TypeScriptauth.service.ts Illustrative
import { Component, Injectable, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { tap } from 'rxjs';


type Session = { token: string; name: string; roles: string[]; expiresAt: number };


function decodeJwt(token: string): { name?: string; role?: string | string[]; exp?: number } {
  const payload = token.split('.')[1] ?? '';
  return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
}


@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private router = inject(Router);
  private readonly session = signal<Session | null>(this.restore());


  readonly isLoggedIn = computed(() => {
    const s = this.session();
    return s !== null && s.expiresAt > Date.now();
  });
  readonly userName = computed(() => this.session()?.name ?? '');
  readonly token = computed(() => (this.isLoggedIn() ? this.session()!.token : null));
  hasRole(role: string) { return this.session()?.roles.includes(role) ?? false; }


  login(email: string, password: string) {
    return this.http.post<{ token: string }>('/api/auth/login', { email, password })
      .pipe(tap(res => this.setSession(res.token)));
  }


  logout() {
    this.session.set(null);
    sessionStorage.removeItem('token');
    this.router.navigate(['/login']);
  }


  private setSession(token: string) {
    const claims = decodeJwt(token);
    const roles = Array.isArray(claims.role) ? claims.role : claims.role ? [claims.role] : [];
    this.session.set({ token, name: claims.name ?? 'user', roles, expiresAt: (claims.exp ?? 0) * 1000 });
    sessionStorage.setItem('token', token);     // survives refresh, cleared when the tab closes
  }


  private restore(): Session | null {
    const token = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('token') : null;
    if (!token) return null;
    try {
      const claims = decodeJwt(token);
      const roles = Array.isArray(claims.role) ? claims.role : claims.role ? [claims.role] : [];
      return { token, name: claims.name ?? 'user', roles, expiresAt: (claims.exp ?? 0) * 1000 };
    } catch {
      return null;
    }
  }
}


@Component({
  selector: 'app-login',
  imports: [FormsModule],
  template: `
    @if (auth.isLoggedIn()) {
      <p>Signed in as {{ auth.userName() }} <button (click)="auth.logout()">Log out</button></p>
    } @else {
      <form (ngSubmit)="submit()">
        <input name="email" [(ngModel)]="email" type="email" placeholder="Email" required />
        <input name="password" [(ngModel)]="password" type="password" placeholder="Password" required />
        <button [disabled]="busy()">Log in</button>
        @if (error()) { <p role="alert">{{ error() }}</p> }
      </form>
    }
  `,
})
export class Login {
  protected auth = inject(AuthService);
  email = '';
  password = '';
  busy = signal(false);
  error = signal('');


  submit() {
    this.busy.set(true);
    this.error.set('');
    this.auth.login(this.email, this.password).subscribe({
      next: () => this.busy.set(false),
      error: () => { this.busy.set(false); this.error.set('Wrong email or password.'); },
    });
  }
}
Expected output
Submitting valid credentials posts them to /api/auth/login; the returned JWT is decoded and the component shows “Signed in as Asha” with a Log out button. Wrong credentials show “Wrong email or password.”. Refreshing the tab keeps the session (sessionStorage) until the token’s exp time passes; Log out clears everything and goes to /login.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. login() posts credentials and, on success, stores the session from the token.
  2. decodeJwt reads the payload (name, role, exp) for display and expiry — the server still validates the signature.
  3. isLoggedIn is computed and also checks expiry.
  4. restore() rebuilds the session after a refresh from sessionStorage.
  5. logout() clears state and storage and navigates to /login.
  6. The token signal is what an auth interceptor would attach to requests.

Real-world use case#

An Angular front end for an ASP.NET Core API: the API issues 30-minute JWTs, the interceptor adds them, a guard protects routes, and the AuthService logs users out on expiry or on any 401.

Common mistakes#

Trusting client-side role checks

Anyone can edit the token in storage or call the API directly. The API must verify the signature and roles.

Long-lived tokens in localStorage

An XSS bug can read localStorage and steal a token valid for weeks. Prefer httpOnly cookies or short-lived tokens with refresh.

Not handling expiry

An expired token makes every call fail with 401. Check exp and handle 401 globally.

Best practices#

  • One AuthService as the single source of truth
  • Short-lived tokens or httpOnly cookies
  • Interceptor for headers, guards for routes
  • Clear everything on logout

Remember this#

API authenticates; Angular remembers the session.

JWT payload = readable claims, not a secret.

Logout = clear state + storage + navigate.

Practice exercise#

Your task: Add a computed isAdmin signal to an AuthService that has a session signal with roles, and use it in a template to show an "Admin" link.

Need a hint?
isAdmin = computed(() => this.session()?.roles.includes("Admin") ?? false).
Show solution
TypeScript Illustrative
import { Component, Injectable, computed, inject, signal } from '@angular/core';


@Injectable({ providedIn: 'root' })
export class AuthState {
  readonly session = signal<{ name: string; roles: string[] } | null>({ name: 'Ravi', roles: ['Admin'] });
  readonly isAdmin = computed(() => this.session()?.roles.includes('Admin') ?? false);
}


@Component({
  selector: 'app-nav',
  template: `<nav>Home @if (auth.isAdmin()) { · <a href="/admin">Admin</a> }</nav>`,
})
export class Nav {
  protected auth = inject(AuthState);
}

Mini quiz#

Authentication quizMultiple choice Question 1 of 3

Who verifies the user’s password?

Score so far: 0/0

Want more? Practise all Angular MCQs · Angular interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.