view 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 source

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