# User Access Control

Course: [Angular](https://www.devtutorials.ai/learn/angular) · Section: Application Security
Source: https://www.devtutorials.ai/learn/angular/security/user-access-control

> Authentication and authorization in Angular: JWTs, an auth service, registration and login, token handling and expiration, interceptors, roles, route guards, error handling and logout, with a full capstone project.

## Authentication Introduction

**Authentication** checks **who** the user is. The user proves their identity, usually with an email and a password, and the app then knows: "This is Asha."

Common ways to authenticate:

- Email and password
- A code sent by text message or email
- Login with another account, such as Google or Microsoft

### Real-World Example

In an online banking app, a customer logs in with their customer ID and password. After that, they can see their own accounts. They cannot see other customers' accounts, and only bank staff can open the screen that approves loans.

The login is authentication. The rules about which screens each person may open are authorization.

## Authorization Introduction

**Authorization** checks **what** a user may do. It always comes after authentication: first the app knows who you are, then it decides what you may open or change.

| Point | Authentication | Authorization |
|---|---|---|
| Question | Who are you? | What may you do? |
| Example | Log in with email and password | Only admins may delete users |
| Fails with | 401 Unauthorized: not logged in | 403 Forbidden: logged in, but not allowed |

<figure className="ngc-fig">
<svg viewBox="0 0 640 140" role="img" aria-label="Login sends email and password, the server returns a JWT, the interceptor adds it to each request, and the server checks the token and role.">
<defs><marker id="ngc-a1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-soft c-blue"><rect x="6" y="36" width="142" height="64" rx="9" /><text x="77.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>1. Login</text><text className="ngc-sub" x="77.0" y="83" textAnchor="middle">email + password</text></g></g><line className="ngc-link ngc-flow c-slate" x1="150" y1="68" x2="162" y2="68" markerEnd="url(#ngc-a1)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-purple"><rect x="164" y="36" width="142" height="64" rx="9" /><text x="235.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>2. Server</text><text className="ngc-sub" x="235.0" y="83" textAnchor="middle">checks, sends JWT</text></g></g><line className="ngc-link ngc-flow c-slate" x1="308" y1="68" x2="320" y2="68" markerEnd="url(#ngc-a1)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-orange"><rect x="322" y="36" width="142" height="64" rx="9" /><text x="393.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>3. Interceptor</text><text className="ngc-sub" x="393.0" y="83" textAnchor="middle">adds the token</text></g></g><line className="ngc-link ngc-flow c-slate" x1="466" y1="68" x2="478" y2="68" markerEnd="url(#ngc-a1)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-green"><rect x="480" y="36" width="142" height="64" rx="9" /><text x="551.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>4. Server</text><text className="ngc-sub" x="551.0" y="83" textAnchor="middle">checks token + role</text></g></g><text className="ngc-note" x="85" y="124" textAnchor="middle">authentication</text><text className="ngc-note" x="555" y="124" textAnchor="middle">authorization: 200, 401 or 403</text>
</svg>
<figcaption>Authentication first, then authorization on every request.</figcaption>
</figure>

## JWT Authentication

After a successful login, the server needs a way to recognize the user in later requests. Most Angular apps use a **JWT** (JSON Web Token): a text that the server creates and signs after the login.

<figure className="ngc-fig">
<svg viewBox="0 0 640 130" role="img" aria-label="A JWT has three parts separated by dots: header, payload and signature.">
<g className="ngc-step ngc-t1"><g className="ngc-node ngc-soft c-red"><rect x="10" y="30" width="150" height="60" rx="9" /><text x="85.0" y="57" textAnchor="middle" style={{fontSize:"14px"}}>Header</text><text className="ngc-sub" x="85.0" y="75" textAnchor="middle">alg, typ</text></g></g><text className="ngc-title c-slate" x="175" y="66" textAnchor="middle">.</text><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-purple"><rect x="190" y="30" width="240" height="60" rx="9" /><text x="310.0" y="57" textAnchor="middle" style={{fontSize:"14px"}}>Payload</text><text className="ngc-sub" x="310.0" y="75" textAnchor="middle">name, role, exp</text></g></g><text className="ngc-title c-slate" x="445" y="66" textAnchor="middle">.</text><g className="ngc-step ngc-t3"><g className="ngc-node ngc-solid c-blue"><rect x="460" y="30" width="170" height="60" rx="9" /><text x="545.0" y="57" textAnchor="middle" style={{fontSize:"14px"}}>Signature</text><text className="ngc-sub" x="545.0" y="75" textAnchor="middle">made with the secret key</text></g></g><text className="ngc-note" x="320" y="116" textAnchor="middle">Header and payload can be decoded by anyone. The signature proves the server made the token.</text>
</svg>
<figcaption>The three parts of a JSON Web Token.</figcaption>
</figure>

A JWT has three parts, separated by dots:

| Part | Contains | Example content |
|---|---|---|
| Header | The type of token and the signing method | `{ "alg": "HS256", "typ": "JWT" }` |
| Payload | Data about the user, called **claims** | `{ "name": "Asha Rao", "role": "user", "exp": 1924992000 }` |
| Signature | A code made with the server's secret key | `SflKxwRJ...` |

The header and payload are only **encoded** in Base64, a way to write data as plain letters and numbers. Anyone can decode them. The signature proves that the server created the token: if someone changes the payload, the signature no longer matches, and the server rejects the token.

The flow:

1. The user logs in. The server checks the password and sends back a token.
2. The app stores the token.
3. The app sends the token with every request, in the header `Authorization: Bearer <token>`.
4. The server checks the signature and the expiration time, and answers.

The next example decodes a JWT. `atob()` is a browser function that decodes Base64.

```angular runnable title="Reading the payload of a JWT"
import { Component } from '@angular/core';

const TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsIm5hbWUiOiJBc2hhIFJhbyIsInJvbGUiOiJ1c2VyIiwiZXhwIjoxOTI0OTkyMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';

function decodeToken(token: string) {
  const payload = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
  return JSON.parse(atob(payload));
}

@Component({
  selector: 'app-root',
  template: `
    <p>Header: {{ parts[0] }}</p>
    <p>Payload: {{ parts[1] }}</p>
    <p>Signature: {{ parts[2] }}</p>
    <hr />
    <p>Name: {{ user.name }}</p>
    <p>Role: {{ user.role }}</p>
    <p>Expires: {{ expires }}</p>
  `,
})
export class App {
  parts = TOKEN.split('.');
  user = decodeToken(TOKEN);
  expires = new Date(this.user.exp * 1000).toISOString().slice(0, 10);
}
```

`exp` is the expiration time in **seconds** since 1 January 1970. JavaScript dates use milliseconds, so the code multiplies it by 1000.

Never put secret data, such as a password, into a JWT payload. Anyone who has the token can decode and read it.

## Auth Service

Keep all authentication code in one service, `AuthService`, as you learned in the State Management lesson. It has:

- **State:** the token, kept in a private signal, and values computed from it, such as `user` and `isLoggedIn`.
- **Methods:** `register()`, `login()` and `logout()`.

```ts
@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private readonly _token = signal<string | null>(null);

  readonly token = this._token.asReadonly();
  readonly user = computed(() => {
    const token = this._token();
    return token ? decodeToken(token) : null;
  });
  readonly isLoggedIn = computed(() => this.user() !== null);

  login(email: string, password: string) { /* ... */ }
  register(name: string, email: string, password: string) { /* ... */ }
  logout() { /* ... */ }
}
```

## User Registration

**Registration** creates a new account. The app sends the name, email and password with a POST request. Many servers answer with a token, so the new user is logged in at once.

```ts
register(name: string, email: string, password: string) {
  return this.http
    .post<{ token: string }>('/api/auth/register', { name, email, password })
    .pipe(tap(res => this._token.set(res.token)));
}
```

`tap()` stores the token when the answer arrives, and the component still gets the answer. The server must check the data, refuse an email that already exists, and store the password only in a scrambled form (a **hash**), never as plain text.

## User Login

**Login** works the same way: send the email and password, and store the token from the answer.

```angular runnable title="Login and authentication state"
import { ApplicationConfig, Component, Injectable, computed, inject, signal } from '@angular/core';
import {
  HttpClient, HttpErrorResponse, HttpHandlerFn, HttpRequest, HttpResponse,
  provideHttpClient, withInterceptors,
} from '@angular/common/http';
import { delay, mergeMap, of, tap, throwError, timer } from 'rxjs';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withInterceptors([practiceServer]))],
};

export function decodeToken(token: string) {
  return JSON.parse(atob(token.split('.')[1]));
}

@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private readonly _token = signal<string | null>(null);

  readonly user = computed(() => {
    const token = this._token();
    return token ? decodeToken(token) : null;
  });
  readonly isLoggedIn = computed(() => this.user() !== null);

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

  logout() {
    this._token.set(null);
  }
}

@Component({
  selector: 'app-root',
  template: `
    <button (click)="login('user123')">Log in as Asha</button>
    <button (click)="login('wrong')">Log in with a wrong password</button>
    <button (click)="auth.logout()">Log out</button>
    <p>Logged in: {{ auth.isLoggedIn() }}</p>
    <p>User: {{ auth.user()?.name ?? '-' }} ({{ auth.user()?.role ?? '-' }})</p>
    @if (error()) {
      <p>⚠ {{ error() }}</p>
    }
  `,
})
export class App {
  auth = inject(AuthService);
  error = signal('');

  login(password: string) {
    this.error.set('');
    this.auth.login('asha@shop.com', password).subscribe({
      error: (err: HttpErrorResponse) => this.error.set(err.error.message),
    });
  }
}
// ─── Practice server ──────────────────────────────────────────────
// Answers the auth API inside the browser. A real server signs the token
// with a secret key; this practice server only imitates the format.
const ACCOUNTS = [
  { name: 'Asha Rao', email: 'asha@shop.com', password: 'user123', role: 'user' },
  { name: 'Admin', email: 'admin@shop.com', password: 'admin123', role: 'admin' },
];

function makeToken(account: any, seconds: number) {
  const encode = (data: object) => btoa(JSON.stringify(data));
  const exp = Math.floor(Date.now() / 1000) + seconds;
  const payload = { name: account.name, email: account.email, role: account.role, exp };
  return `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode(payload)}.practice-signature`;
}

function readToken(req: HttpRequest<any>) {
  const header = req.headers.get('Authorization') ?? '';
  try {
    const payload = JSON.parse(atob(header.replace('Bearer ', '').split('.')[1]));
    return payload.exp * 1000 > Date.now() ? payload : null;
  } catch {
    return null;
  }
}

function practiceServer(req: HttpRequest<any>, next: HttpHandlerFn) {
  const body = req.body ?? {};
  if (req.url === '/api/auth/register') {
    if (ACCOUNTS.some(a => a.email === body.email)) return fail(409, 'This email is already registered.');
    const account = { name: body.name, email: body.email, password: body.password, role: 'user' };
    ACCOUNTS.push(account);
    return reply({ token: makeToken(account, 900) });
  }
  if (req.url === '/api/auth/login') {
    const account = ACCOUNTS.find(a => a.email === body.email && a.password === body.password);
    return account ? reply({ token: makeToken(account, body.seconds ?? 900) }) : fail(401, 'Wrong email or password.');
  }
  const user = readToken(req);
  if (!user) return fail(401, 'Please log in.');
  if (req.url === '/api/me') return reply(user);
  if (req.url === '/api/admin/report') {
    return user.role === 'admin' ? reply({ sales: 12400, orders: 310 }) : fail(403, 'Administrators only.');
  }
  return fail(404, 'Not Found');
}

function reply(body: unknown) {
  return of(new HttpResponse({ status: 200, body })).pipe(delay(200));
}

function fail(status: number, message: string) {
  return timer(200).pipe(mergeMap(() => throwError(() => new HttpErrorResponse({ status, error: { message } }))));
}

```

## Authentication State

The **authentication state** is everything the app knows about the current login: the token, the user, and whether someone is logged in. Many components need it, for example the header shows "Log out" only for a logged-in user.

Because the state is in signals, every component that reads `auth.isLoggedIn()` or `auth.user()` updates when the user logs in or out.

## Token Handling

The token is lost when the page reloads, because signals live only in memory. To keep the user logged in, save the token in the browser:

```ts
localStorage.setItem('auth_token', token);              // after login
const saved = localStorage.getItem('auth_token');       // when the app starts
localStorage.removeItem('auth_token');                  // on logout
```

| Where to store the token | Survives a reload | Risk |
|---|---|---|
| Only in memory (a signal) | No | Lowest. The user must log in again after a reload |
| `localStorage` | Yes | Malicious script code on the page could read it |
| An `HttpOnly` cookie set by the server | Yes | JavaScript cannot read it at all. The server must be set up for cookies |

The downloadable project uses `localStorage`, because it is simple. For apps with sensitive data, ask your back-end team for `HttpOnly` cookies.

## HTTP Interceptor

The server needs the token in every request. Do not add it by hand in every service. An interceptor, which you learned in the API Integration lesson, adds it to all requests:

```angular runnable title="An interceptor sends the token"
import { ApplicationConfig, Component, Injectable, computed, inject, signal } from '@angular/core';
import {
  HttpClient, HttpErrorResponse, HttpHandlerFn, HttpInterceptorFn, HttpRequest, HttpResponse,
  provideHttpClient, withInterceptors,
} from '@angular/common/http';
import { delay, mergeMap, of, tap, throwError, timer } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private readonly _token = signal<string | null>(null);
  readonly token = this._token.asReadonly();
  readonly isLoggedIn = computed(() => this._token() !== null);

  login() {
    return this.http
      .post<{ token: string }>('/api/auth/login', { email: 'asha@shop.com', password: 'user123' })
      .pipe(tap(res => this._token.set(res.token)));
  }
}

// Adds "Authorization: Bearer <token>" when the user is logged in
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).token();
  if (!token) {
    return next(req);
  }
  return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withInterceptors([authInterceptor, practiceServer]))],
};

@Component({
  selector: 'app-root',
  template: `
    <p>Logged in: {{ auth.isLoggedIn() }}</p>
    <button (click)="loadProfile()">Load my profile</button>
    <button (click)="auth.login().subscribe()">Log in</button>
    <p>{{ result() }}</p>
  `,
})
export class App {
  auth = inject(AuthService);
  private http = inject(HttpClient);
  result = signal('');

  loadProfile() {
    this.http.get<any>('/api/me').subscribe({
      next: user => this.result.set(`Hello, ${user.name}!`),
      error: (err: HttpErrorResponse) => this.result.set(`Error ${err.status}: ${err.error.message}`),
    });
  }
}
// ─── Practice server ──────────────────────────────────────────────
// Answers the auth API inside the browser. A real server signs the token
// with a secret key; this practice server only imitates the format.
const ACCOUNTS = [
  { name: 'Asha Rao', email: 'asha@shop.com', password: 'user123', role: 'user' },
  { name: 'Admin', email: 'admin@shop.com', password: 'admin123', role: 'admin' },
];

function makeToken(account: any, seconds: number) {
  const encode = (data: object) => btoa(JSON.stringify(data));
  const exp = Math.floor(Date.now() / 1000) + seconds;
  const payload = { name: account.name, email: account.email, role: account.role, exp };
  return `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode(payload)}.practice-signature`;
}

function readToken(req: HttpRequest<any>) {
  const header = req.headers.get('Authorization') ?? '';
  try {
    const payload = JSON.parse(atob(header.replace('Bearer ', '').split('.')[1]));
    return payload.exp * 1000 > Date.now() ? payload : null;
  } catch {
    return null;
  }
}

function practiceServer(req: HttpRequest<any>, next: HttpHandlerFn) {
  const body = req.body ?? {};
  if (req.url === '/api/auth/register') {
    if (ACCOUNTS.some(a => a.email === body.email)) return fail(409, 'This email is already registered.');
    const account = { name: body.name, email: body.email, password: body.password, role: 'user' };
    ACCOUNTS.push(account);
    return reply({ token: makeToken(account, 900) });
  }
  if (req.url === '/api/auth/login') {
    const account = ACCOUNTS.find(a => a.email === body.email && a.password === body.password);
    return account ? reply({ token: makeToken(account, body.seconds ?? 900) }) : fail(401, 'Wrong email or password.');
  }
  const user = readToken(req);
  if (!user) return fail(401, 'Please log in.');
  if (req.url === '/api/me') return reply(user);
  if (req.url === '/api/admin/report') {
    return user.role === 'admin' ? reply({ sales: 12400, orders: 310 }) : fail(403, 'Administrators only.');
  }
  return fail(404, 'Not Found');
}

function reply(body: unknown) {
  return of(new HttpResponse({ status: 200, body })).pipe(delay(200));
}

function fail(status: number, message: string) {
  return timer(200).pipe(mergeMap(() => throwError(() => new HttpErrorResponse({ status, error: { message } }))));
}

```

