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