view mrjunejune/auth_api.h @ 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
line wrap: on
line source

#ifndef MRJUNEJUNE_AUTH_API_H
#define MRJUNEJUNE_AUTH_API_H

#include "dowa/dowa.h"
#include "auth/auth_http.h"
#include "auth/auth_store.h"
#include "seobeo/seobeo.h"

/* Cookie names */
#define AUTH_API_SESSION_COOKIE_NAME AUTH_HTTP_SESSION_COOKIE_NAME
#define AUTH_API_GUEST_COOKIE_NAME   "mjj_guest"

/* Default TTLs (seconds) */
#define AUTH_API_SESSION_IDLE_TTL_DEFAULT  (7  * 24 * 3600)
#define AUTH_API_SESSION_ABS_TTL_DEFAULT   (30 * 24 * 3600)
#define AUTH_API_GUEST_TTL_DEFAULT         (30 * 24 * 3600)

/* Auth-only paths permitted during forced-password-change */
#define AUTH_API_PATH_SESSION       "/api/auth/session"
#define AUTH_API_PATH_LOGIN         "/api/auth/login"
#define AUTH_API_PATH_LOGOUT        "/api/auth/logout"
#define AUTH_API_PATH_PASSWORD      "/api/auth/password"
#define AUTH_API_PATH_PASSWORD_PAGE "/account/password"

typedef enum {
  AUTH_PRINCIPAL_GUEST = 0,
  AUTH_PRINCIPAL_USER  = 1,
} Auth_Principal_Kind;

/*
 * Resolved identity for a single request.
 * For users:  user_id, username, role, must_change_password are valid.
 * For guests: guest_id is valid.
 * csrf_token: a derived CSRF token safe to return to the client (never stored
 *             raw; only its digest appears in the store).
 * _token_digest: internal session binding for CSRF derivation; not for logging.
 */
typedef struct {
  Auth_Principal_Kind kind;

  /* --- user fields --- */
  char    user_id[37];
  char    username[AUTH_STORE_USERNAME_MAX + 1];
  char    role[8];
  boolean must_change_password;

  /* --- guest fields --- */
  char    guest_id[37];

  /* --- common --- */
  char    csrf_token[AUTH_CRYPTO_TOKEN_SIZE]; /* base64url, return to client */

  /* internal: session token digest (user) or guest_id (guest) used as CSRF binding */
  char    _binding[AUTH_CRYPTO_TOKEN_DIGEST_SIZE];
} Auth_Principal;

/*
 * Optional callback that provides guest quota JSON for the session endpoint.
 * Registered by conversation_api on init; called from auth_session_handler.
 * json_out: buffer of json_capacity bytes; write null-terminated JSON or "null".
 * Returns TRUE on success; on FALSE the session response uses "null".
 */
typedef boolean (*Auth_API_Guest_Quota_Cb)(
    const char *guest_id,
    int64       current_unix,
    char       *json_out,
    size_t      json_capacity);

void Auth_API_Register_Guest_Quota_Cb(Auth_API_Guest_Quota_Cb cb);

/*
 * Hook called after a successful login to initiate guest-resource transfer.
 * Called with the logged-out guest_id and the newly authenticated user_id.
 * Must not call any Auth_API function; executes on the request thread.
 *
 * Returns TRUE on success.  On FALSE the login handler revokes the new
 * session and returns 500; the guest cookie is preserved.
 * The hook must be idempotent: it may be called more than once for the
 * same (guest_id, user_id) pair during retries.
 */
typedef boolean (*Auth_Guest_Transfer_Hook)(
    const char *guest_id,
    const char *user_id,
    void       *context);

/*
 * Initialise the auth module.
 *
 * cookie_secret       must be at least AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES.
 * bootstrap_username  / bootstrap_password_hash: create bootstrap admin on
 *   first startup only when no admin exists; pass NULL to skip.
 * trusted_proxy_ip    exact direct peer IP that may forward X-Real-IP; NULL
 *   to disable proxy trust.
 * dev_insecure_cookie TRUE allows non-Secure cookies; only valid on loopback.
 *
 * Returns FALSE and fails closed if cookie_secret is missing/too short.
 */