Click **Load my profile** first: the server answers 401, because no token was sent. Click **Log in**, then **Load my profile** again.

## Token Expiration

A JWT has an expiration time in its `exp` claim. After this time, the server rejects it with 401. Short times, such as 15 minutes, limit the damage if a token is stolen.

<figure className="ngc-fig">
<svg viewBox="0 0 640 134" role="img" aria-label="Login stores a token with exp; a timer runs until exp; then the app logs out automatically.">
<defs><marker id="ngc-j4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-solid c-blue"><rect x="6" y="30" width="145" height="64" rx="9" /><text x="78.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Login</text><text className="ngc-sub" x="78.5" y="77" textAnchor="middle">token with exp</text></g></g><line className="ngc-link ngc-flow c-slate" x1="152" y1="62" x2="166" y2="62" markerEnd="url(#ngc-j4)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-orange"><rect x="167" y="30" width="145" height="64" rx="9" /><text x="239.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Timer starts</text><text className="ngc-sub" x="239.5" y="77" textAnchor="middle">exp − now</text></g></g><line className="ngc-link ngc-flow c-slate" x1="313" y1="62" x2="327" y2="62" markerEnd="url(#ngc-j4)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-red"><rect x="328" y="30" width="145" height="64" rx="9" /><text x="400.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Time is over</text><text className="ngc-sub" x="400.5" y="77" textAnchor="middle">token expired</text></g></g><line className="ngc-link ngc-flow c-slate" x1="474" y1="62" x2="488" y2="62" markerEnd="url(#ngc-j4)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-green"><rect x="489" y="30" width="145" height="64" rx="9" /><text x="561.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>logout()</text><text className="ngc-sub" x="561.5" y="77" textAnchor="middle">"Session expired"</text></g></g><circle className="ngc-packet c-orange" r="5"><animateMotion dur="3.6s" repeatCount="indefinite" path="M78 106 L562 106" /></circle><text className="ngc-note" x="320" y="128" textAnchor="middle">The app logs out before the server starts to answer 401.</text>
</svg>
<figcaption>Automatic logout when the token expires.</figcaption>
</figure>

The app should react before the user gets errors:

- **When the app starts**, ignore a saved token that has already expired.
- **While the user works**, start a timer when the token is stored, and log out when the time is over.
- **If the server answers 401**, log out. The interceptor can do this for every request.

```angular runnable title="Automatic logout when the token expires"
import { ApplicationConfig, Component, Injectable, computed, inject, signal } from '@angular/core';
import {
  HttpClient, HttpErrorResponse, HttpHandlerFn, HttpRequest, HttpResponse,
  provideHttpClient, withInterceptors,
} from '@angular/common/http';
import { delay, mergeMap, of, throwError, timer } from 'rxjs';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withInterceptors([practiceServer]))],
};

function decodeToken(token: string) {
  return JSON.parse(atob(token.split('.')[1]));
}

@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private readonly _token = signal<string | null>(null);
  private expiryTimer: any;
  readonly isLoggedIn = computed(() => this._token() !== null);
  readonly message = signal('');
  readonly expiresAt = signal('');

  login() {
    // The practice server accepts "seconds" to create a short token.
    this.http
      .post<{ token: string }>('/api/auth/login', { email: 'asha@shop.com', password: 'user123', seconds: 5 })
      .subscribe(res => this.setToken(res.token));
  }

  logout(message = '') {
    clearTimeout(this.expiryTimer);
    this._token.set(null);
    this.message.set(message);
  }

  private setToken(token: string) {
    const exp = decodeToken(token).exp * 1000; // milliseconds
    this._token.set(token);
    this.message.set('');
    this.expiresAt.set(new Date(exp).toLocaleTimeString());
    clearTimeout(this.expiryTimer);
    this.expiryTimer = setTimeout(() => this.logout('Your session has expired. Please log in again.'), exp - Date.now());
  }
}

@Component({
  selector: 'app-root',
  template: `
    <button (click)="auth.login()">Log in (token valid for 5 seconds)</button>
    @if (auth.isLoggedIn()) {
      <p>✅ Logged in. The token expires at {{ auth.expiresAt() }}.</p>
    } @else {
      <p>Logged out.</p>
    }
    @if (auth.message()) {
      <p>⚠ {{ auth.message() }}</p>
    }
  `,
})
export class App {
  auth = inject(AuthService);
}
// ─── Practice server ──────────────────────────────────────────────
// Answers the auth API inside the browser. A real server signs the token
// with a secret key; this practice server only imitates the format.
const ACCOUNTS = [
  { name: 'Asha Rao', email: 'asha@shop.com', password: 'user123', role: 'user' },
  { name: 'Admin', email: 'admin@shop.com', password: 'admin123', role: 'admin' },
];

function makeToken(account: any, seconds: number) {
  const encode = (data: object) => btoa(JSON.stringify(data));
  const exp = Math.floor(Date.now() / 1000) + seconds;
  const payload = { name: account.name, email: account.email, role: account.role, exp };
  return `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode(payload)}.practice-signature`;
}

function readToken(req: HttpRequest<any>) {
  const header = req.headers.get('Authorization') ?? '';
  try {
    const payload = JSON.parse(atob(header.replace('Bearer ', '').split('.')[1]));
    return payload.exp * 1000 > Date.now() ? payload : null;
  } catch {
    return null;
  }
}

function practiceServer(req: HttpRequest<any>, next: HttpHandlerFn) {
  const body = req.body ?? {};
  if (req.url === '/api/auth/register') {
    if (ACCOUNTS.some(a => a.email === body.email)) return fail(409, 'This email is already registered.');
    const account = { name: body.name, email: body.email, password: body.password, role: 'user' };
    ACCOUNTS.push(account);
    return reply({ token: makeToken(account, 900) });
  }
  if (req.url === '/api/auth/login') {
    const account = ACCOUNTS.find(a => a.email === body.email && a.password === body.password);
    return account ? reply({ token: makeToken(account, body.seconds ?? 900) }) : fail(401, 'Wrong email or password.');
  }
  const user = readToken(req);
  if (!user) return fail(401, 'Please log in.');
  if (req.url === '/api/me') return reply(user);
  if (req.url === '/api/admin/report') {
    return user.role === 'admin' ? reply({ sales: 12400, orders: 310 }) : fail(403, 'Administrators only.');
  }
  return fail(404, 'Not Found');
}

function reply(body: unknown) {
  return of(new HttpResponse({ status: 200, body })).pipe(delay(200));
}

function fail(status: number, message: string) {
  return timer(200).pipe(mergeMap(() => throwError(() => new HttpErrorResponse({ status, error: { message } }))));
}

```

Click the button and wait 5 seconds. The app logs out and shows a message.

## User Roles

A **role** is a group of permissions, for example `user` or `admin`. The server puts the role into the token payload, so the app can read it from `auth.user().role`.

```ts
readonly isAdmin = computed(() => this.user()?.role === 'admin');
```

## Role-Based Access Control

**Role-Based Access Control (RBAC)** means that the role decides what a user may do. In the app, you use the role in two places:

- **In templates:** show a link or button only for allowed roles, for example `@if (auth.isAdmin()) { <a routerLink="/admin">Admin</a> }`.
- **In routes:** block a page for other roles with a route guard. You learn guards in the next section.

| Feature | Public (guest) | user | admin |
|---|---|---|---|
| Home page | ✅ | ✅ | ✅ |
| My profile | ❌ | ✅ | ✅ |
| Admin reports | ❌ | ❌ | ✅ |

Hiding a button or blocking a route in Angular does **not** protect data. A user can change the code in their browser or call the API directly. The **server** must check the token and the role for every request. Angular checks are only for a good user experience.

## Route Guards

A **route guard** is a function that the router calls **before** it opens a route. It returns:

<figure className="ngc-fig">
<svg viewBox="0 0 640 204" role="img" aria-label="The router calls the guard before opening a route: true opens it, a UrlTree redirects to login.">
<defs><marker id="ngc-j3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-solid c-blue"><rect x="10" y="70" width="160" height="60" rx="9" /><text x="90.0" y="97" textAnchor="middle" style={{fontSize:"14px"}}>routerLink="/profile"</text><text className="ngc-sub" x="90.0" y="115" textAnchor="middle">user clicks</text></g></g><line className="ngc-link ngc-flow c-slate" x1="172" y1="100" x2="206" y2="100" markerEnd="url(#ngc-j3)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-solid c-orange"><rect x="208" y="70" width="150" height="60" rx="9" /><text x="283.0" y="97" textAnchor="middle" style={{fontSize:"14px"}}>authGuard</text><text className="ngc-sub" x="283.0" y="115" textAnchor="middle">logged in?</text></g></g><line className="ngc-link ngc-flow c-slate" x1="360" y1="90" x2="432" y2="40" markerEnd="url(#ngc-j3)" /><line className="ngc-link ngc-flow c-slate" x1="360" y1="110" x2="432" y2="160" markerEnd="url(#ngc-j3)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-green"><rect x="434" y="10" width="196" height="56" rx="9" /><text x="532.0" y="35" textAnchor="middle" style={{fontSize:"14px"}}>true → Profile</text><text className="ngc-sub" x="532.0" y="53" textAnchor="middle">the page opens</text></g></g><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-red"><rect x="434" y="134" width="196" height="56" rx="9" /><text x="532.0" y="159" textAnchor="middle" style={{fontSize:"14px"}}>UrlTree → /login</text><text className="ngc-sub" x="532.0" y="177" textAnchor="middle">redirected</text></g></g>
</svg>
<figcaption>A guard decides before the route opens.</figcaption>
</figure>

