Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →
Agent-native authentication

Agent-native authentication

fremforge accepts three OAuth2 / OIDC authentication patterns for non-human callers (agents, scripts, AI assistants, CI runners, vendor integrations). All three sit on the same per-tenant scope-allowlist and audit-chain machinery as user sign-in — no shadow path, no separate trust store.

The shared pre-condition for any flow that involves the customer’s IdP: register your IdP as an OIDC auth source at Org admin → SSO → Auth sources, verify the domain, and configure per-app scope allowlists at Org admin → SSO → Edit auth source → Exchangeable scopes. The same registration powers human sign-in federation, so a customer using OIDC SSO already has the foundation in place.

The three flows at a glance

FlowSpecCustomer IdP roleBest for
1. Direct JWT bearer— (informal, “Bearer JWT”)Issues the JWT; fremforge verifiesWorkload identities (Entra workload identity, Okta service account, Google service account) that already mint tenant-bound JWTs
2. Token exchange (RFC 8693)RFC 8693Issues subject_token (+ optional actor_token for OBO)CI workflows, scheduled agents, on-behalf-of (OBO) flows where audit must record both the agent and the user
3. client_credentials (RFC 6749 §4.4)RFC 6749None — fremforge issues both client_id + client_secretApps that don’t have an IdP at all, or where the customer prefers fremforge-as-IdP

All three flows produce a short-lived (≤ 1 hour) ffp_ access token bound to one tenant and one scope set. RBAC, rate-limiting, audit emission, and tenant-isolation gates are identical to a human-minted PAT.

Flow 1 — Direct customer-IdP JWT as bearer

