comparison mrjunejune/auth_api.c @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents 04fee26ecce0
children
comparison
equal deleted inserted replaced
278:8d560f50ed4c 279:b3b547563ec7
1 #include "mrjunejune/auth_api.h" 1 #include "mrjunejune/auth_api.h"
2 #include "mrjunejune/template_renderer.h" 2 #include "mrjunejune/template_renderer.h"
3 3
4 #include "auth/auth_crypto.h" 4 #include "auth/auth_crypto.h"
5 #include "auth/auth_http.h"
5 #include "auth/auth_store.h" 6 #include "auth/auth_store.h"
6 #include "seobeo/seobeo.h" 7 #include "seobeo/seobeo.h"
7 #include "dowa/dowa.h" 8 #include "dowa/dowa.h"
8 9
9 #include <openssl/hmac.h> 10 #include <openssl/hmac.h>
23 /* Constants */ 24 /* Constants */
24 /* ------------------------------------------------------------------ */ 25 /* ------------------------------------------------------------------ */
25 26
26 #define BODY_MAX_BYTES 4096 27 #define BODY_MAX_BYTES 4096
27 #define COOKIE_VALUE_MAX 512 28 #define COOKIE_VALUE_MAX 512
28 #define CSRF_SUFFIX ":csrf:v1"
29 #define RATE_TABLE_SIZE 512 /* must be power of 2 */ 29 #define RATE_TABLE_SIZE 512 /* must be power of 2 */
30 #define RATE_PROBE_LIMIT 16 30 #define RATE_PROBE_LIMIT 16
31 #define RATE_LIMIT_MAX_FAILURES 5 31 #define RATE_LIMIT_MAX_FAILURES 5
32 #define RATE_LIMIT_WINDOW_SECS (15 * 60) 32 #define RATE_LIMIT_WINDOW_SECS (15 * 60)
33 #define SESSION_COOKIE_MAX 512 33 #define SESSION_COOKIE_MAX 512
34 #define GUEST_COOKIE_MAX (AUTH_CRYPTO_GUEST_COOKIE_SIZE + 256) 34 #define GUEST_COOKIE_MAX (AUTH_CRYPTO_GUEST_COOKIE_SIZE + 256)
35 #define RATE_KEY_MAX 65 35 #define RATE_KEY_MAX 65
36 #define BINDING_INPUT_MAX (AUTH_CRYPTO_TOKEN_DIGEST_SIZE + 16)
37 36
38 /* 37 /*
39 * Fixed precomputed scrypt hash of the constant string "dummy-zenbu-timing". 38 * Fixed precomputed scrypt hash of the constant string "dummy-zenbu-timing".
40 * Used only for timing-attack mitigation on nonexistent-username lookups. 39 * Used only for timing-attack mitigation on nonexistent-username lookups.
41 * Never used as an account credential; salt+hash are intentionally public. 40 * Never used as an account credential; salt+hash are intentionally public.
110 rnd[10], rnd[11], rnd[12], rnd[13], rnd[14], rnd[15]); 109 rnd[10], rnd[11], rnd[12], rnd[13], rnd[14], rnd[15]);
111 OPENSSL_cleanse(rnd, sizeof(rnd)); 110 OPENSSL_cleanse(rnd, sizeof(rnd));
112 return TRUE; 111 return TRUE;
113 } 112 }
114 113
115 /*
116 * base64url encode src_len bytes from src into dst.
117 * dst must have capacity ceil(src_len * 4 / 3) + 1.
118 * Returns length of encoded string (without NUL).
119 *
120 * Delegates to Auth_Crypto_Base64url_Encode which uses the correct loop
121 * boundary (processes remaining bytes after full 3-byte groups).
122 */
123 static size_t auth_base64url_encode(
124 const uint8 *src,
125 size_t src_len,
126 char *dst,
127 size_t dst_capacity)
128 {
129 return Auth_Crypto_Base64url_Encode(src, src_len, dst, dst_capacity);
130 }
131
132 static void auth_hex_encode( 114 static void auth_hex_encode(
133 const uint8 *src, 115 const uint8 *src,
134 size_t src_len, 116 size_t src_len,
135 char *dst, 117 char *dst,
136 size_t dst_capacity) 118 size_t dst_capacity)
154 static boolean auth_derive_csrf( 136 static boolean auth_derive_csrf(
155 const char *binding, 137 const char *binding,
156 char *csrf_out, 138 char *csrf_out,
157 size_t csrf_capacity) 139 size_t csrf_capacity)
158 { 140 {
159 if (!binding || !csrf_out || csrf_capacity < AUTH_CRYPTO_TOKEN_SIZE) 141 return Auth_HTTP_Derive_CSRF(
160 return FALSE; 142 g_cookie_secret, g_cookie_secret_length,
161 143 binding, csrf_out, csrf_capacity);
162 size_t binding_len = strlen(binding);
163 size_t suffix_len = strlen(CSRF_SUFFIX);
164 size_t input_len = binding_len + suffix_len;
165
166 if (input_len >= BINDING_INPUT_MAX)
167 return FALSE;
168
169 char input[BINDING_INPUT_MAX];
170 memcpy(input, binding, binding_len);
171 memcpy(input + binding_len, CSRF_SUFFIX, suffix_len);
172
173 uint8 digest[32];
174 uint32 digest_len = 32;
175 if (!HMAC(EVP_sha256(),
176 g_cookie_secret, (int)g_cookie_secret_length,
177 (const uint8 *)input, input_len,
178 digest, &digest_len))
179 {
180 OPENSSL_cleanse(input, sizeof(input));
181 OPENSSL_cleanse(digest, sizeof(digest));
182 return FALSE;
183 }
184 OPENSSL_cleanse(input, sizeof(input));
185
186 size_t encoded_length =
187 auth_base64url_encode(digest, 32, csrf_out, csrf_capacity);
188 OPENSSL_cleanse(digest, sizeof(digest));
189 return encoded_length == AUTH_CRYPTO_TOKEN_SIZE - 1;
190 } 144 }
191 145
192 /* 146 /*
193 * Build the rate-limit key: HMAC(secret, peer_digest + ":" + norm_user). 147 * Build the rate-limit key: HMAC(secret, peer_digest + ":" + norm_user).
194 * Output: 64-char hex string. 148 * Output: 64-char hex string.
394 const char *cookie_header, 348 const char *cookie_header,
395 const char *name, 349 const char *name,
396 char *value_out, 350 char *value_out,
397 size_t capacity) 351 size_t capacity)
398 { 352 {
399 if (!cookie_header || !name || !value_out || capacity == 0) 353 return Auth_HTTP_Parse_Cookie(
400 return FALSE; 354 cookie_header, name, value_out, capacity);
401
402 size_t name_len = strlen(name);
403 const char *p = cookie_header;
404
405 while (*p)
406 {
407 /* skip whitespace */
408 while (*p == ' ' || *p == '\t') p++;
409
410 /* check for name= */
411 if (strncmp(p, name, name_len) == 0 && p[name_len] == '=')
412 {
413 p += name_len + 1;
414 const char *start = p;
415 while (*p && *p != ';') p++;
416 size_t vlen = (size_t)(p - start);
417 if (vlen >= capacity) return FALSE;
418 memcpy(value_out, start, vlen);
419 value_out[vlen] = '\0';
420 return TRUE;
421 }
422
423 /* skip to next ; */
424 while (*p && *p != ';') p++;
425 if (*p == ';') p++;
426 }
427 return FALSE;
428 } 355 }
429 356
430 /* 357 /*
431 * Resolve effective peer IP. 358 * Resolve effective peer IP.
432 * If Remote-Addr matches configured trusted proxy, accept X-Real-IP. 359 * If Remote-Addr matches configured trusted proxy, accept X-Real-IP.
460 return TRUE; 387 return TRUE;
461 } 388 }
462 389
463 static boolean auth_same_origin(Seobeo_Request_Entry *p_req) 390 static boolean auth_same_origin(Seobeo_Request_Entry *p_req)
464 { 391 {
465 const char *host = auth_req_value(p_req, "Host"); 392 return Auth_HTTP_Same_Origin(p_req);
466 const char *origin = auth_req_value(p_req, "Origin");
467 if (!host || !origin) return FALSE;
468
469 const char *host_in_origin = strstr(origin, "://");
470 if (!host_in_origin) return FALSE;
471 host_in_origin += 3;
472
473 const char *end = strchr(host_in_origin, '/');
474 size_t len = end ? (size_t)(end - host_in_origin) : strlen(host_in_origin);
475 return strlen(host) == len && strncmp(host, host_in_origin, len) == 0;
476 } 393 }
477 394
478 /* ------------------------------------------------------------------ */ 395 /* ------------------------------------------------------------------ */
479 /* Response builders */ 396 /* Response builders */
480 /* ------------------------------------------------------------------ */ 397 /* ------------------------------------------------------------------ */
586 503
587 const char *cookie_header = auth_req_value(p_request, "Cookie"); 504 const char *cookie_header = auth_req_value(p_request, "Cookie");
588 int64 now = auth_now(); 505 int64 now = auth_now();
589 506
590 /* --- Try authenticated session first --- */ 507 /* --- Try authenticated session first --- */
591 char session_token[COOKIE_VALUE_MAX] = {0}; 508 Auth_HTTP_Authenticated_User authenticated_user;
592 if (cookie_header && 509 Auth_HTTP_Resolve_Result user_result =
593 auth_parse_cookie(cookie_header, AUTH_API_SESSION_COOKIE_NAME, 510 Auth_HTTP_Resolve_Authenticated_User(
594 session_token, sizeof(session_token)) && 511 p_request, g_auth_store, g_cookie_secret, g_cookie_secret_length,
595 session_token[0] != '\0') 512 now, g_session_idle_ttl, &authenticated_user);
596 { 513 if (user_result == AUTH_HTTP_RESOLVE_ERROR)
597 char token_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; 514 return FALSE;
598 if (Auth_Crypto_Token_Digest(session_token, token_digest, 515 if (user_result == AUTH_HTTP_RESOLVE_OK)
599 sizeof(token_digest)) == AUTH_CRYPTO_OK) 516 {
600 { 517 p_principal->kind = AUTH_PRINCIPAL_USER;
601 Auth_Session_Record session; 518 strncpy(p_principal->user_id, authenticated_user.user.id,
602 Auth_User_Record user; 519 sizeof(p_principal->user_id) - 1);
603 Auth_Store_Result result = Auth_Store_Find_Session( 520 strncpy(p_principal->username, authenticated_user.user.username,
604 g_auth_store, token_digest, now, &session, &user); 521 sizeof(p_principal->username) - 1);
605 522 strncpy(p_principal->role, authenticated_user.user.role,
606 if (result == AUTH_STORE_OK) 523 sizeof(p_principal->role) - 1);
607 { 524 p_principal->must_change_password =
608 Auth_Store_Touch_Session( 525 authenticated_user.user.must_change_password;
609 g_auth_store, token_digest, now, g_session_idle_ttl); 526 strncpy(p_principal->_binding, authenticated_user.token_digest,
610 527 sizeof(p_principal->_binding) - 1);
611 p_principal->kind = AUTH_PRINCIPAL_USER; 528 strncpy(p_principal->csrf_token, authenticated_user.csrf_token,
612 strncpy(p_principal->user_id, user.id, sizeof(p_principal->user_id) - 1); 529 sizeof(p_principal->csrf_token) - 1);
613 strncpy(p_principal->username, user.username, sizeof(p_principal->username) - 1); 530 OPENSSL_cleanse(&authenticated_user, sizeof(authenticated_user));
614 strncpy(p_principal->role, user.role, sizeof(p_principal->role) - 1); 531 return TRUE;
615 p_principal->must_change_password = user.must_change_password;
616 strncpy(p_principal->_binding, token_digest, sizeof(p_principal->_binding) - 1);
617 if (!auth_derive_csrf(token_digest, p_principal->csrf_token,
618 sizeof(p_principal->csrf_token)))
619 {
620 OPENSSL_cleanse(session_token, sizeof(session_token));
621 OPENSSL_cleanse(token_digest, sizeof(token_digest));
622 memset(p_principal, 0, sizeof(*p_principal));
623 return FALSE;
624 }
625 OPENSSL_cleanse(session_token, sizeof(session_token));
626 OPENSSL_cleanse(token_digest, sizeof(token_digest));
627 return TRUE;
628 }
629 }
630 OPENSSL_cleanse(session_token, sizeof(session_token));
631 } 532 }
632 533
633 /* --- Try guest cookie --- */ 534 /* --- Try guest cookie --- */
634 char peer_ip[AUTH_CRYPTO_IP_MAX_BYTES] = {0}; 535 char peer_ip[AUTH_CRYPTO_IP_MAX_BYTES] = {0};
635 boolean have_ip = auth_peer_ip(p_request, peer_ip, sizeof(peer_ip)); 536 boolean have_ip = auth_peer_ip(p_request, peer_ip, sizeof(peer_ip));
759 660
760 const char *cookie_header = auth_req_value(p_request, "Cookie"); 661 const char *cookie_header = auth_req_value(p_request, "Cookie");
761 int64 now = auth_now(); 662 int64 now = auth_now();
762 663
763 /* --- Try authenticated session first --- */ 664 /* --- Try authenticated session first --- */
764 char session_token[COOKIE_VALUE_MAX] = {0}; 665 Auth_HTTP_Authenticated_User authenticated_user;
765 if (cookie_header && 666 Auth_HTTP_Resolve_Result user_result =
766 auth_parse_cookie(cookie_header, AUTH_API_SESSION_COOKIE_NAME, 667 Auth_HTTP_Resolve_Authenticated_User(
767 session_token, sizeof(session_token)) && 668 p_request, g_auth_store, g_cookie_secret, g_cookie_secret_length,
768 session_token[0] != '\0') 669 now, g_session_idle_ttl, &authenticated_user);
769 { 670 if (user_result == AUTH_HTTP_RESOLVE_ERROR)
770 char token_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE]; 671 return FALSE;
771 if (Auth_Crypto_Token_Digest(session_token, token_digest, 672 if (user_result == AUTH_HTTP_RESOLVE_OK)
772 sizeof(token_digest)) == AUTH_CRYPTO_OK) 673 {
773 { 674 p_principal->kind = AUTH_PRINCIPAL_USER;
774 Auth_Session_Record session; 675 strncpy(p_principal->user_id, authenticated_user.user.id,
775 Auth_User_Record user; 676 sizeof(p_principal->user_id) - 1);
776 Auth_Store_Result result = Auth_Store_Find_Session( 677 strncpy(p_principal->username, authenticated_user.user.username,
777 g_auth_store, token_digest, now, &session, &user); 678 sizeof(p_principal->username) - 1);
778 679 strncpy(p_principal->role, authenticated_user.user.role,
779 if (result == AUTH_STORE_OK) 680 sizeof(p_principal->role) - 1);
780 { 681 p_principal->must_change_password =
781 Auth_Store_Touch_Session( 682 authenticated_user.user.must_change_password;
782 g_auth_store, token_digest, now, g_session_idle_ttl); 683 strncpy(p_principal->_binding, authenticated_user.token_digest,
783 684 sizeof(p_principal->_binding) - 1);
784 p_principal->kind = AUTH_PRINCIPAL_USER; 685 strncpy(p_principal->csrf_token, authenticated_user.csrf_token,
785 strncpy(p_principal->user_id, user.id, sizeof(p_principal->user_id) - 1); 686 sizeof(p_principal->csrf_token) - 1);
786 strncpy(p_principal->username, user.username, sizeof(p_principal->username) - 1); 687 OPENSSL_cleanse(&authenticated_user, sizeof(authenticated_user));
787 strncpy(p_principal->role, user.role, sizeof(p_principal->role) - 1); 688 *p_found = TRUE;
788 p_principal->must_change_password = user.must_change_password; 689 return TRUE;
789 strncpy(p_principal->_binding, token_digest, sizeof(p_principal->_binding) - 1);
790 if (!auth_derive_csrf(token_digest, p_principal->csrf_token,
791 sizeof(p_principal->csrf_token)))
792 {
793 OPENSSL_cleanse(session_token, sizeof(session_token));
794 OPENSSL_cleanse(token_digest, sizeof(token_digest));
795 memset(p_principal, 0, sizeof(*p_principal));
796 return FALSE;
797 }
798 OPENSSL_cleanse(session_token, sizeof(session_token));
799 OPENSSL_cleanse(token_digest, sizeof(token_digest));
800 *p_found = TRUE;
801 return TRUE;
802 }
803 }
804 OPENSSL_cleanse(session_token, sizeof(session_token));
805 } 690 }
806 691
807 /* --- Try existing guest cookie (no new guest created) --- */ 692 /* --- Try existing guest cookie (no new guest created) --- */
808 char peer_ip[AUTH_CRYPTO_IP_MAX_BYTES] = {0}; 693 char peer_ip[AUTH_CRYPTO_IP_MAX_BYTES] = {0};
809 boolean have_ip = auth_peer_ip(p_request, peer_ip, sizeof(peer_ip)); 694 boolean have_ip = auth_peer_ip(p_request, peer_ip, sizeof(peer_ip));
884 */ 769 */
885 static boolean auth_verify_csrf( 770 static boolean auth_verify_csrf(
886 const char *provided_token, 771 const char *provided_token,
887 const char *binding) 772 const char *binding)
888 { 773 {
889 if (!provided_token || !binding || provided_token[0] == '\0') 774 return Auth_HTTP_Verify_CSRF_Token(
890 return FALSE; 775 g_cookie_secret, g_cookie_secret_length, binding, provided_token);
891
892 char expected[AUTH_CRYPTO_TOKEN_SIZE];
893 if (!auth_derive_csrf(binding, expected, sizeof(expected)))
894 return FALSE;
895
896 /* Compare SHA-256 digests of both tokens (constant-time length comparison) */
897 char digest_provided[AUTH_CRYPTO_TOKEN_DIGEST_SIZE];
898 char digest_expected[AUTH_CRYPTO_TOKEN_DIGEST_SIZE];
899
900 if (Auth_Crypto_Token_Digest(provided_token, digest_provided,
901 sizeof(digest_provided)) != AUTH_CRYPTO_OK ||
902 Auth_Crypto_Token_Digest(expected, digest_expected,
903 sizeof(digest_expected)) != AUTH_CRYPTO_OK)
904 {
905 OPENSSL_cleanse(expected, sizeof(expected));
906 OPENSSL_cleanse(digest_provided, sizeof(digest_provided));
907 OPENSSL_cleanse(digest_expected, sizeof(digest_expected));
908 return FALSE;
909 }
910
911 int match = CRYPTO_memcmp(digest_provided, digest_expected,
912 sizeof(digest_provided));
913 OPENSSL_cleanse(expected, sizeof(expected));
914 OPENSSL_cleanse(digest_provided, sizeof(digest_provided));
915 OPENSSL_cleanse(digest_expected, sizeof(digest_expected));
916 return match == 0;
917 } 776 }
918 777
919 /* ------------------------------------------------------------------ */ 778 /* ------------------------------------------------------------------ */
920 /* Public: Auth_API_Verify_CSRF */ 779 /* Public: Auth_API_Verify_CSRF */
921 /* ------------------------------------------------------------------ */ 780 /* ------------------------------------------------------------------ */
922 781
923 boolean Auth_API_Verify_CSRF( 782 boolean Auth_API_Verify_CSRF(
924 Seobeo_Request_Entry *p_request, 783 Seobeo_Request_Entry *p_request,
925 const Auth_Principal *p_principal) 784 const Auth_Principal *p_principal)
926 { 785 {
927 if (!p_request || !p_principal) 786 if (!p_principal)
928 return FALSE; 787 return FALSE;
929 if (!auth_same_origin(p_request)) 788 return Auth_HTTP_Verify_CSRF(
930 return FALSE; 789 p_request, g_cookie_secret, g_cookie_secret_length,
931 const char *csrf = auth_req_value(p_request, "X-CSRF-Token"); 790 p_principal->_binding);
932 if (!csrf || csrf[0] == '\0')
933 return FALSE;
934 return auth_verify_csrf(csrf, p_principal->_binding);
935 } 791 }
936 792
937 /* ------------------------------------------------------------------ */ 793 /* ------------------------------------------------------------------ */
938 /* Route: GET /api/auth/session */ 794 /* Route: GET /api/auth/session */
939 /* ------------------------------------------------------------------ */ 795 /* ------------------------------------------------------------------ */