AACsearch
Architecture

Types de clés et modèle de sécurité

Les quatre catégories de clés AACsearch — search, connector, scoped et admin — leurs préfixes ss_*, leur stockage en hash uniquement, le HMAC + TTL + filtre des scoped tokens et la manière dont chacune est vérifiée à la requête.

AACsearch émet une ligne physique par credential dans SearchApiKey, distinguée par la colonne scopes[]. La chaîne brute de la clé n'est jamais stockée — seul un hash sha256. Les scoped tokens sont une quatrième catégorie dérivée qui vit entièrement dans la chaîne bearer (pas de ligne) et est vérifiée par HMAC.

Les quatre catégories de clés

Description

Le diagramme regroupe les credentials AACsearch en deux buckets : trois types de ligne persistés dans SearchApiKey (admin, ingest/connector, search) — chacun stocké uniquement en hash sha256 avec son propre préfixe et son set de scopes — et un type dérivé de scoped token qui n'est jamais persisté mais signé par HMAC-SHA256 sur BETTER_AUTH_SECRET. Les flèches annotent la surface de déploiement prévue (serveur uniquement, CMS, backend de confiance, widget navigateur) et montrent que les scoped tokens héritent d'une clé search parente.

graph TD
    classDef admin fill:#fde68a,stroke:#b45309,color:#78350f
    classDef ingest fill:#bae6fd,stroke:#0369a1,color:#075985
    classDef search fill:#bbf7d0,stroke:#15803d,color:#14532d
    classDef scoped fill:#ddd6fe,stroke:#6d28d9,color:#4c1d95

    subgraph Persisted["SearchApiKey rows — sha256(raw) only"]
        Admin["Admin key<br/>prefix: ss_search_<br/>scopes: [admin]<br/>full index control"]:::admin
        Ingest["Ingest / Connector key<br/>prefix: ss_connector_ (or ss_search_)<br/>scopes: [connector_write] / [ingest]<br/>writes to SearchSyncOutbox"]:::ingest
        Search["Search key<br/>prefix: ss_search_<br/>scopes: [search]<br/>read-only, origin-restricted"]:::search
    end

    subgraph Derived["Not persisted — verified by HMAC"]
        Scoped["Scoped token<br/>prefix: ss_scoped_<br/>payload: { keyId, parentRawKey, filterBy, exp }<br/>HMAC-SHA256 over BETTER_AUTH_SECRET"]:::scoped
    end

    Search -. parent of .-> Scoped

    UseAdmin["Server-side only<br/>NEVER in browser/CMS"] --- Admin
    UseIngest["CMS connector / sync job"] --- Ingest
    UseSearch["Server-rendered search<br/>(trusted backend)"] --- Search
    UseScoped["Browser widget / per-user filter<br/>(short-lived, embeddable)"] --- Scoped

Flux de vérification par requête

Description

Le diagramme est le flux de vérification par requête : le préfixe oriente soit vers HMAC-vérifier-puis-hacher (pour ss_scoped_*), soit vers hash-et-lookup direct (pour ss_search_* / ss_connector_*), suivi du check de scope, de l'allow-list d'origine, du bucket de rate-limit et enfin de la combinaison AND du filtre tenant avant que le handler ne s'exécute. Toute défaillance court-circuite en 401, 403 ou 429.

flowchart TD
    Req["Request: Authorization: Bearer &lt;token&gt;"]
    Shape{"Prefix?"}
    Scoped["ss_scoped_*"]
    Search["ss_search_*"]
    Connector["ss_connector_*"]
    Reject["401 missing_bearer_token"]

    HMAC["1. base64url decode payload<br/>2. HMAC-SHA256(payload, BETTER_AUTH_SECRET)<br/>3. timingSafeEqual(sig, expected)<br/>4. exp &gt; now()"]
    Hash["sha256(normalizeKeyForHash(raw))"]
    Lookup["SELECT * FROM search_api_key<br/>WHERE hash = ?<br/>AND revokedAt IS NULL<br/>AND deletedAt IS NULL"]
    ScopeCheck{"Required scope<br/>in scopes[]?"}
    OriginCheck{"Origin in<br/>allowedOrigins?"}
    RateLimit{"RateLimitBucket<br/>&le; rateLimitPerMinute?"}
    Filter["Combine: userFilter && tenantId:&lt;org&gt; && (scopedFilter)"]
    Allow["Proceed to handler"]

    Req --> Shape
    Shape --> Scoped --> HMAC --> Hash
    Shape --> Search --> Hash
    Shape --> Connector --> Hash
    Shape -. unknown .-> Reject

    Hash --> Lookup --> ScopeCheck
    ScopeCheck -- no --> Reject
    ScopeCheck -- yes --> OriginCheck
    OriginCheck -- no --> Reject
    OriginCheck -- yes --> RateLimit
    RateLimit -- 429 --> Reject
    RateLimit -- ok --> Filter --> Allow

Règles de stockage

PropriétéAdminIngest / ConnectorSearchScoped
Préfixess_search_*ss_connector_* (ou ss_search_*)ss_search_*ss_scoped_*
Persisté dans SearchApiKey ?OuiOuiOuiNon — dérivé
Stocké commehash sha256hash sha256 (espace partagé)hash sha256Payload signé auto-contenu
Scope requisadminconnector_write / ingestsearchHérite du parent (doit être search)
ExpirationexpiresAt optionnelexpiresAt optionnelexpiresAt optionnelexp obligatoire (≤ 24 h, typ. 15 min)
Filtre tenant appliqué ?n/a (admin)Auto-injecté à l'écritureAuto-injecté à la rechercheAuto-injecté plus filtre scoped
Autorisé dans le navigateur ?JamaisJamaisSeulement origin-lockOui (usage prévu)
RévocationrevokedAtrevokedAtrevokedAtAttendre exp (ou rotation du parent)

Hard invariants imposées

  • #3 Stockage hash-only. Aucun chemin de code ne persiste la clé brute. hashSearchApiKey() est la seule fonction qui touche le secret, et elle renvoie un digest hex.
  • #4 Scoped tokens. Chaque boîte de recherche orientée client utilise un token ss_scoped_*, pas une clé ss_search_* longue durée. HMAC + TTL + filtre est non négociable.
  • #5 Isolation tenant. tenantId:<org> est combiné en AND dans filter_by au niveau de l'adaptateur de recherche, jamais envoyé en en-tête — infalsifiable.

Liens associés

On this page