The agent presents its IdP-issued JWT directly as Authorization: Bearer <jwt> on any /api/v1/* endpoint. fremforge verifies the JWT against your registered auth source, applies scope intersection from the source’s app-grant, and returns the response — no extra round-trip, no stored credential.

Endpoint: any frem.sh/api/v1/* route.

Setup (one-time, per auth source):

  1. Org admin → SSO → register an OIDC source for your IdP (or reuse an existing one used for human sign-in).
  2. Configure per-app scope allowlists at Edit auth source → App grants. The allowlist binds an IdP application (matched on the JWT’s azp or aud[0] claim) to a maximum scope set.
  3. Org admin → Agent authentication → enable Allow direct JWT bearer on the source.
  4. (Multi-tenant IdPs only) Enter a claim assertion that binds the source to your tenant — {"tid": "<your-Entra-tenant-guid>"} for Entra /common/, {"hd": "yourdomain.com"} for Google Workspace, etc. The admin form refuses to enable direct bearer on a multi-tenant issuer URL without claim assertions.

Token requirements:

  • iss matches the registered source URL (normalised case-insensitive, trailing-slash-tolerant).
  • aud matches either the source’s configured Audience override OR the canonical per-tenant audience https://frem.sh/<your-tenant-slug>.
  • exp in the future, iat not unreasonably in the past (jose enforces 30-second clock skew).
  • All claim_assertions configured on the source match the verified payload.
  • Signing key resolves via OIDC discovery + JWKS (cached 10 minutes, customer-controllable through standard IdP key-rotation).

Example (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/api/v1/orgs/acme/repos

Security properties:

  • The JWT is the credential. No PAT row created in fremforge’s DB. Revocation = let exp elapse, OR remove the IdP source / disable direct-bearer (next request is rejected).
  • Cross-tenant binding is impossible by construction: a per-tenant audience binds the JWT to one slug, and the auth_sources table enforces a database-level UNIQUE constraint preventing two tenants from registering the same issuer for direct bearer.
  • Scope-cap from the source’s per-app grant. Money-moving scopes (billing:write, sso:write, policy:write, orgs:write) require explicit per-app allowlist entries — they are NEVER granted by a NULL allowlist.

Flow 2 — RFC 8693 token exchange (with OBO)

The agent presents an IdP-issued JWT as the subject_token and exchanges it for a short-lived fremforge PAT (ffp_). The actor_token parameter enables on-behalf-of delegation — the agent acts for a named user, and the audit chain records both principals.

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

Example (basic exchange):

curl -X POST https://frem.sh/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"
  }'

Example (OBO — agent acts on behalf of Alice):

curl -X POST https://frem.sh/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": ["repos:read", "issues:write"]
  }'

The minted PAT’s audit events record actor=oidc:agent:<agent-sub> and on_behalf_of=<alice-sub>. Compliance queries like “every action my Anthropic agent took on behalf of Alice last quarter” run against the indexed on_behalf_of column.

Difference from Flow 1: token exchange creates a stored PAT (so it can carry an explicit scope subset and a custom name). Flow 1 is ephemeral. Pick exchange when the agent will do many calls over a single workflow run; pick direct bearer when each call is independent.

Flow 3 — client_credentials (fremforge as IdP)

For apps without an IdP, or where the customer prefers a single credential set managed inside fremforge. Tenant admin mints a (client_id, client_secret) pair, the app authenticates with Authorization: Basic (or POST form), receives a short-lived ffp_ PAT.

Setup:

  1. Org admin → Agent authentication → Create new client.
  2. Pick a name, scope-cap, optional audience allowlist.
  3. Copy the client_secret immediately — it is shown once and never displayed again.

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

Example:

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

Response:

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

Rotation: at any time, hit the Rotate button in the admin UI. The current secret remains valid alongside the new one for 24 hours so you can roll your config without downtime. Past the 24-hour window, only the new secret authenticates.

Revocation: clicking Revoke invalidates the client AND cascade-revokes every ffp_ token it ever minted in a single transaction — fail-safe even if the operator pod is killed mid-revocation.

What the three flows share

PropertyHow it’s enforced
Tenant isolationEvery minted token has tenant_id set; requireTenantBinding rejects 404 on cross-tenant calls. Database-level UNIQUE prevents one issuer from being claimed by two tenants for direct bearer.
Scope ceilingAll three intersect against EXCHANGEABLE_SCOPES (operator-controlled) AND the customer’s per-IdP / per-client allowlist. EXPLICIT_OPT_IN_SCOPES (billing:write, sso:write, policy:write, orgs:write) require explicit allowlist entries — never granted by default.
TTLAll minted tokens cap at 1 hour. Customers wanting longer-lived credentials use the admin-UI minting path (Personal Access Tokens).
Audit chainEvery mint emits an audit_event with actor, action, scopes, on_behalf_of (for OBO). The chain hash covers fields_json including on_behalf_of.
Rate limitingBunny edge limits per IP. /auth/token-exchange and /oauth/token share a 30/min cap to bound the JWT-verify CPU cost from bogus inputs.
JWKS reachabilityAll outbound JWKS + discovery fetches go through outbound-proxy-strict — SSRF + private-IP + cloud-metadata blocked at the network hop. JWKS cached 10 min.
EU residencyThe pipeline never reaches a US-hosted Sigstore / Auth0 / Okta endpoint — verification is entirely inside fremforge’s T Cloud eu-de footprint. Customer-IdP egress goes through the SSRF-proxied fetch only.

Threat-model summary

ThreatMitigation
Customer registers a multi-tenant Entra /common/ URL without tid claim_assertionAdmin form refuses; helper isMultiTenantIssuerUrl() flags Entra (v1 + v2), Google Workspace, Microsoft Gov / China clouds
Two tenants share an IdP iss URLPartial UNIQUE index on (LOWER(RTRIM(iss,'/'))) WHERE direct-bearer enabled — second registration is rejected at the DB layer
Customer-IdP JWT replayed against the wrong tenantPer-tenant audience https://frem.sh/<slug> — a token minted for aud=https://frem.sh/acme literally cannot satisfy tenant initech’s audience check
Leaked client_secretTenant admin runs Revoke → cascade transaction kills the client AND every PAT it minted; rotation supports a 24h dual-secret window for zero-downtime swap
Spoofed claims in OBO actor_tokenjose verifies signature + iss + aud against the same source as subject; sub immutable, email/name never trusted
JWKS-cache stale after IdP key rotation10-min hard TTL — operationally bounded
Network-internal hostname in IdP discovery doc (DNS rebind / SSRF)outbound-proxy-strict allowlist blocks the fetch; code-level https:// guard layer adds defence-in-depth

Quick reference

PathMethodAuthTTLBody
/api/v1/auth/token-exchangePOSTThe JWT IS the auth1hsubject_token (+ optional actor_token), tenant_slug, scopes, name
/api/v1/oauth/tokenPOSTBasic OR form-body1hgrant_type=client_credentials, client_id, client_secret, scope, audience
/api/v1/* (any)anyAuthorization: Bearer <jwt or ffp_*>per exp / per PAT row

Related

  • SSO setup (OIDC + SAML) — register your IdP for both human sign-in and agent auth
  • Workflow OIDC for cloud auth — the outbound direction (runner pod → AWS/GCP/Azure trust policies)
  • Audit log + chain — verify what every agent did, including OBO on_behalf_of filtering
  • API tokens (PATs) — human-minted long-lived tokens for the same /api/v1/* surface; mint at User settings → API tokens, see Authentication policy for max-lifetime + rotation policy