Commit Graph
16 Commits
Author SHA1 Message Date
Bobby HoandClaude Sonnet 5 d814dfad88 feat(coderd): support public OAuth2 client tokens at the schema layer (#27712)
Layer 1 of a multi-PR split of #27195 (public/secretless PKCE-only
OAuth2 clients), broken up for easier review: **database schema (this
PR)** → oauth2provider handler logic → API/e2e integration tests.

## Goal

Coder's OAuth2 provider only works correctly for confidential clients
today. Public clients — native apps that can't safely hold a shared
secret, such as the CLI's browser-based login flow, IDE plugins (VS
Code, JetBrains), desktop apps, and MCP clients — cannot complete a real
OAuth2 flow against Coder, even though OAuth 2.1 §2.1 explicitly defines
this client type and RFC 8252 §8.5 requires PKCE alone to be sufficient
authentication for it. Every MCP client, CLI login flow, and IDE plugin
is a public client by construction, and none of them can complete a
secretless flow against Coder today: dynamic registration always
classifies a client as confidential regardless of what it asks for, the
token endpoint unconditionally requires a `client_secret`, and discovery
metadata never advertises `"none"` as a supported auth method.

Full write-up:
[ENG-3029](https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client)

### Overall design (end state across the full PR stack)

`[PR2]` marks handler-layer changes landing in the next PR in this
stack. The green box is what this PR implements.

```mermaid
sequenceDiagram
    autonumber
    participant C as Public Client (CLI/MCP/IDE plugin)
    participant S as coderd (chi router)
    participant H as oauth2provider handlers
    participant DB as PostgreSQL

    Note over C,S: Discovery
    C->>S: GET /.well-known/oauth-authorization-server
    S->>H: GetAuthorizationServerMetadata()
    Note over H: [PR2] add "none" to<br/>the returned auth methods list
    H-->>C: [PR2] 200 { token_endpoint_auth_methods_supported:<br/>[..., "none"] }

    Note over C,S: Dynamic Client Registration
    C->>S: POST /oauth2/register<br/>{redirect_uris, token_endpoint_auth_method: "none"}
    S->>H: CreateDynamicClientRegistration()
    Note over H: [PR2] client type now reads<br/>the request -> "public"
    Note over H: [PR2] skip secret generation<br/>for public clients
    H->>DB: [PR2] INSERT app row<br/>(client_type = 'public')
    DB-->>H: app row
    Note over H: [PR2] skip secret insert entirely
    H-->>C: [PR2] 201 { client_id }<br/>(no client_secret field)

    Note over C,S: Authorization Code + PKCE flow
    C->>S: GET /oauth2/authorize?client_id=...&code_challenge=...
    C->>S: POST /oauth2/tokens (grant_type=authorization_code)<br/>no client_secret
    S->>H: extractTokenRequest()
    Note over H: [PR2] client_secret no longer required<br/>for public clients
    H->>H: authorizationCodeGrant()
    Note over H: [PR2] skip secret lookup for public clients
    Note over H: PKCE verification — already mandatory, unchanged
    rect rgb(198, 239, 206)
    Note over H,DB: [THIS PR] oauth2_provider_app_tokens.app_id<br/>column added (NOT NULL, populated at insert<br/>time from app.ID) and app_secret_id loosened<br/>to nullable. Revocation now checks app_id<br/>directly. Confidential-client behavior is<br/>unchanged — no public client can be created yet.
    H->>DB: [PR2] INSERT refresh token row<br/>(no secret reference, for public clients)
    end
    DB-->>H: token row
    H-->>C: 200 { access_token, refresh_token }
```

## This PR: database schema

A public client has no `client_secret`, so it has nothing to put in
`oauth2_provider_app_tokens.app_secret_id`, which was `NOT NULL`. This
PR makes that column nullable and instead attributes a token to its
owning app through a new, always-populated `app_id` column — so
ownership checks (e.g. revocation) work identically for public and
confidential clients without joining through a secret that may not
exist.

| Column | Before | After (this PR) |
|---|---|---|
| `app_secret_id` | `uuid NOT NULL` | **nullable** |
| `app_id` | — | **new**: `uuid NOT NULL`, `FOREIGN KEY →
oauth2_provider_apps(id) ON DELETE CASCADE`, backfilled for every
existing row and populated on every new insert from that point on |

