Mercurial
diff mrjunejune/auth_api.h @ 264:04fee26ecce0
add authenticated JRPG conversation platform
Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 07:34:12 -0700 |
| parents | |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/auth_api.h Fri Aug 07 07:34:12 2026 -0700 @@ -0,0 +1,204 @@ +#ifndef MRJUNEJUNE_AUTH_API_H +#define MRJUNEJUNE_AUTH_API_H + +#include "dowa/dowa.h" +#include "auth/auth_store.h" +#include "seobeo/seobeo.h" + +/* Cookie names */ +#define AUTH_API_SESSION_COOKIE_NAME "mjj_session" +#define AUTH_API_GUEST_COOKIE_NAME "mjj_guest" + +/* Default TTLs (seconds) */ +#define AUTH_API_SESSION_IDLE_TTL_DEFAULT (7 * 24 * 3600) +#define AUTH_API_SESSION_ABS_TTL_DEFAULT (30 * 24 * 3600) +#define AUTH_API_GUEST_TTL_DEFAULT (30 * 24 * 3600) + +/* Auth-only paths permitted during forced-password-change */ +#define AUTH_API_PATH_SESSION "/api/auth/session" +#define AUTH_API_PATH_LOGIN "/api/auth/login" +#define AUTH_API_PATH_LOGOUT "/api/auth/logout" +#define AUTH_API_PATH_PASSWORD "/api/auth/password" +#define AUTH_API_PATH_PASSWORD_PAGE "/account/password" + +typedef enum { + AUTH_PRINCIPAL_GUEST = 0, + AUTH_PRINCIPAL_USER = 1, +} Auth_Principal_Kind; + +/* + * Resolved identity for a single request. + * For users: user_id, username, role, must_change_password are valid. + * For guests: guest_id is valid. + * csrf_token: a derived CSRF token safe to return to the client (never stored + * raw; only its digest appears in the store). + * _token_digest: internal session binding for CSRF derivation; not for logging. + */ +typedef struct { + Auth_Principal_Kind kind; + + /* --- user fields --- */ + char user_id[37]; + char username[AUTH_STORE_USERNAME_MAX + 1]; + char role[8]; + boolean must_change_password; + + /* --- guest fields --- */ + char guest_id[37]; + + /* --- common --- */ + char csrf_token[AUTH_CRYPTO_TOKEN_SIZE]; /* base64url, return to client */ + + /* internal: session token digest (user) or guest_id (guest) used as CSRF binding */ + char _binding[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; +} Auth_Principal; + +/* + * Optional callback that provides guest quota JSON for the session endpoint. + * Registered by conversation_api on init; called from auth_session_handler. + * json_out: buffer of json_capacity bytes; write null-terminated JSON or "null". + * Returns TRUE on success; on FALSE the session response uses "null". + */ +typedef boolean (*Auth_API_Guest_Quota_Cb)( + const char *guest_id, + int64 current_unix, + char *json_out, + size_t json_capacity); + +void Auth_API_Register_Guest_Quota_Cb(Auth_API_Guest_Quota_Cb cb); + +/* + * Hook called after a successful login to initiate guest-resource transfer. + * Called with the logged-out guest_id and the newly authenticated user_id. + * Must not call any Auth_API function; executes on the request thread. + * + * Returns TRUE on success. On FALSE the login handler revokes the new + * session and returns 500; the guest cookie is preserved. + * The hook must be idempotent: it may be called more than once for the + * same (guest_id, user_id) pair during retries. + */ +typedef boolean (*Auth_Guest_Transfer_Hook)( + const char *guest_id, + const char *user_id, + void *context); + +/* + * Initialise the auth module. + * + * cookie_secret must be at least AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES. + * bootstrap_username / bootstrap_password_hash: create bootstrap admin on + * first startup only when no admin exists; pass NULL to skip. + * trusted_proxy_ip exact direct peer IP that may forward X-Real-IP; NULL + * to disable proxy trust. + * dev_insecure_cookie TRUE allows non-Secure cookies; only valid on loopback. + * + * Returns FALSE and fails closed if cookie_secret is missing/too short. + */ +boolean Auth_API_Init( + const char *database_path, + const uint8 *cookie_secret, + size_t cookie_secret_length, + const char *bootstrap_username, + const char *bootstrap_password_hash, + const char *trusted_proxy_ip, + int64 session_idle_ttl_secs, + int64 session_absolute_ttl_secs, + int64 guest_ttl_secs, + boolean dev_insecure_cookie); + +void Auth_API_Destroy(void); +void Auth_API_Register_Routes(void); + +/* + * Register a hook for guest-to-user resource transfer on login. + * Only one hook is supported; a second call replaces the previous one. + */ +void Auth_API_Register_Guest_Transfer_Hook( + Auth_Guest_Transfer_Hook hook, + void *context); + +/* + * Resolve the caller's identity from request cookies. + * Creates a guest identity if no valid session or guest cookie is found. + * new_guest_cookie_out: if non-NULL and non-empty on return, the caller + * should include a Set-Cookie header with this value in the response. + * Returns TRUE on success; FALSE only on internal error (treat as 500). + */ +boolean Auth_API_Resolve_Principal( + Seobeo_Request_Entry *p_request, + Auth_Principal *p_principal, + Dowa_Arena *p_arena, + char *new_guest_cookie_out, + size_t new_guest_cookie_capacity); + +/* + * Resolve identity from request cookies WITHOUT creating a new guest. + * Returns TRUE on success (no internal error): + * - If an existing user session or valid guest cookie is found, + * p_principal is filled and *p_found is set to TRUE. + * - If no valid session/guest is found, *p_found is set to FALSE; + * the caller must return HTTP 401. + * Returns FALSE on internal error (treat as 500). + * Never writes a guest identity row or generates a Set-Cookie directive. + */ +boolean Auth_API_Resolve_Existing_Principal( + Seobeo_Request_Entry *p_request, + Auth_Principal *p_principal, + Dowa_Arena *p_arena, + boolean *p_found); + +/* + * Returns TRUE if the path is permitted for forced-password-change sessions + * (i.e., the principal should NOT be blocked at this path). + * Conversation and admin code gate their routes with: + * if (principal.must_change_password && + * !Auth_API_Is_Forced_Password_Change_Only(path)) { return 403; } + */ +boolean Auth_API_Is_Forced_Password_Change_Only(const char *http_path); + +/* + * Verify same-origin AND CSRF for state-changing routes. + * + * Enforces: + * 1. The Origin header matches the Host header (same-origin). + * 2. The X-CSRF-Token request header is present and matches the token + * derived from the principal's session binding. + * + * Use this as the single centralized CSRF gate. Do not duplicate the + * origin-check or CSRF-derivation logic in other modules. + * + * Returns TRUE on success; the caller MUST return HTTP 403 on FALSE. + */ +boolean Auth_API_Verify_CSRF( + Seobeo_Request_Entry *p_request, + const Auth_Principal *p_principal); + +/* + * Returns the initialized auth store pointer. + * Valid only after Auth_API_Init returns TRUE; NULL before that. + * Admin API uses this to issue store operations directly. + */ +Auth_Store *Auth_API_Get_Store(void); + +#ifdef AUTH_API_TEST_HOOKS +typedef void (*Auth_API_Test_Login_Pre_Create_Hook)(void *p_context); + +void Auth_API_Test_Set_Login_Pre_Create_Hook( + Auth_API_Test_Login_Pre_Create_Hook hook, + void *p_context); + +/* + * Direct handler entry-points for in-process testing. + * Only available when AUTH_API_TEST_HOOKS is defined (test builds). + */ +Seobeo_Request_Entry *Auth_API_Test_Session_Handler( + Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena); +Seobeo_Request_Entry *Auth_API_Test_Login_Handler( + Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena); +Seobeo_Request_Entry *Auth_API_Test_Logout_Handler( + Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena); +Seobeo_Request_Entry *Auth_API_Test_Password_Handler( + Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena); +#endif /* AUTH_API_TEST_HOOKS */ + +#endif /* MRJUNEJUNE_AUTH_API_H */