Authentication
Bearer-token auth — Personal Access Tokens for humans and workflow tokens for automation, both verified via the same JWT middleware.
Every API request needs a Authorization: Bearer <token> header (or x-api-key: <token> on the LLM gateway, Anthropic-compatibility). The middleware in internal/auth/auth.go parses and validates the token, then injects userId, email, and orgId into the request context.
Token types
| Type | Format | Issued by | Use for |
|---|---|---|---|
| Personal Access Token (PAT) | pfai_ + 40 hex chars | User from Settings → Personal Access Tokens | Scripts, CI, CLI in non-interactive mode |
| Workflow execution token | Standard JWT (HS256) | Workflow runtime, automatic | Agent containers, pipelines, anywhere PFAI_TOKEN is auto-set |
| OIDC session token | Standard JWT | After OIDC login at /api/v1/oidc/authorize | Browser; cookie-set, you don’t see it directly |
All three flow through the same JWT validator. PAT scopes restrict what calls succeed; OIDC sessions inherit the user’s full RBAC; workflow tokens carry the executing workflow’s runtime_config.permissions (defaulting to the dispatcher’s role if unset).
curl http://localhost:3000/api/v1/issues \
-H "Authorization: Bearer pfai_a1b2c3..."
Creating a PAT
Settings → Personal Access Tokens → New token
Name + scopes + expiration. The token value is shown once at creation — it’s stored as sha256(token) thereafter, so you can’t recover it. Treat it as a credential.
pfai token create --name "ci" --scopes write,pipeline --expires 90d
# Returns: pfai_a1b2c3d4...
Scopes
11 scopes cover most use cases (models.go — search for TokenScope):
| Scope | Implies |
|---|---|
read | All *:read scopes |
write | read + all *:write scopes + pipeline |
repo:read · repo:write | Forge browse + push, releases, deploy keys |
issues:read · issues:write | Plan API |
pr:read · pr:write | Pull-request review and merge |
admin:read · admin:write | Super-admin endpoints under /api/v1/admin/* |
pipeline | Dispatch and manage CI/CD runs |
Scope hierarchy: write ⊇ read, *:write ⊇ *:read. A read-scoped token gets 403 on POST /api/v1/issues; repo:read gets 403 on POST /api/v1/forge/{owner}/{repo}/pulls.
PAT scopes layer on top of RBAC permissions. The effective check is scope ALLOWS the operation AND user's role has the required permission. A write-scoped PAT for a Member-roled user still can’t delete projects (lacks projects.delete).
Verifying a token
curl http://localhost:3000/api/v1/me/permissions \
-H "Authorization: Bearer $PFAI_TOKEN"
Returns the effective permission set the request would carry. Useful for “does this token actually let me do X?” checks before running a sensitive operation. Returns 401 if the token is invalid/expired.
{
"userId": "user_abc",
"email": "[email protected]",
"orgId": "org_default",
"permissions": ["issues.read", "issues.create", "issues.edit", "code.read", ...]
}
Workflow tokens
When a workflow execution starts, the runtime mints a standard JWT (HS256) via GenerateExecutionToken and injects it into the agent container as PFAI_TOKEN along with PFAI_SERVER and PFAI_EXECUTION_ID.
The JWT carries these claims:
| Claim | Purpose |
|---|---|
sub | The execution ID |
execution_id | Same — duplicated in the namespaced claim |
workflow_id | Workflow this execution belongs to |
org_id | Tenant scope |
permissions | Array of dot-notation perms (e.g. ["issues.read", "code.write"]); from runtime_config.permissions, else empty (deny-all default) |
token_type | Constant "execution" to distinguish from PATs |
iss | Constant "proxifai" |
exp | Now + 24 hours |
The signing secret is PFAI_JWT_SECRET (with JWT_SECRET as fallback). The same JWT validator that handles PATs also accepts these — same Authorization: Bearer … header.
Default is deny-all. If runtime_config.permissions isn’t set on the workflow, the execution token gets an empty permission array and every API call returns 403. Grant explicit permissions like ["issues.read", "code.read"] on the workflow to give containers the access they need.
There is a separate HMAC-based token format (pfai_<execID>_<sig16>) used only by the LLM gateway middleware (llmgateway/middleware/auth.go) to authenticate gateway requests cheaply without a JWT round-trip — this is invisible to API consumers; you don’t construct or pass it manually.
Workflow tokens aren’t creatable outside an execution. For long-lived service access, mint a PAT.
Browser sessions (OIDC)
When the platform is configured with OIDC (OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET), users land on /api/v1/oidc/authorize to start the flow. After consent, the cookie set is a standard signed JWT — it works against the same API surface as a PAT. Programmatic clients should use PATs instead; cookies are for the browser SPA.
Errors
| Status | Meaning |
|---|---|
401 Unauthorized | Missing/invalid Authorization header, expired token, or HMAC mismatch on workflow tokens |
402 Payment Required | Credit check failed (Enterprise/SaaS only, requires account-api) |
403 Forbidden | Token is valid but lacks the required scope or RBAC permission |
The body shape is {"error":"<message>"} — see Errors.
See also
The full auth story — JWT internals, OIDC config, PAT lifecycle, MFA delegation.
The 35 RBAC permissions that gate every endpoint.
pfai auth login uses the same PKCE flow OIDC clients use.
Gateway-specific auth — accepts both PATs and pfai_ workflow tokens.