Authentication and Authorization
Building secure applications requires proper authentication (verifying user identity) and authorization (controlling access to resources). This tutorial demonstrates how to implement authentication and route guards using Aurelia 2's router (@aurelia/router).
Table of Contents
Overview
In this tutorial, you'll learn how to:
Create an authentication service with login/logout functionality
Manage JWT tokens securely
Protect routes using the
canLoadlifecycle hookImplement global authentication guards
Handle unauthorized access and redirects
Persist authentication state across page refreshes
Setting Up Authentication Service
First, let's create an authentication service that manages user state.
// src/services/auth-service.ts
import { DI } from '@aurelia/kernel';
export const IAuthService = DI.createInterface<IAuthService>(
'IAuthService',
x => x.singleton(AuthService)
);
export interface IAuthService extends AuthService {}
export interface User {
id: number;
username: string;
email: string;
token: string;
}
export interface LoginCredentials {
email: string;
password: string;
}
export class AuthService {
private currentUser: User | null = null;
get user(): User | null {
return this.currentUser;
}
get isAuthenticated(): boolean {
return this.currentUser !== null;
}
async login(credentials: LoginCredentials): Promise<boolean> {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) {
return false;
}
const data = await response.json();
this.currentUser = data.user;
return true;
} catch (error) {
console.error('Login failed:', error);
return false;
}
}
logout(): void {
this.currentUser = null;
}
async loadCurrentUser(): Promise<void> {
// Load user from token or session
// This will be implemented in the JWT section
}
}JWT Token Management
For production applications, use JWT tokens for authentication. Create a token service to handle token storage and validation.
// src/services/jwt-service.ts
import { DI, IPlatform } from '@aurelia/kernel';
export const IJwtService = DI.createInterface<IJwtService>(
'IJwtService',
x => x.singleton(JwtService)
);
export interface IJwtService extends JwtService {}
const TOKEN_KEY = 'auth_token';
export class JwtService {
getToken(): string | null {
if (typeof window === 'undefined') return null;
return window.localStorage.getItem(TOKEN_KEY);
}
saveToken(token: string): void {
if (typeof window === 'undefined') return;
window.localStorage.setItem(TOKEN_KEY, token);
}
destroyToken(): void {
if (typeof window === 'undefined') return;
window.localStorage.removeItem(TOKEN_KEY);
}
isTokenValid(): boolean {
const token = this.getToken();
if (!token) return false;
try {
// Parse JWT token to check expiration
const payload = this.parseJwt(token);
const expiry = payload.exp * 1000; // Convert to milliseconds
return Date.now() < expiry;
} catch {
return false;
}
}
private parseJwt(token: string): any {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
}
}Now update the AuthService to use JWT tokens:
// src/services/auth-service.ts (updated)
import { DI, resolve } from '@aurelia/kernel';
import { IJwtService } from './jwt-service';
export const IAuthService = DI.createInterface<IAuthService>(
'IAuthService',
x => x.singleton(AuthService)
);
export interface IAuthService extends AuthService {}
export interface User {
id: number;
username: string;
email: string;
token: string;
}
export interface LoginCredentials {
email: string;
password: string;
}
export class AuthService {
private jwtService = resolve(IJwtService);
private currentUser: User | null = null;
get user(): User | null {
return this.currentUser;
}
get isAuthenticated(): boolean {
return this.currentUser !== null && this.jwtService.isTokenValid();
}
async login(credentials: LoginCredentials): Promise<boolean> {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) {
return false;
}
const data = await response.json();
this.setAuth(data.user);
return true;
} catch (error) {
console.error('Login failed:', error);
return false;
}
}
logout(): void {
this.clearAuth();
}
async loadCurrentUser(): Promise<void> {
if (!this.jwtService.isTokenValid()) {
this.clearAuth();
return;
}
try {
const token = this.jwtService.getToken();
const response = await fetch('/api/auth/user', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
this.currentUser = data.user;
} else {
this.clearAuth();
}
} catch (error) {
console.error('Failed to load user:', error);
this.clearAuth();
}
}
private setAuth(user: User): void {
this.jwtService.saveToken(user.token);
this.currentUser = user;
}
private clearAuth(): void {
this.jwtService.destroyToken();
this.currentUser = null;
}
getAuthHeaders(): Record<string, string> {
const token = this.jwtService.getToken();
return token ? { 'Authorization': `Bearer ${token}` } : {};
}
}Creating Login and Logout
Create login and logout components that use the authentication service.
Login Component
// src/pages/login.ts
import { customElement, resolve } from '@aurelia/runtime-html';
import { IRouter } from '@aurelia/router';
import { IAuthService } from '../services/auth-service';
@customElement('login-page')
export class LoginPage {
private authService = resolve(IAuthService);
private router = resolve(IRouter);
email = '';
password = '';
errorMessage = '';
isLoading = false;
async submit(): Promise<void> {
if (!this.email || !this.password) {
this.errorMessage = 'Please enter email and password';
return;
}
this.isLoading = true;
this.errorMessage = '';
const success = await this.authService.login({
email: this.email,
password: this.password
});
this.isLoading = false;
if (success) {
// Redirect to home or intended page
await this.router.load('/dashboard');
} else {
this.errorMessage = 'Invalid email or password';
}
}
}<!-- src/pages/login.html -->
<div class="login-page">
<div class="login-form">
<h2>Login</h2>
<form submit.trigger="submit()">
<div class="form-group">
<label for="email">Email</label>
<input
id="email"
type="email"
value.bind="email"
disabled.bind="isLoading"
placeholder="Enter your email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input
id="password"
type="password"
value.bind="password"
disabled.bind="isLoading"
placeholder="Enter your password">
</div>
<div if.bind="errorMessage" class="error-message">
${errorMessage}
</div>
<button type="submit" disabled.bind="isLoading">
<span if.bind="!isLoading">Login</span>
<span if.bind="isLoading">Logging in...</span>
</button>
</form>
</div>
</div>Navigation with Logout
// src/components/nav-bar.ts
import { customElement, resolve } from '@aurelia/runtime-html';
import { IRouter } from '@aurelia/router';
import { IAuthService } from '../services/auth-service';
@customElement('nav-bar')
export class NavBar {
private authService = resolve(IAuthService);
private router = resolve(IRouter);
get isAuthenticated(): boolean {
return this.authService.isAuthenticated;
}
get username(): string {
return this.authService.user?.username ?? '';
}
async logout(): Promise<void> {
this.authService.logout();
await this.router.load('/login');
}
}<!-- src/components/nav-bar.html -->
<nav class="navbar">
<div class="nav-brand">
<a href="/">My App</a>
</div>
<div class="nav-menu">
<a load="/home">Home</a>
<div if.bind="isAuthenticated">
<a load="/dashboard">Dashboard</a>
<a load="/profile">Profile</a>
<span class="username">${username}</span>
<button click.trigger="logout()">Logout</button>
</div>
<div if.bind="!isAuthenticated">
<a load="/login">Login</a>
</div>
</div>
</nav>Protecting Routes with canLoad
The canLoad lifecycle hook allows you to protect individual routes by preventing navigation if certain conditions aren't met.
Component-Level Route Guard
// src/pages/dashboard.ts
import { customElement, resolve } from '@aurelia/runtime-html';
import { IRouteViewModel, Params, RouteNode, NavigationInstruction } from '@aurelia/router';
import { IAuthService } from '../services/auth-service';
@customElement('dashboard-page')
export class DashboardPage implements IRouteViewModel {
private authService = resolve(IAuthService);
canLoad(
params: Params,
next: RouteNode,
current: RouteNode | null
): boolean | NavigationInstruction {
if (!this.authService.isAuthenticated) {
// Redirect to login page
return 'login';
}
// Allow navigation
return true;
}
// Component implementation
dashboardData = [];
async loading(): Promise<void> {
// Load dashboard data
await this.loadDashboardData();
}
private async loadDashboardData(): Promise<void> {
const response = await fetch('/api/dashboard', {
headers: this.authService.getAuthHeaders()
});
if (response.ok) {
this.dashboardData = await response.json();
}
}
}<!-- src/pages/dashboard.html -->
<div class="dashboard">
<h1>Dashboard</h1>
<div class="dashboard-content">
<!-- Dashboard content -->
</div>
</div>Preserving Intended Route
When redirecting to login, you often want to return to the intended page after successful authentication:
// src/pages/dashboard.ts (enhanced)
import { customElement, resolve } from '@aurelia/runtime-html';
import {
IRouteViewModel,
Params,
RouteNode,
NavigationInstruction,
INavigationOptions
} from '@aurelia/router';
import { IAuthService } from '../services/auth-service';
@customElement('dashboard-page')
export class DashboardPage implements IRouteViewModel {
private authService = resolve(IAuthService);
canLoad(
params: Params,
next: RouteNode,
current: RouteNode | null,
options: INavigationOptions
): boolean | NavigationInstruction {
if (!this.authService.isAuthenticated) {
// Store intended route and redirect to login
const returnUrl = encodeURIComponent(next.path);
return `login?returnUrl=${returnUrl}`;
}
return true;
}
}Update login to handle return URL:
// src/pages/login.ts (enhanced)
import { customElement, resolve } from '@aurelia/runtime-html';
import { IRouter, IRouteViewModel, Params, RouteNode } from '@aurelia/router';
import { IAuthService } from '../services/auth-service';
@customElement('login-page')
export class LoginPage implements IRouteViewModel {
private authService = resolve(IAuthService);
private router = resolve(IRouter);
email = '';
password = '';
errorMessage = '';
isLoading = false;
returnUrl = '/dashboard';
loading(params: Params): void {
// Get return URL from query params
this.returnUrl = params.returnUrl || '/dashboard';
}
async submit(): Promise<void> {
if (!this.email || !this.password) {
this.errorMessage = 'Please enter email and password';
return;
}
this.isLoading = true;
this.errorMessage = '';
const success = await this.authService.login({
email: this.email,
password: this.password
});
this.isLoading = false;
if (success) {
// Redirect to intended page
await this.router.load(this.returnUrl);
} else {
this.errorMessage = 'Invalid email or password';
}
}
}Global Authentication Hooks
For applications with many protected routes, create a reusable authentication hook that can be applied globally.
// src/hooks/auth-hook.ts
import { DI, resolve } from '@aurelia/kernel';
import { ILifecycleHooks, lifecycleHooks } from '@aurelia/runtime-html';
import {
IRouteViewModel,
Params,
RouteNode,
NavigationInstruction,
INavigationOptions
} from '@aurelia/router';
import { IAuthService } from '../services/auth-service';
export const IAuthHook = DI.createInterface<IAuthHook>(
'IAuthHook',
x => x.singleton(AuthHook)
);
export interface IAuthHook extends AuthHook {}
@lifecycleHooks()
export class AuthHook implements ILifecycleHooks<IRouteViewModel, 'canLoad'> {
private authService = resolve(IAuthService);
canLoad(
vm: IRouteViewModel,
params: Params,
next: RouteNode,
current: RouteNode | null,
options: INavigationOptions
): boolean | NavigationInstruction {
if (!this.authService.isAuthenticated) {
const returnUrl = encodeURIComponent(next.path);
return `login?returnUrl=${returnUrl}`;
}
return true;
}
}Applying Global Hook to Routes
// src/app.ts
import { customElement, resolve } from '@aurelia/runtime-html';
import { route } from '@aurelia/router';
import { IAuthHook } from './hooks/auth-hook';
@route({
routes: [
{
path: '',
redirectTo: 'home'
},
{
path: 'home',
component: () => import('./pages/home'),
title: 'Home'
},
{
path: 'login',
component: () => import('./pages/login'),
title: 'Login'
},
{
path: 'dashboard',
component: () => import('./pages/dashboard'),
title: 'Dashboard',
data: {
auth: IAuthHook
}
},
{
path: 'profile',
component: () => import('./pages/profile'),
title: 'Profile',
data: {
auth: IAuthHook
}
}
]
})
@customElement({
name: 'app-root',
template: `
<nav-bar></nav-bar>
<div class="main-content">
<au-viewport></au-viewport>
</div>
`
})
export class App {}Register the hook in your main file:
// src/main.ts
import Aurelia from 'aurelia';
import { RouterConfiguration } from '@aurelia/router';
import { IAuthHook, AuthHook } from './hooks/auth-hook';
import { IAuthService, AuthService } from './services/auth-service';
import { IJwtService, JwtService } from './services/jwt-service';
import { App } from './app';
Aurelia
.register(
RouterConfiguration,
IAuthService,
IJwtService,
IAuthHook
)
.app(App)
.start();Handling Unauthorized Access
Handle 401 Unauthorized responses from your API by automatically logging out and redirecting to login.
// src/services/api-client.ts
import { DI, resolve } from '@aurelia/kernel';
import { IRouter } from '@aurelia/router';
import { IAuthService } from './auth-service';
export const IApiClient = DI.createInterface<IApiClient>(
'IApiClient',
x => x.singleton(ApiClient)
);
export interface IApiClient extends ApiClient {}
export class ApiClient {
private authService = resolve(IAuthService);
private router = resolve(IRouter);
private baseUrl = '/api';
async get<T>(endpoint: string): Promise<T> {
return this.request<T>('GET', endpoint);
}
async post<T>(endpoint: string, data: any): Promise<T> {
return this.request<T>('POST', endpoint, data);
}
async put<T>(endpoint: string, data: any): Promise<T> {
return this.request<T>('PUT', endpoint, data);
}
async delete<T>(endpoint: string): Promise<T> {
return this.request<T>('DELETE', endpoint);
}
private async request<T>(
method: string,
endpoint: string,
data?: any
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...this.authService.getAuthHeaders()
};
const options: RequestInit = {
method,
headers
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(url, options);
if (response.status === 401) {
// Unauthorized - clear auth and redirect to login
this.authService.logout();
await this.router.load('/login');
throw new Error('Unauthorized');
}
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return response.json();
}
}Persisting Authentication State
Load the user's authentication state when the application starts.
// src/app.ts (enhanced)
import { customElement, resolve } from '@aurelia/runtime-html';
import { route, IRouter } from '@aurelia/router';
import { IAuthService } from './services/auth-service';
@route({
routes: [
// ... routes configuration
]
})
@customElement({
name: 'app-root',
template: `
<div if.bind="loading" class="loading">Loading...</div>
<div if.bind="!loading">
<nav-bar></nav-bar>
<div class="main-content">
<au-viewport></au-viewport>
</div>
</div>
`
})
export class App {
private authService = resolve(IAuthService);
loading = true;
async binding(): Promise<void> {
// Load authentication state before rendering
await this.authService.loadCurrentUser();
this.loading = false;
}
}Complete Example
Here's a complete working example that ties everything together:
Project Structure
src/
├── app.ts
├── main.ts
├── components/
│ └── nav-bar.ts
│ └── nav-bar.html
├── pages/
│ ├── home.ts
│ ├── home.html
│ ├── login.ts
│ ├── login.html
│ ├── dashboard.ts
│ └── dashboard.html
├── services/
│ ├── auth-service.ts
│ ├── jwt-service.ts
│ └── api-client.ts
└── hooks/
└── auth-hook.tsMain Configuration
// src/main.ts
import Aurelia from 'aurelia';
import { RouterConfiguration } from '@aurelia/router';
import { IAuthService } from './services/auth-service';
import { IJwtService } from './services/jwt-service';
import { IApiClient } from './services/api-client';
import { IAuthHook } from './hooks/auth-hook';
import { App } from './app';
Aurelia
.register(
RouterConfiguration,
IAuthService,
IJwtService,
IApiClient,
IAuthHook
)
.app(App)
.start();App Root with Routes
// src/app.ts
import { customElement, resolve } from '@aurelia/runtime-html';
import { route } from '@aurelia/router';
import { IAuthService } from './services/auth-service';
import { IAuthHook } from './hooks/auth-hook';
@route({
routes: [
{
path: '',
redirectTo: 'home'
},
{
path: 'home',
component: () => import('./pages/home'),
title: 'Home'
},
{
path: 'login',
component: () => import('./pages/login'),
title: 'Login'
},
{
path: 'dashboard',
component: () => import('./pages/dashboard'),
title: 'Dashboard',
data: {
auth: IAuthHook
}
}
]
})
@customElement({
name: 'app-root',
template: `
<div if.bind="loading" class="app-loading">
<div class="spinner"></div>
<p>Loading...</p>
</div>
<div if.bind="!loading" class="app-container">
<nav-bar></nav-bar>
<main class="main-content">
<au-viewport></au-viewport>
</main>
</div>
`
})
export class App {
private authService = resolve(IAuthService);
loading = true;
async binding(): Promise<void> {
await this.authService.loadCurrentUser();
this.loading = false;
}
}Conclusion
You now have a complete authentication and authorization system with:
✅ Secure JWT token management
✅ Login and logout functionality
✅ Protected routes using
canLoadlifecycle hooks✅ Global authentication guards
✅ Automatic handling of unauthorized requests
✅ Persistent authentication state
✅ Return URL support after login
Security Best Practices
Never store sensitive data in localStorage - Only store tokens, not passwords
Always use HTTPS in production - Protect tokens in transit
Implement token refresh - Use refresh tokens for better security
Set appropriate token expiration - Balance security and user experience
Validate tokens server-side - Never trust client-side validation alone
Use HTTP-only cookies - For even better security, consider HTTP-only cookies instead of localStorage
Implement CSRF protection - Protect against cross-site request forgery
Rate limit authentication endpoints - Prevent brute force attacks
Next Steps
Implement role-based authorization
Add password reset functionality
Implement multi-factor authentication (MFA)
Add social login (OAuth)
Implement refresh token rotation
For more information on the Aurelia router, see the official router documentation.
Last updated
Was this helpful?