Adds the admin-controlled OAuth2 Dynamic Client Registration setting landed by #27316 (`GET`/`PUT /api/v2/oauth2-provider/settings`) to the OAuth2 Applications deployment settings page, since it was previously only reachable via the API or `coder oauth2-provider dcr enable|disable`. The page is now tabbed, **Applications** and **Settings**, so DCR has a home that further OAuth2 settings can share (an Initial Access Token setting is a likely next one). The active tab is backed by a `tab` search param, so `?tab=settings` links straight to it, and an unpermitted deep link falls back to **Applications** rather than selecting nothing. On the Settings tab, DCR renders as a titled section with a description, an `Enabled` badge when active, and an Enable/Disable button. Enabling opens a confirmation dialog, since it lets any OAuth2 client self-register against the deployment without prior admin approval (RFC 7591). Disabling is immediate, no confirmation. The control is a button rather than a switch on design feedback: a switch reads as an immediate on/off flip, which conflicts with a confirmation dialog standing in front of it, and it left the only explanation of the risk inside a dialog that disappears. A button carries the confirmation step without misrepresenting what a click costs, the always-visible description explains the setting on the page, and the `Enabled` badge gives the active state a persistent indicator. The layout follows Tracy's mockup on `tj/oauth2-apps-pagination`; the apps-table pagination work that shares that branch is deliberately not included here. Visibility and editability are gated on the same `ResourceDeploymentConfig` RBAC checks the endpoint itself enforces (`viewDeploymentConfig` / `editDeploymentConfig`), not a separate hardcoded check. The view takes the settings values as one optional `settings` prop, absent when the viewer lacks `viewDeploymentConfig`, so "cannot view" is the shape of the prop rather than a flag the caller keeps consistent with the values beside it, and the tab is not rendered at all. Closes https://github.com/coder/coder/issues/27432 ## Where this sits in the request path ```mermaid sequenceDiagram autonumber actor Admin participant View as OAuth2AppsSettingsPageView<br/>(Tabs + Enable/Disable + Dialog) participant Page as OAuth2AppsSettingsPage<br/>(React Query) participant S as coderd Note over Page: On mount Page->>S: GET /api/v2/oauth2-provider/settings S-->>Page: { dynamic_client_registration_enabled } Page-->>View: settings: { dynamicClientRegistrationEnabled, canEdit, ... } Note over Admin,View: Admin opens the Settings tab and enables DCR Admin->>View: click "Enable" View->>View: open confirmation dialog<br/>(no request sent yet) Admin->>View: click Confirm View->>Page: settings.onDynamicClientRegistrationChange(true) Page->>S: PUT /api/v2/oauth2-provider/settings<br/>{dynamic_client_registration_enabled: true} S-->>Page: 200 OK (audited) Page->>S: GET /api/v2/oauth2-provider/settings (refetch) S-->>Page: { dynamic_client_registration_enabled: true } Page-->>View: section shows the "Enabled" badge and a Disable button Note over Admin,View: Admin disables DCR Admin->>View: click "Disable" View->>Page: onDynamicClientRegistrationChange(false)<br/>(no dialog, disable is immediate) Page->>S: PUT ... {dynamic_client_registration_enabled: false} S-->>Page: 200 OK (audited) ``` ## Files changed All 10 files are hand-written; nothing in this PR is `make gen` output. | File | What changed | |---|---| | `site/src/api/api.ts` | New `getOAuth2ProviderSettings`/`putOAuth2ProviderSettings` methods, thin typed wrappers around the two endpoints #27316 added to `main`. | | `site/src/api/api.test.ts` | Covers both methods against the request they issue and the error they propagate. | | `site/src/api/queries/oauth2.ts` | A `getSettings` query and a `putSettings` mutation that invalidates the settings key on success. Both the app and settings keys now derive from a shared `oauth2ProviderKey` constant. | | `site/src/api/queries/oauth2.test.ts` | 4 tests: the key nesting, both delegations, and that a successful update invalidates the settings key without touching app queries. | | `.../OAuth2AppsSettingsPage.tsx` | Wires query and mutation into the page and passes the settings values down as one object, or omits it entirely without `viewDeploymentConfig`. The apps error stays its own prop, since the view gates the applications empty state on it. | | `.../OAuth2AppsSettingsPageView.tsx` | `Tabs` splitting Applications from Settings. The settings tab distinguishes loading, failed, and a value the server omitted rather than rendering nothing, and the header's "Add application" action is scoped to the applications tab. | | `.../OAuth2AppsSettingsPageView.stories.tsx` | 14 stories, covering the tab wiring, both permission boundaries, the header action's scope, and the settings tab's loading, fetch-error, update-error, and value-omitted states. | | `.../DynamicClientRegistrationSetting.tsx` | The section itself: heading, description including what disabling does not undo, `Enabled` badge, a permission explanation when the viewer cannot edit, and one button that confirms only in the enable direction. | | `.../DynamicClientRegistrationSetting.stories.tsx` | 11 stories, including focus surviving an in-flight request and the dialog ignoring a value that changes underneath it. | | `docs/admin/integrations/oauth2-provider.md` | Adds the web UI route to the DCR section, which previously enumerated only the CLI and the management API. | ## Suggested review order Follows the direction data actually flows, from the raw HTTP call up to the rendered section. 1. **`site/src/api/api.ts`**: the two new methods. Confirms they match the `codersdk.OAuth2ProviderSettings` shape #27316 landed and sit next to the existing OAuth2 app methods they mirror. 2. **`site/src/api/queries/oauth2.ts`**: the query/mutation pair. The mutation's `onSuccess` → `invalidateQueries` is the one detail worth double-checking: it's what makes the on-screen state catch up with what was just saved, rather than trusting the PUT payload. 3. **`OAuth2AppsSettingsPage.tsx`**: the container. Check the two separate permission gates (`viewDeploymentConfig` on the query's `enabled` option, `editDeploymentConfig` on the button's editability) match the RBAC the backend enforces. 4. **`OAuth2AppsSettingsPageView.tsx`**: the tabs and the settings tab's four states. The `settings` prop being optional is what hides the tab; the error inside the tab is deliberately separate from the page-level `error`, which gates the applications empty state. 5. **`DynamicClientRegistrationSetting.tsx`**: the section. Two things worth reading closely: the enable path opens the dialog while the disable path calls straight through, and lacking permission uses the native `disabled` attribute while an in-flight request uses `aria-disabled`, so a keyboard user is not blurred mid-flip. 6. **The two story files**: read last, as they exercise everything above without a real server. The dialog stories query `canvasElement.ownerDocument.body` rather than `canvasElement`, since the dialog renders into a portal attached to `<body>`. ## Deliberately not in this PR - **ENG-3116**: the applications list cannot distinguish self-registered clients from admin-created ones. Surfacing that needs a new field on `codersdk.OAuth2ProviderApp`, which is an API addition this PR does not need. - **ENG-3118**: reusing the shared `EnabledBadge` and `SettingsHeader` primitives for this section. Both hinge on what the mockup intends, and the badge in particular is a visible change either here or on the four other pages that share it. ## Screenshots Default (disabled): <img width="1676" height="497" alt="image" src="https://github.com/user-attachments/assets/cfa60266-8678-410e-9577-16ef474491e3" /> Enabling (confirmation dialog): <img width="1661" height="558" alt="image" src="https://github.com/user-attachments/assets/a7d54fdd-f65d-4fec-9ed9-3bfdcfdae5be" /> Enabled: <img width="1666" height="559" alt="image" src="https://github.com/user-attachments/assets/d39251c2-771c-4608-81c2-dda151b35c3d" /> --------- Co-authored-by: Tracy Johnson <tracy@coder.com>
13 KiB
OAuth2 Provider (Experimental)
Warning
The OAuth2 provider functionality is currently experimental and unstable. This feature:
- Is subject to breaking changes without notice
- May have incomplete functionality
- Is not recommended for production use
- Requires the
oauth2experiment flag to be enabledUse this feature for development and testing purposes only.
Coder can act as an OAuth2 authorization server, allowing third-party applications to authenticate users through Coder and access the Coder API on their behalf. This enables integrations where external applications can leverage Coder's authentication and user management.
Requirements
- Admin privileges in Coder
- OAuth2 experiment flag enabled
- HTTPS recommended for production deployments
Enable OAuth2 Provider
Add the oauth2 experiment flag to your Coder server:
coder server --experiments oauth2
Or set the environment variable:
CODER_EXPERIMENTS=oauth2
Creating OAuth2 Applications
Method 1: Web UI
- Navigate to Deployment Settings > OAuth2 Applications.
- On the Applications tab, select Add application.
- Fill in the application details:
- Name: Your application name
- Callback URL:
https://yourapp.example.com/callback(web) ormyapp://callback(native/desktop) - Icon: Optional icon URL
Method 2: Management API
Create an application using the Coder API:
curl -X POST \
-H "Authorization: Bearer $CODER_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Application",
"callback_url": "https://myapp.example.com/callback",
"icon": "https://myapp.example.com/icon.png"
}' \
"$CODER_URL/api/v2/oauth2-provider/apps"
Generate a client secret:
curl -X POST \
-H "Authorization: Bearer $CODER_SESSION_TOKEN" \
"$CODER_URL/api/v2/oauth2-provider/apps/$APP_ID/secrets"
Dynamic Client Registration
Dynamic Client Registration (RFC 7591) lets a client register itself against /oauth2/register instead of an admin creating the application manually. It's disabled by default; an owner must turn it on before any client can self-register.
Change the setting in the web UI:
- Navigate to Deployment Settings > OAuth2 Applications.
- Select the Settings tab.
- Select Enable or Disable next to Dynamic Client Registration.
Enabling asks you to confirm first.
Disabling does not.
The tab is linkable directly at https://$CODER_ACCESS_URL/deployment/oauth2-provider/apps?tab=settings.
Viewing the tab requires permission to view deployment configuration, and changing the setting requires permission to edit it. Without edit permission the button is present but inactive, and the page says why.
Check or change the setting with the CLI:
coder oauth2-provider dcr enable
coder oauth2-provider dcr disable
Or with the management API:
curl -X PUT \
-H "Authorization: Bearer $CODER_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"dynamic_client_registration_enabled": true}' \
"$CODER_URL/api/v2/oauth2-provider/settings"
curl -H "Authorization: Bearer $CODER_SESSION_TOKEN" \
"$CODER_URL/api/v2/oauth2-provider/settings"
Disabling only blocks new self-registrations. Applications that already registered while it was enabled keep authorizing and exchanging tokens normally; disabling does not revoke or otherwise affect them.
Integration Patterns
Client Authentication Methods
Coder supports the following OAuth2 client authentication methods at the token endpoint (/oauth2/tokens):
client_secret_basic(recommended): HTTP Basic authentication (RFC 6749 §2.3.1). The username isclient_idand the password isclient_secret.client_secret_post: Form-based authentication whereclient_idandclient_secretare sent in the request body.
Coder supports both methods for compatibility; existing integrations using client_secret_post do not need to change.
If you use Dynamic Client Registration (RFC 7591) and omit token_endpoint_auth_method, clients default to client_secret_basic. To request client_secret_post, set token_endpoint_auth_method to client_secret_post in the registration request.
If client authentication fails, the token endpoint returns HTTP 401 with an OAuth2 invalid_client error and a WWW-Authenticate: Basic realm="coder" response header.
Standard OAuth2 Flow
-
Authorization Request: Redirect users to Coder's authorization endpoint:
https://coder.example.com/oauth2/authorize? client_id=your-client-id& response_type=code& redirect_uri=https://yourapp.example.com/callback& state=random-string -
Token Exchange: Exchange the authorization code for an access token.
Option A: HTTP Basic authentication (
client_secret_basic, recommended)curl -X POST \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=$AUTH_CODE" \ -d "redirect_uri=https://yourapp.example.com/callback" \ "$CODER_URL/oauth2/tokens"Option B: Form parameters (
client_secret_post)curl -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=$AUTH_CODE" \ -d "client_id=$CLIENT_ID" \ -d "client_secret=$CLIENT_SECRET" \ -d "redirect_uri=https://yourapp.example.com/callback" \ "$CODER_URL/oauth2/tokens" -
API Access: Use the access token to call Coder's API:
curl -H "Authorization: Bearer $ACCESS_TOKEN" \ "$CODER_URL/api/v2/users/me"
Note
The PKCE flow below is the required integration path. The example above is shown for reference but omits the mandatory
code_challengeparameter. See PKCE Flow for the complete flow.
PKCE Flow (Required)
PKCE is required for all OAuth2 authorization code flows. Coder enforces PKCE in compliance with the OAuth 2.1 specification. Both public and confidential clients must include PKCE parameters:
-
Generate a code verifier and challenge:
CODE_VERIFIER=$(openssl rand -base64 96 | tr -d "=+/" | cut -c1-128) CODE_CHALLENGE=$(echo -n $CODE_VERIFIER | openssl dgst -sha256 -binary | base64 | tr -d "=+/" | cut -c1-43) -
Include PKCE parameters in the authorization request:
https://coder.example.com/oauth2/authorize? client_id=your-client-id& response_type=code& code_challenge=$CODE_CHALLENGE& code_challenge_method=S256& redirect_uri=https://yourapp.example.com/callback -
Include the code verifier in the token exchange (see Client Authentication Methods):
curl -X POST \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=$AUTH_CODE" \ -d "code_verifier=$CODE_VERIFIER" \ -d "redirect_uri=https://yourapp.example.com/callback" \ "$CODER_URL/oauth2/tokens"
Discovery Endpoints
Coder provides OAuth2 discovery endpoints for programmatic integration:
- Authorization Server Metadata:
GET /.well-known/oauth-authorization-server - Protected Resource Metadata:
GET /.well-known/oauth-protected-resource
These endpoints return server capabilities and endpoint URLs according to RFC 8414 and RFC 9728.
Token Management
Refresh Tokens
Refresh an expired access token.
Option A: HTTP Basic authentication (client_secret_basic)
curl -X POST \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=$REFRESH_TOKEN" \
"$CODER_URL/oauth2/tokens"
Option B: Form parameters (client_secret_post)
curl -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=$REFRESH_TOKEN" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
"$CODER_URL/oauth2/tokens"
Revoke Access
Revoke all tokens for an application:
curl -X DELETE \
-H "Authorization: Bearer $CODER_SESSION_TOKEN" \
"$CODER_URL/oauth2/tokens?client_id=$CLIENT_ID"
This ends existing sessions but leaves the application registered, so it can authorize again.
Delete an Application
Deleting an application is a separate operation from revoking its tokens. It removes the registration itself, so the client cannot authorize again without being registered anew.
In the web UI, navigate to Deployment Settings > OAuth2 Applications, select the application on the Applications tab, then select Delete. This requires permission to delete OAuth2 applications.
Or with the management API:
curl -X DELETE \
-H "Authorization: Bearer $CODER_SESSION_TOKEN" \
"$CODER_URL/api/v2/oauth2-provider/apps/$APP_ID"
This is also how you remove clients that registered themselves while dynamic client registration was enabled. Turning the setting off stops new registrations; it does not remove the ones already there.
Testing and Development
Coder provides comprehensive test scripts for OAuth2 development:
# Navigate to the OAuth2 test scripts
cd scripts/oauth2/
# Run the full automated test suite
./test-mcp-oauth2.sh
# Create a test application for manual testing
eval $(./setup-test-app.sh)
# Run an interactive browser-based test
./test-manual-flow.sh
# Clean up when done
./cleanup-test-app.sh
For more details on testing, see the OAuth2 test scripts README.
Common Issues
"OAuth2 experiment not enabled"
Add oauth2 to your experiment flags: coder server --experiments oauth2
"Invalid redirect_uri"
Ensure the redirect URI in your request exactly matches the one registered for your application.
"Invalid Callback URL" on the consent page
If you see this error when authorizing, the registered callback URL uses a
blocked scheme (javascript:, data:, file:, or ftp:). Update the
application's callback URL to a valid scheme (see
Callback URL schemes).
"PKCE verification failed"
Verify that the code_verifier used in the token request matches the one used to generate the code_challenge.
Callback URL schemes
Custom URI schemes (myapp://, vscode://, jetbrains://, etc.) are fully supported for native and desktop applications. The OS routes the redirect back to the registered application without requiring a running HTTP server.
The following schemes are blocked for security reasons: javascript:, data:, file:, ftp:.
Security Considerations
- Use HTTPS: Always use HTTPS in production to protect tokens in transit
- Implement PKCE: PKCE is mandatory for all authorization code clients (public and confidential)
- Validate redirect URLs: Only register trusted redirect URIs. Dangerous
schemes (
javascript:,data:,file:,ftp:) are blocked by the server, but custom URI schemes for native apps (myapp://) are permitted - Rotate secrets: Periodically rotate client secrets using the management API
Limitations
As an experimental feature, the current implementation has limitations:
- No scope system - all tokens have full API access
- No client credentials grant support
- Implicit grant (
response_type=token) is not supported; OAuth 2.1 deprecated this flow due to token leakage risks, and requests returnunsupported_response_type - Limited to opaque access tokens (no JWT support)
Standards Compliance
This implementation follows established OAuth2 standards including RFC 6749 (OAuth2 core), RFC 7636 (PKCE), and the OAuth 2.1 draft. Coder enforces OAuth 2.1 requirements including mandatory PKCE for all authorization code grants, exact redirect URI string matching, rejection of the implicit grant, and CSRF protections on consent pages.
Next Steps
- Review the API Reference for complete endpoint documentation
- Check External Authentication for configuring Coder as an OAuth2 client
- See Security Best Practices for deployment security guidance
Feedback
This is an experimental feature under active development. Please report issues and feedback through GitHub Issues with the oauth2 label.