Skip to content

[Feature]: OAuth/OIDC login to the model gateway — acquire + inject a short-lived token on LLM calls (apiKeyHelper analog) #455

Description

@initializ-mk

Problem

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 both Authorization: Bearer and X-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.).
  • Consider matching Claude Code's "send as both Authorization: Bearer and the api-key header" behavior when the gateway expects either — configurable via auth_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).
  • 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions