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