GitHub
Concept

Authentication

How ProxifAI authenticates users and machines — JWT for the API, OIDC for browser login, PKCE for the CLI, and HMAC-signed pfai_ tokens for workflow executions.

ProxifAI’s authentication is intentionally simple in OSS: an HS256 JWT signed by JWT_SECRET is the source of truth for API access, with optional Keycloak OIDC bolted on top for browser login and SSO. The CLI uses PKCE OAuth, and workflow runtimes get short-lived HMAC-signed pfai_<execID>_<sig> tokens that double as API credentials with the executing workflow’s identity.

How it works

Three auth surfaces, all funneling into the same JWT validator:

                                     ┌── Bearer <jwt>
 Browser  ──► OIDC (Keycloak) ──┐    │
                                ├──► /api/v1/* ──► auth.Middleware ──► JWT verified (HS256)
 CLI      ──► PKCE OAuth ───────┤    │
                                │    │
 Workflow ──► pfai_ token ──────┘    └── Authorization: Bearer pfai_<execID>_<sig>

                                          └── HMAC verified by gateway middleware

All paths converge on internal/auth/Middleware (auth.go), which:

  1. Reads Authorization: Bearer <token>
  2. Parses and validates with jwt.ParseWithClaims using JWTSecret and HMAC
  3. Injects userId, email, orgId and the full Claims struct into the request context

If no token is present, requests pass through — handlers decide whether auth is required.

JWT claims

Issued by ProxifAI on login (after OIDC exchange or first-party password auth). Format defined in auth.Claims:

ClaimSourceNotes
subUser IDThe canonical user identifier
emailUser emailCase-normalized
nameUser display nameOptional
org_idActive orgSet when the user is scoped to an org
iat, exp, iss, audStandardexp controls token lifetime

Tokens are HS256-signed with JWT_SECRET. There’s no key rotation in OSS — rotating the secret invalidates every active token (logout-everyone). Set the secret to a long random value at deployment time and store it in your secret manager.

OIDC (browser login)

Routes mounted under /api/v1/oidc/* (oidc_auth.go):

EndpointUse
GET /api/v1/oidc/authorize?rd=...Redirects to the configured IdP (Keycloak by default)
GET /api/v1/oidc/callbackIdP returns the user here; ProxifAI exchanges the code, mints a JWT, sets a cookie
GET /api/v1/oidc/logoutClears the cookie and bounces through the IdP’s logout

OIDC_* env vars wire up the provider. Keycloak is the default but any standards-compliant OIDC provider works (Auth0, Okta, Google Workspace, Azure AD).

# Required
OIDC_ISSUER=https://id.example.com/realms/proxifai
OIDC_CLIENT_ID=proxifai
OIDC_CLIENT_SECRET=

# Optional
OIDC_SCOPES="openid profile email"     # default
OIDC_REDIRECT_URL=https://your-host/api/v1/oidc/callback   # auto-derived from BASE_URL if unset

OIDC is opt-in. If OIDC_ISSUER is unset, the routes return 404 and the platform falls back to first-party email/password (admin user seeded from ADMIN_EMAIL / ADMIN_PASSWORD).

The local k3d setup ships a Keycloak realm at http://id.proxifai.com with [email protected] / admin and [email protected] / user pre-seeded for development.

CLI authentication (PKCE)

pfai auth login runs a standards-compliant PKCE OAuth flow (internal/pfai/cmd/auth.go):

  1. GET /api/v1/auth/cli-config returns the PKCE-shaped config (authorizationEndpoint, tokenEndpoint, clientId)
  2. The CLI generates a code verifier + challenge, opens the user’s browser to the authorize URL with the challenge attached
  3. After consent, the IdP redirects to http://127.0.0.1:<random>/callback with an authorization code
  4. The CLI exchanges the code + verifier for an access token, then calls ProxifAI to exchange the access token for a long-lived pfai token
  5. The result is written to ~/.proxifai/credentials.json (and mirrored into ~/.proxifai/contexts.yaml)

For non-interactive use:

ApproachWhen
PFAI_TOKEN + PFAI_SERVER envCI pipelines, scripts
pfai context add ci --server X --token YMultiple servers/identities to switch between
pfai auth login --as <name> once, then export the tokenOne-time setup, ship the token as a secret

See CLI for the full surface.

Workflow tokens (pfai_<execID>_<sig>)

Workflow executions need API credentials too — pfai exec status, agent containers calling the LLM gateway, etc. These get an HMAC-signed token shaped:

pfai_<execID>_<sig>

Where:

  • execID is the workflow execution ID
  • sig is hex(HMAC-SHA256(execID, JWT_SECRET))[:16]

The verifier in internal/llmgateway/middleware/auth.go (and parallel checks in core handlers) recomputes the MAC and constant-time compares. Tokens carry the executing workflow’s identity, so:

  • LLM gateway calls attribute usage to the right workflow
  • Credit checks (Enterprise) deduct against the right org
  • API calls inherit the workflow’s runtime_config.permissions scope

The LLM_GATEWAY_HMAC_SECRET env var overrides JWT_SECRET for the gateway-only path; default is to use JWT_SECRET.

Personal access tokens (PATs)

For human-controlled scripts and integrations, create a PAT under Settings → Personal Access Tokens. Token format is pfai_ + 40 hex chars; the full value is shown once at creation, stored as SHA-256 hash thereafter.

PAT scopes (from TokenScope in models.go):

ScopeImplies
readAll *:read scopes
writeread + all *:write scopes
repo:read · repo:writeGit protocol + forge API
issues:read · issues:writePlan API
pr:read · pr:writePull-request API
admin:read · admin:writeAdmin endpoints
pipelinePipeline dispatch and management

PATs use the same Authorization header as JWTs:

curl http://localhost:3000/api/v1/issues \
  -H "Authorization: Bearer pfai_a1b2c3..."

Manage PATs from pfai token or via:

Method · PathPurpose
GET /api/v1/tokensList your PATs
POST /api/v1/tokensCreate — value returned once in response
PATCH /api/v1/tokens/{id}Rename, change scopes
DELETE /api/v1/tokens/{id}Revoke immediately

Environment variables

VariableRequiredEffect
JWT_SECRETyes (auto-generated on first boot if unset)HMAC secret for HS256 token signing/verifying
LLM_GATEWAY_HMAC_SECRETnoOverride JWT_SECRET for pfai_<execID> token verification only
BASE_URLderivedUsed to build redirect URIs and link generation; defaults to http://localhost:<PORT>
OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRETOIDC onlyEnable OIDC browser login
OIDC_SCOPESoptionalDefault openid profile email
OIDC_REDIRECT_URLoptionalAuto-derived from BASE_URL if unset
ADMIN_EMAIL, ADMIN_PASSWORDoptionalSeeded admin when OIDC is disabled — defaults admin / admin

Multi-factor authentication

MFA isn’t built into the OSS first-party auth. To enforce MFA for browser login, configure it on your OIDC provider (Keycloak, Okta, etc.) — ProxifAI honors whatever the IdP returns and the IdP can challenge for MFA before issuing the access token. There’s no MFA layer on PATs or pfai_ workflow tokens; protect those by scoping permissions narrowly and rotating regularly.

See also