- `true`: the route opens.
- A **`UrlTree`**: the router goes to that URL instead. Create it with `router.createUrlTree(['/login'])`.
- `false`: the navigation is cancelled.

A guard of type `CanActivateFn` is added to a route with `canActivate`:

```ts
export const authGuard: CanActivateFn = () => {
  return inject(AuthService).isLoggedIn() ? true : inject(Router).createUrlTree(['/login']);
};

export const routes: Routes = [
  { path: 'profile', component: Profile, canActivate: [authGuard] },
  { path: 'admin', component: Admin, canActivate: [roleGuard], data: { roles: ['admin'] } },
];
```

`data` is an object with extra values for a route. The `roleGuard` reads the allowed roles from `route.data['roles']`.

```angular runnable title="Guards for logged-in users and admins"
import { Component, Injectable, computed, inject, signal } from '@angular/core';
import { CanActivateFn, Router, RouterLink, RouterOutlet, Routes } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class AuthService {
  readonly role = signal<string | null>(null);
  readonly isLoggedIn = computed(() => this.role() !== null);
}

export const authGuard: CanActivateFn = () => {
  return inject(AuthService).isLoggedIn() ? true : inject(Router).createUrlTree(['/login']);
};

export const roleGuard: CanActivateFn = route => {
  const auth = inject(AuthService);
  const allowed: string[] = route.data['roles'] ?? [];
  if (allowed.includes(auth.role() ?? '')) {
    return true;
  }
  return inject(Router).createUrlTree(auth.isLoggedIn() ? ['/forbidden'] : ['/login']);
};

@Component({ selector: 'app-home', template: `<p>🏠 Home: public page.</p>` })
export class Home {}

@Component({ selector: 'app-login', template: `<p>🔑 Please log in.</p>` })
export class Login {}

@Component({ selector: 'app-profile', template: `<p>👤 My profile.</p>` })
export class Profile {}

@Component({ selector: 'app-admin', template: `<p>🛠 Admin reports.</p>` })
export class Admin {}

@Component({ selector: 'app-forbidden', template: `<p>⛔ 403: you may not open this page.</p>` })
export class Forbidden {}

export const routes: Routes = [
  { path: '', component: Home },
  { path: 'login', component: Login },
  { path: 'profile', component: Profile, canActivate: [authGuard] },
  { path: 'admin', component: Admin, canActivate: [roleGuard], data: { roles: ['admin'] } },
  { path: 'forbidden', component: Forbidden },
];

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <p>
      Role: <b>{{ auth.role() ?? 'guest' }}</b>
      <button (click)="auth.role.set('user')">Be user</button>
      <button (click)="auth.role.set('admin')">Be admin</button>
      <button (click)="auth.role.set(null)">Log out</button>
    </p>
    <a routerLink="/">Home</a> | <a routerLink="/profile">Profile</a> | <a routerLink="/admin">Admin</a>
    <router-outlet />
  `,
})
export class App {
  auth = inject(AuthService);
}
```

As a guest, click **Profile**: the guard sends you to the login page. As a user, click **Admin**: you get the 403 page. As an admin, every link opens.

## Authentication Errors

Show clear messages for authentication problems. Never show raw technical errors.

| Status | Situation | What the app does |
|---|---|---|
| 400 | The form data is incomplete | Show the server's message next to the form |
| 401 at login | Wrong email or password | Show "Wrong email or password." Do not say which one was wrong |
| 401 later | The token expired or is invalid | Log out and show "Your session has expired" |
| 403 | The user's role is not allowed | Show an "Access denied" page |
| 409 at registration | The email already exists | Show "This email is already registered." |
| 0 | The server cannot be reached | Show "Could not reach the server." |

The interceptor is a good place to handle 401 for every request:

```ts
return next(request).pipe(
  catchError((error: HttpErrorResponse) => {
    if (error.status === 401 && token) {
      auth.logout('Your session has ended. Please log in again.');
    }
    return throwError(() => error); // the component can still show its own message
  }),
);
```

## Logout

**Logout** ends the session in the app:

1. Stop the expiration timer.
2. Set the token signal to `null`. Every component that reads the state updates.
3. Remove the saved token from `localStorage`.
4. Navigate to the login or home page.

```ts
logout(message = '') {
  clearTimeout(this.expiryTimer);
  this._token.set(null);
  localStorage.removeItem('auth_token');
  this.message.set(message);
  this.router.navigate(['/login']);
}
```

A JWT stays valid on the server until it expires, even after logout. Use short expiration times, such as 15 minutes, so a copied token stops working soon.

## Capstone Project

You now combine everything in one app.

### Registration and Login

Two forms with `ngModel` call `auth.register()` and `auth.login()`. Errors from the server are shown under the form.

### JWT Authentication

`AuthService` stores the token in a signal. `user`, `isLoggedIn` and `isAdmin` are computed from the payload.

### Public Access

The home page has no guard. Everybody can open it.

### User Permissions

The profile page uses `authGuard`, and it requests `/api/me`. The interceptor adds the token.

### Administrator Permissions

The admin page uses `roleGuard` with `data: { roles: ['admin'] }`. The server also checks the role and answers 403 for other users.

### Protected Routes

A guest who opens a protected route is sent to the login page. A user without the admin role is sent to the "Access denied" page.

### Token Expiration

The **Log in** form has a "Short session" checkbox. It creates a 10-second token, so you can see the automatic logout.

### Logout

**Log out** in the header clears the state and opens the login page.

```angular runnable title="Capstone: User Access Control"
import { ApplicationConfig, Component, Injectable, computed, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
  HttpClient, HttpErrorResponse, HttpHandlerFn, HttpInterceptorFn, HttpRequest, HttpResponse,
  provideHttpClient, withInterceptors,
} from '@angular/common/http';
import { CanActivateFn, Router, RouterLink, RouterOutlet, Routes, provideRouter } from '@angular/router';
import { catchError, delay, mergeMap, of, tap, throwError, timer } from 'rxjs';

