Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 263:ee04e4e69fed | 264:04fee26ecce0 |
|---|---|
| 1 #ifndef MRJUNEJUNE_AUTH_API_H | |
| 2 #define MRJUNEJUNE_AUTH_API_H | |
| 3 | |
| 4 #include "dowa/dowa.h" | |
| 5 #include "auth/auth_store.h" | |
| 6 #include "seobeo/seobeo.h" | |
| 7 | |
| 8 /* Cookie names */ | |
| 9 #define AUTH_API_SESSION_COOKIE_NAME "mjj_session" | |
| 10 #define AUTH_API_GUEST_COOKIE_NAME "mjj_guest" | |
| 11 | |
| 12 /* Default TTLs (seconds) */ | |
| 13 #define AUTH_API_SESSION_IDLE_TTL_DEFAULT (7 * 24 * 3600) | |
| 14 #define AUTH_API_SESSION_ABS_TTL_DEFAULT (30 * 24 * 3600) | |
| 15 #define AUTH_API_GUEST_TTL_DEFAULT (30 * 24 * 3600) | |
| 16 | |
| 17 /* Auth-only paths permitted during forced-password-change */ | |
| 18 #define AUTH_API_PATH_SESSION "/api/auth/session" | |
| 19 #define AUTH_API_PATH_LOGIN "/api/auth/login" | |
| 20 #define AUTH_API_PATH_LOGOUT "/api/auth/logout" | |
| 21 #define AUTH_API_PATH_PASSWORD "/api/auth/password" | |
| 22 #define AUTH_API_PATH_PASSWORD_PAGE "/account/password" | |
| 23 | |
| 24 typedef enum { | |
| 25 AUTH_PRINCIPAL_GUEST = 0, | |
| 26 AUTH_PRINCIPAL_USER = 1, | |
| 27 } Auth_Principal_Kind; | |
| 28 | |
| 29 /* | |
| 30 * Resolved identity for a single request. | |
| 31 * For users: user_id, username, role, must_change_password are valid. | |
| 32 * For guests: guest_id is valid. | |
| 33 * csrf_token: a derived CSRF token safe to return to the client (never stored | |
| 34 * raw; only its digest appears in the store). | |
| 35 * _token_digest: internal session binding for CSRF derivation; not for logging. | |
| 36 */ | |
| 37 typedef struct { | |
| 38 Auth_Principal_Kind kind; | |
| 39 | |
| 40 /* --- user fields --- */ | |
| 41 char user_id[37]; | |
| 42 char username[AUTH_STORE_USERNAME_MAX + 1]; | |
| 43 char role[8]; | |
| 44 boolean must_change_password; | |
| 45 | |
| 46 /* --- guest fields --- */ | |
| 47 char guest_id[37]; | |
| 48 | |
| 49 /* --- common --- */ | |
| 50 char csrf_token[AUTH_CRYPTO_TOKEN_SIZE]; /* base64url, return to client */ | |
| 51 | |
| 52 /* internal: session token digest (user) or guest_id (guest) used as CSRF binding */ | |
| 53 char _binding[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; | |
| 54 } Auth_Principal; | |
| 55 | |
| 56 /* | |
| 57 * Optional callback that provides guest quota JSON for the session endpoint. | |
| 58 * Registered by conversation_api on init; called from auth_session_handler. | |
| 59 * json_out: buffer of json_capacity bytes; write null-terminated JSON or "null". | |
| 60 * Returns TRUE on success; on FALSE the session response uses "null". | |
| 61 */ | |
| 62 typedef boolean (*Auth_API_Guest_Quota_Cb)( | |
| 63 const char *guest_id, | |
| 64 int64 current_unix, | |
| 65 char *json_out, | |
| 66 size_t json_capacity); | |
| 67 | |
| 68 void Auth_API_Register_Guest_Quota_Cb(Auth_API_Guest_Quota_Cb cb); | |
| 69 | |
| 70 /* | |
| 71 * Hook called after a successful login to initiate guest-resource transfer. | |
| 72 * Called with the logged-out guest_id and the newly authenticated user_id. | |
| 73 * Must not call any Auth_API function; executes on the request thread. | |
| 74 * | |
| 75 * Returns TRUE on success. On FALSE the login handler revokes the new | |
| 76 * session and returns 500; the guest cookie is preserved. | |
| 77 * The hook must be idempotent: it may be called more than once for the | |
| 78 * same (guest_id, user_id) pair during retries. | |
| 79 */ | |
| 80 typedef boolean (*Auth_Guest_Transfer_Hook)( | |
| 81 const char *guest_id, | |
| 82 const char *user_id, | |
| 83 void *context); | |
| 84 | |
| 85 /* | |
| 86 * Initialise the auth module. | |
| 87 * | |
| 88 * cookie_secret must be at least AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES. | |
| 89 * bootstrap_username / bootstrap_password_hash: create bootstrap admin on | |
| 90 * first startup only when no admin exists; pass NULL to skip. | |
| 91 * trusted_proxy_ip exact direct peer IP that may forward X-Real-IP; NULL | |
| 92 * to disable proxy trust. | |
| 93 * dev_insecure_cookie TRUE allows non-Secure cookies; only valid on loopback. | |
| 94 * | |
| 95 * Returns FALSE and fails closed if cookie_secret is missing/too short. | |
| 96 */ | |
| 97 boolean Auth_API_Init( | |
| 98 const char *database_path, | |
| 99 const uint8 *cookie_secret, | |
| 100 size_t cookie_secret_length, | |
| 101 const char *bootstrap_username, | |
| 102 const char *bootstrap_password_hash, | |
| 103 const char *trusted_proxy_ip, | |
| 104 int64 session_idle_ttl_secs, | |
| 105 int64 session_absolute_ttl_secs, | |
| 106 int64 guest_ttl_secs, | |
| 107 boolean dev_insecure_cookie); | |
| 108 | |
| 109 void Auth_API_Destroy(void); | |
| 110 void Auth_API_Register_Routes(void); | |
| 111 | |
| 112 /* | |
| 113 * Register a hook for guest-to-user resource transfer on login. | |
| 114 * Only one hook is supported; a second call replaces the previous one. | |
| 115 */ | |
| 116 void Auth_API_Register_Guest_Transfer_Hook( | |
| 117 Auth_Guest_Transfer_Hook hook, | |
| 118 void *context); | |
| 119 | |
| 120 /* | |
| 121 * Resolve the caller's identity from request cookies. | |
| 122 * Creates a guest identity if no valid session or guest cookie is found. | |
| 123 * new_guest_cookie_out: if non-NULL and non-empty on return, the caller | |
| 124 * should include a Set-Cookie header with this value in the response. | |
| 125 * Returns TRUE on success; FALSE only on internal error (treat as 500). | |
| 126 */ | |
| 127 boolean Auth_API_Resolve_Principal( | |
| 128 Seobeo_Request_Entry *p_request, | |
| 129 Auth_Principal *p_principal, | |
| 130 Dowa_Arena *p_arena, | |
| 131 char *new_guest_cookie_out, | |
| 132 size_t new_guest_cookie_capacity); | |
| 133 | |
| 134 /* | |
| 135 * Resolve identity from request cookies WITHOUT creating a new guest. | |
| 136 * Returns TRUE on success (no internal error): | |
| 137 * - If an existing user session or valid guest cookie is found, | |
| 138 * p_principal is filled and *p_found is set to TRUE. | |
| 139 * - If no valid session/guest is found, *p_found is set to FALSE; | |
| 140 * the caller must return HTTP 401. | |
| 141 * Returns FALSE on internal error (treat as 500). | |
| 142 * Never writes a guest identity row or generates a Set-Cookie directive. | |
| 143 */ | |
| 144 boolean Auth_API_Resolve_Existing_Principal( | |
| 145 Seobeo_Request_Entry *p_request, | |
| 146 Auth_Principal *p_principal, | |
| 147 Dowa_Arena *p_arena, | |
| 148 boolean *p_found); | |
| 149 | |
| 150 /* | |
| 151 * Returns TRUE if the path is permitted for forced-password-change sessions | |
| 152 * (i.e., the principal should NOT be blocked at this path). | |
| 153 * Conversation and admin code gate their routes with: | |
| 154 * if (principal.must_change_password && | |
| 155 * !Auth_API_Is_Forced_Password_Change_Only(path)) { return 403; } | |
| 156 */ | |
| 157 boolean Auth_API_Is_Forced_Password_Change_Only(const char *http_path); | |
| 158 | |
| 159 /* | |
| 160 * Verify same-origin AND CSRF for state-changing routes. | |
| 161 * | |
| 162 * Enforces: | |
| 163 * 1. The Origin header matches the Host header (same-origin). | |
| 164 * 2. The X-CSRF-Token request header is present and matches the token | |
| 165 * derived from the principal's session binding. | |
| 166 * | |
| 167 * Use this as the single centralized CSRF gate. Do not duplicate the | |
| 168 * origin-check or CSRF-derivation logic in other modules. | |
| 169 * | |
| 170 * Returns TRUE on success; the caller MUST return HTTP 403 on FALSE. | |
| 171 */ | |
| 172 boolean Auth_API_Verify_CSRF( | |
| 173 Seobeo_Request_Entry *p_request, | |
| 174 const Auth_Principal *p_principal); | |
| 175 | |
| 176 /* | |
| 177 * Returns the initialized auth store pointer. | |
| 178 * Valid only after Auth_API_Init returns TRUE; NULL before that. | |
| 179 * Admin API uses this to issue store operations directly. | |
| 180 */ | |
| 181 Auth_Store *Auth_API_Get_Store(void); | |
| 182 | |
| 183 #ifdef AUTH_API_TEST_HOOKS | |
| 184 typedef void (*Auth_API_Test_Login_Pre_Create_Hook)(void *p_context); | |
| 185 | |
| 186 void Auth_API_Test_Set_Login_Pre_Create_Hook( | |
| 187 Auth_API_Test_Login_Pre_Create_Hook hook, | |
| 188 void *p_context); | |
| 189 | |
| 190 /* | |
| 191 * Direct handler entry-points for in-process testing. | |
| 192 * Only available when AUTH_API_TEST_HOOKS is defined (test builds). | |
| 193 */ | |
| 194 Seobeo_Request_Entry *Auth_API_Test_Session_Handler( | |
| 195 Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena); | |
| 196 Seobeo_Request_Entry *Auth_API_Test_Login_Handler( | |
| 197 Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena); | |
| 198 Seobeo_Request_Entry *Auth_API_Test_Logout_Handler( | |
| 199 Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena); | |
| 200 Seobeo_Request_Entry *Auth_API_Test_Password_Handler( | |
| 201 Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena); | |
| 202 #endif /* AUTH_API_TEST_HOOKS */ | |
| 203 | |
| 204 #endif /* MRJUNEJUNE_AUTH_API_H */ |