view auth/auth_http.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
children
line wrap: on
line source

#include "auth/auth_http.h"

#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>

#include <string.h>

#define AUTH_HTTP_COOKIE_VALUE_MAX 512
#define AUTH_HTTP_CSRF_SUFFIX ":csrf:v1"
#define AUTH_HTTP_BINDING_INPUT_MAX (AUTH_CRYPTO_TOKEN_DIGEST_SIZE + 16)

const char *Auth_HTTP_Request_Value(
    Seobeo_Request_Entry *p_request,
    const char           *key)
{
  if (!p_request || !key)
    return NULL;
  void *p_value = Dowa_HashMap_Get_Ptr(p_request, (char *)key);
  return p_value ? ((Seobeo_Request_Entry *)p_value)->value : NULL;
}

boolean Auth_HTTP_Parse_Cookie(
    const char *cookie_header,
    const char *name,
    char       *value_out,
    size_t      capacity)
{
  if (!value_out || capacity == 0)
    return FALSE;
  value_out[0] = '\0';
  if (!cookie_header || !name || name[0] == '\0')
    return FALSE;
  size_t name_length = strlen(name);
  const char *cursor = cookie_header;

  while (*cursor)
  {
    while (*cursor == ' ' || *cursor == '\t')
      cursor++;

    if (strncmp(cursor, name, name_length) == 0 &&
        cursor[name_length] == '=')
    {
      cursor += name_length + 1;
      const char *start = cursor;
      while (*cursor && *cursor != ';')
        cursor++;
      size_t value_length = (size_t)(cursor - start);
      if (value_length >= capacity)
        return FALSE;
      memcpy(value_out, start, value_length);
      value_out[value_length] = '\0';
      return TRUE;
    }

    while (*cursor && *cursor != ';')
      cursor++;
    if (*cursor == ';')
      cursor++;
  }
  return FALSE;
}

boolean Auth_HTTP_Same_Origin(Seobeo_Request_Entry *p_request)
{
  const char *host = Auth_HTTP_Request_Value(p_request, "Host");
  const char *origin = Auth_HTTP_Request_Value(p_request, "Origin");
  if (!host || !origin)
    return FALSE;

  const char *origin_host = strstr(origin, "://");
  if (!origin_host)
    return FALSE;
  origin_host += 3;

  const char *end = strchr(origin_host, '/');
  size_t origin_host_length =
      end ? (size_t)(end - origin_host) : strlen(origin_host);
  return strlen(host) == origin_host_length &&
         strncmp(host, origin_host, origin_host_length) == 0;
}

boolean Auth_HTTP_Derive_CSRF(
    const uint8 *cookie_secret,
    size_t       cookie_secret_length,
    const char  *binding,
    char        *csrf_out,
    size_t       csrf_capacity)
{
  if (!cookie_secret ||
      cookie_secret_length < AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES ||
      cookie_secret_length > AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES ||
      !binding || !csrf_out || csrf_capacity < AUTH_CRYPTO_TOKEN_SIZE)
    return FALSE;

  size_t binding_length = strlen(binding);
  size_t suffix_length = strlen(AUTH_HTTP_CSRF_SUFFIX);
  size_t input_length = binding_length + suffix_length;
  if (input_length >= AUTH_HTTP_BINDING_INPUT_MAX)
    return FALSE;

  char input[AUTH_HTTP_BINDING_INPUT_MAX] = {0};
  memcpy(input, binding, binding_length);
  memcpy(input + binding_length, AUTH_HTTP_CSRF_SUFFIX, suffix_length);

  uint8 digest[32] = {0};
  uint32 digest_length = sizeof(digest);
  if (!HMAC(EVP_sha256(), cookie_secret, (int)cookie_secret_length,
            (const uint8 *)input, input_length, digest, &digest_length))
  {
    OPENSSL_cleanse(input, sizeof(input));
    OPENSSL_cleanse(digest, sizeof(digest));
    return FALSE;
  }
  OPENSSL_cleanse(input, sizeof(input));

  size_t encoded_length = Auth_Crypto_Base64url_Encode(
      digest, sizeof(digest), csrf_out, csrf_capacity);
  OPENSSL_cleanse(digest, sizeof(digest));
  return encoded_length == AUTH_CRYPTO_TOKEN_SIZE - 1;
}