// ── Auth service ──
function decodeToken(token: string) {
  return JSON.parse(atob(token.split('.')[1]));
}

@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private router = inject(Router);
  private expiryTimer: any;
  private readonly _token = signal<string | null>(null);

  readonly token = this._token.asReadonly();
  readonly user = computed(() => {
    const token = this._token();
    return token ? decodeToken(token) : null;
  });
  readonly isLoggedIn = computed(() => this.user() !== null);
  readonly isAdmin = computed(() => this.user()?.role === 'admin');
  readonly message = signal('');

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

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

  logout(message = '') {
    clearTimeout(this.expiryTimer);
    this._token.set(null);
    this.message.set(message);
    this.router.navigate(['/login']);
  }

  private setToken(token: string) {
    this._token.set(token);
    this.message.set('');
    clearTimeout(this.expiryTimer);
    const msLeft = decodeToken(token).exp * 1000 - Date.now();
    this.expiryTimer = setTimeout(() => this.logout('Your session has expired. Please log in again.'), msLeft);
  }
}

// ── Interceptor ──
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  const token = auth.token();
  const request = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req;
  return next(request).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401 && token) {
        auth.logout('Your session has ended. Please log in again.');
      }
      return throwError(() => error);
    }),
  );
};

// ── Guards ──
export const authGuard: CanActivateFn = () => {
  return inject(AuthService).isLoggedIn() ? true : inject(Router).createUrlTree(['/login']);
};

