|
@@ -1,576 +0,0 @@
|
|
|
-using Inversion;
|
|
|
|
|
-using InvercargillJson;
|
|
|
|
|
-using Invercargill.DataStructures;
|
|
|
|
|
-using Json;
|
|
|
|
|
-using Astralis;
|
|
|
|
|
-
|
|
|
|
|
-namespace Spry.Authentication {
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Error domain for session-related operations.
|
|
|
|
|
- */
|
|
|
|
|
- public errordomain SessionError {
|
|
|
|
|
- SESSION_NOT_FOUND,
|
|
|
|
|
- SESSION_EXPIRED,
|
|
|
|
|
- INVALID_SESSION_TOKEN,
|
|
|
|
|
- COOKIE_NOT_FOUND,
|
|
|
|
|
- STORAGE_ERROR
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Result of session token validation containing session and user info.
|
|
|
|
|
- */
|
|
|
|
|
- public class SessionValidationResult : GLib.Object {
|
|
|
|
|
- /**
|
|
|
|
|
- * Whether the session token was successfully validated.
|
|
|
|
|
- */
|
|
|
|
|
- public bool is_valid { get; set; }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * The session object if validation was successful.
|
|
|
|
|
- */
|
|
|
|
|
- public Session? session { get; set; }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * The user object if validation was successful.
|
|
|
|
|
- */
|
|
|
|
|
- public User? user { get; set; }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Error message describing why validation failed.
|
|
|
|
|
- */
|
|
|
|
|
- public string? error_message { get; set; }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Creates a successful validation result.
|
|
|
|
|
- */
|
|
|
|
|
- public SessionValidationResult.success(Session session, User? user = null) {
|
|
|
|
|
- GLib.Object(
|
|
|
|
|
- is_valid: true,
|
|
|
|
|
- session: session,
|
|
|
|
|
- user: user,
|
|
|
|
|
- error_message: null
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Creates a failed validation result.
|
|
|
|
|
- */
|
|
|
|
|
- public SessionValidationResult.failure(string error_message) {
|
|
|
|
|
- GLib.Object(
|
|
|
|
|
- is_valid: false,
|
|
|
|
|
- session: null,
|
|
|
|
|
- user: null,
|
|
|
|
|
- error_message: error_message
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Result of authenticating a request.
|
|
|
|
|
- */
|
|
|
|
|
- public class AuthResult : GLib.Object {
|
|
|
|
|
- /**
|
|
|
|
|
- * Whether the request was successfully authenticated.
|
|
|
|
|
- */
|
|
|
|
|
- public bool is_authenticated { get; set; }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * The authenticated user, or null if not authenticated.
|
|
|
|
|
- */
|
|
|
|
|
- public User? user { get; set; }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * The session associated with the request, or null if not authenticated.
|
|
|
|
|
- */
|
|
|
|
|
- public Session? session { get; set; }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Error message describing why authentication failed.
|
|
|
|
|
- */
|
|
|
|
|
- public string? error_message { get; set; }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Creates a successful authentication result.
|
|
|
|
|
- */
|
|
|
|
|
- public AuthResult.success(User user, Session session) {
|
|
|
|
|
- GLib.Object(
|
|
|
|
|
- is_authenticated: true,
|
|
|
|
|
- user: user,
|
|
|
|
|
- session: session,
|
|
|
|
|
- error_message: null
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Creates a failed authentication result.
|
|
|
|
|
- */
|
|
|
|
|
- public AuthResult.failure(string error_message) {
|
|
|
|
|
- GLib.Object(
|
|
|
|
|
- is_authenticated: false,
|
|
|
|
|
- user: null,
|
|
|
|
|
- session: null,
|
|
|
|
|
- error_message: error_message
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * SessionService handles session creation, validation, cookie management, and cleanup.
|
|
|
|
|
- *
|
|
|
|
|
- * Cookie Configuration:
|
|
|
|
|
- * - Cookie name: spry_session (configurable)
|
|
|
|
|
- * - HttpOnly: true
|
|
|
|
|
- * - Secure: true (configurable for development)
|
|
|
|
|
- * - SameSite: Strict
|
|
|
|
|
- * - Path: /
|
|
|
|
|
- *
|
|
|
|
|
- * Integration with Authorisation:
|
|
|
|
|
- * - Uses AuthorisationTokenService for token generation/validation
|
|
|
|
|
- * - Sessions can be converted to AuthorisationTokens for the Authorisation system
|
|
|
|
|
- *
|
|
|
|
|
- * This service uses the inject<> pattern for dependency injection.
|
|
|
|
|
- * All methods are async to work with the repository async API.
|
|
|
|
|
- */
|
|
|
|
|
- public class SessionService : GLib.Object {
|
|
|
|
|
-
|
|
|
|
|
- private const string NAMESPACE = "session-token";
|
|
|
|
|
-
|
|
|
|
|
- private SessionRepository _repository = inject<SessionRepository>();
|
|
|
|
|
- private CryptographyProvider _crypto = inject<CryptographyProvider>();
|
|
|
|
|
- private Authorisation.AuthorisationTokenService? _token_service = inject<Authorisation.AuthorisationTokenService>();
|
|
|
|
|
-
|
|
|
|
|
- // Cookie configuration
|
|
|
|
|
- private string _cookie_name = "spry_session";
|
|
|
|
|
- private bool _cookie_secure = true;
|
|
|
|
|
- private TimeSpan _session_duration = TimeSpan.HOUR * 24;
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * The name of the session cookie.
|
|
|
|
|
- */
|
|
|
|
|
- public string cookie_name {
|
|
|
|
|
- get { return _cookie_name; }
|
|
|
|
|
- set { _cookie_name = value; }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Whether the session cookie should be Secure.
|
|
|
|
|
- */
|
|
|
|
|
- public bool cookie_secure {
|
|
|
|
|
- get { return _cookie_secure; }
|
|
|
|
|
- set { _cookie_secure = value; }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * The session expiry duration.
|
|
|
|
|
- */
|
|
|
|
|
- public TimeSpan session_duration {
|
|
|
|
|
- get { return _session_duration; }
|
|
|
|
|
- set { _session_duration = value; }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Creates a new SessionService instance with default configuration.
|
|
|
|
|
- * Use properties to customize cookie name, security, and duration.
|
|
|
|
|
- */
|
|
|
|
|
- public SessionService() {
|
|
|
|
|
- // Default configuration - use properties to customize
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Session Creation
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Creates a new session for a user.
|
|
|
|
|
- *
|
|
|
|
|
- * This method:
|
|
|
|
|
- * - Generates a UUID for the session
|
|
|
|
|
- * - Sets expiry based on configured duration
|
|
|
|
|
- * - Stores optional IP and user agent
|
|
|
|
|
- *
|
|
|
|
|
- * @param user_id The user's unique identifier
|
|
|
|
|
- * @param ip_address Optional IP address for tracking
|
|
|
|
|
- * @param user_agent Optional user agent for tracking
|
|
|
|
|
- * @return The created Session
|
|
|
|
|
- * @throws Error on failure
|
|
|
|
|
- */
|
|
|
|
|
- public async Session create_session_async(string user_id, string? ip_address = null, string? user_agent = null) throws Error {
|
|
|
|
|
- stdout.printf("SESSION DEBUG: create_session_async() called for user: %s\n", user_id);
|
|
|
|
|
- stdout.printf("SESSION DEBUG: IP address: %s, User-Agent: %s\n",
|
|
|
|
|
- ip_address ?? "null", user_agent ?? "null");
|
|
|
|
|
-
|
|
|
|
|
- // Generate UUID for session
|
|
|
|
|
- var session_id = generate_uuid();
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Generated session ID: %s\n", session_id);
|
|
|
|
|
-
|
|
|
|
|
- // Calculate expiry
|
|
|
|
|
- var expires_at = new DateTime.now_utc().add(_session_duration);
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Session expires at: %s\n", expires_at.format_iso8601());
|
|
|
|
|
-
|
|
|
|
|
- // Create session via repository
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Creating session via repository...\n");
|
|
|
|
|
- var session = yield _repository.create(session_id, user_id, expires_at);
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Session created via repository\n");
|
|
|
|
|
-
|
|
|
|
|
- // Set optional fields
|
|
|
|
|
- if (ip_address != null) {
|
|
|
|
|
- session.ip_address = ip_address;
|
|
|
|
|
- }
|
|
|
|
|
- if (user_agent != null) {
|
|
|
|
|
- session.user_agent = user_agent;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Update session with optional fields if any were set
|
|
|
|
|
- if (ip_address != null || user_agent != null) {
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Updating session with IP/User-Agent...\n");
|
|
|
|
|
- yield _repository.update(session);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Session created successfully: %s\n", session_id);
|
|
|
|
|
- return session;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Session Token Generation
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Generates a signed and encrypted session token.
|
|
|
|
|
- *
|
|
|
|
|
- * Creates a JSON payload with session_id, user_id, expires_at
|
|
|
|
|
- * and uses CryptographyProvider.author() with the session-token namespace.
|
|
|
|
|
- *
|
|
|
|
|
- * Note: For integration with the Authorisation system, use
|
|
|
|
|
- * generate_authorisation_token() which creates an AuthorisationToken.
|
|
|
|
|
- *
|
|
|
|
|
- * @param session The session to generate a token for
|
|
|
|
|
- * @return The encrypted token string (URL-safe Base64)
|
|
|
|
|
- */
|
|
|
|
|
- public string generate_session_token(Session session) {
|
|
|
|
|
- stdout.printf("SESSION DEBUG: generate_session_token() called for session: %s\n", session.id);
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Session user_id: %s, expires_at: %s\n",
|
|
|
|
|
- session.user_id, session.expires_at.format_iso8601());
|
|
|
|
|
-
|
|
|
|
|
- // Create JSON payload
|
|
|
|
|
- var payload_obj = new Json.Object();
|
|
|
|
|
- payload_obj.set_string_member("session_id", session.id);
|
|
|
|
|
- payload_obj.set_string_member("user_id", session.user_id);
|
|
|
|
|
- payload_obj.set_string_member("expires_at", session.expires_at.format_iso8601());
|
|
|
|
|
-
|
|
|
|
|
- // Wrap object in a node for serialization
|
|
|
|
|
- var node = new Json.Node(Json.NodeType.OBJECT);
|
|
|
|
|
- node.set_object(payload_obj);
|
|
|
|
|
- var payload = Json.to_string(node, false);
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Token payload JSON: %s\n", payload);
|
|
|
|
|
-
|
|
|
|
|
- // Sign and seal the token
|
|
|
|
|
- try {
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Calling author()...\n");
|
|
|
|
|
- var blob = _crypto.author(NAMESPACE, payload);
|
|
|
|
|
- var token = Base64.encode(blob).replace("+", "-").replace("/", "_");
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Token generated successfully (length: %d)\n", token.length);
|
|
|
|
|
- return token;
|
|
|
|
|
- } catch (Error e) {
|
|
|
|
|
- warning("Failed to generate session token: %s", e.message);
|
|
|
|
|
- return "";
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Generates an AuthorisationToken for a user session.
|
|
|
|
|
- *
|
|
|
|
|
- * This method creates a token compatible with the Authorisation system,
|
|
|
|
|
- * using the AuthorisationTokenService if available.
|
|
|
|
|
- *
|
|
|
|
|
- * @param user The authenticated user
|
|
|
|
|
- * @param session The session for the user
|
|
|
|
|
- * @return The encrypted authorisation token string
|
|
|
|
|
- */
|
|
|
|
|
- public string generate_authorisation_token(User user, Session session) {
|
|
|
|
|
- // If AuthorisationTokenService is available, use it
|
|
|
|
|
- if (_token_service != null) {
|
|
|
|
|
- return _token_service.generate_token(user, session.expires_at);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Fall back to session token generation
|
|
|
|
|
- return generate_session_token(session);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Session Validation
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Validates a session token and returns the result.
|
|
|
|
|
- *
|
|
|
|
|
- * This method:
|
|
|
|
|
- * - Uses CryptographyProvider.read() with session-token namespace
|
|
|
|
|
- * - Checks expiry
|
|
|
|
|
- * - Loads session from storage
|
|
|
|
|
- * - Verifies session exists and matches token data
|
|
|
|
|
- *
|
|
|
|
|
- * @param token The encrypted token string (URL-safe Base64)
|
|
|
|
|
- * @return A SessionValidationResult with session and user info
|
|
|
|
|
- */
|
|
|
|
|
- public async SessionValidationResult validate_session_token_async(string token) throws Error {
|
|
|
|
|
- // Decode Base64
|
|
|
|
|
- var decoded = Base64.decode(token.replace("-", "+").replace("_", "/"));
|
|
|
|
|
-
|
|
|
|
|
- // Decrypt and verify the token
|
|
|
|
|
- var payload = _crypto.read(NAMESPACE, decoded);
|
|
|
|
|
-
|
|
|
|
|
- if (payload == null) {
|
|
|
|
|
- return new SessionValidationResult.failure("Could not decrypt token");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var json = new JsonElement.from_string((!)payload);
|
|
|
|
|
- var obj = json.as<JsonObject>();
|
|
|
|
|
-
|
|
|
|
|
- var session_id = obj.get("session_id").as<string>();
|
|
|
|
|
- var user_id = obj.get("user_id").as<string>();
|
|
|
|
|
- var expires_at_str = obj.get("expires_at").as<string>();
|
|
|
|
|
- var expires_at = new DateTime.from_iso8601(expires_at_str, new TimeZone.utc());
|
|
|
|
|
-
|
|
|
|
|
- // Check expiry
|
|
|
|
|
- if (expires_at.compare(new DateTime.now_utc()) <= 0) {
|
|
|
|
|
- return new SessionValidationResult.failure("Session has expired");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Load session from storage
|
|
|
|
|
- var session = yield get_session_async(session_id);
|
|
|
|
|
- if (session == null) {
|
|
|
|
|
- return new SessionValidationResult.failure("Session not found");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Verify session matches token data
|
|
|
|
|
- if (session.user_id != user_id) {
|
|
|
|
|
- return new SessionValidationResult.failure("Session user mismatch");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return new SessionValidationResult.success(session);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Session Retrieval
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Gets a session by its unique ID.
|
|
|
|
|
- *
|
|
|
|
|
- * @param session_id The session's unique identifier
|
|
|
|
|
- * @return The Session, or null if not found or expired
|
|
|
|
|
- * @throws Error on storage failure
|
|
|
|
|
- */
|
|
|
|
|
- public async Session? get_session_async(string session_id) throws Error {
|
|
|
|
|
- var session = yield _repository.get_by_id(session_id);
|
|
|
|
|
-
|
|
|
|
|
- // Don't return expired sessions
|
|
|
|
|
- if (session != null && session.is_expired()) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return session;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Gets all sessions for a user.
|
|
|
|
|
- *
|
|
|
|
|
- * @param user_id The user's unique identifier
|
|
|
|
|
- * @return A Vector of active (non-expired) sessions
|
|
|
|
|
- * @throws Error on storage failure
|
|
|
|
|
- */
|
|
|
|
|
- public async Vector<Session> get_sessions_for_user_async(string user_id) throws Error {
|
|
|
|
|
- var all_sessions = yield _repository.get_by_user_id(user_id);
|
|
|
|
|
-
|
|
|
|
|
- // Filter out expired sessions
|
|
|
|
|
- var active_sessions = new Vector<Session>();
|
|
|
|
|
- foreach (var session in all_sessions) {
|
|
|
|
|
- if (!session.is_expired()) {
|
|
|
|
|
- active_sessions.add(session);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return active_sessions;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Session Deletion
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Deletes a session by its unique ID.
|
|
|
|
|
- *
|
|
|
|
|
- * @param session_id The session's unique identifier
|
|
|
|
|
- * @throws Error on failure
|
|
|
|
|
- */
|
|
|
|
|
- public async void delete_session_async(string session_id) throws Error {
|
|
|
|
|
- // Get session first to verify it exists
|
|
|
|
|
- var session = yield get_session_async(session_id);
|
|
|
|
|
- if (session == null) {
|
|
|
|
|
- throw new SessionError.SESSION_NOT_FOUND("Session not found");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Delete session via repository
|
|
|
|
|
- yield _repository.delete(session_id);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Deletes all sessions for a user.
|
|
|
|
|
- *
|
|
|
|
|
- * @param user_id The user's unique identifier
|
|
|
|
|
- * @throws Error on storage failure
|
|
|
|
|
- */
|
|
|
|
|
- public async void delete_all_sessions_for_user_async(string user_id) throws Error {
|
|
|
|
|
- yield _repository.delete_by_user_id(user_id);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Cookie Handling
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Sets the session cookie on an HTTP response.
|
|
|
|
|
- *
|
|
|
|
|
- * Cookie configuration:
|
|
|
|
|
- * - HttpOnly: true
|
|
|
|
|
- * - Secure: configurable (default true)
|
|
|
|
|
- * - SameSite: Strict
|
|
|
|
|
- * - Path: /
|
|
|
|
|
- *
|
|
|
|
|
- * @param result The HttpResult to set the cookie header on
|
|
|
|
|
- * @param token The session token to set
|
|
|
|
|
- */
|
|
|
|
|
- public void set_session_cookie(HttpResult result, string token) {
|
|
|
|
|
- stdout.printf("SESSION DEBUG: set_session_cookie() called\n");
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Cookie name: %s, Token length: %d\n", _cookie_name, token.length);
|
|
|
|
|
-
|
|
|
|
|
- // Build the Set-Cookie header value manually
|
|
|
|
|
- var max_age = (int)(_session_duration / TimeSpan.SECOND);
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Max-Age: %d seconds\n", max_age);
|
|
|
|
|
-
|
|
|
|
|
- var cookie_value = @"$_cookie_name=$token; Path=/; Max-Age=$max_age; HttpOnly";
|
|
|
|
|
-
|
|
|
|
|
- if (_cookie_secure) {
|
|
|
|
|
- cookie_value += "; Secure";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- cookie_value += "; SameSite=Strict";
|
|
|
|
|
-
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Cookie header value (length: %d): %s\n",
|
|
|
|
|
- cookie_value.length, cookie_value.substring(0, int.min(100, cookie_value.length)) + "...");
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Calling result.set_header('Set-Cookie', ...)\n");
|
|
|
|
|
- result.set_header("Set-Cookie", cookie_value);
|
|
|
|
|
- stdout.printf("SESSION DEBUG: Cookie header set successfully\n");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Clears the session cookie on an HTTP response.
|
|
|
|
|
- *
|
|
|
|
|
- * @param result The HttpResult to clear the cookie on
|
|
|
|
|
- */
|
|
|
|
|
- public void clear_session_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);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Gets the session cookie value from an HTTP request.
|
|
|
|
|
- *
|
|
|
|
|
- * @param http_context The HttpContext containing the request
|
|
|
|
|
- * @return The session cookie value, or null if not present
|
|
|
|
|
- */
|
|
|
|
|
- public string? get_session_cookie(HttpContext http_context) {
|
|
|
|
|
- return http_context.request.get_cookie(_cookie_name);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Session Cleanup
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Removes all expired sessions from storage.
|
|
|
|
|
- *
|
|
|
|
|
- * @throws Error on storage failure
|
|
|
|
|
- */
|
|
|
|
|
- public async void cleanup_expired_sessions_async() throws Error {
|
|
|
|
|
- yield _repository.delete_expired();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Authentication Helper
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Authenticates an HTTP request using the session cookie.
|
|
|
|
|
- *
|
|
|
|
|
- * This method:
|
|
|
|
|
- * - Gets session cookie from request
|
|
|
|
|
- * - Validates token
|
|
|
|
|
- * - Loads user via UserService
|
|
|
|
|
- * - Returns result with user and session
|
|
|
|
|
- *
|
|
|
|
|
- * @param http_context The HttpContext containing the request to authenticate
|
|
|
|
|
- * @param user_service The UserService to load users from
|
|
|
|
|
- * @return An AuthResult with authentication status and user/session info
|
|
|
|
|
- * @throws Error on storage failure
|
|
|
|
|
- */
|
|
|
|
|
- public async AuthResult authenticate_request_async(HttpContext http_context, UserService user_service) throws Error {
|
|
|
|
|
- // Get session cookie
|
|
|
|
|
- var token = get_session_cookie(http_context);
|
|
|
|
|
-
|
|
|
|
|
- if (token == null) {
|
|
|
|
|
- return new AuthResult.failure("No session cookie found");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Validate token
|
|
|
|
|
- var validation = yield validate_session_token_async((!)token);
|
|
|
|
|
-
|
|
|
|
|
- if (!validation.is_valid) {
|
|
|
|
|
- return new AuthResult.failure(validation.error_message ?? "Invalid session");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var session = validation.session;
|
|
|
|
|
- if (session == null) {
|
|
|
|
|
- return new AuthResult.failure("Session not found");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Load user
|
|
|
|
|
- var user = yield user_service.get_user_async(session.user_id);
|
|
|
|
|
- if (user == null) {
|
|
|
|
|
- return new AuthResult.failure("User not found");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return new AuthResult.success(user, session);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Private Helper Methods
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- private string generate_uuid() {
|
|
|
|
|
- // Generate UUID v4 using libsodium random bytes
|
|
|
|
|
- uint8[] bytes = new uint8[16];
|
|
|
|
|
- Sodium.Random.random_bytes(bytes);
|
|
|
|
|
-
|
|
|
|
|
- // Set version (4) and variant bits
|
|
|
|
|
- bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
|
|
|
|
|
- bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant 1
|
|
|
|
|
-
|
|
|
|
|
- // Format as UUID string
|
|
|
|
|
- return "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x".printf(
|
|
|
|
|
- bytes[0], bytes[1], bytes[2], bytes[3],
|
|
|
|
|
- bytes[4], bytes[5], bytes[6], bytes[7],
|
|
|
|
|
- bytes[8], bytes[9], bytes[10], bytes[11],
|
|
|
|
|
- bytes[12], bytes[13], bytes[14], bytes[15]
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|