AACsearch
Architektur

Schlüsseltypen & Sicherheitsmodell

Die vier AACsearch-Schlüsselkategorien — search, connector, scoped und admin — ihre ss_*-Präfixe, ausschließlich gehashte Speicherung, Scoped-Token HMAC + TTL + Filter und wie sie zur Request-Zeit verifiziert werden.

AACsearch gibt eine physische Zeile pro Credential in SearchApiKey aus, unterschieden durch die Spalte scopes[]. Der Roh-Schlüssel-String wird nie gespeichert — nur ein sha256-Hash. Scoped-Tokens sind eine vierte, abgeleitete Credential-Art, die vollständig im Bearer-String lebt (keine Zeile) und per HMAC verifiziert wird.

Die vier Schlüsselkategorien

Beschreibung

Das Diagramm gruppiert AACsearch-Credentials in zwei Buckets: drei persistierte Zeilentypen in SearchApiKey (admin, ingest/connector, search) — jeder nur als sha256-Hash mit eigenem Prefix und Scope-Set gespeichert — sowie einen abgeleiteten Scoped-Token-Typ, der niemals persistiert, aber per HMAC-SHA256 über BETTER_AUTH_SECRET signiert wird. Pfeile annotieren die vorgesehene Einsatzoberfläche (nur Server, CMS, vertrauenswürdiges Backend, Browser-Widget) und zeigen, dass Scoped-Tokens von einem Parent-Search-Key erben.

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

Verifikationsfluss pro Request

Beschreibung

Das Diagramm zeigt den Verifikationsfluss pro Request: Das Prefix entscheidet zwischen HMAC-prüfen-und-dann-hashen (für ss_scoped_*) oder direktem Hash-und-Lookup (für ss_search_* / ss_connector_*), gefolgt von Scope-Check, Origin-Allow-List, Rate-Limit-Bucket und schließlich AND-Kombination des Tenant-Filters, bevor der Handler läuft. Jeder Fehler springt sofort auf 401, 403 oder 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

Speicherregeln

EigenschaftAdminIngest / ConnectorSearchScoped
Präfixss_search_*ss_connector_* (oder ss_search_*)ss_search_*ss_scoped_*
In SearchApiKey persistiert?JaJaJaNein — abgeleitet
Gespeichert alssha256-Hashsha256-Hash (geteilter Raum)sha256-HashSelbst-enthaltene signierte Payload
Erforderlicher Scopeadminconnector_write / ingestsearchErbt vom Parent (muss search sein)
AblaufOptional expiresAtOptional expiresAtOptional expiresAtPflicht-exp (≤ 24 h, typ. 15 min)
Mandantenfilter angewendet?n/a (admin)Beim Write automatischBei der Suche automatischAutomatisch plus Scoped-Filter
Im Browser erlaubt?NiemalsNiemalsNur origin-gebundenJa (Verwendungszweck)
WiderrufrevokedAtrevokedAtrevokedAtAuf exp warten (oder Parent rotieren)

Hard Invariants, die das durchsetzt

  • #3 Hash-Only-Speicherung. Kein Codepfad persistiert den Roh-Schlüssel. hashSearchApiKey() ist die einzige Funktion, die das Secret berührt, und sie gibt einen Hex-Digest zurück.
  • #4 Scoped-Tokens. Jede kundenseitige Suchbox verwendet ein ss_scoped_*-Token, keinen langlebigen ss_search_*-Schlüssel. HMAC + TTL + Filter ist nicht verhandelbar.
  • #5 Mandanten-Isolation. tenantId:<org> wird auf Adapter-Ebene per AND in filter_by kombiniert, niemals als Header gesendet — fälschungssicher.

Verwandt

  • Lesepfad — wie die Verifikationskette in der Request-Pipeline sitzt.
  • Schreibpfad — wie Ingest-Schlüssel die Outbox speisen.

On this page