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 Sprysrc/Authorisation: Token generation, validation, and permission checkingIdentity interface, not a concrete User modelflowchart 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
src/Authorisation Structuresrc/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
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
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; }
}
}
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;
}
}
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;
}
}
}
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;
}
}
}
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);
}
}
}
namespace Spry.Authorisation {
/**
* Error domain for authorisation-related errors.
*/
public errordomain AuthorisationError {
NOT_AUTHORISED,
PERMISSION_DENIED,
INVALID_TOKEN,
TOKEN_EXPIRED,
IDENTITY_NOT_FOUND
}
}
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;
}
}
}
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) ...
}
}
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);
}
}
}
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
}
}
}
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)
}
}
}
// 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>();
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>();
}
}
}
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");
}
}
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
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
Spry.Users to Spry.AuthenticationAuthorisationModule before AuthenticationModuleinject<AuthorisationContext>() instead of inject<PermissionService>()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;
src/Authorisation/ directory structureIdentity interfaceIdentityProvider interfaceTokenPayload classAuthorisationContext classAuthorisationService classAuthorisationError domainAuthorisationModule classmeson.build configurationsrc/Users/ to src/Authentication/User to implement Identity interfaceUserIdentityProvider classUserService namespaceSessionService namespacePermissionService namespaceAuthenticationModule classLoginFormComponent to use AuthorisationServicemeson.build configurationmeson.build to include both modulesBase64url(
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"
}
)
)
)
Set-Cookie: spry_session=<token>; Path=/; Max-Age=86400; HttpOnly; Secure; SameSite=Strict
Authorization: Bearer <token>