boolean Auth_HTTP_Verify_CSRF(
    Seobeo_Request_Entry *p_request,
    const uint8          *cookie_secret,
    size_t                cookie_secret_length,
    const char           *binding)
{
  if (!Auth_HTTP_Same_Origin(p_request) || !binding)
    return FALSE;

  const char *provided = Auth_HTTP_Request_Value(p_request, "X-CSRF-Token");
  return Auth_HTTP_Verify_CSRF_Token(
      cookie_secret, cookie_secret_length, binding, provided);
}

boolean Auth_HTTP_Verify_CSRF_Token(
    const uint8 *cookie_secret,
    size_t       cookie_secret_length,
    const char  *binding,
    const char  *provided_token)
{
  if (!binding || !provided_token || provided_token[0] == '\0')
    return FALSE;
  char expected[AUTH_CRYPTO_TOKEN_SIZE] = {0};
  char provided_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE] = {0};
  char expected_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE] = {0};
  boolean valid = FALSE;

  if (Auth_HTTP_Derive_CSRF(
          cookie_secret, cookie_secret_length, binding,
          expected, sizeof(expected)) &&
      Auth_Crypto_Token_Digest(
          provided_token, provided_digest, sizeof(provided_digest)) ==
          AUTH_CRYPTO_OK &&
      Auth_Crypto_Token_Digest(
          expected, expected_digest, sizeof(expected_digest)) ==
          AUTH_CRYPTO_OK)
  {
    valid = CRYPTO_memcmp(
        provided_digest, expected_digest, sizeof(provided_digest)) == 0;
  }

  OPENSSL_cleanse(expected, sizeof(expected));
  OPENSSL_cleanse(provided_digest, sizeof(provided_digest));
  OPENSSL_cleanse(expected_digest, sizeof(expected_digest));
  return valid;
}

Auth_HTTP_Resolve_Result Auth_HTTP_Resolve_Authenticated_User(
    Seobeo_Request_Entry         *p_request,
    Auth_Store                   *p_store,
    const uint8                  *cookie_secret,
    size_t                        cookie_secret_length,
    int64                         current_unix,
    int64                         session_idle_ttl_secs,
    Auth_HTTP_Authenticated_User *p_user)
{
  if (!p_user)
    return AUTH_HTTP_RESOLVE_ERROR;
  memset(p_user, 0, sizeof(*p_user));
  if (!p_request || !p_store || !cookie_secret ||
      cookie_secret_length < AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES ||
      cookie_secret_length > AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES ||
      current_unix < 0 || session_idle_ttl_secs <= 0)
    return AUTH_HTTP_RESOLVE_ERROR;

  char session_token[AUTH_HTTP_COOKIE_VALUE_MAX] = {0};
  const char *cookie_header = Auth_HTTP_Request_Value(p_request, "Cookie");
  if (!cookie_header ||
      !Auth_HTTP_Parse_Cookie(
          cookie_header, AUTH_HTTP_SESSION_COOKIE_NAME,
          session_token, sizeof(session_token)) ||
      session_token[0] == '\0')
  {
    OPENSSL_cleanse(session_token, sizeof(session_token));
    return AUTH_HTTP_RESOLVE_NOT_FOUND;
  }

  if (Auth_Crypto_Token_Digest(
          session_token, p_user->token_digest,
          sizeof(p_user->token_digest)) != AUTH_CRYPTO_OK)
  {
    OPENSSL_cleanse(session_token, sizeof(session_token));
    OPENSSL_cleanse(p_user, sizeof(*p_user));
    return AUTH_HTTP_RESOLVE_NOT_FOUND;
  }
  OPENSSL_cleanse(session_token, sizeof(session_token));

  Auth_Store_Result result = Auth_Store_Find_Session(
      p_store, p_user->token_digest, current_unix,
      &p_user->session, &p_user->user);
  if (result != AUTH_STORE_OK)
  {
    OPENSSL_cleanse(p_user, sizeof(*p_user));
    return result == AUTH_STORE_ERROR
        ? AUTH_HTTP_RESOLVE_ERROR
        : AUTH_HTTP_RESOLVE_NOT_FOUND;
  }

  Auth_Store_Touch_Session(
      p_store, p_user->token_digest, current_unix, session_idle_ttl_secs);
  if (!Auth_HTTP_Derive_CSRF(
          cookie_secret, cookie_secret_length, p_user->token_digest,
          p_user->csrf_token, sizeof(p_user->csrf_token)))
  {
    OPENSSL_cleanse(p_user, sizeof(*p_user));
    return AUTH_HTTP_RESOLVE_ERROR;
  }
  return AUTH_HTTP_RESOLVE_OK;
}