export const roleGuard: CanActivateFn = route => {
  const auth = inject(AuthService);
  const allowed: string[] = route.data['roles'] ?? [];
  if (allowed.includes(auth.user()?.role)) {
    return true;
  }
  return inject(Router).createUrlTree(auth.isLoggedIn() ? ['/forbidden'] : ['/login']);
};

// ── Pages ──
@Component({ selector: 'app-home', template: `<h4>Home</h4><p>Public page. Everybody can see it.</p>` })
export class Home {}

@Component({
  selector: 'app-login',
  imports: [FormsModule],
  template: `
    <h4>Log in</h4>
    @if (auth.message()) {
      <p>ℹ {{ auth.message() }}</p>
    }
    <form (ngSubmit)="submit()">
      <p><input name="email" [(ngModel)]="email" placeholder="Email" /></p>
      <p><input name="password" type="password" [(ngModel)]="password" placeholder="Password" /></p>
      <p><label><input type="checkbox" name="short" [(ngModel)]="shortSession" /> Short session (10 seconds)</label></p>
      <button type="submit">Log in</button>
    </form>
    @if (error()) {
      <p>⚠ {{ error() }}</p>
    }
    <p>Try asha&#64;shop.com / user123 or admin&#64;shop.com / admin123</p>
  `,
})
export class Login {
  auth = inject(AuthService);
  private router = inject(Router);
  email = 'asha@shop.com';
  password = 'user123';
  shortSession = false;
  error = signal('');

