User Access Control in Angular
By Syncfusion®, Inc. · Last updated
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 |
1. Loginemail + password2. Serverchecks, sends JWT3. Interceptoradds the token4. Serverchecks token + roleauthenticationauthorization: 200, 401 or 403
Authentication first, then authorization on every request.
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.
Headeralg, typ.Payloadname, role, exp.Signaturemade with the secret keyHeader and payload can be decoded by anyone. The signature proves the server made the token.
The three parts of a JSON Web Token.
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:
- The user logs in. The server checks the password and sends back a token.
- The app stores the token.
- The app sends the token with every request, in the header
Authorization: Bearer <token>. - 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.
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
userandisLoggedIn. - Methods:
register(),login()andlogout().
@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.
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.
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:
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:
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.
Logintoken with expTimer startsexp − nowTime is overtoken expiredlogout()"Session expired"The app logs out before the server starts to answer 401.
Automatic logout when the token expires.
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.
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.
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:
routerLink="/profile"user clicksauthGuardlogged in?true → Profilethe page opensUrlTree → /loginredirected
A guard decides before the route opens.
true: the route opens.- A
UrlTree: the router goes to that URL instead. Create it withrouter.createUrlTree(['/login']). false: the navigation is cancelled.
A guard of type CanActivateFn is added to a route with canActivate:
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'].
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:
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:
- Stop the expiration timer.
- Set the token signal to
null. Every component that reads the state updates. - Remove the saved token from
localStorage. - Navigate to the login or home page.
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.
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@shop.com / user123 or admin@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
Download angular-auth-portal.zip
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.
- Unzip the file and open a terminal in the
auth-portalfolder. Install the packages:
npm installStart the API server on
http://localhost:3000. Keep this terminal open:npm run apiIn a second terminal, start the Angular app:
npm startOpen
http://localhost:4200and 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
| 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." |
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
Authorizationheader 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 (
CanActivateFnreturningtrueor aUrlTree) 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.