# Spry Authentication & Authorisation Refactor Architecture ## 1. Overview This document describes the architecture for refactoring Spry's `src/Users` system into two separate concerns: - **`src/Authentication`** (renamed from `src/Users`): ONE method of authentication provided by Spry - **`src/Authorisation`**: Token generation, validation, and permission checking ### Key Design Principles 1. **Separation of Concerns**: Authentication (who you are) is separate from Authorisation (what you can do) 2. **Identity Abstraction**: Authorisation works with a generic `Identity` interface, not a concrete User model 3. **Stateless Tokens**: Tokens are self-contained and validated cryptographically 4. **Multiple Token Delivery**: Same token via cookie or Bearer header 5. **Extensibility**: Applications can implement custom authentication while using Spry's Authorisation --- ## 2. High-Level Architecture ```mermaid flowchart TB subgraph Request Flow Request[HTTP Request] --> TokenExtractor TokenExtractor --> TokenValidator TokenValidator --> AuthorisationContext end subgraph Authorisation System TokenExtractor[TokenExtractor] TokenValidator[TokenValidator] AuthorisationContext[AuthorisationContext] AuthorisationService[AuthorisationService] end subgraph Authentication System IdentityProvider[IdentityProvider Interface] UserIdentityProvider[UserIdentityProvider] UserService[UserService] SessionService[SessionService] LoginFormComponent[LoginFormComponent] end subgraph Storage Implexus[(Implexus Storage)] end TokenExtractor --> |Cookie or Bearer| TokenValidator TokenValidator --> |Valid Token| AuthorisationContext AuthorisationContext --> |get_current_identity| IdentityProvider IdentityProvider --> |implemented by| UserIdentityProvider UserIdentityProvider --> UserService UserIdentityProvider --> SessionService UserService --> Implexus SessionService --> Implexus LoginFormComponent --> UserService LoginFormComponent --> SessionService LoginFormComponent --> AuthorisationService ``` --- ## 3. File Structure ### 3.1 New `src/Authorisation` Structure ``` src/Authorisation/ ├── ARCHITECTURE.md # This document ├── meson.build # Build configuration ├── Identity.vala # Identity interface ├── AuthorisationContext.vala # Request-scoped authorisation state ├── AuthorisationService.vala # Token generation and validation ├── TokenPayload.vala # Token data structure ├── AuthorisationError.vala # Error domain └── AuthorisationModule.vala # IoC module registration ``` ### 3.2 Refactored `src/Authentication` Structure (renamed from `src/Users`) ``` src/Authentication/ ├── ARCHITECTURE.md # Architecture documentation ├── meson.build # Build configuration ├── User.vala # User data model ├── Session.vala # Session data model ├── UserService.vala # User CRUD and authentication ├── SessionService.vala # Session management ├── PermissionService.vala # Permission management on users ├── UsersMigration.vala # Database migrations ├── UserIdentityProvider.vala # IdentityProvider implementation for User ├── AuthenticationModule.vala # IoC module registration └── Components/ ├── LoginFormComponent.vala ├── UserFormComponent.vala ├── UserListComponent.vala ├── UserListItemComponent.vala ├── PermissionEditorComponent.vala └── UserManagementPage.vala ``` --- ## 4. Core Interfaces and Classes ### 4.1 Identity Interface The `Identity` interface is the contract between Authentication and Authorisation. Any authentication provider must implement this interface. ```vala namespace Spry.Authorisation { /** * Interface representing an authenticated identity. * * Implementations provide identity data that gets embedded in tokens * and can be retrieved on subsequent requests. * * Built-in implementation: Spry.Authentication.UserIdentityProvider * Custom implementations: OAuth providers, certificate auth, etc. */ public interface Identity : GLib.Object { /** * Unique identifier for this identity. * Used to look up the full identity object. */ public abstract string id { get; } /** * Human-readable name for this identity. * Typically username or email. */ public abstract string username { get; } /** * Permissions granted to this identity. * Returns ImmutableLot for thread-safety. */ public abstract ImmutableLot permissions { get; } /** * Additional data to embed in the token. * Implementation-specific data (e.g., roles, preferences). */ public abstract Properties token_data { get; } } } ``` ### 4.2 IdentityProvider Interface The `IdentityProvider` interface allows the Authorisation system to retrieve full identity objects. ```vala namespace Spry.Authorisation { /** * Interface for retrieving Identity objects by ID. * * The AuthorisationContext uses this to get_current_identity(). * Applications register their implementation during startup. */ public interface IdentityProvider : GLib.Object { /** * Retrieves an Identity by its unique ID. * * @param id The identity ID from the token * @return The Identity, or null if not found/inactive */ public abstract async Identity? get_identity_async(string id) throws Error; } } ``` ### 4.3 TokenPayload Model The `TokenPayload` contains all data embedded in the encrypted token. ```vala namespace Spry.Authorisation { /** * Data structure embedded in authorisation tokens. * * Token contents (JSON, encrypted and signed): * - id: Identity unique identifier * - username: Human-readable name * - permissions: Array of permission strings * - data: Additional properties * - issued_at: Token issuance timestamp * - expires_at: Token expiry timestamp */ public class TokenPayload : GLib.Object { // Identity fields public string id { get; set; } public string username { get; set; } public ImmutableLot permissions { get; set; } public Properties data { get; set; } // Token metadata public DateTime issued_at { get; set; } public DateTime expires_at { get; set; } /** * Creates a TokenPayload from an Identity. */ public TokenPayload.from_identity(Identity identity, TimeSpan duration) { GLib.Object( id: identity.id, username: identity.username, permissions: identity.permissions, data: identity.data, issued_at: new DateTime.now_utc(), expires_at: new DateTime.now_utc().add(duration) ); } /** * Checks if the token has expired. */ public bool is_expired() { return expires_at.compare(new DateTime.now_utc()) <= 0; } /** * Serializes to JSON for token encryption. */ public Json.Object to_json() { var obj = new Json.Object(); obj.set_string_member("id", id ?? ""); obj.set_string_member("username", username ?? ""); // Permissions array var perms_array = new Json.Array(); foreach (var perm in permissions) { perms_array.add_string_element(perm); } obj.set_array_member("permissions", perms_array); // Data object var data_obj = new Json.Object(); if (data != null) { var iter = data.iterator(); while (iter.next()) { var pair = iter.get(); // Serialize based on type data_obj.set_string_member(pair.key, pair.value.to_string()); } } obj.set_object_member("data", data_obj); // Metadata obj.set_string_member("issued_at", issued_at.format_iso8601()); obj.set_string_member("expires_at", expires_at.format_iso8601()); return obj; } /** * Deserializes from JSON after token decryption. */ public static TokenPayload from_json(Json.Object obj) { var payload = new TokenPayload(); payload.id = obj.get_string_member("id") ?? ""; payload.username = obj.get_string_member("username") ?? ""; // Permissions var perms = new LotBuilder(); if (obj.has_member("permissions")) { var arr = obj.get_array_member("permissions"); foreach (var element in arr.get_elements()) { perms.add(element.get_string() ?? ""); } } payload.permissions = perms.to_immutable(); // Data payload.data = new PropertyDictionary(); if (obj.has_member("data")) { var data_obj = obj.get_object_member("data"); foreach (var member in data_obj.get_members()) { payload.data.set(member, new NativeElement( data_obj.get_string_member(member) ?? "" )); } } // Metadata if (obj.has_member("issued_at")) { payload.issued_at = new DateTime.from_iso8601( obj.get_string_member("issued_at"), new TimeZone.utc() ); } if (obj.has_member("expires_at")) { payload.expires_at = new DateTime.from_iso8601( obj.get_string_member("expires_at"), new TimeZone.utc() ); } return payload; } } } ``` ### 4.4 AuthorisationContext The `AuthorisationContext` is the primary interface for applications to check authorisation. ```vala namespace Spry.Authorisation { /** * Request-scoped authorisation context. * * Provides access to the current identity's authorisation state. * Automatically populated from cookie or Bearer token on each request. * * Usage: * var auth = inject(); * if (!auth.is_authorised) { * // Redirect to login * } * if (auth.has_permission("admin")) { * // Show admin content * } */ public class AuthorisationContext : GLib.Object { private TokenPayload? _payload = null; private IdentityProvider? _identity_provider = null; private Identity? _cached_identity = null; /** * Whether the request has a valid authorisation token. */ public bool is_authorised { get { return _payload != null; } } /** * The identity ID from the token. * Returns null if not authorised. */ public string? user_id { get { return _payload?.id; } } /** * The username from the token. * Returns null if not authorised. */ public string? username { get { return _payload?.username; } } /** * The permissions from the token. * Returns empty lot if not authorised. */ public ImmutableLot permissions { get { return _payload?.permissions ?? new ImmutableLot(); } } /** * Additional data from the token. * Returns empty properties if not authorised. */ public Properties data { get { return _payload?.data ?? new PropertyDictionary(); } } /** * The token payload (for advanced use). */ public TokenPayload? payload { get { return _payload; } } /** * Sets the token payload (called by AuthorisationService). */ internal void set_payload(TokenPayload? payload) { _payload = payload; _cached_identity = null; } /** * Sets the identity provider (called by IoC during initialization). */ internal void set_identity_provider(IdentityProvider? provider) { _identity_provider = provider; } /** * Checks if the current identity has a specific permission. * * Supports wildcard matching: * - "admin" matches everything * - "user-*" matches "user-create", "user-delete", etc. * - "*" matches everything * * @param permission The permission to check * @return true if the identity has the permission */ public bool has_permission(string permission) { if (_payload == null) return false; foreach (var user_perm in _payload.permissions) { if (permission_matches(user_perm, permission)) { return true; } } return false; } /** * Requires a specific permission, throws if not present. * * @param permission The required permission * @throws AuthorisationError.PERMISSION_DENIED if not authorised */ public void require_permission(string permission) throws Error { if (!is_authorised) { throw new AuthorisationError.NOT_AUTHORISED( "Authentication required" ); } if (!has_permission(permission)) { throw new AuthorisationError.PERMISSION_DENIED( @"Permission '$permission' required" ); } } /** * Checks if the identity has ANY of the specified permissions. */ public bool has_any_permission(Lot permissions) { foreach (var perm in permissions) { if (has_permission(perm)) return true; } return false; } /** * Checks if the identity has ALL of the specified permissions. */ public bool has_all_permissions(Lot permissions) { foreach (var perm in permissions) { if (!has_permission(perm)) return false; } return true; } /** * Retrieves the full Identity object from the provider. * * This calls the registered IdentityProvider to get the complete * identity object (e.g., User from Spry.Authentication). * * @return The Identity, or null if not found */ public async Identity? get_current_identity_async() throws Error { if (_payload == null) return null; if (_cached_identity != null) return _cached_identity; if (_identity_provider == null) return null; _cached_identity = yield _identity_provider.get_identity_async(_payload.id); return _cached_identity; } /** * Synchronous version for cases where async is not available. */ public Identity? get_current_identity() { if (_payload == null) return null; if (_cached_identity != null) return _cached_identity; // Cannot call async - return null return null; } // Private helper for permission matching private bool permission_matches(string pattern, string permission) { if (pattern == "admin" || pattern == "*") return true; if (pattern == permission) return true; if (pattern.has_suffix("*")) { string prefix = pattern.substring(0, pattern.length - 1); return permission.has_prefix(prefix); } return false; } } } ``` ### 4.5 AuthorisationService The `AuthorisationService` handles token generation, extraction, and validation. ```vala namespace Spry.Authorisation { /** * Service for generating and validating authorisation tokens. * * Token Sources (in order of precedence): * 1. Authorization: Bearer header * 2. Cookie named in cookie_name property * * Token Format: * - JSON payload containing identity data and metadata * - Signed with Ed25519 (server signing key) * - Encrypted with X25519 (server sealing key) * - Base64url encoded */ public class AuthorisationService : GLib.Object { private CryptographyProvider _crypto = inject(); private AuthorisationContext _context = inject(); private IdentityProvider? _identity_provider = inject(); private HttpContext _http_context = inject(); // Configuration private string _cookie_name = "spry_session"; private bool _cookie_secure = true; private TimeSpan _token_duration = TimeSpan.HOUR * 24; /** * Cookie name for session tokens. */ public string cookie_name { get { return _cookie_name; } set { _cookie_name = value; } } /** * Whether cookies should be Secure (HTTPS only). */ public bool cookie_secure { get { return _cookie_secure; } set { _cookie_secure = value; } } /** * Default token validity duration. */ public TimeSpan token_duration { get { return _token_duration; } set { _token_duration = value; } } /** * Creates a new AuthorisationService. */ public AuthorisationService() { // Set up identity provider in context if available if (_identity_provider != null) { _context.set_identity_provider(_identity_provider); } } // ========================================================================= // Token Generation // ========================================================================= /** * Generates an authorisation token for an identity. * * @param identity The identity to create a token for * @param duration Optional custom duration (defaults to token_duration) * @return The encrypted token string */ public string generate_token(Identity identity, TimeSpan? duration = null) { var actual_duration = duration ?? _token_duration; var payload = new TokenPayload.from_identity(identity, actual_duration); return generate_token_from_payload(payload); } /** * Generates a token from an existing payload. */ public string generate_token_from_payload(TokenPayload payload) { var json_obj = payload.to_json(); var node = new Json.Node(Json.NodeType.OBJECT); node.set_object(json_obj); var json_str = Json.to_string(node, false); // Sign and seal using CryptographyProvider return _crypto.sign_then_seal_token(json_str, payload.expires_at); } // ========================================================================= // Token Validation // ========================================================================= /** * Validates a token string and returns the payload. * * @param token The encrypted token string * @return The TokenPayload, or null if invalid */ public TokenPayload? validate_token(string token) { try { var result = _crypto.unseal_then_verify_token(token); if (!result.is_valid || result.is_expired) { return null; } var json_str = result.payload; if (json_str == null) return null; var parser = new Json.Parser(); parser.load_from_data((!)json_str); var root = parser.get_root(); if (root.get_node_type() != Json.NodeType.OBJECT) { return null; } var payload = TokenPayload.from_json(root.get_object()); // Double-check expiry if (payload.is_expired()) { return null; } return payload; } catch (Error e) { return null; } } // ========================================================================= // Request Processing // ========================================================================= /** * Extracts and validates token from the current HTTP request. * * Checks Authorization header first, then cookie. * Populates the AuthorisationContext if valid token found. */ public async void process_request_async() throws Error { string? token = null; // 1. Check Authorization: Bearer header var auth_header = _http_context.request.headers.get_any_or_default( "Authorization", null ); if (auth_header != null && auth_header.has_prefix("Bearer ")) { token = ((!)auth_header).substring(7).strip(); } // 2. Check cookie if (token == null) { token = _http_context.request.get_cookie(_cookie_name); } // 3. Validate and populate context if (token != null) { var payload = validate_token((!)token); if (payload != null) { _context.set_payload(payload); } } } // ========================================================================= // Cookie Management // ========================================================================= /** * Sets the authorisation cookie on an HTTP response. * * @param result The HttpResult to set the cookie on * @param token The token to set */ public void set_cookie(HttpResult result, string token) { var max_age = (int)(_token_duration / TimeSpan.SECOND); var cookie_value = @"$_cookie_name=$token; Path=/; Max-Age=$max_age; HttpOnly"; if (_cookie_secure) { cookie_value += "; Secure"; } cookie_value += "; SameSite=Strict"; result.set_header("Set-Cookie", cookie_value); } /** * Clears the authorisation cookie. */ public void clear_cookie(HttpResult result) { var cookie_value = @"$_cookie_name=; Path=/; Max-Age=0; HttpOnly"; if (_cookie_secure) { cookie_value += "; Secure"; } cookie_value += "; SameSite=Strict"; result.set_header("Set-Cookie", cookie_value); } } } ``` ### 4.6 AuthorisationError ```vala namespace Spry.Authorisation { /** * Error domain for authorisation-related errors. */ public errordomain AuthorisationError { NOT_AUTHORISED, PERMISSION_DENIED, INVALID_TOKEN, TOKEN_EXPIRED, IDENTITY_NOT_FOUND } } ``` --- ## 5. Authentication System Refactoring ### 5.1 UserIdentityProvider The `UserIdentityProvider` implements `IdentityProvider` for the User model. ```vala namespace Spry.Authentication { /** * IdentityProvider implementation for Spry's built-in User model. * * This bridges the Authentication User model with the Authorisation * Identity interface. */ public class UserIdentityProvider : GLib.Object, Authorisation.IdentityProvider { private UserService _user_service = inject(); /** * Retrieves a User as an Identity by ID. */ public async Authorisation.Identity? get_identity_async(string id) throws Error { var user = yield _user_service.get_user_async(id); if (user == null) return null; // User implements Identity, so we can return directly return user as Authorisation.Identity; } } } ``` ### 5.2 User Model (Updated) The `User` model now implements the `Identity` interface. ```vala namespace Spry.Authentication { /** * User data model implementing the Identity interface. */ public class User : GLib.Object, Authorisation.Identity { // Identity fields (from interface) public string id { get; set; } public string username { get; set; } // User-specific fields public string email { get; set; } public string password_hash { get; set; } public DateTime created_at { get; set; } public DateTime? updated_at { get; set; } // Permissions stored on user private Vector _permissions = new Vector(); // Application data private Dictionary _app_data = new Dictionary(); // ========================================================================= // Identity Interface Implementation // ========================================================================= /** * Unique identifier (Identity interface). */ public string identity_id { get { return id; } } /** * Human-readable name (Identity interface). */ public string identity_username { get { return username; } } /** * Permissions as ImmutableLot (Identity interface). */ public ImmutableLot identity_permissions { get { var builder = new LotBuilder(); foreach (var perm in _permissions) { builder.add(perm); } return builder.to_immutable(); } } /** * Additional token data (Identity interface). */ public Properties identity_data { get { var props = new PropertyDictionary(); props.set("email", new NativeElement(email ?? "")); var iter = _app_data.iterator(); while (iter.next()) { props.set(iter.get().key, new NativeElement(iter.get().value)); } return props; } } // ========================================================================= // User-Specific Properties // ========================================================================= /** * Mutable permissions collection for management. */ public Vector permissions { get { return _permissions; } set { _permissions.clear(); foreach (var perm in value) { _permissions.add(perm); } } } /** * Mutable application data for management. */ public Dictionary app_data { get { return _app_data; } set { _app_data.clear(); var iter = value.iterator(); while (iter.next()) { _app_data.set(iter.get().key, iter.get().value); } } } // ... existing methods (from_json, to_json) ... } } ``` ### 5.3 Updated LoginFormComponent The login form now uses the AuthorisationService for token generation. ```vala namespace Spry.Authentication.Components { public class LoginFormComponent : Component { private UserService _user_service = inject(); private AuthorisationService _auth_service = inject(); private HttpContext _http_context = inject(); public string redirect_url { get; set; default = "/"; } public string? error_message { get; private set; default = null; } private async void handle_login_async() throws Error { var query = _http_context.request.query_params; var username = query.get_any_or_default("username", "").strip(); var password = query.get_any_or_default("password", ""); // Authenticate user var user = yield _user_service.authenticate_async(username, password); if (user == null) { error_message = "Invalid username or password"; return; } // Generate authorisation token var token = _auth_service.generate_token(user); // Set cookie and redirect response.set_cookie( _auth_service.cookie_name, token, (int)(_auth_service.token_duration / TimeSpan.SECOND), "/", _auth_service.cookie_secure, true, // HttpOnly "Strict" // SameSite ); response.redirect(redirect_url); } } } ``` --- ## 6. IoC Module Registration ### 6.1 AuthorisationModule ```vala namespace Spry.Authorisation { /** * IoC module for the Authorisation system. * * Registration order: * 1. AuthorisationContext (scoped - per request) * 2. AuthorisationService (scoped - per request) * 3. IdentityProvider (optional - provided by Authentication or custom) */ public class AuthorisationModule : GLib.Object, Inversion.Module { public void register(Inversion.Application application) { // Context is scoped (per-request) application.add_scoped(); // Service is scoped (per-request, needs HttpContext) application.add_scoped(); } public void initialize(Inversion.Application application) { // Wire up request processing // This would integrate with Spry's request pipeline } } } ``` ### 6.2 AuthenticationModule ```vala namespace Spry.Authentication { /** * IoC module for the Authentication system. * * Depends on: AuthorisationModule (must be registered first) */ public class AuthenticationModule : GLib.Object, Inversion.Module { public void register(Inversion.Application application) { // Register as IdentityProvider for Authorisation application.add_scoped(); application.add_alias(); // User management services application.add_scoped(); application.add_scoped(); application.add_scoped(); // Register UI components var spry_cfg = application.configure_with(); spry_cfg.add_component(); spry_cfg.add_component(); spry_cfg.add_component(); spry_cfg.add_component(); spry_cfg.add_component(); spry_cfg.add_page( new EndpointRoute("/admin/users") ); } public void initialize(Inversion.Application application) { // Initialize storage structure try { var engine = application.resolve(); initialize_storage_async.begin(engine); } catch (Error e) { error("Failed to initialize Authentication storage: %s", e.message); } } private async void initialize_storage_async(Implexus.Core.Engine engine) throws Error { // Create /spry/users container hierarchy // ... (same as current UsersModule) } } } ``` ### 6.3 Application Startup ```vala // In application startup var application = new Inversion.Application(); // 1. Register core Spry modules application.add_module(); // 2. Register Authorisation (must be before Authentication) application.add_module(); // 3. Register built-in Authentication (optional) application.add_module(); // OR: Register custom authentication provider // application.add_module(); ``` --- ## 7. Custom Authentication Provider Integration ### 7.1 Implementing a Custom Provider To implement custom authentication (e.g., OAuth, certificates): ```vala namespace MyApp.Auth { /** * Custom identity for OAuth authentication. */ public class OAuthIdentity : GLib.Object, Authorisation.Identity { public string id { get; set; } public string username { get; set; } public ImmutableLot permissions { get; set; } public Properties data { get; set; } // OAuth-specific fields public string provider { get; set; } // e.g., "google", "github" public string? avatar_url { get; set; } } /** * Custom identity provider for OAuth. */ public class OAuthIdentityProvider : GLib.Object, Authorisation.IdentityProvider { private OAuthService _oauth_service = inject(); public async Authorisation.Identity? get_identity_async(string id) throws Error { return yield _oauth_service.get_identity_async(id); } } /** * Custom authentication module. */ public class OAuthModule : GLib.Object, Inversion.Module { public void register(Inversion.Application application) { // Register as IdentityProvider application.add_scoped(); application.add_alias(); // OAuth-specific services application.add_singleton(); application.add_scoped(); } } } ``` ### 7.2 OAuth Login Flow Example ```vala public class OAuthCallbackComponent : Component { private OAuthService _oauth = inject(); private AuthorisationService _auth = inject(); public override async void prepare() throws Error { var code = _http_context.request.query_params.get_any_or_default("code", ""); // Exchange code for OAuth identity var identity = yield _oauth.exchange_code_async(code); if (identity == null) { response.redirect("/login?error=oauth_failed"); return; } // Generate Spry authorisation token var token = _auth.generate_token(identity); // Set cookie response.set_cookie(_auth.cookie_name, token, ...); response.redirect("/dashboard"); } } ``` --- ## 8. Request Processing Flow ### 8.1 Sequence Diagram ```mermaid sequenceDiagram participant Client participant Spry as Spry Framework participant AuthZ as AuthorisationService participant Context as AuthorisationContext participant Provider as IdentityProvider participant AuthN as UserService Client->>Spry: HTTP Request with Cookie/Bearer Spry->>AuthZ: process_request_async AuthZ->>AuthZ: Extract token from header/cookie AuthZ->>AuthZ: Validate token signature/encryption AuthZ->>Context: set_payload - TokenPayload Spry->>Context: has_permission or require_permission Context->>Context: Check permissions in payload alt Full identity needed Spry->>Context: get_current_identity_async Context->>Provider: get_identity_async Provider->>AuthN: get_user_async AuthN-->>Provider: User Provider-->>Context: Identity Context-->>Spry: Identity end Spry-->>Client: Response ``` ### 8.2 Component Diagram ```mermaid graph TB subgraph Authorisation System AuthorisationModule[AuthorisationModule] AuthorisationService[AuthorisationService] AuthorisationContext[AuthorisationContext] TokenPayload[TokenPayload] Identity[Identity Interface] IdentityProvider[IdentityProvider Interface] end subgraph Authentication System AuthenticationModule[AuthenticationModule] UserService[UserService] SessionService[SessionService] PermissionService[PermissionService] User[User Model] UserIdentityProvider[UserIdentityProvider] LoginFormComponent[LoginFormComponent] end subgraph Custom Auth Example OAuthModule[OAuthModule] OAuthIdentityProvider[OAuthIdentityProvider] OAuthIdentity[OAuthIdentity] end AuthorisationModule --> AuthorisationService AuthorisationModule --> AuthorisationContext AuthorisationService --> AuthorisationContext AuthorisationService --> IdentityProvider AuthorisationContext --> IdentityProvider AuthorisationContext --> TokenPayload IdentityProvider -.implements.-> IdentityProvider Identity -.implements.-> Identity AuthenticationModule --> UserService AuthenticationModule --> UserIdentityProvider AuthenticationModule --> LoginFormComponent User -.implements.-> Identity UserIdentityProvider -.implements.-> IdentityProvider UserIdentityProvider --> UserService UserIdentityProvider --> User LoginFormComponent --> UserService LoginFormComponent --> AuthorisationService OAuthModule --> OAuthIdentityProvider OAuthIdentity -.implements.-> Identity OAuthIdentityProvider -.implements.-> IdentityProvider OAuthIdentityProvider --> OAuthIdentity ``` --- ## 9. Migration Guide ### 9.1 For Existing Applications 1. **Update imports**: Change `Spry.Users` to `Spry.Authentication` 2. **Update module registration**: Register `AuthorisationModule` before `AuthenticationModule` 3. **Update permission checks**: Use `inject()` instead of `inject()` ### 9.2 Before/After Comparison **Before (current system):** ```vala // Module registration application.add_module(); // Permission check var perms = inject(); if (!perms.has_permission(user, "admin")) { throw new UserError.PERMISSION_DENIED("Admin required"); } // Getting current user var session = inject(); var user = yield session.get_current_user_async(); ``` **After (refactored system):** ```vala // Module registration application.add_module(); application.add_module(); // Permission check var auth = inject(); auth.require_permission("admin"); // Throws if not authorised // Getting current user var auth = inject(); var user = yield auth.get_current_identity_async() as User; ``` --- ## 10. Implementation Checklist ### Phase 1: Authorisation System - [ ] Create `src/Authorisation/` directory structure - [ ] Implement `Identity` interface - [ ] Implement `IdentityProvider` interface - [ ] Implement `TokenPayload` class - [ ] Implement `AuthorisationContext` class - [ ] Implement `AuthorisationService` class - [ ] Implement `AuthorisationError` domain - [ ] Implement `AuthorisationModule` class - [ ] Create `meson.build` configuration ### Phase 2: Authentication Refactoring - [ ] Rename `src/Users/` to `src/Authentication/` - [ ] Update `User` to implement `Identity` interface - [ ] Create `UserIdentityProvider` class - [ ] Update `UserService` namespace - [ ] Update `SessionService` namespace - [ ] Update `PermissionService` namespace - [ ] Update all component namespaces - [ ] Create `AuthenticationModule` class - [ ] Update `LoginFormComponent` to use `AuthorisationService` - [ ] Update `meson.build` configuration ### Phase 3: Integration - [ ] Update root `meson.build` to include both modules - [ ] Update demo application - [ ] Update documentation - [ ] Create migration guide for existing applications --- ## 11. Open Questions 1. **Token Refresh**: Should we support token refresh without re-authentication? 2. **Multiple Sessions**: Should we track active sessions per identity? 3. **Permission Caching**: Should permissions be cached beyond token lifetime? 4. **Token Revocation**: Future support for revoking tokens before expiry? --- ## 12. Appendix: Token Format Details ### Token Structure (Internal) ``` Base64url( X25519-Seal( Ed25519-Sign( JSON { "id": "user-uuid", "username": "johndoe", "permissions": ["admin", "user-management"], "data": { "email": "john@example.com" }, "issued_at": "2026-03-15T12:00:00Z", "expires_at": "2026-03-16T12:00:00Z" } ) ) ) ``` ### Cookie Format ``` Set-Cookie: spry_session=; Path=/; Max-Age=86400; HttpOnly; Secure; SameSite=Strict ``` ### Bearer Header Format ``` Authorization: Bearer ```