Repository navigation
Description
Problem
Currently opencode serve only supports HTTP Basic Auth via OPENCODE_SERVER_PASSWORD. This creates friction in common deployment scenarios:
-
Reverse proxy / API gateway integration — Most ingress controllers (nginx, Caddy, Traefik, Cloudflare Tunnel) and API gateways use Bearer tokens for upstream authentication. Operators must add a translation layer to convert Bearer → Basic.
-
Programmatic access — External tools (CI pipelines, scripts, desktop apps) typically pass
Authorization: Bearer <token>rather than encodingusername:passwordin base64. The current Basic Auth requirement forces each client to know the username convention ("opencode"). -
Credential leakage surface — Basic Auth embeds the password in every request header as reversible base64, whereas a Bearer token is opaque and easier to rotate/revoke without changing the underlying secret.
Related issues that reflect current auth pain points:
- opencode attach ignores OPENCODE_SERVER_USERNAME, hardcodes "opencode" #18611 —
opencode attachhardcodes username "opencode", ignoringOPENCODE_SERVER_USERNAME - Health check password protected #12805 — Health check endpoint is password-protected (no way to exempt it without disabling all auth)
- opencode-web Basic Auth Bug #18325 — Web client Basic Auth bugs
Current Implementation
From packages/opencode/src/server/middleware.ts:
export const AuthMiddleware: MiddlewareHandler = (c, next) => {
if (c.req.method === "OPTIONS") return next()
const password = Flag.OPENCODE_SERVER_PASSWORD
if (!password) return next()
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
if (c.req.query("auth_token"))
c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
return basicAuth({ username, password })(c, next)
}The Authorization Effect layer (server.ts#L69-L83) only handles the basic scheme — no bearer branch exists.
Proposal
Add a new env var OPENCODE_SERVER_TOKEN that enables Bearer token auth:
# Option A: Bearer token only
OPENCODE_SERVER_TOKEN=my-secret-token
# Option B: Both schemes simultaneously (backward compatible)
OPENCODE_SERVER_PASSWORD=legacy-pass
OPENCODE_SERVER_TOKEN=my-secret-tokenExpected behavior
| Header | Result |
|---|---|
Authorization: Bearer <OPENCODE_SERVER_TOKEN> |
✅ Authenticated |
Authorization: Basic base64(user:OPENCODE_SERVER_PASSWORD) |
✅ Authenticated (existing) |
?auth_token=<OPENCODE_SERVER_TOKEN> |
✅ Authenticated (query param) |
| No header, no token set | ✅ Unsecured (existing) |
| Wrong token/password | ❌ 401 |
Suggested middleware change
export const AuthMiddleware: MiddlewareHandler = (c, next) => {
if (c.req.method === "OPTIONS") return next()
const password = Flag.OPENCODE_SERVER_PASSWORD
const token = Flag.OPENCODE_SERVER_TOKEN
// No auth configured — pass through
if (!password && !token) return next()
const authHeader = c.req.header("authorization") ?? ""
// Bearer token check
if (token) {
if (authHeader === `Bearer ${token}`) return next()
if (c.req.query("auth_token") === token) return next()
}
// Fall back to Basic Auth
if (password) {
if (c.req.query("auth_token"))
c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
return basicAuth({ username, password })(c, next)
}
return c.text("Unauthorized", 401)
}Changes needed
- Add
OPENCODE_SERVER_TOKENtoflag.ts - Update
AuthMiddlewareinmiddleware.tsto handle Bearer scheme - Add
bearerbranch to the Effect-basedAuthorizationlayer inserver.ts - Update
opencode attach --passwordto also accept--tokenflag - Update the
webcommand warning to mention both env vars
Alternatives Considered
- Reuse
OPENCODE_SERVER_PASSWORDas Bearer token — Conflates two auth schemes; clients can't know which header format to use. - Only support Bearer, drop Basic — Breaking change for existing deployments.
- Reverse proxy workaround — Works but adds operational complexity for a common use case.
Activity
feat(adapters): #449 OpenCode headless adapter (Phase C — implementat…
github-actions commentedon Jul 3, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.