  submit() {
    this.error.set('');
    this.auth.login(this.email, this.password, this.shortSession ? 10 : 900).subscribe({
      next: () => this.router.navigate(['/profile']),
      error: (err: HttpErrorResponse) => this.error.set(err.error.message),
    });
  }
}

@Component({
  selector: 'app-register',
  imports: [FormsModule],
  template: `
    <h4>Register</h4>
    <form (ngSubmit)="submit()">
      <p><input name="name" [(ngModel)]="name" placeholder="Name" /></p>
      <p><input name="email" [(ngModel)]="email" placeholder="Email" /></p>
      <p><input name="password" type="password" [(ngModel)]="password" placeholder="Password" /></p>
      <button type="submit">Create account</button>
    </form>
    @if (error()) {
      <p>⚠ {{ error() }}</p>
    }
  `,
})
export class Register {
  private auth = inject(AuthService);
  private router = inject(Router);
  name = '';
  email = '';
  password = '';
  error = signal('');

  submit() {
    if (!this.name || !this.email || this.password.length < 6) {
      this.error.set('Enter a name, an email and a password of at least 6 characters.');
      return;
    }
    this.auth.register(this.name, this.email, this.password).subscribe({
      next: () => this.router.navigate(['/profile']),
      error: (err: HttpErrorResponse) => this.error.set(err.error.message),
    });
  }
}

@Component({
  selector: 'app-profile',
  template: `
    <h4>My profile</h4>
    @if (me(); as me) {
      <p>{{ me.name }} ({{ me.email }}), role: {{ me.role }}</p>
    }
  `,
})
export class Profile {
  me = signal<any>(null);
  constructor() {
    inject(HttpClient).get<any>('/api/me').subscribe(data => this.me.set(data));
  }
}

@Component({
  selector: 'app-admin',
  template: `
    <h4>Admin reports</h4>
    @if (report(); as report) {
      <p>Sales: {{ report.sales }}, orders: {{ report.orders }}</p>
    }
  `,
})
export class Admin {
  report = signal<any>(null);
  constructor() {
    inject(HttpClient).get<any>('/api/admin/report').subscribe(data => this.report.set(data));
  }
}

@Component({ selector: 'app-forbidden', template: `<h4>Access denied</h4><p>You may not open this page.</p>` })
export class Forbidden {}