This is a single, complete migration — not staged across multiple PRs.
An earlier version of this branch deferred `app_secret_id`'s nullability
and the insert-time population of `app_id` to a later PR, keeping this
PR's diff limited to `coderd/database`. [Automated
review](https://github.com/coder/coder/pull/27712#discussion_r3686851911)
correctly flagged that as unsafe: the migration would backfill existing
rows once, but nothing would populate `app_id` for rows written
afterward, so the moment this PR merged, new tokens would start
accumulating a permanently `NULL` app_id — and if a release happened to
be cut before the follow-up PR landed, that gap could ship to customers
and would need a second, later backfill to close. Doing the full
migration now avoids that: `app_id` is correct from the first row
written, and the promised `NOT NULL` constraint requires no data repair
because it's already enforced.

Closing that gap requires a few mechanical, non-branching touches
outside `coderd/database`:
- `revoke.go`'s two ownership checks now compare `dbToken.AppID`
directly instead of looking up the app through `app_secret_id` — a
genuine simplification (and slightly less code), not a temporary shim.
- `tokens.go`'s two `InsertOAuth2ProviderAppToken` call sites supply the
new `app_id` column and wrap `app_secret_id` as a `NullUUID`.
- `oauth2_test.go`'s one direct-insert test fixture does the same.

None of these introduce client-type branching or new capability — every
client today is still confidential-only, still always presents a secret,
and behavior is unchanged. The full repo builds, vets, and all existing
tests pass unmodified in behavior.

## Coming next

- **PR2 (handler layer)**: `codersdk`'s `DetermineClientType()` reading
the requested `token_endpoint_auth_method`; `registration.go` skipping
secret generation for public clients (and wrapping the app+secret insert
in a single transaction, fixing a pre-existing
orphan-row/visibility-race gap); `tokens.go` making the secret check
conditional so PKCE alone authenticates a public client; `metadata.go`
advertising `"none"` in discovery. No further migration is needed — the
schema this PR ships is already final.
- **PR3 (API/e2e layer)**: integration tests through the real HTTP API
(`coderd/oauth2_test.go`), the MCP OAuth2 e2e flow
(`coderd/mcp/mcp_e2e_test.go`), and the manual test script
(`scripts/oauth2/test-mcp-oauth2.sh`).

Depends on: #27195 (original combined PR, being superseded by this
stack)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 10:41:06 -07:00
Bobby Ho fbac602456 feat!: add admin-controlled dynamic client registration toggle (#27316)
`POST /oauth2/register` (RFC 7591 Dynamic Client Registration) has
exactly one gate today: `ExperimentOAuth2`, a static, process-lifetime
flag that wraps the entire `/oauth2/*` route tree as an all-or-nothing
switch. That flag is scheduled for removal at GA, which would leave DCR
with zero admin control at all once it is gone.

Add a persistent, DCR-specific `oauth2_dcr_enabled` deployment setting,
independent of the experiment system, so admin control over DCR survives
GA. `POST /oauth2/register` checks the flag and rejects new
registrations with an RFC 7591-shaped `403` when disabled; discovery
metadata (`GET /.well-known/oauth-authorization-server`) conditionally
omits `registration_endpoint`. A new audited `GET`/`PUT
/api/v2/oauth2-provider/settings` endpoint lets an owner toggle it live,
no restart required. The setting defaults to disabled, matching the
canonical design proposal; disabling only stops new self-registrations,
clients that already registered continue to authorize and exchange
tokens normally.

Address issue described in
[ENG-3056](https://linear.app/codercom/issue/ENG-3056/oauth2-dcr-admin-configurable-enabledisable).

## Where this sits in the request path

```mermaid
sequenceDiagram
    autonumber
    participant A as Admin
    participant S as coderd
    participant DB as site_configs<br/>(oauth2_dcr_enabled)
    participant C as OAuth2/MCP Client

    Note over A,S: Admin toggles DCR (new)
    A->>S: PUT /api/v2/oauth2-provider/settings<br/>{dynamic_client_registration_enabled: false}
    S->>S: authorizeContext(ActionUpdate, ResourceDeploymentConfig)
    S->>DB: UPSERT oauth2_dcr_enabled = false
    S-->>A: 200 OK (audited)

    Note over C,S: Client discovery + registration afterward
    C->>S: GET /.well-known/oauth-authorization-server
    S->>DB: GetOAuth2DCREnabled (system ctx, every request, no cache)
    DB-->>S: false
    S-->>C: 200 metadata, registration_endpoint omitted

    C->>S: POST /oauth2/register
    S->>DB: GetOAuth2DCREnabled (system ctx, every request, no cache)
    DB-->>S: false
    S-->>C: 403 invalid_request,<br/>"Dynamic client registration is disabled"

    Note over C,S: A client that registered before the change is unaffected
    C->>S: GET /oauth2/authorize?client_id=...
    Note over S: no DCR-enabled check on this path
    S-->>C: 200 (proceeds normally)

    C->>S: PUT/DELETE /oauth2/clients/{client_id} (RFC 7592 self-management)
    Note over S: no DCR-enabled check on this path either
    S-->>C: 200 (proceeds normally)
```

## Files changed: manual vs. generated

Reviewers should focus on the **manual** files. The **generated** ones
are `make gen` output that follows mechanically from the manual changes
and don't need direct review.

<details>
<summary><b>Manual files (26)</b> — click to expand, grouped the same
way as "Suggested review order" below</summary>

**1. Database**

| File | What changed |
|---|---|
| `coderd/database/queries/siteconfig.sql` | New
`GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled` query pair on the
existing generic `site_configs` table. No schema change. |
| `coderd/database/dbauthz/dbauthz.go` | RBAC check
(`rbac.ResourceDeploymentConfig`) on the two new query methods; extends
the `subjectSystemOAuth2` system-actor role with read-only
`ResourceDeploymentConfig` access, needed so the public
discovery/registration endpoints can read the flag via
`dbauthz.AsSystemOAuth2`. |
| `coderd/database/dbauthz/dbauthz_test.go` | RBAC assertion coverage
for `GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled` in the
method-coverage test suite. |

**2. Request gating (the actual feature)**

| File | What changed |
|---|---|
| `coderd/oauth2provider/registration.go` | The actual gate:
`CreateDynamicClientRegistration` reads the flag first and returns an
RFC 7591-shaped `403` when disabled (defaults disabled if never
configured). |
| `coderd/oauth2provider/registration_test.go` | New unit test,
`TestCreateDynamicClientRegistration_DCREnabled`: calls the handler
directly (no HTTP server), covering enabled / explicitly disabled /
never-configured. |
| `coderd/oauth2provider/metadata.go` | `GetAuthorizationServerMetadata`
conditionally omits `registration_endpoint` from discovery metadata when
DCR is disabled. |
| `coderd/oauth2provider/metadata_test.go` | New unit test,
`TestGetAuthorizationServerMetadata_DCREnabled`: same three states, for
the discovery handler. |

**3. Admin settings endpoint**

| File | What changed |
|---|---|
| `codersdk/oauth2.go` | New `OAuth2ProviderSettings` SDK type plus
`Client.OAuth2ProviderSettings`/`PutOAuth2ProviderSettings` methods. |
| `coderd/oauth2.go` | New
`oauth2ProviderSettings`/`putOAuth2ProviderSettings` admin handlers
(audited via `audit.InitRequest`); updates the
`GetAuthorizationServerMetadata` call site to pass `api.Database`. |
| `coderd/coderd.go` | Registers `GET`/`PUT
/api/v2/oauth2-provider/settings`. |
| `coderd/oauth2_provider_settings_test.go` | New test file: admin
`GET`/`PUT` round-trip, default-disabled-before-any-`PUT`, and `403` for
a non-owner on both `GET` and `PUT`. |

**4. Audit wiring**

| File | What changed |
|---|---|
| `coderd/database/types.go` | New `database.OAuth2ProviderSettings`
audit-only struct (mirrors `NotificationsSettings`). |
| `coderd/audit/diff.go` | Adds the new struct to the `Auditable` type
union. |
| `coderd/audit/request.go` | Adds the new struct to all four dispatch
switches (`ResourceTarget`, `ResourceID`, `ResourceType`,
`ResourceRequiresOrgID`). |
| `codersdk/audit.go` | New API-facing
`ResourceTypeOAuth2ProviderSettings` constant and its `FriendlyString`
case. |
| `enterprise/audit/table.go` | Field-level audit action map
(`ActionTrack`/`ActionIgnore`) for the new struct. |
|
`coderd/database/migrations/000546_audit_oauth2_provider_settings.up.sql`
| Adds `oauth2_provider_settings` to the `resource_type` Postgres enum,
required for the audit wiring above (`resource_type` is a real enum, not
a Go-only value). |
|
`coderd/database/migrations/000546_audit_oauth2_provider_settings.down.sql`
| No-op (`ALTER TYPE ... ADD VALUE` can't be reverted). |

**5. Test-suite ripple from the disabled-by-default flip**

| File | What changed |
|---|---|
| `coderd/oauth2provider/oauth2providertest/helpers.go` | New shared
test helper, `EnableDCR`, since DCR now defaults to disabled and many
pre-existing tests need it turned on to register a client. |
| `coderd/oauth2_test.go` | Adds
`TestOAuth2DynamicClientRegistrationDisabled` (registers a client,
disables DCR, verifies new registration is rejected while the existing
client's self-management, authorize, and token exchange all keep
working); calls `EnableDCR` in every pre-existing test that registers a
client. |
| `coderd/oauth2_error_compliance_test.go` | Calls `EnableDCR` in every
test that registers a client, so RFC-error-format assertions aren't
masked by the new disabled-by-default gate. |
| `coderd/oauth2_metadata_validation_test.go` | Same: `EnableDCR` added
to every registration-dependent test. |
| `coderd/oauth2_security_test.go` | Same. |
| `coderd/oauth2provider/validation_test.go` | Same (near-duplicate of
`oauth2_metadata_validation_test.go` in a different package). |
| `coderd/oauth2provider/provider_test.go` | Same. |
| `coderd/mcp/mcp_e2e_test.go` | Same, for the MCP end-to-end
dynamic-registration flow test. |

</details>

<details>
<summary><b>Generated files (12)</b> — from <code>make gen</code>, no
need to review directly</summary>

`coderd/apidoc/docs.go`, `coderd/apidoc/swagger.json`,
`coderd/database/dbmetrics/querymetrics.go`,
`coderd/database/dbmock/dbmock.go`, `coderd/database/dump.sql`,
`coderd/database/models.go`, `coderd/database/querier.go`,
`coderd/database/queries.sql.go`, `docs/admin/security/audit-logs.md`,
`docs/reference/api/enterprise.md`, `docs/reference/api/schemas.md`,
`site/src/api/typesGenerated.ts`.

</details>

## Suggested review order

### 1. Database

Establishes the persisted setting and its RBAC rule; everything else
builds on `GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled`.

1. `coderd/database/queries/siteconfig.sql` — the two new queries. Same
boolean-encoding pattern as the existing
`oauth2_github_default_eligible` key right above them in the same file.
2. `coderd/database/dbauthz/dbauthz.go` — the RBAC wrapper for those two
queries, plus the `subjectSystemOAuth2` role extension (search this file
for `ResourceDeploymentConfig`, it appears in both spots).
3. `coderd/database/dbauthz/dbauthz_test.go` — asserts the RBAC checks
from (2) actually fire.

### 2. Request gating (the actual feature)

Where `POST /oauth2/register` and discovery metadata change behavior.

1. `coderd/oauth2provider/registration.go` — the primary gate. Read this
first; it's the feature.
2. `coderd/oauth2provider/registration_test.go` — its new unit test,
exercising the gate's three states directly against the handler.
3. `coderd/oauth2provider/metadata.go` — the same gating pattern applied
to the discovery `GET` endpoint.
4. `coderd/oauth2provider/metadata_test.go` — its new unit test.

### 3. Admin settings endpoint

How an owner flips the setting live.

1. `codersdk/oauth2.go` — the `OAuth2ProviderSettings` SDK type and
`Client` methods first; this is the public contract everything below
implements against.
2. `coderd/oauth2.go` — the `GET`/`PUT` handlers themselves.
3. `coderd/coderd.go` — route registration, to see where those handlers
get wired in.
4. `coderd/oauth2_provider_settings_test.go` — round-trip and permission
tests.

### 4. Audit wiring

Plumbing required so step 3's `PUT` is auditable; mechanical except for
(3).

1. `coderd/database/types.go` — the audit-only struct; everything else
in this layer exists to plumb it through.
2. `coderd/audit/diff.go` — adds it to the `Auditable` type union (the
compiler enforces this one).
3. `coderd/audit/request.go` — the four dispatch switches; the one part
of this layer worth reading closely.
4. `codersdk/audit.go` — the API-facing resource type constant.
5. `enterprise/audit/table.go` — the field-action map.
6.
`coderd/database/migrations/000546_audit_oauth2_provider_settings.{up,down}.sql`
— read last; a consequence of needing a new `resource_type` enum value
for (1)-(5), not a design decision of its own.

### 5. Test-suite ripple from the disabled-by-default flip

1. `coderd/oauth2provider/oauth2providertest/helpers.go` — the new
`EnableDCR` helper. Read first to understand the fix pattern before
seeing it applied repeatedly.
2. `coderd/oauth2_test.go` — next, since it also contains the new
`TestOAuth2DynamicClientRegistrationDisabled`, not just `EnableDCR` call
sites.
3. The rest, in any order, they're mechanical repeats of the same
one-line addition: `coderd/oauth2_error_compliance_test.go`,
`coderd/oauth2_metadata_validation_test.go`,
`coderd/oauth2_security_test.go`,
`coderd/oauth2provider/validation_test.go`,
`coderd/oauth2provider/provider_test.go`, `coderd/mcp/mcp_e2e_test.go`.

## Explicitly out of scope

Per the design proposal: rate limiting on `POST /oauth2/register`
(tracked separately), retroactively affecting already-registered clients
when DCR is disabled (this only gates new self-registration), and an
Initial Access Token requirement (a separate, follow-up ticket).
2026-07-28 16:59:33 -07:00
Cian Johnston 81468323e0 fix(coderd): use dbtime.Now() instead of time.Now() in test assertions against DB timestamps (#22685)
`time.Now()` has nanosecond precision while Postgres timestamps are
microsecond precision. When tests compare `time.Now()` against
DB-sourced timestamps using `Before`/`After`/`WithinRange`/etc., there
is a non-zero flake risk from the precision mismatch.

This replaces `time.Now()` with `dbtime.Now()` (which rounds to
microsecond precision) in all test assertions that compare against
database timestamps.

Follows from #22684.

## Changes (11 files)

| File | Changes |
|---|---|
| `coderd/apikey_test.go` | 11 comparisons with `ExpiresAt` |
| `coderd/users_test.go` | 2 comparisons with `ExpiresAt` |
| `coderd/oauth2_test.go` | 1 comparison with `token.Expiry` |
| `coderd/workspaces_test.go` | 2 comparisons with `DormantAt` |
| `coderd/workspaceagents_test.go` | 3 comparisons with
`ConnectedAt`/`DisconnectedAt` |
| `coderd/workspaceapps/db_test.go` | 1 comparison with `token.Expiry` |
| `coderd/provisionerdserver/provisionerdserver_test.go` | 1 comparison
with `key.ExpiresAt` |
| `enterprise/coderd/workspaces_test.go` | 1 comparison with `DormantAt`
|
| `enterprise/coderd/license/license_test.go` | 3 `NotBefore` values |
| `enterprise/coderd/licenses_test.go` | 2 `NotBefore` values |
| `enterprise/coderd/users_test.go` | 3 `Next()` comparisons |

## Not changed (intentionally)

- `scaletest/placebo/run_test.go` — compares wall-clock elapsed time,
not DB timestamps
- `cli/server_test.go`, `coderd/jwtutils/jwt_test.go`,
`enterprise/aibridgeproxyd/aibridgeproxyd_test.go` — TLS cert fields,
not DB-stored
- `coderd/azureidentity/azureidentity_test.go` — Azure cert expiry, not
DB


🤖 Generated by Claude Opus 4.6 but reviewed manually.
2026-03-06 09:14:11 +00:00
Thomas Kosiewski b776a14b46 fix(coderd): harden OAuth2 provider security (#22194)
## Summary

Harden the OAuth2 provider with multiple security fixes addressing
`coder/security#121` (CSRF session takeover) and converge on OAuth 2.1
compliance.

### Security Fixes

| Fix | Description | Commits |
|-----|-------------|---------|
| **CSRF on `/oauth2/authorize`** | Enforce CSRF protection on the
authorize endpoint POST (consent form submission) | `ba7d646`, `b94a64e`
|
| **Clickjacking: `frame-ancestors` CSP** | Prevent consent page from
being iframed (`Content-Security-Policy: frame-ancestors 'none'` +
`X-Frame-Options: DENY`) | `597aeb2` |
| **Exact redirect URI matching** | Changed from prefix matching to full
string exact matching per OAuth 2.1 §4.1.2.1 | `73d64b1`, `93897f1` |
| **Store & verify `redirect_uri`** | Store redirect_uri with auth code
in DB, verify at token exchange matches exactly (RFC 6749 §4.1.3) |
`50569b9`, `d7ca315` |
| **Mandatory PKCE** | Require `code_challenge` at authorization (for
`response_type=code`) + unconditional `code_verifier` verification at
token exchange | `d7ca315`, `1cda1a9` |
| **Reject implicit grant** | `response_type=token` now returns
`unsupported_response_type` error page (OAuth 2.1 removes implicit flow)
| `d7ca315`, `91b8863` |

### Changes by File

**`coderd/httpmw/csrf.go`** — Extended the CSRF `ExemptFunc` to enforce
CSRF on `/oauth2/authorize` in addition to `/api` routes. The consent
form POST is now CSRF-protected to prevent cross-site authorization code
theft.

**`site/site.go`** — Added `Content-Security-Policy: frame-ancestors
'none'` and `X-Frame-Options: DENY` headers to `RenderOAuthAllowPage`
(consent page only — does not affect the SPA/global CSP used by AI
tasks).

**`coderd/httpapi/queryparams.go`** — Changed `RedirectURL` from prefix
matching (`strings.HasPrefix(v.Path, base.Path)`) to full URI exact
matching (`v.String() != base.String()`), comparing scheme, host, path,
and query.

**`coderd/oauth2provider/authorize.go`** — Added PKCE enforcement:
`code_challenge` is required when `response_type=code` (via a
conditional check, not `RequiredNotEmpty`, so `response_type=token` can
reach the explicit rejection path). `ShowAuthorizePage` (GET) validates
`response_type` before rendering and returns a 400 error page for
unsupported types. `ProcessAuthorize` (POST) stores the `redirect_uri`
with the auth code when explicitly provided.

**`coderd/oauth2provider/tokens.go`** — PKCE verification is now
unconditional (not gated on `code_challenge` being present in DB). If
the stored code has a `redirect_uri`, the token endpoint verifies it
matches exactly — mismatch returns `errBadCode` → `invalid_grant`.
Missing `code_verifier` returns `invalid_grant`.

**`codersdk/oauth2.go`** — `OAuth2ProviderResponseTypeToken` constant
and `Valid()` acceptance are **kept** so the authorize handler can parse
`response_type=token` and return the proper `unsupported_response_type`
error rather than failing at parameter validation.

**`coderd/database/migrations/000421_*`** — Added `redirect_uri text`
column to `oauth2_provider_app_codes`.

### Design Decisions

**`state` parameter remains optional** — The plan initially required
`state` via `RequiredNotEmpty`, but this was reverted in `376a753` to
avoid breaking existing clients. The `state` is still hashed and stored
when provided (via `state_hash` column), securing clients that opt in.

**`response_type=token` kept in `Valid()`** — Removing it from `Valid()`
would cause the parameter parser to reject the request before the
authorize handler can return the proper `unsupported_response_type`
error. The constant is kept for correct error handling flow.

**CSP scoped to consent page only** — `frame-ancestors 'none'` is set
only on the OAuth consent page renderer, not globally. The SPA/global
CSP was previously changed to allow framing for AI tasks
([#18102](https://github.com/coder/coder/pull/18102)); this change does
not regress that.

### Out of Scope (follow-up PRs)

- Bearer tokens in query strings (needs internal caller audit)
- Scope enforcement on OAuth2 tokens
- Rate limiting on dynamic client registration


---

<details>
<summary>📋 Implementation Plan</summary>

# Plan: Harden OAuth2 Provider — Security Fixes + OAuth 2.1 Compliance

## Context & Why

Security issue `coder/security#121` reports a critical session takeover
via CSRF on the OAuth2 provider. This plan covers all remaining security
fixes from that issue **plus** convergence on OAuth 2.1 requirements.
The goal is a single PR that closes all actionable gaps.

## Current State (already committed on branch `csrf-sjx1`)

| Fix | Status | Commits |
|-----|--------|---------|
| Fix 1: CSRF on `/oauth2/authorize` | ✅ Done | `ba7d646`, `b94a64e` |
| CSRF token in consent form HTML | ✅ Done | `b94a64e` |
| `state_hash` column + storage | ✅ Done (hash stored, but state still
optional) | `9167d83`, `b94a64e` |
| Tests for CSRF + state hash | ✅ Done | `e4119b5` |

## Remaining Work

### ~~Fix 2 — Require `state` parameter~~ (DROPPED)

> **Decision:** Do not enforce `state` as required. The `state`
parameter is still hashed and stored when provided (via
`hashOAuth2State` / `state_hash` column from prior commits), but clients
are not forced to supply it. This avoids breaking existing integrations
that omit state.

**Rollback:** Remove `"state"` from the `RequiredNotEmpty` call in
`coderd/oauth2provider/authorize.go:42`:

```go
// BEFORE (current on branch)
p.RequiredNotEmpty("response_type", "client_id", "state", "code_challenge")

// AFTER
p.RequiredNotEmpty("response_type", "client_id", "code_challenge")
```

No test changes needed — tests already pass `state` voluntarily.

### Fix 4 — Exact redirect URI matching

Currently `coderd/httpapi/queryparams.go:233` uses prefix matching:

```go
// CURRENT — prefix match
if v.Host != base.Host || !strings.HasPrefix(v.Path, base.Path) {
```

OAuth 2.1 requires **exact string matching**. Change to:

```go
// AFTER — exact match (OAuth 2.1 §4.1.2.1)
if v.Host != base.Host || v.Path != base.Path {
```

**File: `coderd/httpapi/queryparams.go` — `RedirectURL` method**

Also update the error message from "must be a subset of" to "must
exactly match".

**Additionally**, store `redirect_uri` with the auth code and verify at
the token endpoint (RFC 6749 §4.1.3):

1. **New migration** (same migration file or a new `000421`): Add
`redirect_uri text` column to `oauth2_provider_app_codes`
2. **Update INSERT query** in `coderd/database/queries/oauth2.sql` to
include `redirect_uri`
3. **`coderd/oauth2provider/authorize.go`**: Store
`params.redirectURL.String()` when inserting the code
4. **`coderd/oauth2provider/tokens.go`**: After retrieving the code from
DB, verify that `redirect_uri` from the token request matches the stored
value exactly. Currently `tokens.go:103` calls `p.RedirectURL(vals,
callbackURL, "redirect_uri")` for prefix validation only — it must
compare against the stored redirect_uri from the code, not just the
app's callback URL.

<details>
<summary>Why both exact match AND store+verify?</summary>

Exact matching at the authorize endpoint prevents open redirectors
(attacker can't use a sub-path).
Storing and verifying at the token endpoint prevents code injection — an
attacker who steals a code can't exchange it with a different
redirect_uri than was originally authorized. This is required by RFC
6749 §4.1.3 and OAuth 2.1.
</details>

### Fix 7 — `frame-ancestors` CSP on consent page

The consent page can be iframed by a workspace app (same-site), which is
the attack vector. Add a `Content-Security-Policy` header to prevent
framing.

**File: `site/site.go` — `RenderOAuthAllowPage` function (~line 731)**

Before writing the response, add:

```go
func RenderOAuthAllowPage(rw http.ResponseWriter, r *http.Request, data RenderOAuthAllowData) {
    rw.Header().Set("Content-Type", "text/html; charset=utf-8")
    // Prevent the consent page from being framed to mitigate
    // clickjacking attacks (coder/security#121).
    rw.Header().Set("Content-Security-Policy", "frame-ancestors 'none'")
    rw.Header().Set("X-Frame-Options", "DENY")
    ...
```

Both headers for defense-in-depth (CSP for modern browsers,
X-Frame-Options for legacy).

### OAuth 2.1 — Mandatory PKCE

Currently PKCE is checked only when `code_challenge` was provided during
authorization (`tokens.go:258`):

```go
// CURRENT — conditional check
if dbCode.CodeChallenge.Valid && dbCode.CodeChallenge.String != "" {
    // verify PKCE
}
```

OAuth 2.1 requires PKCE for ALL authorization code flows. Change to:

**File: `coderd/oauth2provider/authorize.go`** — Add `"code_challenge"`
to required params:

```go
p.RequiredNotEmpty("response_type", "client_id", "code_challenge")
```

**File: `coderd/oauth2provider/tokens.go:257-265`** — Make PKCE
verification unconditional:

```go
// AFTER — PKCE always required (OAuth 2.1)
if req.CodeVerifier == "" {
    return codersdk.OAuth2TokenResponse{}, errInvalidPKCE
}
if !dbCode.CodeChallenge.Valid || dbCode.CodeChallenge.String == "" {
    // Code was issued without a challenge — should not happen
    // with the authorize endpoint enforcement, but defend in
    // depth.
    return codersdk.OAuth2TokenResponse{}, errInvalidPKCE
}
if !VerifyPKCE(dbCode.CodeChallenge.String, req.CodeVerifier) {
    return codersdk.OAuth2TokenResponse{}, errInvalidPKCE
}
```

**File: `codersdk/oauth2.go`** — Remove
`OAuth2ProviderResponseTypeToken` from the enum or reject it explicitly
in the authorize handler. Currently it's defined at line 216 but the
handler ignores `response_type` and always issues a code. We should
either:
- (a) Remove the `"token"` variant from the enum and reject it with
`unsupported_response_type`, OR
- (b) Add an explicit check in `ProcessAuthorize` that rejects
`response_type=token`

Option (b) is simpler and more backwards-compatible:

```go
// In ProcessAuthorize, after extracting params:
if params.responseType != codersdk.OAuth2ProviderResponseTypeCode {
    httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest,
        codersdk.OAuth2ErrorCodeUnsupportedResponseType,
        "Only response_type=code is supported")
    return
}
```

### OAuth 2.1 — Bearer tokens in query strings

`coderd/httpmw/apikey.go:743` accepts `access_token` from URL query
parameters. OAuth 2.1 prohibits this. However, this may be used
internally (e.g., workspace apps, DERP). Need to audit callers before
removing.

**Approach:** This is a larger change with potential breakage. Mark as a
**separate follow-up issue** rather than including in this PR. Document
the finding.

### OAuth 2.1 — Removed flows

✅ **Already compliant.** `tokens.go` only supports `authorization_code`
and `refresh_token` grant types. The implicit grant
(`response_type=token`) will be explicitly rejected per the PKCE section
above.

### OAuth 2.1 — Refresh token rotation

✅ **Already compliant.** `tokens.go:442` deletes the old API key when a
refresh token is used.

## Migration Plan

All DB changes can go in a single new migration (or extend 000420 if the
branch is rebased before merge). Columns to add:
- `redirect_uri text` on `oauth2_provider_app_codes`

The `state_hash` column is already added by migration 000420.

## Implementation Order

1. **Fix 7** — CSP headers on consent page (isolated, no deps)
2. ~~**Fix 2** — Require `state` parameter~~ (DROPPED — state stays
optional)
3. **Fix 4** — Exact redirect URI matching + store/verify redirect_uri
4. **PKCE mandatory** — Require `code_challenge` + reject
`response_type=token`
5. **Rollback** — Remove `"state"` from `RequiredNotEmpty` in
`authorize.go`
6. **Tests** — Update/add tests for all changes
7. **`make gen`** after DB changes

## Out of Scope (separate PRs)

- Bearer tokens in query strings (needs internal caller audit)
- Scope enforcement on OAuth2 tokens
- Rate limiting / quota on dynamic client registration

</details>

---
_Generated with [`mux`](https://github.com/coder/mux) • Model:
`anthropic:claude-opus-4-6` • Thinking: `xhigh`_
2026-02-23 12:18:44 +01:00
Ehab Younes 6683d807ac refactor: add RFC-compliant enum types and use SDK as source of truth (#21468)
Add comprehensive OAuth2 enum types to codersdk following RFC specifications:
- OAuth2ProviderGrantType (RFC 6749)
- OAuth2ProviderResponseType (RFC 6749)
- OAuth2TokenEndpointAuthMethod (RFC 7591)
- OAuth2PKCECodeChallengeMethod (RFC 7636)
- OAuth2TokenType (RFC 6749, RFC 9449)
- OAuth2RevocationTokenTypeHint (RFC 7009)
- OAuth2ErrorCode (RFC 6749, RFC 7009, RFC 8707)

Add OAuth2TokenRequest, OAuth2TokenResponse, OAuth2TokenRevocationRequest,
and OAuth2Error structs to the SDK. Update OAuth2ClientRegistrationRequest,
OAuth2ClientRegistrationResponse, OAuth2ClientConfiguration, and
OAuth2AuthorizationServerMetadata to use typed enums instead of raw strings.

This makes codersdk the single source of truth for OAuth2 types, eliminating
duplication between SDK and server-side structs.

Closes #21476
2026-01-15 12:41:28 +03:00
Steven Masley 13ca9ead3a chore!: ensure consistent secret token generation and hashing (#20388)
This PR uses the same sha256 hashing technique as we use for APIKeys. So
now all randomly generated secrets will be hashed with sha256 for
consistency.

This is a breaking change for the oauth tokens. Since oauth is only
allowed for dev builds and experimental, this is ok.
2025-10-23 15:38:49 -05:00
Steven Masley 4bd7c7b7e0 feat: implement oauth2 RFC 7009 token revocation endpoint (#20362)
Adds RFC 7009 token revocation endpoint
2025-10-22 15:18:42 -05:00
Thomas Kosiewski 088d14933c feat: ensure OAuth2 refresh tokens outlive access tokens (#19769) 2025-09-13 08:57:26 +02:00
Thomas Kosiewski 4607e5113b refactor: organize OAuth2 provider tests into dedicated packages (#18747)
# OAuth2 Provider Code Reorganization

This PR reorganizes the OAuth2 provider code to improve separation of concerns and maintainability. The changes include:

1. Migrating OAuth2 provider app validation tests from `coderd/oauth2_test.go` to `oauth2provider/provider_test.go`
2. Moving OAuth2 client registration validation tests to `oauth2provider/validation_test.go`
3. Adding new comprehensive test files for metadata and validation edge cases
4. Renaming `OAuth2ProviderAppSecret` to `AppSecret` for better naming consistency
5. Simplifying the main integration test in `oauth2_test.go` to focus on core functionality

The PR maintains all existing test coverage while organizing the code more logically, making it easier to understand and maintain the OAuth2 provider implementation. This reorganization will help with future enhancements to the OAuth2 provider functionality.
2025-07-03 20:41:47 +02:00
Thomas Kosiewski c65013384a refactor: move OAuth2 provider code to dedicated package (#18746)
# Refactor OAuth2 Provider Code into Dedicated Package

This PR refactors the OAuth2 provider functionality by moving it from the main `coderd` package into a dedicated `oauth2provider` package. The change improves code organization and maintainability without changing functionality.

Key changes:

- Created a new `oauth2provider` package to house all OAuth2 provider-related code
- Moved existing OAuth2 provider functionality from `coderd/identityprovider` to the new package
- Refactored handler functions to follow a consistent pattern of returning `http.HandlerFunc` instead of being handlers directly
- Split large files into smaller, more focused files organized by functionality:
  - `app_secrets.go` - Manages OAuth2 application secrets
  - `apps.go` - Handles OAuth2 application CRUD operations
  - `authorize.go` - Implements the authorization flow
  - `metadata.go` - Provides OAuth2 metadata endpoints
  - `registration.go` - Handles dynamic client registration
  - `revoke.go` - Implements token revocation
  - `secrets.go` - Manages secret generation and validation
  - `tokens.go` - Handles token issuance and validation

This refactoring improves code organization and makes the OAuth2 provider functionality more maintainable while preserving all existing behavior.
2025-07-03 20:24:45 +02:00
Thomas Kosiewski 60b08f0960 fix: remove unique constraint on OAuth2 provider app names (#18669)
# Remove unique constraint on OAuth2 provider app names

This PR removes the unique constraint on the `name` field in the `oauth2_provider_apps` table to comply with RFC 7591, which only requires unique client IDs, not unique client names.

Changes include:
- Removing the unique constraint from the database schema
- Adding migration files for both up and down migrations
- Removing the name uniqueness check in the in-memory database implementation
- Updating the unique constraint constants

Change-Id: Iae7a1a06546fbc8de541a52e291f8a4510d57e8a
Signed-off-by: Thomas Kosiewski <tk@coder.com>
2025-07-03 19:13:13 +02:00
Thomas Kosiewski 74e1d5c4b6 feat: implement OAuth2 dynamic client registration (RFC 7591/7592) (#18645)
# Implement OAuth2 Dynamic Client Registration (RFC 7591/7592)

This PR implements OAuth2 Dynamic Client Registration according to RFC 7591 and Client Configuration Management according to RFC 7592. These standards allow OAuth2 clients to register themselves programmatically with Coder as an authorization server.

Key changes include:

1. Added database schema extensions to support RFC 7591/7592 fields in the `oauth2_provider_apps` table
2. Implemented `/oauth2/register` endpoint for dynamic client registration (RFC 7591)
3. Added client configuration management endpoints (RFC 7592):
   - GET/PUT/DELETE `/oauth2/clients/{client_id}`
   - Registration access token validation middleware

4. Added comprehensive validation for OAuth2 client metadata:
   - URI validation with support for custom schemes for native apps
   - Grant type and response type validation
   - Token endpoint authentication method validation

5. Enhanced developer documentation with:
   - RFC compliance guidelines
   - Testing best practices to avoid race conditions
   - Systematic debugging approaches for OAuth2 implementations

The implementation follows security best practices from the RFCs, including proper token handling, secure defaults, and appropriate error responses. This enables third-party applications to integrate with Coder's OAuth2 provider capabilities programmatically.
2025-07-03 18:33:47 +02:00
Thomas Kosiewski f0c9c4dbcd feat: oauth2 - add RFC 8707 resource indicators and audience validation (#18575)
This pull request implements RFC 8707, Resource Indicators for OAuth 2.0 (https://datatracker.ietf.org/doc/html/rfc8707), to enhance the security of our OAuth 2.0 provider. 

This change enables proper audience validation and binds access tokens to their intended resource, which is crucial
  for preventing token misuse in multi-tenant environments or deployments with multiple resource servers.

##  Key Changes:


   * Resource Parameter Support: Adds support for the resource parameter in both the authorization (`/oauth2/authorize`) and token (`/oauth2/token`) endpoints, allowing clients to specify the intended resource server.
   * Audience Validation: Implements server-side validation to ensure that the resource parameter provided during the token exchange matches the one from the authorization request.
   * API Middleware Enforcement: Introduces a new validation step in the API authentication middleware (`coderd/httpmw/apikey.go`) to verify that the audience of the access token matches the resource server being accessed.
   * Database Schema Updates:
       * Adds a `resource_uri` column to the `oauth2_provider_app_codes` table to store the resource requested during authorization.
       * Adds an `audience` column to the `oauth2_provider_app_tokens` table to bind the issued token to a specific audience.
   * Enhanced PKCE: Includes a minor enhancement to the PKCE implementation to protect against timing attacks.
   * Comprehensive Testing: Adds extensive new tests to `coderd/oauth2_test.go` to cover various RFC 8707 scenarios, including valid flows, mismatched resources, and refresh token validation.

##  How it Works:


   1. An OAuth2 client specifies the target resource (e.g., https://coder.example.com) using the resource parameter in the authorization request.
   2. The authorization server stores this resource URI with the authorization code.
   3. During the token exchange, the server validates that the client provides the same resource parameter.
   4. The server issues an access token with an audience claim set to the validated resource URI.
   5. When the client uses the access token to call an API endpoint, the middleware verifies that the token's audience matches the URL of the Coder deployment, rejecting any tokens intended for a different resource.


  This ensures that a token issued for one Coder deployment cannot be used to access another, significantly strengthening our authentication security.

---

Change-Id: I3924cb2139e837e3ac0b0bd40a5aeb59637ebc1b
Signed-off-by: Thomas Kosiewski <tk@coder.com>
2025-07-02 17:49:00 +02:00
Thomas Kosiewski 6f2834f62a feat: oauth2 - add authorization server metadata endpoint and PKCE support (#18548)
## Summary

  This PR implements critical MCP OAuth2 compliance features for Coder's authorization server, adding PKCE support, resource parameter handling, and OAuth2 server metadata discovery. This brings Coder's OAuth2 implementation significantly closer to production readiness for MCP (Model Context Protocol)
  integrations.

  ## What's Added

  ### OAuth2 Authorization Server Metadata (RFC 8414)
  - Add `/.well-known/oauth-authorization-server` endpoint for automatic client discovery
  - Returns standardized metadata including supported grant types, response types, and PKCE methods
  - Essential for MCP client compatibility and OAuth2 standards compliance

  ### PKCE Support (RFC 7636)
  - Implement Proof Key for Code Exchange with S256 challenge method
  - Add `code_challenge` and `code_challenge_method` parameters to authorization flow
  - Add `code_verifier` validation in token exchange
  - Provides enhanced security for public clients (mobile apps, CLIs)

  ### Resource Parameter Support (RFC 8707)
  - Add `resource` parameter to authorization and token endpoints
  - Store resource URI and bind tokens to specific audiences
  - Critical for MCP's resource-bound token model

  ### Enhanced OAuth2 Error Handling
  - Add OAuth2-compliant error responses with proper error codes
  - Use standard error format: `{"error": "code", "error_description": "details"}`
  - Improve error consistency across OAuth2 endpoints

  ### Authorization UI Improvements
  - Fix authorization flow to use POST-based consent instead of GET redirects
  - Remove dependency on referer headers for security decisions
  - Improve CSRF protection with proper state parameter validation

  ## Why This Matters

  **For MCP Integration:** MCP requires OAuth2 authorization servers to support PKCE, resource parameters, and metadata discovery. Without these features, MCP clients cannot securely authenticate with Coder.

  **For Security:** PKCE prevents authorization code interception attacks, especially critical for public clients. Resource binding ensures tokens are only valid for intended services.

  **For Standards Compliance:** These are widely adopted OAuth2 extensions that improve interoperability with modern OAuth2 clients.

  ## Database Changes

  - **Migration 000343:** Adds `code_challenge`, `code_challenge_method`, `resource_uri` to `oauth2_provider_app_codes`
  - **Migration 000343:** Adds `audience` field to `oauth2_provider_app_tokens` for resource binding
  - **Audit Updates:** New OAuth2 fields properly tracked in audit system
  - **Backward Compatibility:** All changes maintain compatibility with existing OAuth2 flows

  ## Test Coverage

  - Comprehensive PKCE test suite in `coderd/identityprovider/pkce_test.go`
  - OAuth2 metadata endpoint tests in `coderd/oauth2_metadata_test.go`
  - Integration tests covering PKCE + resource parameter combinations
  - Negative tests for invalid PKCE verifiers and malformed requests

  ## Testing Instructions

  ```bash
  # Run the comprehensive OAuth2 test suite
  ./scripts/oauth2/test-mcp-oauth2.sh

  Manual Testing with Interactive Server

  # Start Coder in development mode
  ./scripts/develop.sh

  # In another terminal, set up test app and run interactive flow
  eval $(./scripts/oauth2/setup-test-app.sh)
  ./scripts/oauth2/test-manual-flow.sh
  # Opens browser with OAuth2 flow, handles callback automatically

  # Clean up when done
  ./scripts/oauth2/cleanup-test-app.sh

  Individual Component Testing

  # Test metadata endpoint
  curl -s http://localhost:3000/.well-known/oauth-authorization-server | jq .

  # Test PKCE generation
  ./scripts/oauth2/generate-pkce.sh

  # Run specific test suites
  go test -v ./coderd/identityprovider -run TestVerifyPKCE
  go test -v ./coderd -run TestOAuth2AuthorizationServerMetadata
```

  ### Breaking Changes

  None. All changes maintain backward compatibility with existing OAuth2 flows.

---

Change-Id: Ifbd0d9a543d545f9f56ecaa77ff2238542ff954a
Signed-off-by: Thomas Kosiewski <tk@coder.com>
2025-07-01 15:39:29 +02:00
ケイラ fae30a00fd chore: remove unnecessary redeclarations in for loops (#18440) 2025-06-20 13:16:55 -06:00
Asher 40e5ad5499 feat: make OAuth2 provider not enterprise-only (#12732) 2024-03-25 11:52:22 -08:00