Mercurial
view mrjunejune/auth_api.c @ 273:e02e2036ef84 default tip
add Layer 2 JRPG component system
Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 08 Aug 2026 02:08:08 -0700 |
| parents | 04fee26ecce0 |
| children |
line wrap: on
line source
#include "mrjunejune/auth_api.h" #include "mrjunejune/template_renderer.h" #include "auth/auth_crypto.h" #include "auth/auth_store.h" #include "seobeo/seobeo.h" #include "dowa/dowa.h" #include <openssl/hmac.h> #include <openssl/evp.h> #include <openssl/rand.h> #include <openssl/crypto.h> #include <arpa/inet.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <time.h> /* ------------------------------------------------------------------ */ /* Constants */ /* ------------------------------------------------------------------ */ #define BODY_MAX_BYTES 4096 #define COOKIE_VALUE_MAX 512 #define CSRF_SUFFIX ":csrf:v1" #define RATE_TABLE_SIZE 512 /* must be power of 2 */ #define RATE_PROBE_LIMIT 16 #define RATE_LIMIT_MAX_FAILURES 5 #define RATE_LIMIT_WINDOW_SECS (15 * 60) #define SESSION_COOKIE_MAX 512 #define GUEST_COOKIE_MAX (AUTH_CRYPTO_GUEST_COOKIE_SIZE + 256) #define RATE_KEY_MAX 65 #define BINDING_INPUT_MAX (AUTH_CRYPTO_TOKEN_DIGEST_SIZE + 16) /* * Fixed precomputed scrypt hash of the constant string "dummy-zenbu-timing". * Used only for timing-attack mitigation on nonexistent-username lookups. * Never used as an account credential; salt+hash are intentionally public. */ #define AUTH_DUMMY_PASSWORD_HASH \ "zenbu-scrypt$v=1$N=32768$r=8$p=1$" \ "c4ff27cc756429b17991b80e4c2c5f77$" \ "34e208b3a51796ebd3b39da1039611b1c7ffeb04090b07ef2bf4968ce8131ce3" /* ------------------------------------------------------------------ */ /* Module state */ /* ------------------------------------------------------------------ */ static Auth_Store *g_auth_store = NULL; static uint8 g_cookie_secret[AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES]; static size_t g_cookie_secret_length = 0; static char g_trusted_proxy_ip[AUTH_CRYPTO_IP_MAX_BYTES]; static boolean g_has_trusted_proxy = FALSE; static int64 g_session_idle_ttl = AUTH_API_SESSION_IDLE_TTL_DEFAULT; static int64 g_session_abs_ttl = AUTH_API_SESSION_ABS_TTL_DEFAULT; static int64 g_guest_ttl = AUTH_API_GUEST_TTL_DEFAULT; static boolean g_dev_insecure_cookie = FALSE; static Auth_Guest_Transfer_Hook g_transfer_hook = NULL; static void *g_transfer_hook_ctx = NULL; static Auth_API_Guest_Quota_Cb g_guest_quota_cb = NULL; #ifdef AUTH_API_TEST_HOOKS static Auth_API_Test_Login_Pre_Create_Hook g_login_pre_create_hook = NULL; static void *g_login_pre_create_context = NULL; #endif /* ------------------------------------------------------------------ */ /* Rate limiter (collision-safe open-addressing with LRU eviction) */ /* ------------------------------------------------------------------ */ typedef struct { char key[RATE_KEY_MAX]; /* HMAC hex binding; empty if slot unused */ uint32 count; int64 window_start; } Auth_Rate_Entry; static Auth_Rate_Entry g_rate_table[RATE_TABLE_SIZE]; static pthread_mutex_t g_rate_mutex = PTHREAD_MUTEX_INITIALIZER; /* ------------------------------------------------------------------ */ /* Internal helpers */ /* ------------------------------------------------------------------ */ static int64 auth_now(void) { return (int64)time(NULL); } /* * Generate a UUID v4 using RAND_bytes. * buf must be at least 37 bytes. */ static boolean auth_uuid4(char *buf, size_t capacity) { if (capacity < 37) return FALSE; uint8 rnd[16]; if (RAND_bytes(rnd, sizeof(rnd)) != 1) return FALSE; /* RFC 4122 version 4 */ rnd[6] = (rnd[6] & 0x0f) | 0x40; rnd[8] = (rnd[8] & 0x3f) | 0x80; snprintf(buf, capacity, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-" "%02x%02x%02x%02x%02x%02x", rnd[0], rnd[1], rnd[2], rnd[3], rnd[4], rnd[5], rnd[6], rnd[7], rnd[8], rnd[9], rnd[10], rnd[11], rnd[12], rnd[13], rnd[14], rnd[15]); OPENSSL_cleanse(rnd, sizeof(rnd)); return TRUE; } /* * base64url encode src_len bytes from src into dst. * dst must have capacity ceil(src_len * 4 / 3) + 1. * Returns length of encoded string (without NUL). * * Delegates to Auth_Crypto_Base64url_Encode which uses the correct loop * boundary (processes remaining bytes after full 3-byte groups). */ static size_t auth_base64url_encode( const uint8 *src, size_t src_len, char *dst, size_t dst_capacity) { return Auth_Crypto_Base64url_Encode(src, src_len, dst, dst_capacity); } static void auth_hex_encode( const uint8 *src, size_t src_len, char *dst, size_t dst_capacity) { static const char kHex[] = "0123456789abcdef"; size_t i = 0, o = 0; while (i < src_len && o + 2 < dst_capacity) { dst[o++] = kHex[(src[i] >> 4) & 0xf]; dst[o++] = kHex[src[i] & 0xf]; i++; } if (o < dst_capacity) dst[o] = '\0'; } /* * Derive a CSRF token deterministically from a session binding. * binding: session token_digest (user) or guest_id (guest). * Output: base64url-encoded HMAC-SHA256, AUTH_CRYPTO_TOKEN_SIZE bytes. */ static boolean auth_derive_csrf( const char *binding, char *csrf_out, size_t csrf_capacity) { if (!binding || !csrf_out || csrf_capacity < AUTH_CRYPTO_TOKEN_SIZE) return FALSE; size_t binding_len = strlen(binding); size_t suffix_len = strlen(CSRF_SUFFIX); size_t input_len = binding_len + suffix_len; if (input_len >= BINDING_INPUT_MAX) return FALSE; char input[BINDING_INPUT_MAX]; memcpy(input, binding, binding_len); memcpy(input + binding_len, CSRF_SUFFIX, suffix_len); uint8 digest[32]; uint32 digest_len = 32; if (!HMAC(EVP_sha256(), g_cookie_secret, (int)g_cookie_secret_length, (const uint8 *)input, input_len, digest, &digest_len)) { OPENSSL_cleanse(input, sizeof(input)); OPENSSL_cleanse(digest, sizeof(digest)); return FALSE; } OPENSSL_cleanse(input, sizeof(input)); size_t encoded_length = auth_base64url_encode(digest, 32, csrf_out, csrf_capacity); OPENSSL_cleanse(digest, sizeof(digest)); return encoded_length == AUTH_CRYPTO_TOKEN_SIZE - 1; } /* * Build the rate-limit key: HMAC(secret, peer_digest + ":" + norm_user). * Output: 64-char hex string. */ static boolean auth_rate_key( const char *peer_binding_digest, const char *normalized_username, char *key_out) { char input[AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE + 1 + AUTH_STORE_USERNAME_MAX + 1]; int n = snprintf(input, sizeof(input), "%s:%s", peer_binding_digest, normalized_username); if (n < 0 || (size_t)n >= sizeof(input)) return FALSE; uint8 digest[32]; uint32 digest_len = 32; if (!HMAC(EVP_sha256(), g_cookie_secret, (int)g_cookie_secret_length, (const uint8 *)input, (size_t)n, digest, &digest_len)) { OPENSSL_cleanse(input, sizeof(input)); OPENSSL_cleanse(digest, sizeof(digest)); return FALSE; } OPENSSL_cleanse(input, sizeof(input)); auth_hex_encode(digest, 32, key_out, RATE_KEY_MAX); OPENSSL_cleanse(digest, sizeof(digest)); return TRUE; } static uint32 auth_rate_index(const char *key) { /* Use the first 8 hex chars of the HMAC key as a uint32 hash seed. */ uint32 h = 0; for (int i = 0; i < 8 && key[i] != '\0'; i++) { char c = key[i]; uint32 nibble = (c >= '0' && c <= '9') ? (uint32)(c - '0') : (c >= 'a' && c <= 'f') ? (uint32)(c - 'a' + 10) : 0; h = (h << 4) | nibble; } return h & (RATE_TABLE_SIZE - 1); } /* * Find or insert an entry for key in the open-addressing rate table. * Probes up to RATE_PROBE_LIMIT slots from the hash index. * On a full probe window, evicts the oldest entry by window_start. * Must be called with g_rate_mutex held. * Returns a pointer to the entry on success, NULL if internal error. */ static Auth_Rate_Entry *auth_rate_find_or_insert(const char *key, int64 now) { uint32 start = auth_rate_index(key); Auth_Rate_Entry *evict_candidate = NULL; int64 evict_time = INT64_MAX; for (uint32 i = 0; i < (uint32)RATE_PROBE_LIMIT; i++) { uint32 idx = (start + i) & (RATE_TABLE_SIZE - 1); Auth_Rate_Entry *e = &g_rate_table[idx]; /* Exact match */ if (e->key[0] != '\0' && memcmp(e->key, key, RATE_KEY_MAX) == 0) return e; /* Empty slot — claim it */ if (e->key[0] == '\0') { memcpy(e->key, key, RATE_KEY_MAX); e->count = 0; e->window_start = now; return e; } /* Expired entry — reuse it immediately */ if (now - e->window_start >= RATE_LIMIT_WINDOW_SECS) { memcpy(e->key, key, RATE_KEY_MAX); e->count = 0; e->window_start = now; return e; } /* Track the oldest live entry for eviction */ if (e->window_start < evict_time) { evict_time = e->window_start; evict_candidate = e; } } /* All probe slots occupied by live, non-matching entries — evict oldest */ if (evict_candidate) { memcpy(evict_candidate->key, key, RATE_KEY_MAX); evict_candidate->count = 0; evict_candidate->window_start = now; return evict_candidate; } return NULL; } /* * Returns TRUE if the caller should be rate-limited (too many failures). * Caller must still call auth_rate_record_failure on a failed attempt. */ static boolean auth_rate_check(const char *key) { pthread_mutex_lock(&g_rate_mutex); int64 now = auth_now(); Auth_Rate_Entry *entry = auth_rate_find_or_insert(key, now); boolean limited = FALSE; if (entry && now - entry->window_start < RATE_LIMIT_WINDOW_SECS) limited = (entry->count >= RATE_LIMIT_MAX_FAILURES); pthread_mutex_unlock(&g_rate_mutex); return limited; } static void auth_rate_record_failure(const char *key) { pthread_mutex_lock(&g_rate_mutex); int64 now = auth_now(); Auth_Rate_Entry *entry = auth_rate_find_or_insert(key, now); if (entry) { if (now - entry->window_start >= RATE_LIMIT_WINDOW_SECS) { /* Window expired; start fresh */ entry->count = 1; entry->window_start = now; } else { entry->count++; } } pthread_mutex_unlock(&g_rate_mutex); } static void auth_rate_reset(const char *key) { pthread_mutex_lock(&g_rate_mutex); uint32 start = auth_rate_index(key); for (uint32 i = 0; i < (uint32)RATE_PROBE_LIMIT; i++) { uint32 idx = (start + i) & (RATE_TABLE_SIZE - 1); Auth_Rate_Entry *e = &g_rate_table[idx]; if (e->key[0] != '\0' && memcmp(e->key, key, RATE_KEY_MAX) == 0) { memset(e, 0, sizeof(*e)); break; } } pthread_mutex_unlock(&g_rate_mutex); } /* ------------------------------------------------------------------ */ /* Request helpers */ /* ------------------------------------------------------------------ */ static const char *auth_req_value( Seobeo_Request_Entry *p_req, const char *key) { void *p = Dowa_HashMap_Get_Ptr(p_req, (char *)key); return p ? ((Seobeo_Request_Entry *)p)->value : NULL; } static boolean auth_extract_secret_field( Dowa_JSON_Entry *obj, const char *key, char *out_buf, size_t max_len) { char *arena_ptr = Dowa_JSON_Get_String(obj, key); if (!arena_ptr || arena_ptr[0] == '\0') return FALSE; size_t field_len = strlen(arena_ptr); if (field_len > max_len) { OPENSSL_cleanse(arena_ptr, field_len); return FALSE; } memcpy(out_buf, arena_ptr, field_len); out_buf[field_len] = '\0'; OPENSSL_cleanse(arena_ptr, field_len); return TRUE; } /* * Parse a named cookie from the Cookie header. * Returns TRUE and fills value_out on success. */ static boolean auth_parse_cookie( const char *cookie_header, const char *name, char *value_out, size_t capacity) { if (!cookie_header || !name || !value_out || capacity == 0) return FALSE; size_t name_len = strlen(name); const char *p = cookie_header; while (*p) { /* skip whitespace */ while (*p == ' ' || *p == '\t') p++; /* check for name= */ if (strncmp(p, name, name_len) == 0 && p[name_len] == '=') { p += name_len + 1; const char *start = p; while (*p && *p != ';') p++; size_t vlen = (size_t)(p - start); if (vlen >= capacity) return FALSE; memcpy(value_out, start, vlen); value_out[vlen] = '\0'; return TRUE; } /* skip to next ; */ while (*p && *p != ';') p++; if (*p == ';') p++; } return FALSE; } /* * Resolve effective peer IP. * If Remote-Addr matches configured trusted proxy, accept X-Real-IP. * Never logs raw IPs. */ static boolean auth_peer_ip( Seobeo_Request_Entry *p_req, char *ip_out, size_t capacity) { const char *direct = auth_req_value(p_req, "Remote-Addr"); if (!direct || direct[0] == '\0') return FALSE; if (g_has_trusted_proxy && strcmp(direct, g_trusted_proxy_ip) == 0) { const char *forwarded = auth_req_value(p_req, "X-Real-IP"); if (forwarded && forwarded[0] != '\0' && strlen(forwarded) < capacity) { strncpy(ip_out, forwarded, capacity - 1); ip_out[capacity - 1] = '\0'; return TRUE; } } if (strlen(direct) >= capacity) return FALSE; strncpy(ip_out, direct, capacity - 1); ip_out[capacity - 1] = '\0'; return TRUE; } static boolean auth_same_origin(Seobeo_Request_Entry *p_req) { const char *host = auth_req_value(p_req, "Host"); const char *origin = auth_req_value(p_req, "Origin"); if (!host || !origin) return FALSE; const char *host_in_origin = strstr(origin, "://"); if (!host_in_origin) return FALSE; host_in_origin += 3; const char *end = strchr(host_in_origin, '/'); size_t len = end ? (size_t)(end - host_in_origin) : strlen(host_in_origin); return strlen(host) == len && strncmp(host, host_in_origin, len) == 0; } /* ------------------------------------------------------------------ */ /* Response builders */ /* ------------------------------------------------------------------ */ /* * Build a Set-Cookie directive string. * expires_max_age = 0 means no Max-Age (persistent); < 0 means Max-Age=0 * (clear the cookie). */ static boolean auth_build_cookie_directive( const char *name, const char *value, int32 max_age, boolean http_only, char *out, size_t capacity) { int n; if (max_age < 0) { n = snprintf(out, capacity, "%s=; Path=/; %sSameSite=Lax; Max-Age=0%s", name, http_only ? "HttpOnly; " : "", g_dev_insecure_cookie ? "" : "; Secure"); } else if (max_age == 0) { n = snprintf(out, capacity, "%s=%s; Path=/; %sSameSite=Lax%s", name, value, http_only ? "HttpOnly; " : "", g_dev_insecure_cookie ? "" : "; Secure"); } else { n = snprintf(out, capacity, "%s=%s; Path=/; %sSameSite=Lax; Max-Age=%d%s", name, value, http_only ? "HttpOnly; " : "", max_age, g_dev_insecure_cookie ? "" : "; Secure"); } return n > 0 && (size_t)n < capacity; } static Seobeo_Request_Entry *auth_json_response( Dowa_Arena *p_arena, const char *status, const char *body) { Seobeo_Request_Entry *resp = NULL; Dowa_HashMap_Push_Arena(resp, "status", (char *)status, p_arena); Dowa_HashMap_Push_Arena( resp, "content-type", "application/json; charset=utf-8", p_arena); Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", p_arena); Dowa_HashMap_Push_Arena(resp, "pragma", "no-cache", p_arena); Dowa_HashMap_Push_Arena(resp, "x-content-type-options", "nosniff", p_arena); Dowa_HashMap_Push_Arena(resp, "body", (char *)body, p_arena); return resp; } static Seobeo_Request_Entry *auth_json_response_with_cookies( Dowa_Arena *p_arena, const char *status, const char *body, const char *cookie1, /* value for "Set-Cookie"; NULL to skip */ const char *cookie2) /* value for "set-cookie"; NULL to skip */ { Seobeo_Request_Entry *resp = auth_json_response(p_arena, status, body); if (cookie1) Dowa_HashMap_Push_Arena(resp, "Set-Cookie", (char *)cookie1, p_arena); if (cookie2) Dowa_HashMap_Push_Arena(resp, "set-cookie", (char *)cookie2, p_arena); return resp; } static Seobeo_Request_Entry *auth_error( Dowa_Arena *p_arena, const char *status, const char *code, const char *message) { char body[512]; snprintf(body, sizeof(body), "{\"error\":{\"code\":\"%s\",\"message\":\"%s\"}}", code, message); char *body_copy = Dowa_Arena_Allocate(p_arena, strlen(body) + 1); if (body_copy) strcpy(body_copy, body); return auth_json_response(p_arena, status, body_copy ? body_copy : "{}"); } /* ------------------------------------------------------------------ */ /* Principal resolution */ /* ------------------------------------------------------------------ */ 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) { if (!g_auth_store || !p_principal) return FALSE; memset(p_principal, 0, sizeof(*p_principal)); if (new_guest_cookie_out && new_guest_cookie_capacity > 0) new_guest_cookie_out[0] = '\0'; const char *cookie_header = auth_req_value(p_request, "Cookie"); int64 now = auth_now(); /* --- Try authenticated session first --- */ char session_token[COOKIE_VALUE_MAX] = {0}; if (cookie_header && auth_parse_cookie(cookie_header, AUTH_API_SESSION_COOKIE_NAME, session_token, sizeof(session_token)) && session_token[0] != '\0') { char token_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; if (Auth_Crypto_Token_Digest(session_token, token_digest, sizeof(token_digest)) == AUTH_CRYPTO_OK) { Auth_Session_Record session; Auth_User_Record user; Auth_Store_Result result = Auth_Store_Find_Session( g_auth_store, token_digest, now, &session, &user); if (result == AUTH_STORE_OK) { Auth_Store_Touch_Session( g_auth_store, token_digest, now, g_session_idle_ttl); p_principal->kind = AUTH_PRINCIPAL_USER; strncpy(p_principal->user_id, user.id, sizeof(p_principal->user_id) - 1); strncpy(p_principal->username, user.username, sizeof(p_principal->username) - 1); strncpy(p_principal->role, user.role, sizeof(p_principal->role) - 1); p_principal->must_change_password = user.must_change_password; strncpy(p_principal->_binding, token_digest, sizeof(p_principal->_binding) - 1); if (!auth_derive_csrf(token_digest, p_principal->csrf_token, sizeof(p_principal->csrf_token))) { OPENSSL_cleanse(session_token, sizeof(session_token)); OPENSSL_cleanse(token_digest, sizeof(token_digest)); memset(p_principal, 0, sizeof(*p_principal)); return FALSE; } OPENSSL_cleanse(session_token, sizeof(session_token)); OPENSSL_cleanse(token_digest, sizeof(token_digest)); return TRUE; } } OPENSSL_cleanse(session_token, sizeof(session_token)); } /* --- Try guest cookie --- */ char peer_ip[AUTH_CRYPTO_IP_MAX_BYTES] = {0}; boolean have_ip = auth_peer_ip(p_request, peer_ip, sizeof(peer_ip)); char ip_binding[AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE] = {0}; if (have_ip && Auth_Crypto_IP_Binding_Digest( g_cookie_secret, g_cookie_secret_length, peer_ip, ip_binding, sizeof(ip_binding)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); return FALSE; } char guest_cookie_val[COOKIE_VALUE_MAX] = {0}; boolean guest_valid = FALSE; Auth_Crypto_Guest_Cookie guest_parsed; memset(&guest_parsed, 0, sizeof(guest_parsed)); if (cookie_header && auth_parse_cookie(cookie_header, AUTH_API_GUEST_COOKIE_NAME, guest_cookie_val, sizeof(guest_cookie_val)) && guest_cookie_val[0] != '\0' && have_ip) { Auth_Crypto_Result cr = Auth_Crypto_Guest_Cookie_Verify( g_cookie_secret, g_cookie_secret_length, guest_cookie_val, (uint64)now, ip_binding, &guest_parsed); guest_valid = (cr == AUTH_CRYPTO_OK); } if (guest_valid) { Auth_Guest_Identity_Record identity; Auth_Store_Result result = Auth_Store_Find_Guest_Identity( g_auth_store, guest_parsed.guest_uuid, now, &identity); if (result == AUTH_STORE_OK) { Auth_Store_Upsert_Guest_Identity( g_auth_store, guest_parsed.guest_uuid, ip_binding, now + g_guest_ttl, &identity); p_principal->kind = AUTH_PRINCIPAL_GUEST; strncpy(p_principal->guest_id, guest_parsed.guest_uuid, sizeof(p_principal->guest_id) - 1); strncpy(p_principal->_binding, guest_parsed.guest_uuid, sizeof(p_principal->_binding) - 1); if (!auth_derive_csrf(guest_parsed.guest_uuid, p_principal->csrf_token, sizeof(p_principal->csrf_token))) { OPENSSL_cleanse(guest_cookie_val, sizeof(guest_cookie_val)); OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); memset(p_principal, 0, sizeof(*p_principal)); return FALSE; } OPENSSL_cleanse(guest_cookie_val, sizeof(guest_cookie_val)); OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); return TRUE; } } OPENSSL_cleanse(guest_cookie_val, sizeof(guest_cookie_val)); /* --- Create new guest identity --- */ char guest_uuid[AUTH_CRYPTO_GUEST_UUID_SIZE]; if (!auth_uuid4(guest_uuid, sizeof(guest_uuid))) { OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); return FALSE; } int64 guest_expires = now + g_guest_ttl; Auth_Guest_Identity_Record new_identity; if (Auth_Store_Upsert_Guest_Identity( g_auth_store, guest_uuid, ip_binding, guest_expires, &new_identity) != AUTH_STORE_OK) { OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); return FALSE; } p_principal->kind = AUTH_PRINCIPAL_GUEST; strncpy(p_principal->guest_id, guest_uuid, sizeof(p_principal->guest_id) - 1); strncpy(p_principal->_binding, guest_uuid, sizeof(p_principal->_binding) - 1); if (!auth_derive_csrf(guest_uuid, p_principal->csrf_token, sizeof(p_principal->csrf_token))) { OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); memset(p_principal, 0, sizeof(*p_principal)); return FALSE; } /* Build new guest cookie for the response */ if (new_guest_cookie_out && new_guest_cookie_capacity > 0 && have_ip) { char signed_cookie[AUTH_CRYPTO_GUEST_COOKIE_SIZE]; if (Auth_Crypto_Guest_Cookie_Create( g_cookie_secret, g_cookie_secret_length, guest_uuid, (uint64)guest_expires, ip_binding, signed_cookie, sizeof(signed_cookie)) == AUTH_CRYPTO_OK) { auth_build_cookie_directive( AUTH_API_GUEST_COOKIE_NAME, signed_cookie, (int32)g_guest_ttl, TRUE, new_guest_cookie_out, new_guest_cookie_capacity); OPENSSL_cleanse(signed_cookie, sizeof(signed_cookie)); } } OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); return TRUE; } boolean Auth_API_Resolve_Existing_Principal( Seobeo_Request_Entry *p_request, Auth_Principal *p_principal, Dowa_Arena *p_arena, boolean *p_found) { if (!g_auth_store || !p_principal || !p_found) return FALSE; memset(p_principal, 0, sizeof(*p_principal)); *p_found = FALSE; const char *cookie_header = auth_req_value(p_request, "Cookie"); int64 now = auth_now(); /* --- Try authenticated session first --- */ char session_token[COOKIE_VALUE_MAX] = {0}; if (cookie_header && auth_parse_cookie(cookie_header, AUTH_API_SESSION_COOKIE_NAME, session_token, sizeof(session_token)) && session_token[0] != '\0') { char token_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; if (Auth_Crypto_Token_Digest(session_token, token_digest, sizeof(token_digest)) == AUTH_CRYPTO_OK) { Auth_Session_Record session; Auth_User_Record user; Auth_Store_Result result = Auth_Store_Find_Session( g_auth_store, token_digest, now, &session, &user); if (result == AUTH_STORE_OK) { Auth_Store_Touch_Session( g_auth_store, token_digest, now, g_session_idle_ttl); p_principal->kind = AUTH_PRINCIPAL_USER; strncpy(p_principal->user_id, user.id, sizeof(p_principal->user_id) - 1); strncpy(p_principal->username, user.username, sizeof(p_principal->username) - 1); strncpy(p_principal->role, user.role, sizeof(p_principal->role) - 1); p_principal->must_change_password = user.must_change_password; strncpy(p_principal->_binding, token_digest, sizeof(p_principal->_binding) - 1); if (!auth_derive_csrf(token_digest, p_principal->csrf_token, sizeof(p_principal->csrf_token))) { OPENSSL_cleanse(session_token, sizeof(session_token)); OPENSSL_cleanse(token_digest, sizeof(token_digest)); memset(p_principal, 0, sizeof(*p_principal)); return FALSE; } OPENSSL_cleanse(session_token, sizeof(session_token)); OPENSSL_cleanse(token_digest, sizeof(token_digest)); *p_found = TRUE; return TRUE; } } OPENSSL_cleanse(session_token, sizeof(session_token)); } /* --- Try existing guest cookie (no new guest created) --- */ char peer_ip[AUTH_CRYPTO_IP_MAX_BYTES] = {0}; boolean have_ip = auth_peer_ip(p_request, peer_ip, sizeof(peer_ip)); char ip_binding[AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE] = {0}; if (have_ip && Auth_Crypto_IP_Binding_Digest( g_cookie_secret, g_cookie_secret_length, peer_ip, ip_binding, sizeof(ip_binding)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); return FALSE; } char guest_cookie_val[COOKIE_VALUE_MAX] = {0}; Auth_Crypto_Guest_Cookie guest_parsed; memset(&guest_parsed, 0, sizeof(guest_parsed)); if (cookie_header && auth_parse_cookie(cookie_header, AUTH_API_GUEST_COOKIE_NAME, guest_cookie_val, sizeof(guest_cookie_val)) && guest_cookie_val[0] != '\0' && have_ip) { Auth_Crypto_Result cr = Auth_Crypto_Guest_Cookie_Verify( g_cookie_secret, g_cookie_secret_length, guest_cookie_val, (uint64)now, ip_binding, &guest_parsed); if (cr == AUTH_CRYPTO_OK) { Auth_Guest_Identity_Record identity; Auth_Store_Result result = Auth_Store_Find_Guest_Identity( g_auth_store, guest_parsed.guest_uuid, now, &identity); if (result == AUTH_STORE_OK) { Auth_Store_Upsert_Guest_Identity( g_auth_store, guest_parsed.guest_uuid, ip_binding, now + g_guest_ttl, &identity); p_principal->kind = AUTH_PRINCIPAL_GUEST; strncpy(p_principal->guest_id, guest_parsed.guest_uuid, sizeof(p_principal->guest_id) - 1); strncpy(p_principal->_binding, guest_parsed.guest_uuid, sizeof(p_principal->_binding) - 1); if (!auth_derive_csrf(guest_parsed.guest_uuid, p_principal->csrf_token, sizeof(p_principal->csrf_token))) { OPENSSL_cleanse(guest_cookie_val, sizeof(guest_cookie_val)); OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); memset(p_principal, 0, sizeof(*p_principal)); return FALSE; } OPENSSL_cleanse(guest_cookie_val, sizeof(guest_cookie_val)); OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); *p_found = TRUE; return TRUE; } } } OPENSSL_cleanse(guest_cookie_val, sizeof(guest_cookie_val)); OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); /* No existing identity found — caller should return 401. * We do NOT create a guest row or generate a Set-Cookie directive. */ *p_found = FALSE; return TRUE; } /* ------------------------------------------------------------------ */ /* CSRF verification helper */ /* ------------------------------------------------------------------ */ /* * Verify a CSRF token provided by the client against the binding for * the current session/guest. Comparison is by SHA-256 digest equality * to avoid timing-oracle attacks on the base64url token directly. */ static boolean auth_verify_csrf( const char *provided_token, const char *binding) { if (!provided_token || !binding || provided_token[0] == '\0') return FALSE; char expected[AUTH_CRYPTO_TOKEN_SIZE]; if (!auth_derive_csrf(binding, expected, sizeof(expected))) return FALSE; /* Compare SHA-256 digests of both tokens (constant-time length comparison) */ char digest_provided[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; char digest_expected[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; if (Auth_Crypto_Token_Digest(provided_token, digest_provided, sizeof(digest_provided)) != AUTH_CRYPTO_OK || Auth_Crypto_Token_Digest(expected, digest_expected, sizeof(digest_expected)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(expected, sizeof(expected)); OPENSSL_cleanse(digest_provided, sizeof(digest_provided)); OPENSSL_cleanse(digest_expected, sizeof(digest_expected)); return FALSE; } int match = CRYPTO_memcmp(digest_provided, digest_expected, sizeof(digest_provided)); OPENSSL_cleanse(expected, sizeof(expected)); OPENSSL_cleanse(digest_provided, sizeof(digest_provided)); OPENSSL_cleanse(digest_expected, sizeof(digest_expected)); return match == 0; } /* ------------------------------------------------------------------ */ /* Public: Auth_API_Verify_CSRF */ /* ------------------------------------------------------------------ */ boolean Auth_API_Verify_CSRF( Seobeo_Request_Entry *p_request, const Auth_Principal *p_principal) { if (!p_request || !p_principal) return FALSE; if (!auth_same_origin(p_request)) return FALSE; const char *csrf = auth_req_value(p_request, "X-CSRF-Token"); if (!csrf || csrf[0] == '\0') return FALSE; return auth_verify_csrf(csrf, p_principal->_binding); } /* ------------------------------------------------------------------ */ /* Route: GET /api/auth/session */ /* ------------------------------------------------------------------ */ static Seobeo_Request_Entry *auth_session_handler( Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena) { if (!g_auth_store) return auth_error(p_arena, "503", "service_unavailable", "Auth not initialised"); Auth_Principal principal; char new_guest_cookie[GUEST_COOKIE_MAX] = {0}; if (!Auth_API_Resolve_Principal(p_req, &principal, p_arena, new_guest_cookie, sizeof(new_guest_cookie))) return auth_error(p_arena, "500", "internal_error", "Session error"); char body[2048]; if (principal.kind == AUTH_PRINCIPAL_USER) { char *safe_username = Dowa_JSON_Escape_String(principal.username, 0, p_arena); char *safe_role = Dowa_JSON_Escape_String(principal.role, 0, p_arena); char *safe_csrf = Dowa_JSON_Escape_String(principal.csrf_token, 0, p_arena); if (!safe_username || !safe_role || !safe_csrf) return auth_error(p_arena, "500", "internal_error", "Encode error"); snprintf(body, sizeof(body), "{\"kind\":\"user\",\"username\":\"%s\",\"role\":\"%s\"," "\"mustChangePassword\":%s,\"csrfToken\":\"%s\"," "\"quota\":null}", safe_username, safe_role, principal.must_change_password ? "true" : "false", safe_csrf); } else { char *safe_csrf = Dowa_JSON_Escape_String(principal.csrf_token, 0, p_arena); if (!safe_csrf) return auth_error(p_arena, "500", "internal_error", "Encode error"); /* Ask conversation layer for quota JSON (null if not registered). */ char quota_json[512] = "null"; if (g_guest_quota_cb) g_guest_quota_cb(principal.guest_id, auth_now(), quota_json, sizeof(quota_json)); snprintf(body, sizeof(body), "{\"kind\":\"guest\",\"csrfToken\":\"%s\",\"quota\":%s}", safe_csrf, quota_json); } char *body_copy = Dowa_Arena_Allocate(p_arena, strlen(body) + 1); if (!body_copy) return auth_error(p_arena, "500", "internal_error", "OOM"); strcpy(body_copy, body); const char *cookie1 = (new_guest_cookie[0] != '\0') ? new_guest_cookie : NULL; return auth_json_response_with_cookies( p_arena, "200", body_copy, cookie1, NULL); } /* ------------------------------------------------------------------ */ /* Route: POST /api/auth/login */ /* ------------------------------------------------------------------ */ static Seobeo_Request_Entry *auth_login_handler( Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena) { if (!g_auth_store) return auth_error(p_arena, "503", "service_unavailable", "Auth not initialised"); if (!auth_same_origin(p_req)) return auth_error(p_arena, "403", "forbidden", "Origin mismatch"); /* --- Parse body --- */ const char *body_str = auth_req_value(p_req, "Body"); if (!body_str) return auth_error(p_arena, "400", "bad_request", "Invalid body"); size_t body_len = strlen(body_str); if (body_len > BODY_MAX_BYTES) { OPENSSL_cleanse((char *)body_str, body_len); return auth_error(p_arena, "400", "bad_request", "Invalid body"); } Dowa_JSON_Value jv = Dowa_JSON_Parse(body_str, (int32)body_len, p_arena); OPENSSL_cleanse((char *)body_str, body_len); if (jv.type != DOWA_JSON_OBJECT) return auth_error(p_arena, "400", "bad_request", "Expected JSON object"); Dowa_JSON_Entry *obj = (Dowa_JSON_Entry *)jv.object_val; char *username_raw = Dowa_JSON_Get_String(obj, "username"); char *csrf_provided = Dowa_JSON_Get_String(obj, "csrfToken"); char password_buf[AUTH_CRYPTO_PASSWORD_MAX_BYTES + 1]; memset(password_buf, 0, sizeof(password_buf)); boolean have_password = auth_extract_secret_field( obj, "password", password_buf, AUTH_CRYPTO_PASSWORD_MAX_BYTES); char *password_raw = password_buf; if (!username_raw || !have_password || !csrf_provided || username_raw[0] == '\0' || csrf_provided[0] == '\0') { OPENSSL_cleanse(password_buf, sizeof(password_buf)); return auth_error(p_arena, "400", "bad_request", "Missing fields"); } /* --- Password length bounds --- */ size_t pw_len = strlen(password_raw); if (pw_len < 12 || pw_len > AUTH_CRYPTO_PASSWORD_MAX_BYTES) { OPENSSL_cleanse(password_raw, pw_len); return auth_error(p_arena, "401", "invalid_credentials", "Invalid credentials"); } /* --- Resolve existing principal for CSRF binding --- */ Auth_Principal principal; char ignored_cookie[GUEST_COOKIE_MAX] = {0}; if (!Auth_API_Resolve_Principal(p_req, &principal, p_arena, ignored_cookie, sizeof(ignored_cookie))) { OPENSSL_cleanse(password_raw, pw_len); return auth_error(p_arena, "500", "internal_error", "Session error"); } /* --- CSRF check --- */ if (!auth_verify_csrf(csrf_provided, principal._binding)) { OPENSSL_cleanse(password_raw, pw_len); return auth_error(p_arena, "403", "csrf_invalid", "CSRF token invalid"); } /* --- Peer IP and rate-limit key --- */ char peer_ip[AUTH_CRYPTO_IP_MAX_BYTES] = {0}; boolean have_ip = auth_peer_ip(p_req, peer_ip, sizeof(peer_ip)); char ip_binding[AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE] = {0}; if (have_ip && Auth_Crypto_IP_Binding_Digest( g_cookie_secret, g_cookie_secret_length, peer_ip, ip_binding, sizeof(ip_binding)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); OPENSSL_cleanse(password_raw, pw_len); return auth_error(p_arena, "500", "internal_error", "Binding error"); } OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); /* Normalize username */ char norm_username[AUTH_STORE_USERNAME_MAX + 1] = {0}; if (!Auth_Store_Normalize_Username( username_raw, norm_username, sizeof(norm_username))) { OPENSSL_cleanse(password_raw, pw_len); return auth_error(p_arena, "401", "invalid_credentials", "Invalid credentials"); } /* Rate limit check */ char rate_key[RATE_KEY_MAX] = {0}; boolean have_rate_key = auth_rate_key(ip_binding, norm_username, rate_key); if (have_rate_key && auth_rate_check(rate_key)) { OPENSSL_cleanse(password_raw, pw_len); OPENSSL_cleanse(norm_username, sizeof(norm_username)); return auth_error(p_arena, "429", "too_many_requests", "Too many login attempts"); } /* --- Fetch user record --- */ Auth_User_Auth_Record auth_record; memset(&auth_record, 0, sizeof(auth_record)); Auth_Store_Result find_result = Auth_Store_Find_User_By_Username( g_auth_store, norm_username, &auth_record); if (find_result != AUTH_STORE_OK) { /* * User not found; run exactly one scrypt verification against a fixed * precomputed hash to consume constant time, then return a generic error. * AUTH_DUMMY_PASSWORD_HASH is a valid zenbu-scrypt hash of a known * constant string — it is never an account credential. */ Auth_Crypto_Password_Verify(password_raw, AUTH_DUMMY_PASSWORD_HASH); OPENSSL_cleanse(password_raw, pw_len); OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); if (have_rate_key) auth_rate_record_failure(rate_key); return auth_error(p_arena, "401", "invalid_credentials", "Invalid credentials"); } if (strcmp(auth_record.user.status, "active") != 0) { Auth_Crypto_Password_Verify(password_raw, auth_record.password_hash); OPENSSL_cleanse(password_raw, pw_len); OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); if (have_rate_key) auth_rate_record_failure(rate_key); return auth_error(p_arena, "401", "invalid_credentials", "Invalid credentials"); } /* --- Verify password --- */ Auth_Crypto_Result verify = Auth_Crypto_Password_Verify(password_raw, auth_record.password_hash); OPENSSL_cleanse(password_raw, pw_len); if (verify != AUTH_CRYPTO_OK) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); if (have_rate_key) auth_rate_record_failure(rate_key); return auth_error(p_arena, "401", "invalid_credentials", "Invalid credentials"); } /* Successful authentication: reset rate limit */ if (have_rate_key) auth_rate_reset(rate_key); /* --- Create new session (prevents session fixation) --- */ char new_token[AUTH_CRYPTO_TOKEN_SIZE]; if (Auth_Crypto_Token_Generate(new_token, sizeof(new_token)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); return auth_error(p_arena, "500", "internal_error", "Token error"); } char new_token_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; if (Auth_Crypto_Token_Digest(new_token, new_token_digest, sizeof(new_token_digest)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(new_token, sizeof(new_token)); return auth_error(p_arena, "500", "internal_error", "Digest error"); } /* CSRF for this new session */ char new_csrf[AUTH_CRYPTO_TOKEN_SIZE]; if (!auth_derive_csrf(new_token_digest, new_csrf, sizeof(new_csrf))) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(new_token, sizeof(new_token)); OPENSSL_cleanse(new_token_digest, sizeof(new_token_digest)); return auth_error(p_arena, "500", "internal_error", "CSRF error"); } char csrf_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; if (Auth_Crypto_Token_Digest(new_csrf, csrf_digest, sizeof(csrf_digest)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(new_token, sizeof(new_token)); OPENSSL_cleanse(new_token_digest, sizeof(new_token_digest)); OPENSSL_cleanse(new_csrf, sizeof(new_csrf)); return auth_error(p_arena, "500", "internal_error", "CSRF digest error"); } int64 now = auth_now(); Auth_Session_Record session; #ifdef AUTH_API_TEST_HOOKS if (g_login_pre_create_hook) g_login_pre_create_hook(g_login_pre_create_context); #endif Auth_Store_Result create_result = Auth_Store_Create_Session_CAS( g_auth_store, auth_record.user.id, auth_record.password_hash, new_token_digest, csrf_digest, g_session_idle_ttl, g_session_abs_ttl, now, &session); OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(csrf_digest, sizeof(csrf_digest)); OPENSSL_cleanse(new_token_digest, sizeof(new_token_digest)); if (create_result != AUTH_STORE_OK) { OPENSSL_cleanse(new_token, sizeof(new_token)); OPENSSL_cleanse(new_csrf, sizeof(new_csrf)); if (create_result == AUTH_STORE_STALE_PASSWORD) return auth_error(p_arena, "401", "invalid_credentials", "Invalid credentials"); return auth_error(p_arena, "500", "internal_error", "Session create failed"); } /* --- Call guest transfer hook (before clearing guest state) --- */ if (g_transfer_hook && principal.kind == AUTH_PRINCIPAL_GUEST && principal.guest_id[0] != '\0') { boolean transferred = g_transfer_hook( principal.guest_id, auth_record.user.id, g_transfer_hook_ctx); if (!transferred) { /* Transfer failed — revoke the new session and return 500. * Guest cookie/data are preserved (no clear_guest in response). */ char rev_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; if (Auth_Crypto_Token_Digest(new_token, rev_digest, sizeof(rev_digest)) == AUTH_CRYPTO_OK) { Auth_Store_Result revoke_result = Auth_Store_Revoke_Session(g_auth_store, rev_digest); if (revoke_result == AUTH_STORE_ERROR) Seobeo_Log(SEOBEO_ERROR, "[AUTH] Failed to revoke session after transfer failure\n"); OPENSSL_cleanse(rev_digest, sizeof(rev_digest)); } OPENSSL_cleanse(new_token, sizeof(new_token)); OPENSSL_cleanse(new_csrf, sizeof(new_csrf)); return auth_error(p_arena, "500", "transfer_failed", "Resource transfer failed"); } } /* --- Build response --- */ char *safe_username = Dowa_JSON_Escape_String(auth_record.user.username, 0, p_arena); char *safe_role = Dowa_JSON_Escape_String(auth_record.user.role, 0, p_arena); char *safe_csrf = Dowa_JSON_Escape_String(new_csrf, 0, p_arena); OPENSSL_cleanse(new_csrf, sizeof(new_csrf)); if (!safe_username || !safe_role || !safe_csrf) { OPENSSL_cleanse(new_token, sizeof(new_token)); return auth_error(p_arena, "500", "internal_error", "Encode error"); } char body_buf[512]; snprintf(body_buf, sizeof(body_buf), "{\"kind\":\"user\",\"username\":\"%s\",\"role\":\"%s\"," "\"mustChangePassword\":%s,\"csrfToken\":\"%s\"," "\"quota\":null}", safe_username, safe_role, auth_record.user.must_change_password ? "true" : "false", safe_csrf); char *body_copy = Dowa_Arena_Allocate(p_arena, strlen(body_buf) + 1); if (!body_copy) { OPENSSL_cleanse(new_token, sizeof(new_token)); return auth_error(p_arena, "500", "internal_error", "OOM"); } strcpy(body_copy, body_buf); /* Session cookie */ char session_cookie[SESSION_COOKIE_MAX]; auth_build_cookie_directive( AUTH_API_SESSION_COOKIE_NAME, new_token, 0, TRUE, session_cookie, sizeof(session_cookie)); OPENSSL_cleanse(new_token, sizeof(new_token)); char *session_cookie_copy = Dowa_Arena_Allocate(p_arena, strlen(session_cookie) + 1); if (session_cookie_copy) strcpy(session_cookie_copy, session_cookie); OPENSSL_cleanse(session_cookie, sizeof(session_cookie)); /* Clear guest cookie */ char clear_guest[256]; auth_build_cookie_directive( AUTH_API_GUEST_COOKIE_NAME, "", -1, TRUE, clear_guest, sizeof(clear_guest)); char *clear_guest_copy = Dowa_Arena_Allocate(p_arena, strlen(clear_guest) + 1); if (clear_guest_copy) strcpy(clear_guest_copy, clear_guest); return auth_json_response_with_cookies( p_arena, "200", body_copy, session_cookie_copy, clear_guest_copy); } /* ------------------------------------------------------------------ */ /* Route: POST /api/auth/logout */ /* ------------------------------------------------------------------ */ static Seobeo_Request_Entry *auth_logout_handler( Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena) { if (!g_auth_store) return auth_error(p_arena, "503", "service_unavailable", "Auth not initialised"); if (!auth_same_origin(p_req)) return auth_error(p_arena, "403", "forbidden", "Origin mismatch"); /* --- Resolve current principal for CSRF binding --- */ Auth_Principal principal; char ignored_cookie[GUEST_COOKIE_MAX] = {0}; if (!Auth_API_Resolve_Principal(p_req, &principal, p_arena, ignored_cookie, sizeof(ignored_cookie))) return auth_error(p_arena, "500", "internal_error", "Session error"); /* --- CSRF check --- */ const char *body_str = auth_req_value(p_req, "Body"); char csrf_provided[AUTH_CRYPTO_TOKEN_SIZE] = {0}; if (body_str && strlen(body_str) <= BODY_MAX_BYTES) { Dowa_JSON_Value jv = Dowa_JSON_Parse(body_str, (int32)strlen(body_str), p_arena); if (jv.type == DOWA_JSON_OBJECT) { char *t = Dowa_JSON_Get_String((Dowa_JSON_Entry *)jv.object_val, "csrfToken"); if (t) strncpy(csrf_provided, t, sizeof(csrf_provided) - 1); } } /* Also accept CSRF from X-CSRF-Token header */ if (csrf_provided[0] == '\0') { const char *hdr = auth_req_value(p_req, "X-CSRF-Token"); if (hdr) strncpy(csrf_provided, hdr, sizeof(csrf_provided) - 1); } if (!auth_verify_csrf(csrf_provided, principal._binding)) return auth_error(p_arena, "403", "csrf_invalid", "CSRF token invalid"); /* --- Revoke session if authenticated --- */ const char *cookie_hdr = auth_req_value(p_req, "Cookie"); char session_token[COOKIE_VALUE_MAX] = {0}; if (cookie_hdr && auth_parse_cookie(cookie_hdr, AUTH_API_SESSION_COOKIE_NAME, session_token, sizeof(session_token)) && session_token[0] != '\0') { char token_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE] = {0}; Auth_Crypto_Result digest_result = Auth_Crypto_Token_Digest( session_token, token_digest, sizeof(token_digest)); if (digest_result == AUTH_CRYPTO_OK) { Auth_Store_Result revoke_result = Auth_Store_Revoke_Session(g_auth_store, token_digest); OPENSSL_cleanse(token_digest, sizeof(token_digest)); if (revoke_result == AUTH_STORE_ERROR) { OPENSSL_cleanse(session_token, sizeof(session_token)); return auth_error(p_arena, "500", "internal_error", "Logout failed"); } } else { OPENSSL_cleanse(token_digest, sizeof(token_digest)); OPENSSL_cleanse(session_token, sizeof(session_token)); return auth_error(p_arena, "500", "internal_error", "Logout failed"); } OPENSSL_cleanse(session_token, sizeof(session_token)); } /* --- Create fresh guest identity --- */ char peer_ip[AUTH_CRYPTO_IP_MAX_BYTES] = {0}; boolean have_ip = auth_peer_ip(p_req, peer_ip, sizeof(peer_ip)); char ip_binding[AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE] = {0}; if (have_ip && Auth_Crypto_IP_Binding_Digest( g_cookie_secret, g_cookie_secret_length, peer_ip, ip_binding, sizeof(ip_binding)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); return auth_error(p_arena, "500", "internal_error", "Binding error"); } OPENSSL_cleanse(peer_ip, sizeof(peer_ip)); char guest_uuid[AUTH_CRYPTO_GUEST_UUID_SIZE] = {0}; if (!auth_uuid4(guest_uuid, sizeof(guest_uuid))) return auth_error(p_arena, "500", "internal_error", "UUID error"); int64 now = auth_now(); int64 guest_expires = now + g_guest_ttl; Auth_Guest_Identity_Record new_identity; Auth_Store_Upsert_Guest_Identity( g_auth_store, guest_uuid, ip_binding, guest_expires, &new_identity); char new_csrf[AUTH_CRYPTO_TOKEN_SIZE] = {0}; if (!auth_derive_csrf(guest_uuid, new_csrf, sizeof(new_csrf))) return auth_error(p_arena, "500", "internal_error", "CSRF error"); char signed_cookie[AUTH_CRYPTO_GUEST_COOKIE_SIZE] = {0}; char new_guest_cookie[GUEST_COOKIE_MAX] = {0}; if (have_ip && Auth_Crypto_Guest_Cookie_Create( g_cookie_secret, g_cookie_secret_length, guest_uuid, (uint64)guest_expires, ip_binding, signed_cookie, sizeof(signed_cookie)) == AUTH_CRYPTO_OK) { auth_build_cookie_directive( AUTH_API_GUEST_COOKIE_NAME, signed_cookie, (int32)g_guest_ttl, TRUE, new_guest_cookie, sizeof(new_guest_cookie)); OPENSSL_cleanse(signed_cookie, sizeof(signed_cookie)); } /* --- Build response --- */ char *safe_csrf = Dowa_JSON_Escape_String(new_csrf, 0, p_arena); OPENSSL_cleanse(new_csrf, sizeof(new_csrf)); if (!safe_csrf) return auth_error(p_arena, "500", "internal_error", "Encode error"); char body_buf[256]; snprintf(body_buf, sizeof(body_buf), "{\"kind\":\"guest\",\"csrfToken\":\"%s\",\"quota\":null}", safe_csrf); char *body_copy = Dowa_Arena_Allocate(p_arena, strlen(body_buf) + 1); if (!body_copy) return auth_error(p_arena, "500", "internal_error", "OOM"); strcpy(body_copy, body_buf); /* Clear session cookie */ char clear_session[256]; auth_build_cookie_directive( AUTH_API_SESSION_COOKIE_NAME, "", -1, TRUE, clear_session, sizeof(clear_session)); char *clear_session_copy = Dowa_Arena_Allocate(p_arena, strlen(clear_session) + 1); if (!clear_session_copy) return auth_error(p_arena, "500", "internal_error", "OOM"); strcpy(clear_session_copy, clear_session); char *new_guest_copy = NULL; if (new_guest_cookie[0] != '\0') { new_guest_copy = Dowa_Arena_Allocate(p_arena, strlen(new_guest_cookie) + 1); if (!new_guest_copy) return auth_error(p_arena, "500", "internal_error", "OOM"); strcpy(new_guest_copy, new_guest_cookie); } return auth_json_response_with_cookies( p_arena, "200", body_copy, clear_session_copy, new_guest_copy); } /* ------------------------------------------------------------------ */ /* Route: POST /api/auth/password */ /* ------------------------------------------------------------------ */ static Seobeo_Request_Entry *auth_password_handler( Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena) { if (!g_auth_store) return auth_error(p_arena, "503", "service_unavailable", "Auth not initialised"); if (!auth_same_origin(p_req)) return auth_error(p_arena, "403", "forbidden", "Origin mismatch"); /* --- Resolve principal — must be authenticated user --- */ Auth_Principal principal; char ignored_cookie[GUEST_COOKIE_MAX] = {0}; if (!Auth_API_Resolve_Principal(p_req, &principal, p_arena, ignored_cookie, sizeof(ignored_cookie))) return auth_error(p_arena, "500", "internal_error", "Session error"); if (principal.kind != AUTH_PRINCIPAL_USER) return auth_error(p_arena, "401", "unauthenticated", "Authentication required"); /* --- Parse body --- */ const char *body_str = auth_req_value(p_req, "Body"); if (!body_str) return auth_error(p_arena, "400", "bad_request", "Invalid body"); size_t body_len = strlen(body_str); if (body_len > BODY_MAX_BYTES) { OPENSSL_cleanse((char *)body_str, body_len); return auth_error(p_arena, "400", "bad_request", "Invalid body"); } Dowa_JSON_Value jv = Dowa_JSON_Parse(body_str, (int32)body_len, p_arena); OPENSSL_cleanse((char *)body_str, body_len); if (jv.type != DOWA_JSON_OBJECT) return auth_error(p_arena, "400", "bad_request", "Expected JSON object"); Dowa_JSON_Entry *obj = (Dowa_JSON_Entry *)jv.object_val; char *csrf_provided = Dowa_JSON_Get_String(obj, "csrfToken"); char current_pw_buf[AUTH_CRYPTO_PASSWORD_MAX_BYTES + 1]; char new_pw_buf[AUTH_CRYPTO_PASSWORD_MAX_BYTES + 1]; memset(current_pw_buf, 0, sizeof(current_pw_buf)); memset(new_pw_buf, 0, sizeof(new_pw_buf)); boolean have_current = auth_extract_secret_field( obj, "currentPassword", current_pw_buf, AUTH_CRYPTO_PASSWORD_MAX_BYTES); boolean have_new = auth_extract_secret_field( obj, "newPassword", new_pw_buf, AUTH_CRYPTO_PASSWORD_MAX_BYTES); char *current_pw_raw = current_pw_buf; char *new_pw_raw = new_pw_buf; /* Also accept CSRF from header */ if (!csrf_provided || csrf_provided[0] == '\0') csrf_provided = (char *)auth_req_value(p_req, "X-CSRF-Token"); if (!have_current || !have_new || !csrf_provided) { OPENSSL_cleanse(current_pw_buf, sizeof(current_pw_buf)); OPENSSL_cleanse(new_pw_buf, sizeof(new_pw_buf)); return auth_error(p_arena, "400", "bad_request", "Missing fields"); } /* --- CSRF check --- */ if (!auth_verify_csrf(csrf_provided, principal._binding)) { OPENSSL_cleanse(current_pw_buf, sizeof(current_pw_buf)); OPENSSL_cleanse(new_pw_buf, sizeof(new_pw_buf)); return auth_error(p_arena, "403", "csrf_invalid", "CSRF token invalid"); } /* --- Password policy --- */ size_t new_pw_len = strlen(new_pw_raw); size_t cur_pw_len = strlen(current_pw_raw); if (new_pw_len < 12 || new_pw_len > AUTH_CRYPTO_PASSWORD_MAX_BYTES || cur_pw_len < 1 || cur_pw_len > AUTH_CRYPTO_PASSWORD_MAX_BYTES) { OPENSSL_cleanse(current_pw_raw, cur_pw_len); OPENSSL_cleanse(new_pw_raw, new_pw_len); return auth_error(p_arena, "400", "password_policy", "New password must be at least 12 characters"); } /* --- Fetch user with password hash --- */ char norm_username[AUTH_STORE_USERNAME_MAX + 1]; if (!Auth_Store_Normalize_Username( principal.username, norm_username, sizeof(norm_username))) { OPENSSL_cleanse(current_pw_raw, cur_pw_len); OPENSSL_cleanse(new_pw_raw, new_pw_len); return auth_error(p_arena, "500", "internal_error", "Username error"); } Auth_User_Auth_Record auth_record; memset(&auth_record, 0, sizeof(auth_record)); if (Auth_Store_Find_User_By_Username( g_auth_store, norm_username, &auth_record) != AUTH_STORE_OK) { OPENSSL_cleanse(current_pw_raw, cur_pw_len); OPENSSL_cleanse(new_pw_raw, new_pw_len); OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); return auth_error(p_arena, "401", "invalid_credentials", "Invalid credentials"); } /* --- Verify current password --- */ Auth_Crypto_Result verify = Auth_Crypto_Password_Verify(current_pw_raw, auth_record.password_hash); OPENSSL_cleanse(current_pw_raw, cur_pw_len); if (verify != AUTH_CRYPTO_OK) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(new_pw_raw, new_pw_len); return auth_error(p_arena, "401", "invalid_credentials", "Invalid credentials"); } /* --- Hash new password --- */ char new_encoded_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; Auth_Crypto_Result hash_result = Auth_Crypto_Password_Hash( new_pw_raw, new_encoded_hash, sizeof(new_encoded_hash)); OPENSSL_cleanse(new_pw_raw, new_pw_len); if (hash_result != AUTH_CRYPTO_OK) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(new_encoded_hash, sizeof(new_encoded_hash)); return auth_error(p_arena, "500", "internal_error", "Hash error"); } /* Generate the replacement session before entering the transaction. */ char new_token[AUTH_CRYPTO_TOKEN_SIZE]; if (Auth_Crypto_Token_Generate(new_token, sizeof(new_token)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(new_encoded_hash, sizeof(new_encoded_hash)); return auth_error(p_arena, "500", "internal_error", "Token error"); } char new_token_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; if (Auth_Crypto_Token_Digest(new_token, new_token_digest, sizeof(new_token_digest)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(new_encoded_hash, sizeof(new_encoded_hash)); OPENSSL_cleanse(new_token, sizeof(new_token)); return auth_error(p_arena, "500", "internal_error", "Digest error"); } char new_csrf[AUTH_CRYPTO_TOKEN_SIZE]; if (!auth_derive_csrf(new_token_digest, new_csrf, sizeof(new_csrf))) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(new_encoded_hash, sizeof(new_encoded_hash)); OPENSSL_cleanse(new_token, sizeof(new_token)); OPENSSL_cleanse(new_token_digest, sizeof(new_token_digest)); return auth_error(p_arena, "500", "internal_error", "CSRF error"); } char new_csrf_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; if (Auth_Crypto_Token_Digest(new_csrf, new_csrf_digest, sizeof(new_csrf_digest)) != AUTH_CRYPTO_OK) { OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(new_encoded_hash, sizeof(new_encoded_hash)); OPENSSL_cleanse(new_token, sizeof(new_token)); OPENSSL_cleanse(new_token_digest, sizeof(new_token_digest)); OPENSSL_cleanse(new_csrf, sizeof(new_csrf)); return auth_error(p_arena, "500", "internal_error", "CSRF digest error"); } int64 now = auth_now(); Auth_Session_Record new_session; Auth_Store_Result password_result = Auth_Store_Self_Change_Password( g_auth_store, principal.user_id, auth_record.password_hash, new_encoded_hash, new_token_digest, new_csrf_digest, g_session_idle_ttl, g_session_abs_ttl, now, &new_session); OPENSSL_cleanse(auth_record.password_hash, sizeof(auth_record.password_hash)); OPENSSL_cleanse(new_encoded_hash, sizeof(new_encoded_hash)); OPENSSL_cleanse(new_csrf_digest, sizeof(new_csrf_digest)); OPENSSL_cleanse(new_token_digest, sizeof(new_token_digest)); if (password_result != AUTH_STORE_OK) { OPENSSL_cleanse(new_token, sizeof(new_token)); OPENSSL_cleanse(new_csrf, sizeof(new_csrf)); return auth_error(p_arena, "500", "internal_error", "Password update failed"); } /* --- Build response --- */ char *safe_username = Dowa_JSON_Escape_String(principal.username, 0, p_arena); char *safe_role = Dowa_JSON_Escape_String(principal.role, 0, p_arena); char *safe_csrf = Dowa_JSON_Escape_String(new_csrf, 0, p_arena); OPENSSL_cleanse(new_csrf, sizeof(new_csrf)); if (!safe_username || !safe_role || !safe_csrf) { OPENSSL_cleanse(new_token, sizeof(new_token)); return auth_error(p_arena, "500", "internal_error", "Encode error"); } char body_buf[512]; snprintf(body_buf, sizeof(body_buf), "{\"kind\":\"user\",\"username\":\"%s\",\"role\":\"%s\"," "\"mustChangePassword\":false,\"csrfToken\":\"%s\"," "\"quota\":null}", safe_username, safe_role, safe_csrf); char *body_copy = Dowa_Arena_Allocate(p_arena, strlen(body_buf) + 1); if (!body_copy) { OPENSSL_cleanse(new_token, sizeof(new_token)); return auth_error(p_arena, "500", "internal_error", "OOM"); } strcpy(body_copy, body_buf); /* Updated session cookie */ char session_cookie[SESSION_COOKIE_MAX]; auth_build_cookie_directive( AUTH_API_SESSION_COOKIE_NAME, new_token, 0, TRUE, session_cookie, sizeof(session_cookie)); OPENSSL_cleanse(new_token, sizeof(new_token)); char *session_cookie_copy = Dowa_Arena_Allocate(p_arena, strlen(session_cookie) + 1); if (session_cookie_copy) strcpy(session_cookie_copy, session_cookie); OPENSSL_cleanse(session_cookie, sizeof(session_cookie)); return auth_json_response_with_cookies( p_arena, "200", body_copy, session_cookie_copy, NULL); } /* ------------------------------------------------------------------ */ /* Route: GET /account/password */ /* ------------------------------------------------------------------ */ static Seobeo_Request_Entry *auth_password_page_handler( Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena) { (void)p_req; char *body = Dowa_Arena_Allocate(p_arena, 128 * 1024); if (!body || !Mjj_Template_Render_File(body, 128 * 1024, "/account/password.html", p_arena)) { Seobeo_Request_Entry *resp = NULL; Dowa_HashMap_Push_Arena(resp, "status", "500", p_arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain; charset=utf-8", p_arena); Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", p_arena); Dowa_HashMap_Push_Arena(resp, "body", "Internal Server Error", p_arena); return resp; } Seobeo_Request_Entry *resp = NULL; Dowa_HashMap_Push_Arena(resp, "body", body, p_arena); Dowa_HashMap_Push_Arena( resp, "content-type", "text/html; charset=utf-8", p_arena); Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", p_arena); Dowa_HashMap_Push_Arena(resp, "pragma", "no-cache", p_arena); Dowa_HashMap_Push_Arena(resp, "x-content-type-options", "nosniff", p_arena); Dowa_HashMap_Push_Arena(resp, "referrer-policy", "no-referrer", p_arena); Dowa_HashMap_Push_Arena( resp, "content-security-policy", "frame-ancestors 'none'", p_arena); return resp; } /* ------------------------------------------------------------------ */ /* Public API */ /* ------------------------------------------------------------------ */ boolean Auth_API_Is_Forced_Password_Change_Only(const char *http_path) { if (!http_path) return FALSE; return strcmp(http_path, AUTH_API_PATH_SESSION) == 0 || strcmp(http_path, AUTH_API_PATH_LOGIN) == 0 || strcmp(http_path, AUTH_API_PATH_LOGOUT) == 0 || strcmp(http_path, AUTH_API_PATH_PASSWORD) == 0 || strcmp(http_path, AUTH_API_PATH_PASSWORD_PAGE) == 0; } void Auth_API_Register_Guest_Transfer_Hook( Auth_Guest_Transfer_Hook hook, void *context) { g_transfer_hook = hook; g_transfer_hook_ctx = context; } void Auth_API_Register_Guest_Quota_Cb(Auth_API_Guest_Quota_Cb cb) { g_guest_quota_cb = cb; } 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) { /* Fail closed: cookie secret required */ if (!cookie_secret || cookie_secret_length < AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES || cookie_secret_length > AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES) { Seobeo_Log(SEOBEO_ERROR, "[AUTH] Init failed: cookie secret missing or invalid length\n"); return FALSE; } if (!database_path || database_path[0] == '\0') { Seobeo_Log(SEOBEO_ERROR, "[AUTH] Init failed: database path required\n"); return FALSE; } g_auth_store = Auth_Store_Create(database_path); if (!g_auth_store) { Seobeo_Log(SEOBEO_ERROR, "[AUTH] Failed to open auth store\n"); return FALSE; } memcpy(g_cookie_secret, cookie_secret, cookie_secret_length); g_cookie_secret_length = cookie_secret_length; if (trusted_proxy_ip && trusted_proxy_ip[0] != '\0') { /* Canonicalize via inet_pton/inet_ntop; reject invalid addresses. */ struct in_addr addr4; struct in6_addr addr6; char canonical[AUTH_CRYPTO_IP_MAX_BYTES]; canonical[0] = '\0'; if (inet_pton(AF_INET, trusted_proxy_ip, &addr4) == 1) { if (!inet_ntop(AF_INET, &addr4, canonical, sizeof(canonical))) { Seobeo_Log(SEOBEO_ERROR, "[AUTH] Init failed: trusted proxy IPv4 canonicalization\n"); Auth_Store_Destroy(g_auth_store); g_auth_store = NULL; return FALSE; } } else if (inet_pton(AF_INET6, trusted_proxy_ip, &addr6) == 1) { if (!inet_ntop(AF_INET6, &addr6, canonical, sizeof(canonical))) { Seobeo_Log(SEOBEO_ERROR, "[AUTH] Init failed: trusted proxy IPv6 canonicalization\n"); Auth_Store_Destroy(g_auth_store); g_auth_store = NULL; return FALSE; } } else { Seobeo_Log(SEOBEO_ERROR, "[AUTH] Init failed: trusted proxy is not a valid IP address\n"); Auth_Store_Destroy(g_auth_store); g_auth_store = NULL; return FALSE; } strncpy(g_trusted_proxy_ip, canonical, sizeof(g_trusted_proxy_ip) - 1); g_trusted_proxy_ip[sizeof(g_trusted_proxy_ip) - 1] = '\0'; g_has_trusted_proxy = TRUE; } if (session_idle_ttl_secs > 0) g_session_idle_ttl = session_idle_ttl_secs; if (session_absolute_ttl_secs > 0) g_session_abs_ttl = session_absolute_ttl_secs; if (guest_ttl_secs > 0) g_guest_ttl = guest_ttl_secs; g_dev_insecure_cookie = dev_insecure_cookie; /* Bootstrap admin — fail closed on any store error. */ if (bootstrap_username && bootstrap_username[0] != '\0' && bootstrap_password_hash && bootstrap_password_hash[0] != '\0') { Auth_Store_Bootstrap_Result bootstrap_result; char bootstrap_id[37]; Auth_Store_Result r = Auth_Store_Bootstrap_Admin( g_auth_store, bootstrap_username, bootstrap_password_hash, &bootstrap_result, bootstrap_id); if (r == AUTH_STORE_OK && bootstrap_result == AUTH_STORE_BOOTSTRAP_CREATED) { Seobeo_Log(SEOBEO_INFO, "[AUTH] Bootstrap admin created\n"); } else if (r == AUTH_STORE_OK && bootstrap_result == AUTH_STORE_BOOTSTRAP_ALREADY_PRESENT) { Seobeo_Log(SEOBEO_INFO, "[AUTH] Bootstrap admin already present\n"); } else { Seobeo_Log(SEOBEO_ERROR, "[AUTH] Bootstrap admin failed: store error %d — refusing to start\n", r); OPENSSL_cleanse(g_cookie_secret, sizeof(g_cookie_secret)); g_cookie_secret_length = 0; g_has_trusted_proxy = FALSE; Auth_Store_Destroy(g_auth_store); g_auth_store = NULL; return FALSE; } } Seobeo_Log(SEOBEO_INFO, "[AUTH] Initialised (dev_insecure=%s)\n", dev_insecure_cookie ? "yes" : "no"); return TRUE; } void Auth_API_Destroy(void) { if (g_auth_store) { Auth_Store_Destroy(g_auth_store); g_auth_store = NULL; } OPENSSL_cleanse(g_cookie_secret, sizeof(g_cookie_secret)); g_cookie_secret_length = 0; g_has_trusted_proxy = FALSE; g_transfer_hook = NULL; g_transfer_hook_ctx = NULL; g_guest_quota_cb = NULL; #ifdef AUTH_API_TEST_HOOKS g_login_pre_create_hook = NULL; g_login_pre_create_context = NULL; #endif } void Auth_API_Register_Routes(void) { Seobeo_Router_Register("GET", "/api/auth/session", auth_session_handler); Seobeo_Router_Register("POST", "/api/auth/login", auth_login_handler); Seobeo_Router_Register("POST", "/api/auth/logout", auth_logout_handler); Seobeo_Router_Register("POST", "/api/auth/password", auth_password_handler); Seobeo_Router_Register("GET", "/account/password", auth_password_page_handler); } Auth_Store *Auth_API_Get_Store(void) { return g_auth_store; } /* ------------------------------------------------------------------ */ /* Test hooks (compiled in only for test builds) */ /* ------------------------------------------------------------------ */ #ifdef AUTH_API_TEST_HOOKS void Auth_API_Test_Set_Login_Pre_Create_Hook( Auth_API_Test_Login_Pre_Create_Hook hook, void *p_context) { g_login_pre_create_hook = hook; g_login_pre_create_context = p_context; } Seobeo_Request_Entry *Auth_API_Test_Session_Handler( Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena) { return auth_session_handler(p_req, p_arena); } Seobeo_Request_Entry *Auth_API_Test_Login_Handler( Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena) { return auth_login_handler(p_req, p_arena); } Seobeo_Request_Entry *Auth_API_Test_Logout_Handler( Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena) { return auth_logout_handler(p_req, p_arena); } Seobeo_Request_Entry *Auth_API_Test_Password_Handler( Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena) { return auth_password_handler(p_req, p_arena); } #endif /* AUTH_API_TEST_HOOKS */