From ad2d849d12bed35b4db6d9fe06df240099729eaf Mon Sep 17 00:00:00 2001 From: Bobby Ho Date: Fri, 11 Sep 2026 13:31:56 -0700 Subject: [PATCH 1/2] feat: add the CODER_OAUTH2_PROVIDER_ENABLE deployment flag Add an opt-in deployment flag for Coder's OAuth 2.1 authorization server. The flag is off by default and nothing reads it yet; a follow-up moves the OAuth2 routes and dashboard navigation off the oauth2 experiment and onto this flag. - Add oauth2.provider.enable (--oauth2-provider-enable, CODER_OAUTH2_PROVIDER_ENABLE) under a new Provider group beneath OAuth2, and reword the OAuth2 group so it covers more than GitHub login. - Add oauth2_provider to the build info response so the dashboard can read the provider state without an admin-only config request. - Turn the flag on by default in coderdtest so existing OAuth2 tests keep working once the routes are gated on it. - Regenerate API docs, TypeScript types, the CLI reference, and golden files. Part of PLAT-492. --- cli/testdata/coder_server_--help.golden | 7 ++++ cli/testdata/server-config.yaml.golden | 6 ++++ coderd/apidoc/docs.go | 15 +++++++++ coderd/apidoc/swagger.json | 15 +++++++++ coderd/coderd.go | 1 + coderd/coderd_test.go | 15 +++++++++ coderd/coderdtest/coderdtest.go | 3 ++ codersdk/deployment.go | 29 +++++++++++++++-- docs/admin/setup/configuration-reference.md | 13 +++++++- docs/reference/api/general.md | 4 +++ docs/reference/api/schemas.md | 32 +++++++++++++++++-- docs/reference/cli/server.md | 11 +++++++ .../cli/testdata/coder_server_--help.golden | 7 ++++ site/src/api/typesGenerated.ts | 15 +++++++++ site/src/testHelpers/entities.ts | 1 + site/src/utils/buildInfo.test.ts | 1 + 16 files changed, 169 insertions(+), 6 deletions(-) diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 2103d10031b..2b51b67a6ec 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -693,6 +693,13 @@ OAUTH2 / GITHUB OPTIONS: Base URL of a GitHub Enterprise deployment to use for Login with GitHub. +OAUTH2 / PROVIDER OPTIONS: + --oauth2-provider-enable bool, $CODER_OAUTH2_PROVIDER_ENABLE (default: false) + Enable the OAuth 2.1 authorization server, which lets external + applications (such as MCP clients) obtain tokens for Coder on behalf + of users. Disabled by default. When disabled, the OAuth2 endpoints and + discovery documents return 404. + OIDC OPTIONS: --oidc-group-auto-create bool, $CODER_OIDC_GROUP_AUTO_CREATE (default: false) Automatically creates missing groups from a user's groups claim. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index a8e96c2b60d..66196ed24a1 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -321,6 +321,12 @@ oauth2: # Base URL of a GitHub Enterprise deployment to use for Login with GitHub. # (default: , type: string) enterpriseBaseURL: "" + provider: + # Enable the OAuth 2.1 authorization server, which lets external applications + # (such as MCP clients) obtain tokens for Coder on behalf of users. Disabled by + # default. When disabled, the OAuth2 endpoints and discovery documents return 404. + # (default: false, type: bool) + enable: false oidc: # Whether new users can sign up with OIDC. # (default: true, type: bool) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 72bf693deb0..57dee3f6d16 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -19263,6 +19263,10 @@ const docTemplate = `{ "description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.", "type": "string" }, + "oauth2_provider": { + "description": "OAuth2Provider reports whether the OAuth 2.1 authorization server is\nenabled. The dashboard uses it to show or hide OAuth2 navigation.", + "type": "boolean" + }, "provisioner_api_version": { "description": "ProvisionerAPIVersion is the current version of the Provisioner API", "type": "string" @@ -25157,6 +25161,9 @@ const docTemplate = `{ "properties": { "github": { "$ref": "#/definitions/codersdk.OAuth2GithubConfig" + }, + "provider": { + "$ref": "#/definitions/codersdk.OAuth2ProviderConfig" } } }, @@ -25333,6 +25340,14 @@ const docTemplate = `{ } } }, + "codersdk.OAuth2ProviderConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + } + } + }, "codersdk.OAuth2ProviderGrantType": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 58f7ca844a1..a8184b49458 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -17309,6 +17309,10 @@ "description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.", "type": "string" }, + "oauth2_provider": { + "description": "OAuth2Provider reports whether the OAuth 2.1 authorization server is\nenabled. The dashboard uses it to show or hide OAuth2 navigation.", + "type": "boolean" + }, "provisioner_api_version": { "description": "ProvisionerAPIVersion is the current version of the Provisioner API", "type": "string" @@ -22989,6 +22993,9 @@ "properties": { "github": { "$ref": "#/definitions/codersdk.OAuth2GithubConfig" + }, + "provider": { + "$ref": "#/definitions/codersdk.OAuth2ProviderConfig" } } }, @@ -23162,6 +23169,14 @@ } } }, + "codersdk.OAuth2ProviderConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + } + } + }, "codersdk.OAuth2ProviderGrantType": { "type": "string", "enum": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index 1b391185eb3..d26f1d3922f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -773,6 +773,7 @@ func New(options *Options) *API { DeploymentID: api.DeploymentID, WebPushPublicKey: api.WebpushDispatcher.PublicKey(), Telemetry: api.Telemetry.Enabled(), + OAuth2Provider: options.DeploymentValues.OAuth2.Provider.Enable.Value(), } api.SiteHandler, err = site.New(&site.Options{ CacheDir: siteCacheDir, diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index 16dda77f86a..6258ff8daeb 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -56,6 +56,21 @@ func TestBuildInfo(t *testing.T) { require.NoError(t, err) require.Equal(t, buildinfo.ExternalURL(), buildInfo.ExternalURL, "external URL") require.Equal(t, buildinfo.Version(), buildInfo.Version, "version") + require.True(t, buildInfo.OAuth2Provider, "coderdtest enables the OAuth2 provider by default") +} + +func TestBuildInfoOAuth2ProviderDisabled(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { + dv.OAuth2.Provider.Enable = false + }), + }) + + ctx := testutil.Context(t, testutil.WaitLong) + buildInfo, err := client.BuildInfo(ctx) + require.NoError(t, err) + require.False(t, buildInfo.OAuth2Provider) } func TestDERP(t *testing.T) { diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 251648866de..90415901c69 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -1861,6 +1861,9 @@ func DeploymentValues(t testing.TB, mut ...func(*codersdk.DeploymentValues)) *co opts := cfg.Options() err := opts.SetDefaults() require.NoError(t, err) + // The OAuth2 provider is off by default in production. Tests turn it on + // so OAuth2 routes are reachable without extra setup. + cfg.OAuth2.Provider.Enable = true for _, fn := range mut { fn(cfg) } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index a5805de74cb..7287eb909c3 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -976,7 +976,14 @@ type PprofConfig struct { } type OAuth2Config struct { - Github OAuth2GithubConfig `json:"github" typescript:",notnull"` + Github OAuth2GithubConfig `json:"github" typescript:",notnull"` + Provider OAuth2ProviderConfig `json:"provider" typescript:",notnull"` +} + +// OAuth2ProviderConfig configures Coder's own OAuth 2.1 authorization server. +// This is separate from the GitHub login integration. +type OAuth2ProviderConfig struct { + Enable serpent.Bool `json:"enable" typescript:",notnull"` } type OAuth2GithubConfig struct { @@ -1595,7 +1602,7 @@ communicating directly.`, } deploymentGroupOAuth2 = serpent.Group{ Name: "OAuth2", - Description: `Configure login and user-provisioning with GitHub via oAuth2.`, + Description: `Configure OAuth2: GitHub login and user-provisioning, and Coder's own OAuth 2.1 authorization server.`, YAML: "oauth2", } deploymentGroupOAuth2GitHub = serpent.Group{ @@ -1603,6 +1610,11 @@ communicating directly.`, Name: "GitHub", YAML: "github", } + deploymentGroupOAuth2Provider = serpent.Group{ + Parent: &deploymentGroupOAuth2, + Name: "Provider", + YAML: "provider", + } deploymentGroupOIDC = serpent.Group{ Name: "OIDC", YAML: "oidc", @@ -2696,6 +2708,16 @@ communicating directly.`, Group: &deploymentGroupOAuth2GitHub, YAML: "enterpriseBaseURL", }, + { + Name: "OAuth2 Provider Enable", + Description: "Enable the OAuth 2.1 authorization server, which lets external applications (such as MCP clients) obtain tokens for Coder on behalf of users. Disabled by default. When disabled, the OAuth2 endpoints and discovery documents return 404.", + Flag: "oauth2-provider-enable", + Env: "CODER_OAUTH2_PROVIDER_ENABLE", + Value: &c.OAuth2.Provider.Enable, + Group: &deploymentGroupOAuth2Provider, + YAML: "enable", + Default: "false", + }, // OIDC settings. { Name: "OIDC Allow Signups", @@ -5104,6 +5126,9 @@ type BuildInfoResponse struct { DashboardURL string `json:"dashboard_url"` // Telemetry is a boolean that indicates whether telemetry is enabled. Telemetry bool `json:"telemetry"` + // OAuth2Provider reports whether the OAuth 2.1 authorization server is + // enabled. The dashboard uses it to show or hide OAuth2 navigation. + OAuth2Provider bool `json:"oauth2_provider"` WorkspaceProxy bool `json:"workspace_proxy"` diff --git a/docs/admin/setup/configuration-reference.md b/docs/admin/setup/configuration-reference.md index 2f81cae4186..3fcd8b98e6b 100644 --- a/docs/admin/setup/configuration-reference.md +++ b/docs/admin/setup/configuration-reference.md @@ -1308,7 +1308,7 @@ The endpoint to which to send webhooks. ## OAuth2 -Configure login and user-provisioning with GitHub via oAuth2. +Configure OAuth2: GitHub login and user-provisioning, and Coder's own OAuth 2.1 authorization server. ### GitHub @@ -1385,6 +1385,17 @@ Base URL of a GitHub Enterprise deployment to use for Login with GitHub. - CLI flag: [`--oauth2-github-enterprise-base-url`](../../reference/cli/server.md#--oauth2-github-enterprise-base-url) - YAML key: `oauth2.github.enterpriseBaseURL` +### Provider + +#### Enable + +Enable the OAuth 2.1 authorization server, which lets external applications (such as MCP clients) obtain tokens for Coder on behalf of users. Disabled by default. When disabled, the OAuth2 endpoints and discovery documents return 404. + +- Environment variable: `CODER_OAUTH2_PROVIDER_ENABLE` +- CLI flag: [`--oauth2-provider-enable`](../../reference/cli/server.md#--oauth2-provider-enable) +- YAML key: `oauth2.provider.enable` +- Default value: `false` + ## OIDC ### Enable OIDC group auto create diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 9ab85e12f1a..20d432f1e49 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -62,6 +62,7 @@ curl -X GET http://coder-server:8080/api/v2/buildinfo \ "dashboard_url": "string", "deployment_id": "string", "external_url": "string", + "oauth2_provider": true, "provisioner_api_version": "string", "telemetry": true, "upgrade_message": "string", @@ -430,6 +431,9 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "default_provider_enable": true, "device_flow": true, "enterprise_base_url": "string" + }, + "provider": { + "enable": true } }, "oidc": { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index ce95802e3e0..85ef297edab 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -2060,6 +2060,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "dashboard_url": "string", "deployment_id": "string", "external_url": "string", + "oauth2_provider": true, "provisioner_api_version": "string", "telemetry": true, "upgrade_message": "string", @@ -2077,6 +2078,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `dashboard_url` | string | false | | Dashboard URL is the URL to hit the deployment's dashboard. For external workspace proxies, this is the coderd they are connected to. | | `deployment_id` | string | false | | Deployment ID is the unique identifier for this deployment. | | `external_url` | string | false | | External URL references the current Coder version. For production builds, this will link directly to a release. For development builds, this will link to a commit. | +| `oauth2_provider` | boolean | false | | Oauth2 provider reports whether the OAuth 2.1 authorization server is enabled. The dashboard uses it to show or hide OAuth2 navigation. | | `provisioner_api_version` | string | false | | Provisioner api version is the current version of the Provisioner API | | `telemetry` | boolean | false | | Telemetry is a boolean that indicates whether telemetry is enabled. | | `upgrade_message` | string | false | | Upgrade message is the message displayed to users when an outdated client is detected. | @@ -7486,6 +7488,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "default_provider_enable": true, "device_flow": true, "enterprise_base_url": "string" + }, + "provider": { + "enable": true } }, "oidc": { @@ -8092,6 +8097,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "default_provider_enable": true, "device_flow": true, "enterprise_base_url": "string" + }, + "provider": { + "enable": true } }, "oidc": { @@ -10769,15 +10777,19 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "default_provider_enable": true, "device_flow": true, "enterprise_base_url": "string" + }, + "provider": { + "enable": true } } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|----------|------------------------------------------------------------|----------|--------------|-------------| -| `github` | [codersdk.OAuth2GithubConfig](#codersdkoauth2githubconfig) | false | | | +| Name | Type | Required | Restrictions | Description | +|------------|----------------------------------------------------------------|----------|--------------|-------------| +| `github` | [codersdk.OAuth2GithubConfig](#codersdkoauth2githubconfig) | false | | | +| `provider` | [codersdk.OAuth2ProviderConfig](#codersdkoauth2providerconfig) | false | | | ## codersdk.OAuth2Error @@ -10946,6 +10958,20 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `client_secret_full` | string | false | | | | `id` | string | false | | | +## codersdk.OAuth2ProviderConfig + +```json +{ + "enable": true +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|----------|---------|----------|--------------|-------------| +| `enable` | boolean | false | | | + ## codersdk.OAuth2ProviderGrantType ```json diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 09160f2cb5b..7235e945dc3 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -461,6 +461,17 @@ Allow all logins, setting this option means allowed orgs and teams must be empty Base URL of a GitHub Enterprise deployment to use for Login with GitHub. +### --oauth2-provider-enable + +| | | +|-------------|--------------------------------------------| +| Type | bool | +| Environment | $CODER_OAUTH2_PROVIDER_ENABLE | +| YAML | oauth2.provider.enable | +| Default | false | + +Enable the OAuth 2.1 authorization server, which lets external applications (such as MCP clients) obtain tokens for Coder on behalf of users. Disabled by default. When disabled, the OAuth2 endpoints and discovery documents return 404. + ### --oidc-allow-signups | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index c1bcdf8aab4..f13ce04d802 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -694,6 +694,13 @@ OAUTH2 / GITHUB OPTIONS: Base URL of a GitHub Enterprise deployment to use for Login with GitHub. +OAUTH2 / PROVIDER OPTIONS: + --oauth2-provider-enable bool, $CODER_OAUTH2_PROVIDER_ENABLE (default: false) + Enable the OAuth 2.1 authorization server, which lets external + applications (such as MCP clients) obtain tokens for Coder on behalf + of users. Disabled by default. When disabled, the OAuth2 endpoints and + discovery documents return 404. + OIDC OPTIONS: --oidc-group-auto-create bool, $CODER_OIDC_GROUP_AUTO_CREATE (default: false) Automatically creates missing groups from a user's groups claim. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 01c3c665030..ac33c4ee9d6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1792,6 +1792,11 @@ export interface BuildInfoResponse { * Telemetry is a boolean that indicates whether telemetry is enabled. */ readonly telemetry: boolean; + /** + * OAuth2Provider reports whether the OAuth 2.1 authorization server is + * enabled. The dashboard uses it to show or hide OAuth2 navigation. + */ + readonly oauth2_provider: boolean; readonly workspace_proxy: boolean; /** * AgentAPIVersion is the current version of the Agent API (back versions @@ -6592,6 +6597,7 @@ export const OAuth2ClientTypes: OAuth2ClientType[] = ["confidential", "public"]; // From codersdk/deployment.go export interface OAuth2Config { readonly github: OAuth2GithubConfig; + readonly provider: OAuth2ProviderConfig; } // From codersdk/oauth2.go @@ -6711,6 +6717,15 @@ export interface OAuth2ProviderAppSecretFull { readonly client_secret_full: string; } +// From codersdk/deployment.go +/** + * OAuth2ProviderConfig configures Coder's own OAuth 2.1 authorization server. + * This is separate from the GitHub login integration. + */ +export interface OAuth2ProviderConfig { + readonly enable: boolean; +} + // From codersdk/oauth2.go export type OAuth2ProviderGrantType = | "authorization_code" diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index d452644dcfc..b785d8c7075 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -248,6 +248,7 @@ export const MockBuildInfo: TypesGen.BuildInfoResponse = { deployment_id: "510d407f-e521-4180-b559-eab4a6d802b8", webpush_public_key: "fake-public-key", telemetry: true, + oauth2_provider: true, }; export const MockSupportLinks: TypesGen.LinkConfig[] = [ diff --git a/site/src/utils/buildInfo.test.ts b/site/src/utils/buildInfo.test.ts index 84282396201..64920d45a7e 100644 --- a/site/src/utils/buildInfo.test.ts +++ b/site/src/utils/buildInfo.test.ts @@ -11,6 +11,7 @@ const baseBuildInfo: BuildInfoResponse = { upgrade_message: "", deployment_id: "test", telemetry: false, + oauth2_provider: false, }; describe("getPrereleaseFlag", () => { From 06c288ff360ddfc2b23d65275392c6892e3c92b8 Mon Sep 17 00:00:00 2001 From: Bobby Ho Date: Fri, 11 Sep 2026 14:26:11 -0700 Subject: [PATCH 2/2] fix: address review notes on the OAuth2 provider flag Read the build info flag value through api.DeploymentValues like the neighbouring fields, and distinguish OAuth2ProviderConfig from OAuth2ProviderSettings in its doc comment. --- coderd/coderd.go | 2 +- codersdk/deployment.go | 5 ++++- site/src/api/typesGenerated.ts | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index d26f1d3922f..f3799ff5a88 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -773,7 +773,7 @@ func New(options *Options) *API { DeploymentID: api.DeploymentID, WebPushPublicKey: api.WebpushDispatcher.PublicKey(), Telemetry: api.Telemetry.Enabled(), - OAuth2Provider: options.DeploymentValues.OAuth2.Provider.Enable.Value(), + OAuth2Provider: api.DeploymentValues.OAuth2.Provider.Enable.Value(), } api.SiteHandler, err = site.New(&site.Options{ CacheDir: siteCacheDir, diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 7287eb909c3..ddec9b189d4 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -981,7 +981,10 @@ type OAuth2Config struct { } // OAuth2ProviderConfig configures Coder's own OAuth 2.1 authorization server. -// This is separate from the GitHub login integration. +// This is separate from the GitHub login integration. It is also distinct +// from OAuth2ProviderSettings: this struct decides whether the server is on +// at all, while OAuth2ProviderSettings holds runtime behavior such as +// dynamic client registration that admins change while it runs. type OAuth2ProviderConfig struct { Enable serpent.Bool `json:"enable" typescript:",notnull"` } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ac33c4ee9d6..a214fc2bde4 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -6720,7 +6720,10 @@ export interface OAuth2ProviderAppSecretFull { // From codersdk/deployment.go /** * OAuth2ProviderConfig configures Coder's own OAuth 2.1 authorization server. - * This is separate from the GitHub login integration. + * This is separate from the GitHub login integration. It is also distinct + * from OAuth2ProviderSettings: this struct decides whether the server is on + * at all, while OAuth2ProviderSettings holds runtime behavior such as + * dynamic client registration that admins change while it runs. */ export interface OAuth2ProviderConfig { readonly enable: boolean;