// ── Routes and configuration ──
const routes: Routes = [
  { path: '', component: Home },
  { path: 'login', component: Login },
  { path: 'register', component: Register },
  { path: 'profile', component: Profile, canActivate: [authGuard] },
  { path: 'admin', component: Admin, canActivate: [roleGuard], data: { roles: ['admin'] } },
  { path: 'forbidden', component: Forbidden },
];

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideHttpClient(withInterceptors([authInterceptor, practiceServer]))],
};

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <nav>
      <a routerLink="/">Home</a> |
      <a routerLink="/profile">Profile</a> |
      <a routerLink="/admin">Admin</a> |
      @if (auth.isLoggedIn()) {
        {{ auth.user()?.name }} ({{ auth.user()?.role }})
        <button (click)="auth.logout()">Log out</button>
      } @else {
        <a routerLink="/login">Log in</a> | <a routerLink="/register">Register</a>
      }
    </nav>
    <router-outlet />
  `,
})
export class App {
  auth = inject(AuthService);
}
// ─── Practice server ──────────────────────────────────────────────
// Answers the auth API inside the browser. A real server signs the token
// with a secret key; this practice server only imitates the format.
const ACCOUNTS = [
  { name: 'Asha Rao', email: 'asha@shop.com', password: 'user123', role: 'user' },
  { name: 'Admin', email: 'admin@shop.com', password: 'admin123', role: 'admin' },
];

function makeToken(account: any, seconds: number) {
  const encode = (data: object) => btoa(JSON.stringify(data));
  const exp = Math.floor(Date.now() / 1000) + seconds;
  const payload = { name: account.name, email: account.email, role: account.role, exp };
  return `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode(payload)}.practice-signature`;
}

function readToken(req: HttpRequest<any>) {
  const header = req.headers.get('Authorization') ?? '';
  try {
    const payload = JSON.parse(atob(header.replace('Bearer ', '').split('.')[1]));
    return payload.exp * 1000 > Date.now() ? payload : null;
  } catch {
    return null;
  }
}

function practiceServer(req: HttpRequest<any>, next: HttpHandlerFn) {
  const body = req.body ?? {};
  if (req.url === '/api/auth/register') {
    if (ACCOUNTS.some(a => a.email === body.email)) return fail(409, 'This email is already registered.');
    const account = { name: body.name, email: body.email, password: body.password, role: 'user' };
    ACCOUNTS.push(account);
    return reply({ token: makeToken(account, 900) });
  }
  if (req.url === '/api/auth/login') {
    const account = ACCOUNTS.find(a => a.email === body.email && a.password === body.password);
    return account ? reply({ token: makeToken(account, body.seconds ?? 900) }) : fail(401, 'Wrong email or password.');
  }
  const user = readToken(req);
  if (!user) return fail(401, 'Please log in.');
  if (req.url === '/api/me') return reply(user);
  if (req.url === '/api/admin/report') {
    return user.role === 'admin' ? reply({ sales: 12400, orders: 310 }) : fail(403, 'Administrators only.');
  }
  return fail(404, 'Not Found');
}

function reply(body: unknown) {
  return of(new HttpResponse({ status: 200, body })).pipe(delay(200));
}

function fail(status: number, message: string) {
  return timer(200).pipe(mergeMap(() => throwError(() => new HttpErrorResponse({ status, error: { message } }))));
}

```

Try these steps: open **Profile** as a guest, log in as Asha, open **Admin**, log out, log in as the admin, register a new account, and log in with a short session.

## Download Project Files

### Download Authentication and Authorization Project Source Code

<a href="/downloads/angular-auth-portal.zip" download>Download angular-auth-portal.zip</a>

The ZIP file contains the capstone as a complete Angular CLI project with a real API server. The server signs real JWTs with a secret key, stores password hashes, and checks the token and the role for every protected request.

| File | What it contains |
|---|---|
| `server/server.js` | The API: register, login, `/api/me` and `/api/admin/users`, with JWT signing |
| `src/app/auth/auth.service.ts` | Authentication state, login, registration, logout, `localStorage` and expiration |
| `src/app/auth/auth.interceptor.ts` | Adds the token and logs out on 401 |
| `src/app/auth/auth.guards.ts` | `authGuard`, `roleGuard` and `guestGuard` |
| `src/app/pages/` | Home, login, register, profile, admin and "access denied" pages |
| `src/environments/` | The API address |

### Project Setup Instructions

You need **Node.js 20.19 or newer**.

1. Unzip the file and open a terminal in the `auth-portal` folder.
2. Install the packages:

   ```bash
   npm install
   ```

3. Start the API server on `http://localhost:3000`. Keep this terminal open:

   ```bash
   npm run api
   ```

4. In a second terminal, start the Angular app:

   ```bash
   npm start
   ```

5. Open `http://localhost:4200` and log in with one of the test accounts:

   | Email | Password | Role |
   |---|---|---|
   | admin@example.com | Admin123! | admin |
   | asha@example.com | User123! | user |

To test token expiration, stop the API and start it with `npm run api:short`. Tokens then expire after 30 seconds.

## Common Mistakes

<div className="ngc-mistakes">

| Mistake | What you see | Fix |
|---|---|---|
| Protecting data only with Angular guards | Users can still call the API directly and get the data | Check the token and role on the server for every request |
| Secret data in the JWT payload | Anyone with the token can read it | Put only an ID, name and role in the payload |
| Adding the token by hand in each service | Some requests forget it and get 401 | Use one interceptor |
| Ignoring `exp` | The app looks logged in, but every request fails | Log out with a timer, and on every 401 |
| Logout that only clears the signal | After a reload, the user is logged in again | Also remove the token from `localStorage` |
| Login error says "Wrong password" | Attackers learn which emails exist | Say "Wrong email or password." |

</div>

## Check Yourself

## Exercises

## What to Remember

- Authentication checks who the user is; authorization checks what they may do. After login, the server sends a signed JWT, and the app sends it back in the `Authorization` header with an interceptor.
- Keep the token and the values computed from it in an `AuthService`. Save it to survive reloads, log out when it expires or the server answers 401, and clear everything on logout.
- Use roles and route guards (`CanActivateFn` returning `true` or a `UrlTree`) for the user experience, and always check permissions again on the server.

Next: **Error Handling**, how to find, handle and log errors in an Angular app.