boolean Auth_API_Init(
    const char *database_path,
    const uint8 *cookie_secret,
    size_t       cookie_secret_length,
    const char  *bootstrap_username,
    const char  *bootstrap_password_hash,
    const char  *trusted_proxy_ip,
    int64        session_idle_ttl_secs,
    int64        session_absolute_ttl_secs,
    int64        guest_ttl_secs,
    boolean      dev_insecure_cookie);

void Auth_API_Destroy(void);
void Auth_API_Register_Routes(void);

/*
 * Register a hook for guest-to-user resource transfer on login.
 * Only one hook is supported; a second call replaces the previous one.
 */
void Auth_API_Register_Guest_Transfer_Hook(
    Auth_Guest_Transfer_Hook hook,
    void                    *context);

/*
 * Resolve the caller's identity from request cookies.
 * Creates a guest identity if no valid session or guest cookie is found.
 * new_guest_cookie_out: if non-NULL and non-empty on return, the caller
 *   should include a Set-Cookie header with this value in the response.
 * Returns TRUE on success; FALSE only on internal error (treat as 500).
 */
boolean Auth_API_Resolve_Principal(
    Seobeo_Request_Entry *p_request,
    Auth_Principal       *p_principal,
    Dowa_Arena           *p_arena,
    char                 *new_guest_cookie_out,
    size_t                new_guest_cookie_capacity);

/*
 * Resolve identity from request cookies WITHOUT creating a new guest.
 * Returns TRUE on success (no internal error):
 *   - If an existing user session or valid guest cookie is found,
 *     p_principal is filled and *p_found is set to TRUE.
 *   - If no valid session/guest is found, *p_found is set to FALSE;
 *     the caller must return HTTP 401.
 * Returns FALSE on internal error (treat as 500).
 * Never writes a guest identity row or generates a Set-Cookie directive.
 */
boolean Auth_API_Resolve_Existing_Principal(
    Seobeo_Request_Entry *p_request,
    Auth_Principal       *p_principal,
    Dowa_Arena           *p_arena,
    boolean              *p_found);

/*
 * Returns TRUE if the path is permitted for forced-password-change sessions
 * (i.e., the principal should NOT be blocked at this path).
 * Conversation and admin code gate their routes with:
 *   if (principal.must_change_password &&
 *       !Auth_API_Is_Forced_Password_Change_Only(path)) { return 403; }
 */
boolean Auth_API_Is_Forced_Password_Change_Only(const char *http_path);

/*
 * Verify same-origin AND CSRF for state-changing routes.
 *
 * Enforces:
 *   1. The Origin header matches the Host header (same-origin).
 *   2. The X-CSRF-Token request header is present and matches the token
 *      derived from the principal's session binding.
 *
 * Use this as the single centralized CSRF gate.  Do not duplicate the
 * origin-check or CSRF-derivation logic in other modules.
 *
 * Returns TRUE on success; the caller MUST return HTTP 403 on FALSE.
 */
boolean Auth_API_Verify_CSRF(
    Seobeo_Request_Entry *p_request,
    const Auth_Principal *p_principal);

/*
 * Returns the initialized auth store pointer.
 * Valid only after Auth_API_Init returns TRUE; NULL before that.
 * Admin API uses this to issue store operations directly.
 */
Auth_Store *Auth_API_Get_Store(void);

#ifdef AUTH_API_TEST_HOOKS
typedef void (*Auth_API_Test_Login_Pre_Create_Hook)(void *p_context);

void Auth_API_Test_Set_Login_Pre_Create_Hook(
    Auth_API_Test_Login_Pre_Create_Hook hook,
    void                               *p_context);

/*
 * Direct handler entry-points for in-process testing.
 * Only available when AUTH_API_TEST_HOOKS is defined (test builds).
 */
Seobeo_Request_Entry *Auth_API_Test_Session_Handler(
    Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena);
Seobeo_Request_Entry *Auth_API_Test_Login_Handler(
    Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena);
Seobeo_Request_Entry *Auth_API_Test_Logout_Handler(
    Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena);
Seobeo_Request_Entry *Auth_API_Test_Password_Handler(
    Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena);
#endif /* AUTH_API_TEST_HOOKS */

#endif /* MRJUNEJUNE_AUTH_API_H */