Gå til hovedindhold

Agent-native autentificering

fremforge accepterer tre mønstre for OAuth2/OIDC-autentificering af ikke-menneskelige kaldere (agenter, scripts, AI-assistenter, CI-runners, integrationer fra leverandører). Alle tre bygger på den samme scope-tilladelsesliste pr. tenant og den samme revisionskæde som brugerlogin: ingen skyggevej og intet separat tillidslager.

Den fælles forudsætning for alle flows, der involverer kundens IdP: registrér din IdP som en OIDC-autentificeringskilde under Administration → SSO → Autentificeringskilder, verificér domænet, og konfigurér scope-tilladelseslister pr. app på kildens redigeringsside under Exchangeable scopes. Den samme registrering driver føderation af menneskeligt login, så en kunde, der bruger OIDC-SSO, allerede har fundamentet på plads.

Note om implementeringen: Endpointene /_app/api/v1/auth/token-exchange og /_app/api/v1/oauth/token leveres af fremforges kontrolplan. De er ikke indbyggede Forgejo-endpoints. SDK-udviklere skal ramme https://frem.sh/_app/api/v1/* præcis som dokumenteret.

De tre flows i overblik

FlowSpecifikationKundens IdP’s rolleBedst til
1. Direkte JWT-bearer— (uformel, “Bearer JWT”)Udsteder JWT’en; fremforge verificererWorkload-identiteter (Entra workload identity, Okta-servicekonto, Google-servicekonto), der allerede udsteder tenant-bundne JWT’er
2. Tokenudveksling (RFC 8693)RFC 8693Udsteder subject_token (+ valgfrit actor_token til OBO)CI-workflows, planlagte agenter og on-behalf-of-flows (OBO), hvor revisionen skal navngive personen og den agent, der handler for vedkommende
3. client_credentials (RFC 6749 §4.4)RFC 6749Ingen; fremforge udsteder både client_id og client_secretApps, der slet ikke har en IdP, eller hvor kunden foretrækker fremforge som IdP

Alle tre flows giver et kortlivet (≤ 1 time) ffp_-adgangstoken, der er bundet til én tenant og ét sæt scopes. RBAC, rate limiting, skrivning til revisionsloggen og tenant-isolering fungerer præcis som for en PAT, et menneske har oprettet.

Flow 1: Direkte JWT fra kundens IdP som bearer

