Authentication and Authorization
Quick reference for authentication and authorization patterns in LARC applications. For detailed tutorials, see Learning LARC Chapter 12.
Overview
Authentication verifies user identity ("who are you?") while authorization determines permissions ("what can you do?"). LARC applications typically use JWT tokens for authentication and role-based access control (RBAC) for authorization.
Key Concepts:- JWT tokens: Cryptographically signed identity tokens with claims
- Token refresh: Automatic renewal before expiry
- Protected routes: Enforce authentication/authorization requirements
- RBAC: Role-based permission system with inheritance
- Session management: Timeout, activity tracking, secure storage
Quick Example
// Initialize authentication
import { authService } from './services/auth.js';
await authService.initialize();
if (authService.getState().isAuthenticated) {
// User logged in
console.log('User:', authService.getState().user);
} else {
// Redirect to login
window.location.href = '/login';
}
// Login
const success = await authService.login({
username: 'alice',
password: 'secret123'
});
// Check permissions
if (authService.hasRole('admin')) {
// Show admin features
}
Authentication Service API
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| initialize() | - | Promise\login(credentials) | {username, password} | Promise\logout() | - | void | Clear tokens and state |
| getState() | - | AuthState | Get current authentication state |
| getAccessToken() | - | string\|null | Get token for API requests |
| hasRole(role) | role: string | boolean | Check if user has role |
| hasPermission(perm) | permission: string | boolean | Check if user has permission |
AuthState Interface
interface AuthState {
isAuthenticated: boolean;
user: UserClaims | null;
tokens: AuthTokens | null;
}
interface UserClaims {
userId: string;
username: string;
roles: string[];
permissions: string[];
}
interface AuthTokens {
accessToken: string;
refreshToken: string;
expiresIn: number;
}
API Interceptor Pattern
Automatically add authentication headers to requests:
// Add bearer token to requests
api.interceptors.request.use(async (config) => {
const token = authService.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle 401 by refreshing token
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 && !error.config._retry) {
error.config._retry = true;
await authService.refreshAccessToken();
const newToken = authService.getAccessToken();
error.config.headers.Authorization = `Bearer ${newToken}`;
return api(error.config);
}
return Promise.reject(error);
}
);
Protected Routes
Route Guard Component
class ProtectedRoute extends HTMLElement {
connectedCallback() {
const state = authService.getState();
if (!state.isAuthenticated) {
const redirect = encodeURIComponent(window.location.pathname);
window.location.href = `/login?redirect=${redirect}`;
return;
}
// Check roles
const requiredRoles = this.getAttribute('roles')?.split(',') || [];
if (requiredRoles.length) {
const hasRole = requiredRoles.some(r => authService.hasRole(r));
if (!hasRole) {
window.location.href = '/forbidden';
return;
}
}
}
}
customElements.define('protected-route', ProtectedRoute);
Usage
<!-- Public route -->
<route path="/" component="home-page"></route>
<!-- Requires authentication -->
<route path="/dashboard">
<protected-route>
<dashboard-page></dashboard-page>
</protected-route>
</route>
<!-- Requires admin role -->
<route path="/admin">
<protected-route roles="admin">
<admin-panel></admin-panel>
</protected-route>
</route>
Role-Based Access Control
RBAC Configuration
const ROLES = {
guest: {
id: 'guest',
permissions: [
{ resource: 'content', action: 'read', scope: 'public' }
]
},
user: {
id: 'user',
inherits: ['guest'],
permissions: [
{ resource: 'profile', action: 'read', scope: 'own' },
{ resource: 'profile', action: 'update', scope: 'own' },
{ resource: 'content', action: 'create', scope: 'own' }
]
},
moderator: {
id: 'moderator',
inherits: ['user'],
permissions: [
{ resource: 'content', action: 'update', scope: 'all' },
{ resource: 'content', action: 'delete', scope: 'all' }
]
},
admin: {
id: 'admin',
inherits: ['moderator'],
permissions: [
{ resource: 'users', action: 'create' },
{ resource: 'users', action: 'update' },
{ resource: 'users', action: 'delete' },
{ resource: 'settings', action: 'update' }
]
}
};
Authorization Service API
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| getPermissions(roleId) | roleId: string | Permission[] | Get all permissions (including inherited) |
| hasPermission(roles, resource, action, scope?) | roles: string[], resource: string, action: string, scope?: string | boolean | Check if any role has permission |
| canAccess(roles, resource, action, ownerId?, userId?) | roles: string[], resource: string, action: string, ownerId?: string, userId?: string | boolean | Check access with ownership |
Conditional Rendering
class AuthorizedContent extends HTMLElement {
connectedCallback() {
const resource = this.getAttribute('resource');
const action = this.getAttribute('action');
const state = authService.getState();
const authorized = state.user &&
authz.hasPermission(state.user.roles, resource, action);
if (!authorized) {
this.style.display = 'none';
}
}
}
customElements.define('authorized-content', AuthorizedContent);
Session Management
Session Timeout
class SessionManager {
constructor(timeoutMinutes, warningMinutes) {
this.timeoutMinutes = timeoutMinutes;
this.warningMinutes = warningMinutes;
this.setupActivityListeners();
}
start() {
this.resetTimeout();
}
resetTimeout() {
clearTimeout(this.timeoutId);
clearTimeout(this.warningId);
// Warning before timeout
const warningMs = this.warningMinutes * 60 * 1000;
this.warningId = setTimeout(() => {
this.showWarning();
}, warningMs);
// Logout on timeout
const timeoutMs = this.timeoutMinutes * 60 * 1000;
this.timeoutId = setTimeout(() => {
authService.logout();
window.location.href = '/login?reason=timeout';
}, timeoutMs);
}
setupActivityListeners() {
['mousedown', 'keydown', 'scroll', 'touchstart'].forEach(event => {
document.addEventListener(event, () => {
if (authService.getState().isAuthenticated) {
this.resetTimeout();
}
}, { passive: true });
});
}
}
// Initialize with 30-minute timeout
const sessionManager = new SessionManager(30, 25);
sessionManager.start();
Security Best Practices
Input Validation
const validators = {
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
password: (value) => {
const errors = [];
if (value.length < 8) errors.push('8+ characters required');
if (!/[A-Z]/.test(value)) errors.push('Uppercase required');
if (!/[a-z]/.test(value)) errors.push('Lowercase required');
if (!/[0-9]/.test(value)) errors.push('Number required');
if (!/[^A-Za-z0-9]/.test(value)) errors.push('Special char required');
return { valid: errors.length === 0, errors };
},
sanitize: (value) =>
value.replace(/[<>]/g, '')
.replace(/javascript:/gi, '')
.trim()
};
CSRF Protection
class CSRFProtection {
generateToken() {
const token = this.randomString(32);
sessionStorage.setItem('csrf-token', token);
return token;
}
getToken() {
return sessionStorage.getItem('csrf-token') || '';
}
addToHeaders(headers) {
return {
...headers,
'X-CSRF-Token': this.getToken()
};
}
randomString(length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array).map(x => chars[x % chars.length]).join('');
}
}
const csrf = new CSRFProtection();
Component Reference
See Chapter 20 for authentication-related components:
- pan-auth: Complete authentication component
- pan-user-menu: User profile dropdown with logout
Complete Example: Login Form
class LoginForm extends HTMLElement {
connectedCallback() {
this.state = {
username: '',
password: '',
loading: false,
error: null
};
csrf.generateToken();
this.render();
}
async handleSubmit(e) {
e.preventDefault();
this.state.loading = true;
this.state.error = null;
this.render();
try {
const success = await authService.login({
username: this.state.username,
password: this.state.password
});
if (success) {
const params = new URLSearchParams(window.location.search);
const redirect = params.get('redirect') || '/dashboard';
window.location.href = redirect;
} else {
this.state.error = 'Invalid credentials';
}
} catch (err) {
this.state.error = 'Login failed. Try again.';
} finally {
this.state.loading = false;
this.render();
}
}
render() {
this.innerHTML = `
<form class="login-form">
${this.state.error ? `<div class="error">${this.state.error}</div>` : ''}
<label>
Username
<input name="username" value="${this.state.username}" required>
</label>
<label>
Password
<input type="password" name="password" value="${this.state.password}" required>
</label>
<button type="submit" ${this.state.loading ? 'disabled' : ''}>
${this.state.loading ? 'Signing in...' : 'Sign In'}
</button>
</form>
`;
this.querySelector('form').addEventListener('submit', (e) => this.handleSubmit(e));
this.querySelector('[name="username"]').addEventListener('input', (e) => {
this.state.username = e.target.value;
});
this.querySelector('[name="password"]').addEventListener('input', (e) => {
this.state.password = e.target.value;
});
}
}
customElements.define('login-form', LoginForm);
Cross-References
- Tutorial: Learning LARC Chapter 12 (Authentication and Authorization)
- Components: Chapter 20 (pan-auth, pan-user-menu)
- Patterns: Appendix E (Security Patterns)
- Related: Chapter 7 (API Authentication), Chapter 9 (WebSocket Authentication)
Common Issues
Issue: Token expires during request
Problem: 401 errors on long-running requests Solution: Refresh token before expiry (subtract 5 minutes from expiry time)Issue: Lost authentication on page refresh
Problem: User logged out after reload Solution: CallauthService.initialize() on app startup to restore session
Issue: Infinite redirect loops
Problem: Protected route redirects to login, login redirects to protected route Solution: Check authentication before redirect; use redirect parameter correctlyIssue: CORS errors with credentials
Problem:Access-Control-Allow-Credentials errors
Solution: Set credentials: 'include' in fetch options, enable CORS on server
Issue: XSS attacks via stored tokens
Problem: Token accessible via JavaScript Solution: Use httpOnly cookies for refresh tokens; store access tokens in memory when possibleSee Learning LARC Chapter 12 for complete authentication flows, OAuth integration, and multi-factor authentication patterns.