auth-refactor-architecture.md 42 KB

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

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.

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<string> 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.

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.

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<string> 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<string>();
            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<string>(
                        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.

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<AuthorisationContext>();
     *   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<string> permissions { 
            get { return _payload?.permissions ?? new ImmutableLot<string>(); } 
        }

        /**
         * 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<string> 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<string> 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.

namespace Spry.Authorisation {

    /**
     * Service for generating and validating authorisation tokens.
     * 
     * Token Sources (in order of precedence):
     * 1. Authorization: Bearer <token> 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<CryptographyProvider>();
        private AuthorisationContext _context = inject<AuthorisationContext>();
        private IdentityProvider? _identity_provider = inject<IdentityProvider>();
        private HttpContext _http_context = inject<HttpContext>();

        // 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

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.

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<UserService>();

        /**
         * 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.

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<string> _permissions = new Vector<string>();
        
        // Application data
        private Dictionary<string, string> _app_data = new Dictionary<string, string>();

        // =========================================================================
        // 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<string> identity_permissions {
            get {
                var builder = new LotBuilder<string>();
                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<string>(email ?? ""));
                var iter = _app_data.iterator();
                while (iter.next()) {
                    props.set(iter.get().key, new NativeElement<string>(iter.get().value));
                }
                return props;
            }
        }

        // =========================================================================
        // User-Specific Properties
        // =========================================================================

        /**
         * Mutable permissions collection for management.
         */
        public Vector<string> permissions {
            get { return _permissions; }
            set { 
                _permissions.clear();
                foreach (var perm in value) {
                    _permissions.add(perm);
                }
            }
        }

        /**
         * Mutable application data for management.
         */
        public Dictionary<string, string> 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.

namespace Spry.Authentication.Components {

    public class LoginFormComponent : Component {

        private UserService _user_service = inject<UserService>();
        private AuthorisationService _auth_service = inject<AuthorisationService>();
        private HttpContext _http_context = inject<HttpContext>();

        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

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<AuthorisationContext>();

            // Service is scoped (per-request, needs HttpContext)
            application.add_scoped<AuthorisationService>();
        }

        public void initialize(Inversion.Application application) {
            // Wire up request processing
            // This would integrate with Spry's request pipeline
        }
    }
}

6.2 AuthenticationModule

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<UserIdentityProvider>();
            application.add_alias<Authorisation.IdentityProvider, UserIdentityProvider>();

            // User management services
            application.add_scoped<UserService>();
            application.add_scoped<SessionService>();
            application.add_scoped<PermissionService>();

            // Register UI components
            var spry_cfg = application.configure_with<SpryConfigurator>();
            spry_cfg.add_component<Components.LoginFormComponent>();
            spry_cfg.add_component<Components.UserListComponent>();
            spry_cfg.add_component<Components.UserListItemComponent>();
            spry_cfg.add_component<Components.UserFormComponent>();
            spry_cfg.add_component<Components.PermissionEditorComponent>();
            spry_cfg.add_page<Components.UserManagementPage>(
                new EndpointRoute("/admin/users")
            );
        }

        public void initialize(Inversion.Application application) {
            // Initialize storage structure
            try {
                var engine = application.resolve<Implexus.Core.Engine>();
                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

// In application startup
var application = new Inversion.Application();

// 1. Register core Spry modules
application.add_module<SpryModule>();

// 2. Register Authorisation (must be before Authentication)
application.add_module<AuthorisationModule>();

// 3. Register built-in Authentication (optional)
application.add_module<AuthenticationModule>();

// OR: Register custom authentication provider
// application.add_module<MyCustomAuthModule>();

7. Custom Authentication Provider Integration

7.1 Implementing a Custom Provider

To implement custom authentication (e.g., OAuth, certificates):

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<string> 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<OAuthService>();
        
        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<OAuthIdentityProvider>();
            application.add_alias<Authorisation.IdentityProvider, OAuthIdentityProvider>();
            
            // OAuth-specific services
            application.add_singleton<OAuthService>();
            application.add_scoped<OAuthCallbackHandler>();
        }
    }
}

7.2 OAuth Login Flow Example

public class OAuthCallbackComponent : Component {
    
    private OAuthService _oauth = inject<OAuthService>();
    private AuthorisationService _auth = inject<AuthorisationService>();
    
    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

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

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<AuthorisationContext>() instead of inject<PermissionService>()

9.2 Before/After Comparison

Before (current system):

// Module registration
application.add_module<Spry.Users.UsersModule>();

// Permission check
var perms = inject<PermissionService>();
if (!perms.has_permission(user, "admin")) {
    throw new UserError.PERMISSION_DENIED("Admin required");
}

// Getting current user
var session = inject<SessionService>();
var user = yield session.get_current_user_async();

After (refactored system):

// Module registration
application.add_module<Spry.Authorisation.AuthorisationModule>();
application.add_module<Spry.Authentication.AuthenticationModule>();

// Permission check
var auth = inject<AuthorisationContext>();
auth.require_permission("admin");  // Throws if not authorised

// Getting current user
var auth = inject<AuthorisationContext>();
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=<token>; Path=/; Max-Age=86400; HttpOnly; Secure; SameSite=Strict

Bearer Header Format

Authorization: Bearer <token>