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.
On this page · 7 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
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.'); },
});
}
}
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#
- login() posts credentials and, on success, stores the session from the token.
- decodeJwt reads the payload (name, role, exp) for display and expiry — the server still validates the signature.
- isLoggedIn is computed and also checks expiry.
- restore() rebuilds the session after a refresh from sessionStorage.
- logout() clears state and storage and navigates to /login.
- 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#
Anyone can edit the token in storage or call the API directly. The API must verify the signature and roles.
An XSS bug can read localStorage and steal a token valid for weeks. Prefer httpOnly cookies or short-lived tokens with refresh.
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?
Show solution
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#
Who verifies the user’s password?
Want more? Practise all Angular MCQs · Angular interview questions
My notes#
Saved in this browser.