You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Forge's model auth is a static credential today: ModelRef.APIKeyEnv supplies a fixed API key from the environment (forge-core/types/config.go:289), and AuthScheme (forge-core/llm/client.go:13 — x_api_key / bearer / apikey_header / apikey_header_only / aws_sigv4) decides which header(s) carry it. There is no way to OAuth-login to an enterprise gateway's OIDC endpoint, acquire a short-lived token, cache it, and send that token on each LLM call — the pattern enterprises use to front a model gateway (Kong/Bedrock/etc.) with their IdP (Okta/Entra/Auth0).
Claude Code supports exactly this via apiKeyHelper: an external command returns a token (the org caches it, refreshing from the JWT exp), and Claude Code sends it as bothAuthorization: BearerandX-Api-Key. Example the requester shared — an Okta helper that caches to ~/.claude/okta-token-cache.json, computes expiry from the JWT exp, and re-mints via a device/auth-code flow when stale.
Goal
Add a gateway credential provider for the model: acquire a token from the gateway's OIDC URL via OAuth, cache it (expiry from the JWT exp minus a refresh buffer), and inject it on every LLM call according to auth_scheme. Two acquisition modes:
Interactive (dev / forge run on a laptop): OAuth device-code or auth-code + PKCE against the OIDC issuer → browser login → token cached under ~/.forge/.
Headless (CI / deployed agent): OAuth client_credentials (2-legged) — forge already has this: oauth.ClientCredentialsTokenCtx (forge-core/llm/oauth/token.go:98), used by the ChatGPT OAuth path (forge-core/llm/providers/oauth_client.go). Extend/generalize it to a gateway OIDC provider.
Plus, as the simplest first cut and a universal escape hatch: an apiKeyHelper-style external command (run a script, take its stdout as the token) — this makes the exact Okta bash script the requester pasted work verbatim.
Injection semantics
The acquired token replaces the static APIKeyEnv value at call time; it flows through the existing AuthScheme header logic (bearer → Authorization: Bearer; apikey_header → the gateway header in addition to the native header; etc.).
Cache: store token + expires_at (parsed from JWT exp) under ~/.forge/ with 0600; re-acquire within a refresh buffer (Claude's example uses 300s). Never log the token; scrub in audit/trace (forge already redacts vendor tokens).
Config source
Gateway base_url, OIDC issuer/authorize/token URLs, client_id, scopes, and grant type are injected from the forge settings surface (companion backlog) — user layer for a developer's own IdP creds, managed layer for the org's gateway. auth_scheme continues to select header placement.
Deliverables
A GatewayTokenProvider in forge-core/llm (generalize the existing oauth package): device-code + auth-code+PKCE + client_credentials against an OIDC issuer, with on-disk cache + expiry-from-exp + refresh buffer.
An apiKeyHelper-style external-command credential source (run command, cache stdout by exp) as the minimal-viable + escape-hatch path.
Wire it into the LLM client construction so the token is resolved (and refreshed) per call and injected per auth_scheme.
forge login affordance for the interactive flow (mirrors forge mcp login / the ChatGPT OAuth login); token stored under ~/.forge/credentials/.
Docs: gateway-OAuth setup (issuer/client_id/scopes/grant) + the apiKeyHelper escape hatch, under docs/security/authentication.md (outbound model auth) — noting the token is never persisted to the agent image, only acquired at runtime.
Acceptance
forge run against a gateway configured (via settings) with an OIDC issuer + client acquires a token (device-code / auth-code) on first use, caches it, and sends it on LLM calls per auth_scheme.
A cached token is reused until within the refresh buffer of its exp, then transparently re-acquired (no stale-token 401 loop).
Headless mode uses client_credentials with no interactive prompt.
An apiKeyHelper-style external command works verbatim (the Okta script), its stdout used as the token and cached by exp.
Token never appears in logs/audit/traces; on-disk cache is 0600.
Related
Companion backlog: forge settings surface — injects the gateway URL + OIDC config this provider consumes. (cross-link on creation)
Problem
Forge's model auth is a static credential today:
ModelRef.APIKeyEnvsupplies a fixed API key from the environment (forge-core/types/config.go:289), andAuthScheme(forge-core/llm/client.go:13—x_api_key/bearer/apikey_header/apikey_header_only/aws_sigv4) decides which header(s) carry it. There is no way to OAuth-login to an enterprise gateway's OIDC endpoint, acquire a short-lived token, cache it, and send that token on each LLM call — the pattern enterprises use to front a model gateway (Kong/Bedrock/etc.) with their IdP (Okta/Entra/Auth0).Claude Code supports exactly this via
apiKeyHelper: an external command returns a token (the org caches it, refreshing from the JWTexp), and Claude Code sends it as bothAuthorization: BearerandX-Api-Key. Example the requester shared — an Okta helper that caches to~/.claude/okta-token-cache.json, computes expiry from the JWTexp, and re-mints via a device/auth-code flow when stale.Goal
Add a gateway credential provider for the model: acquire a token from the gateway's OIDC URL via OAuth, cache it (expiry from the JWT
expminus a refresh buffer), and inject it on every LLM call according toauth_scheme. Two acquisition modes:forge runon a laptop): OAuth device-code or auth-code + PKCE against the OIDC issuer → browser login → token cached under~/.forge/.oauth.ClientCredentialsTokenCtx(forge-core/llm/oauth/token.go:98), used by the ChatGPT OAuth path (forge-core/llm/providers/oauth_client.go). Extend/generalize it to a gateway OIDC provider.Plus, as the simplest first cut and a universal escape hatch: an
apiKeyHelper-style external command (run a script, take its stdout as the token) — this makes the exact Okta bash script the requester pasted work verbatim.Injection semantics
APIKeyEnvvalue at call time; it flows through the existingAuthSchemeheader logic (bearer→Authorization: Bearer;apikey_header→ the gateway header in addition to the native header; etc.).Authorization: Bearerand the api-key header" behavior when the gateway expects either — configurable viaauth_scheme/auth_header_name(already present, issues LLM client: apikey-header auth scheme for Kong AI Gateway (key-auth) #302/feat(llm): apikey_header auth scheme for Kong AI Gateway key-auth #303).expires_at(parsed from JWTexp) under~/.forge/with0600; re-acquire within a refresh buffer (Claude's example uses 300s). Never log the token; scrub in audit/trace (forge already redacts vendor tokens).Config source
Gateway
base_url, OIDC issuer/authorize/token URLs,client_id,scopes, and grant type are injected from the forge settings surface (companion backlog) — user layer for a developer's own IdP creds, managed layer for the org's gateway.auth_schemecontinues to select header placement.Deliverables
GatewayTokenProviderinforge-core/llm(generalize the existingoauthpackage): device-code + auth-code+PKCE + client_credentials against an OIDC issuer, with on-disk cache + expiry-from-exp+ refresh buffer.apiKeyHelper-style external-command credential source (run command, cache stdout byexp) as the minimal-viable + escape-hatch path.auth_scheme.forgelogin affordance for the interactive flow (mirrorsforge mcp login/ the ChatGPT OAuth login); token stored under~/.forge/credentials/.apiKeyHelperescape hatch, underdocs/security/authentication.md(outbound model auth) — noting the token is never persisted to the agent image, only acquired at runtime.Acceptance
forge runagainst a gateway configured (via settings) with an OIDC issuer + client acquires a token (device-code / auth-code) on first use, caches it, and sends it on LLM calls perauth_scheme.exp, then transparently re-acquired (no stale-token 401 loop).apiKeyHelper-style external command works verbatim (the Okta script), its stdout used as the token and cached byexp.0600.Related
forge-core/llm/oauth/token.go(ClientCredentialsTokenCtx),forge-core/llm/providers/oauth_client.go(ChatGPT OAuth path).AuthScheme(forge-core/llm/client.go:13),ModelRef(forge-core/types/config.go:281), issues LLM client: apikey-header auth scheme for Kong AI Gateway (key-auth) #302/feat(llm): apikey_header auth scheme for Kong AI Gateway key-auth #303 (apikey_header/apikey_header_only).apiKeyHelper— returns a token, sent asAuthorization: Bearer+X-Api-Key, reloaded on change.