Agenten sender sin IdP-udstedte JWT direkte som Authorization: Bearer <jwt> til et hvilket som helst /api/v1/*-endpoint. fremforge verificerer JWT’en mod din registrerede autentificeringskilde, anvender fællesmængden af scopes fra kildens app-tildeling og returnerer svaret: ingen ekstra rundtur og ingen gemt legitimationsoplysning.

Endpoint: enhver frem.sh/_app/api/v1/*-route.

Opsætning (én gang pr. autentificeringskilde):

  1. Administration → SSO → registrér en OIDC-kilde for din IdP (eller genbrug en, der allerede bruges til menneskeligt login).
  2. Konfigurér scope-tilladelseslister pr. app under Edit auth source → App grants. Tilladelseslisten binder en IdP-applikation (matchet på JWT’ens azp- eller aud[0]-claim) til et maksimalt sæt scopes.
  3. Administration → Programmatisk adgang → Administrér JWT-bearer og udveksling → slå Tillad direkte JWT-bearer til på kilden.
  4. (Kun multi-tenant-IdP’er) Indtast en claim-assertion, der binder kilden til din tenant: {"tid": "<your-Entra-tenant-guid>"} for Entra /common/, {"hd": "yourdomain.com"} for Google Workspace osv. Administrationsformularen nægter at slå direkte bearer til på en multi-tenant-udsteder-URL uden claim-assertions.

Krav til tokenet:

  • iss matcher den registrerede kilde-URL (normaliseret uden hensyn til store og små bogstaver og med tolerance for en afsluttende skråstreg).
  • aud matcher enten kildens konfigurerede audience-tilsidesættelse ELLER den kanoniske audience pr. tenant https://frem.sh/<your-tenant-slug>.
  • exp ligger i fremtiden, og iat ligger ikke urimeligt langt tilbage (jose håndhæver 30 sekunders tidsforskydning).
  • Alle claim_assertions, der er konfigureret på kilden, matcher den verificerede payload.
  • Signeringsnøglen findes via OIDC discovery + JWKS (cachet i 10 minutter og styret af kunden gennem IdP’ens normale nøglerotation).

Eksempel (Entra workload identity):

# 1. Acquire a JWT from your IdP (managed identity, federated identity,
#    or workload-identity SDK) with aud=https://frem.sh/<your-slug>
ACCESS_TOKEN=$(az account get-access-token \
  --resource https://frem.sh/acme \
  --query accessToken -o tsv)

# 2. Call any fremforge API directly
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
  https://frem.sh/_app/api/v1/orgs/acme/repos

Sikkerhedsegenskaber:

  • JWT’en er legitimationsoplysningen. Der oprettes ingen PAT-række i fremforges database. Tilbagekaldelse = lad exp udløbe, ELLER fjern IdP-kilden / slå direkte bearer fra (næste request afvises).
  • Binding på tværs af tenants er umulig i kraft af designet: en audience pr. tenant binder JWT’en til én slug, og tabellen auth_sources har en UNIQUE-begrænsning på databaseniveau, der forhindrer to tenants i at registrere den samme udsteder til direkte bearer.
  • Loftet over scopes kommer fra kildens app-tildeling. Scopes, der flytter penge (billing:write, sso:write, policy:write, orgs:write), kræver udtrykkelige poster i tilladelseslisten pr. app. De gives ALDRIG af en NULL-tilladelsesliste.

Flow 2: Tokenudveksling efter RFC 8693 (med OBO)

Agenten sender en IdP-udstedt JWT som subject_token og udveksler den til en kortlivet fremforge-PAT (ffp_). Parameteren actor_token giver on-behalf-of-delegering: agenten handler for en navngiven person. fremforge har ingen agentidentiteter: personen er aktøren i revisionsloggen, og agenten er en detalje i hver række.

Endpoint: POST frem.sh/_app/api/v1/auth/token-exchange.

Eksempel (almindelig udveksling):

curl -X POST https://frem.sh/_app/api/v1/auth/token-exchange \
  -H "Content-Type: application/json" \
  -d '{
    "subject_token": "<customer-idp-jwt>",
    "tenant_slug": "acme",
    "scopes": ["orgs:read", "findings:write"],
    "name": "ci-build-bot"
  }'

Eksempel (OBO: agenten handler på vegne af Alice):

curl -X POST https://frem.sh/_app/api/v1/auth/token-exchange \
  -H "Content-Type: application/json" \
  -d '{
    "subject_token": "<alice-idp-jwt>",
    "actor_token":   "<agent-idp-jwt>",
    "tenant_slug": "acme",
    "scopes": ["findings:read", "audit:read"]
  }'

Bed kun om scopes, som tokenudveksling kan give: det udvekslelige sæt (orgs:read, orgs:write, audit:read, exports:read, exports:write, findings:read, findings:write, components:read, deployments:report, runners:read, runners:write, sboms:write, seats:read, seats:write, billing:read, billing:write, sso:read, sso:write, policy:read, policy:write), indsnævret af app-tildelingen på din autentificeringskilde. Et scope uden for et af dem droppes, og en request, hvor der intet er tilbage, afvises med no-scopes-grantable. Adgang til repositorier og issues er ikke en del af fremforge-API’ets scopes og kan derfor ikke udveksles.

Hver revisionshændelse, som den udstedte PAT skriver, navngiver Alice som actor, som oidc:idp:<alice-sub> (den verificerede sub fra subject-tokenet). Agenten er en detalje i rækken: fields.agent er oidc:agent:<agent-sub> (den verificerede sub fra actor-tokenet), og fields.via_token er id’et på den udstedte PAT. Selve udvekslingen registreres som api.token.exchange.obo, også med Alice som aktør og agenten i fields.agent. Begge værdier kommer udelukkende fra de verificerede JWT’er; intet i request-body’en kan navngive eller ændre personen.

Uden et actor_token navngiver en udveksling med kundens IdP stadig personen som aktør, med via_token og uden agent. En udveksling med runner-OIDC har ingen person (et CI-job er ikke en person), så dens hændelser beholder actor=token:<id>.

Compliance-forespørgsler som “alle handlinger, min Anthropic-agent udførte på vegne af Alice i sidste kvartal” filtrerer revisionsloggen på Alice og læser fields.agent. Aktørfilteret matcher aktøren, kolonnen on_behalf_of og fields.agent, så et filter på oidc:agent:<agent-sub> finder alt, hvad den agent har gjort, for enhver person. I administrationens revisionslog matcher filteret en delstreng, og sådanne rækker har en via agent-chip; GET /orgs/{org}/audit?actor= matcher præcist. Hændelser skrevet før 2026-10-23 navngav agenten som aktør og Alice i on_behalf_of. Hashkæden dækker dem, så de beholder den form, vises som på vegne af Alice, og de samme filtre finder dem.

Forskellen fra flow 1: tokenudveksling opretter en gemt PAT (så den kan have en udtrykkelig delmængde af scopes og et selvvalgt navn). Flow 1 er flygtigt. Vælg udveksling, når agenten skal foretage mange kald i løbet af én workflow-kørsel; vælg direkte bearer, når hvert kald er uafhængigt.

Flow 3: client_credentials (fremforge som IdP)

Til apps uden en IdP, eller hvor kunden foretrækker ét sæt legitimationsoplysninger, der administreres i fremforge. Tenant-administratoren opretter et (client_id, client_secret)-par, appen autentificerer sig med Authorization: Basic (eller en POST-formular) og modtager en kortlivet ffp_-PAT.

Opsætning:

  1. Administration → Programmatisk adgang → Administrér JWT-bearer og udveksling → Opret ny klient (under afsnittet client_credentials).
  2. Vælg et navn, et loft over scopes og eventuelt en tilladelsesliste for audience.
  3. Kopiér client_secret med det samme: den vises én gang og aldrig igen.

Endpoint: POST frem.sh/_app/api/v1/oauth/token.

Eksempel:

curl -X POST https://frem.sh/_app/api/v1/oauth/token \
  -u "ffc_<client-id>:ffs_<client-secret>" \
  -d "grant_type=client_credentials" \
  -d "scope=findings:write runners:read"

Svar:

{
  "access_token": "ffp_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "findings:write runners:read",
  "audience": "https://frem.sh/acme"
}

Rotation: tryk når som helst på knappen Rotér i administrationen. Den nuværende secret forbliver gyldig sammen med den nye i 24 timer, så du kan skifte din konfiguration uden nedetid. Efter de 24 timer autentificerer kun den nye secret.

Tilbagekaldelse: et klik på Tilbagekald gør klienten ugyldig OG tilbagekalder i samme transaktion alle ffp_-tokens, den nogensinde har udstedt. Det er fejlsikkert, selv hvis operatørens pod bliver dræbt midt i tilbagekaldelsen.

Hvad de tre flows har til fælles

EgenskabSådan håndhæves den
Tenant-isoleringHvert udstedt token har tenant_id sat; requireTenantBinding afviser kald på tværs af tenants med 404. En UNIQUE-begrænsning på databaseniveau forhindrer, at to tenants gør krav på samme udsteder til direkte bearer.
Loft over scopesAlle tre tager fællesmængden med et udvekslingsbart sæt af scopes, som operatøren styrer, OG kundens tilladelsesliste pr. IdP / pr. klient. Scopes, der flytter penge (billing:write, sso:write, policy:write, orgs:write), kræver udtrykkelige poster i tilladelseslisten og gives aldrig som standard.
TTLAlle udstedte tokens har et loft på 1 time. Kunder, der ønsker legitimationsoplysninger med længere levetid, bruger udstedelsen i administrationen (personlige adgangstokens).
RevisionskædeHver udstedelse skriver en audit_event med actor, handling og scopes. Ved en udveksling med kundens IdP er aktøren personen, og agenten (ved OBO) står i fields.agent; hver senere hændelse, tokenet skriver, har personen som aktør plus agent og via_token. Kædens hash dækker fields_json, inklusive begge.
Rate limitingToken-endpointene er rate-begrænset pr. IP. /auth/token-exchange og /oauth/token deler et loft på 30/min for at begrænse CPU-omkostningen ved JWT-verificering af falske input.
Adgang til JWKSAlle udgående hentninger af JWKS og discovery går gennem en begrænset egress-gateway: SSRF, private IP’er og cloud-metadata blokeres på netværksniveau. JWKS caches i 10 min.
EU-residensPipelinen rammer aldrig et Sigstore-, Auth0- eller Okta-endpoint i USA; verificeringen foregår udelukkende inden for fremforges T Cloud Public eu-de-fodaftryk. Egress til kundens IdP går kun gennem den SSRF-beskyttede proxy.

Trusselsmodel kort fortalt

TrusselAfbødning
Kunden registrerer en multi-tenant Entra /common/-URL uden en tid-claim_assertionAdministrationsformularen afviser; hjælpefunktionen isMultiTenantIssuerUrl() markerer Entra (v1 + v2), Google Workspace og Microsofts Gov-/Kina-clouds
To tenants deler en IdP-iss-URLDelvist UNIQUE-indeks på (LOWER(RTRIM(iss,'/'))) WHERE direkte bearer er slået til; den anden registrering afvises på databaseniveau
En JWT fra kundens IdP genbruges mod den forkerte tenantAudience pr. tenant https://frem.sh/<slug>: et token udstedt til aud=https://frem.sh/acme kan bogstaveligt talt ikke opfylde tjekket af tenanten initechs audience
Lækket client_secretTenant-administratoren kører Tilbagekald → én transaktion dræber klienten OG hver PAT, den har udstedt; rotation giver et vindue på 24 timer med to secrets, så skiftet kan ske uden nedetid
Forfalskede claims i OBO-actor_tokenjose verificerer signatur + iss + aud mod den samme kilde som subject; sub kan ikke ændres, og email/name stoles der aldrig på
Forældet JWKS-cache efter nøglerotation hos IdP’enFast TTL på 10 min: begrænset i drift
Netværksinternt værtsnavn i IdP’ens discovery-dokument (DNS rebind / SSRF)Den begrænsede egress-gateway blokerer hentningen; et https://-værn i koden giver et ekstra forsvarslag

Hurtigt opslag

StiMetodeAutentificeringTTLBody
/_app/api/v1/auth/token-exchangePOSTJWT’en ER autentificeringen1tsubject_token (+ valgfrit actor_token), tenant_slug, scopes, name
/_app/api/v1/oauth/tokenPOSTBasic ELLER formular-body1tgrant_type=client_credentials, client_id, client_secret, scope, audience
/api/v1/* (alle)alleAuthorization: Bearer <jwt or ffp_*>efter exp / efter PAT-rækken—

Relateret

  • SSO-opsætning (OIDC): registrér din IdP til både menneskeligt login og agent-autentificering
  • Workflow-OIDC til cloud-autentificering: den udgående retning (runner-VM → tillidspolitikker i AWS/GCP/Azure)
  • Revisionslog og kæde: verificér, hvad hver agent har gjort, ved at filtrere på personen eller på agenten
  • API-tokens (PAT’er): langlivede tokens, som mennesker opretter, til den samme /api/v1/*-flade; opret dem under Brugerindstillinger → API-tokens, og se Godkendelsespolitik for maksimal levetid og rotationspolitik