From 74bdb7aadd16b5e3ed2c8e05ee458ddd7d9c6b86 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 30 Jul 2025 15:28:56 +0200 Subject: [PATCH 1/7] feat: validate presets on template import (#18844) Typos and other errors often result in invalid presets in a template. Coder would import these broken templates and present them to users when they create workspaces. An unsuspecting user who chooses a broken preset would then experience a failed workspace build with no obvious error message. This PR adds additional validation beyond what is possible in the Terraform provider schema. Coder will now present a more helpful error message to template authors when they upload a new template version: Screenshot 2025-07-14 at 12 22 49 The frontend warning is less helpful right now, but I'd like to address that in a follow-up since I need frontend help: image closes https://github.com/coder/coder/issues/17333 ## Summary by CodeRabbit * **New Features** * Improved validation and error reporting for template presets, providing clearer feedback when presets cannot be parsed or reference undefined parameters. * **Bug Fixes** * Enhanced error handling during template version creation to better detect and report issues with presets. * **Tests** * Added new tests to verify validation of both valid and invalid Terraform presets during template version creation. * Improved test reliability by enabling dynamic control over error injection in database-related tests. * **Chores** * Updated a dependency to the latest version for improved stability and features. (cherry picked from commit f256a23a773e235213f6fc53cf5dcf527c0601d8) --- coderd/dynamicparameters/error.go | 8 ++ coderd/dynamicparameters/presets.go | 28 +++++++ coderd/dynamicparameters/tags.go | 4 + coderd/templateversions.go | 8 ++ coderd/templateversions_test.go | 113 ++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+) create mode 100644 coderd/dynamicparameters/presets.go diff --git a/coderd/dynamicparameters/error.go b/coderd/dynamicparameters/error.go index 4c27905bfa832..ae2217936b9dd 100644 --- a/coderd/dynamicparameters/error.go +++ b/coderd/dynamicparameters/error.go @@ -26,6 +26,14 @@ func tagValidationError(diags hcl.Diagnostics) *DiagnosticError { } } +func presetValidationError(diags hcl.Diagnostics) *DiagnosticError { + return &DiagnosticError{ + Message: "Unable to validate presets", + Diagnostics: diags, + KeyedDiagnostics: make(map[string]hcl.Diagnostics), + } +} + type DiagnosticError struct { // Message is the human-readable message that will be returned to the user. Message string diff --git a/coderd/dynamicparameters/presets.go b/coderd/dynamicparameters/presets.go new file mode 100644 index 0000000000000..24974962e029f --- /dev/null +++ b/coderd/dynamicparameters/presets.go @@ -0,0 +1,28 @@ +package dynamicparameters + +import ( + "github.com/hashicorp/hcl/v2" + + "github.com/coder/preview" +) + +// CheckPresets extracts the preset related diagnostics from a template version preset +func CheckPresets(output *preview.Output, diags hcl.Diagnostics) *DiagnosticError { + de := presetValidationError(diags) + if output == nil { + return de + } + + presets := output.Presets + for _, preset := range presets { + if hcl.Diagnostics(preset.Diagnostics).HasErrors() { + de.Extend(preset.Name, hcl.Diagnostics(preset.Diagnostics)) + } + } + + if de.HasError() { + return de + } + + return nil +} diff --git a/coderd/dynamicparameters/tags.go b/coderd/dynamicparameters/tags.go index 38a9bf4691571..d9037db5dd909 100644 --- a/coderd/dynamicparameters/tags.go +++ b/coderd/dynamicparameters/tags.go @@ -11,6 +11,10 @@ import ( func CheckTags(output *preview.Output, diags hcl.Diagnostics) *DiagnosticError { de := tagValidationError(diags) + if output == nil { + return de + } + failedTags := output.WorkspaceTags.UnusableTags() if len(failedTags) == 0 && !de.HasError() { return nil // No errors, all is good! diff --git a/coderd/templateversions.go b/coderd/templateversions.go index cc106b390f73c..2a6e09d94978e 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -1822,6 +1822,14 @@ func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.Response return nil, false } + // Fails early if presets are invalid to prevent downstream workspace creation errors + presetErr := dynamicparameters.CheckPresets(output, nil) + if presetErr != nil { + code, resp := presetErr.Response() + httpapi.Write(ctx, rw, code, resp) + return nil, false + } + return output.WorkspaceTags.Tags(), true } diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 912bca1c5fec1..0b5bf6fcf2302 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -620,6 +620,119 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { }) } }) + + t.Run("Presets", func(t *testing.T) { + t.Parallel() + store, ps := dbtestutil.NewDB(t) + client := coderdtest.New(t, &coderdtest.Options{ + Database: store, + Pubsub: ps, + }) + owner := coderdtest.CreateFirstUser(t, client) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + for _, tt := range []struct { + name string + files map[string]string + expectError string + }{ + { + name: "valid preset", + files: map[string]string{ + `main.tf`: ` + terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.8.0" + } + } + } + data "coder_parameter" "valid_parameter" { + name = "valid_parameter_name" + default = "valid_option_value" + option { + name = "valid_option_name" + value = "valid_option_value" + } + } + data "coder_workspace_preset" "valid_preset" { + name = "valid_preset" + parameters = { + "valid_parameter_name" = "valid_option_value" + } + } + `, + }, + }, + { + name: "invalid preset", + files: map[string]string{ + `main.tf`: ` + terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.8.0" + } + } + } + data "coder_parameter" "valid_parameter" { + name = "valid_parameter_name" + default = "valid_option_value" + option { + name = "valid_option_name" + value = "valid_option_value" + } + } + data "coder_workspace_preset" "invalid_parameter_name" { + name = "invalid_parameter_name" + parameters = { + "invalid_parameter_name" = "irrelevant_value" + } + } + `, + }, + expectError: "Undefined Parameter", + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + // Create an archive from the files provided in the test case. + tarFile := testutil.CreateTar(t, tt.files) + + // Post the archive file + fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarFile)) + require.NoError(t, err) + + // Create a template version from the archive + tvName := testutil.GetRandomNameHyphenated(t) + tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: tvName, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + Provisioner: codersdk.ProvisionerTypeTerraform, + FileID: fi.ID, + }) + + if tt.expectError == "" { + require.NoError(t, err) + // Assert the expected provisioner job is created from the template version import + pj, err := store.GetProvisionerJobByID(ctx, tv.Job.ID) + require.NoError(t, err) + require.NotNil(t, pj) + // Also assert that we get the expected information back from the API endpoint + require.Zero(t, tv.MatchedProvisioners.Count) + require.Zero(t, tv.MatchedProvisioners.Available) + require.Zero(t, tv.MatchedProvisioners.MostRecentlySeen.Time) + } else { + require.ErrorContains(t, err, tt.expectError) + require.Equal(t, tv.Job.ID, uuid.Nil) + } + }) + } + }) } func TestPatchCancelTemplateVersion(t *testing.T) { From 4b227f56597da2d66fdc22074a4b95916604e3a5 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 4 Aug 2025 16:21:13 +0100 Subject: [PATCH 2/7] chore(agent/agentcontainers): disable project autostart by default (#19114) We disable the logic that allows autostarting discovered devcontainers by default. We want this behavior to be opt-in rather than opt-out. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> (cherry picked from commit b8e2344ef54b1f88af33935f1afb588f566f42a6) --- agent/agentcontainers/api.go | 26 +++++++--- agent/agentcontainers/api_test.go | 70 ++++++++++++++++++++++++++ cli/agent.go | 43 +++++++++------- cli/testdata/coder_agent_--help.golden | 4 ++ 4 files changed, 119 insertions(+), 24 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index c8cba67cea73f..dce0fb27bfc25 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -77,7 +77,8 @@ type API struct { subAgentURL string subAgentEnv []string - projectDiscovery bool // If we should perform project discovery or not. + projectDiscovery bool // If we should perform project discovery or not. + discoveryAutostart bool // If we should autostart discovered projects. ownerName string workspaceName string @@ -144,7 +145,8 @@ func WithCommandEnv(ce CommandEnv) Option { strings.HasPrefix(s, "CODER_AGENT_TOKEN=") || strings.HasPrefix(s, "CODER_AGENT_AUTH=") || strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_ENABLE=") || - strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE=") + strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE=") || + strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE=") }) return shell, dir, env, nil } @@ -287,6 +289,14 @@ func WithProjectDiscovery(projectDiscovery bool) Option { } } +// WithDiscoveryAutostart sets if the API should attempt to autostart +// projects that have been discovered +func WithDiscoveryAutostart(discoveryAutostart bool) Option { + return func(api *API) { + api.discoveryAutostart = discoveryAutostart + } +} + // ScriptLogger is an interface for sending devcontainer logs to the // controlplane. type ScriptLogger interface { @@ -542,11 +552,13 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error { Container: nil, } - config, err := api.dccli.ReadConfig(api.ctx, workspaceFolder, path, []string{}) - if err != nil { - logger.Error(api.ctx, "read project configuration", slog.Error(err)) - } else if config.Configuration.Customizations.Coder.AutoStart { - dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStarting + if api.discoveryAutostart { + config, err := api.dccli.ReadConfig(api.ctx, workspaceFolder, path, []string{}) + if err != nil { + logger.Error(api.ctx, "read project configuration", slog.Error(err)) + } else if config.Configuration.Customizations.Coder.AutoStart { + dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStarting + } } api.knownDevcontainers[workspaceFolder] = dc diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index e6e25ed92558e..76e0fca6714d5 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -3792,6 +3792,7 @@ func TestDevcontainerDiscovery(t *testing.T) { agentcontainers.WithContainerCLI(&fakeContainerCLI{}), agentcontainers.WithDevcontainerCLI(mDCCLI), agentcontainers.WithProjectDiscovery(true), + agentcontainers.WithDiscoveryAutostart(true), ) api.Start() defer api.Close() @@ -3813,5 +3814,74 @@ func TestDevcontainerDiscovery(t *testing.T) { // Then: We expect the mock infra to not fail. }) } + + t.Run("Disabled", func(t *testing.T) { + t.Parallel() + var ( + ctx = testutil.Context(t, testutil.WaitShort) + logger = testutil.Logger(t) + mClock = quartz.NewMock(t) + mDCCLI = acmock.NewMockDevcontainerCLI(gomock.NewController(t)) + + fs = map[string]string{ + "/home/coder/.git/HEAD": "", + "/home/coder/.devcontainer/devcontainer.json": "", + } + + r = chi.NewRouter() + ) + + // We expect that neither `ReadConfig`, nor `Up` are called as we + // have explicitly disabled the agentcontainers API from attempting + // to autostart devcontainers that it discovers. + mDCCLI.EXPECT().ReadConfig(gomock.Any(), + "/home/coder", + "/home/coder/.devcontainer/devcontainer.json", + []string{}, + ).Return(agentcontainers.DevcontainerConfig{ + Configuration: agentcontainers.DevcontainerConfiguration{ + Customizations: agentcontainers.DevcontainerCustomizations{ + Coder: agentcontainers.CoderCustomization{ + AutoStart: true, + }, + }, + }, + }, nil).Times(0) + + mDCCLI.EXPECT().Up(gomock.Any(), + "/home/coder", + "/home/coder/.devcontainer/devcontainer.json", + gomock.Any(), + ).Return("", nil).Times(0) + + api := agentcontainers.NewAPI(logger, + agentcontainers.WithClock(mClock), + agentcontainers.WithWatcher(watcher.NewNoop()), + agentcontainers.WithFileSystem(initFS(t, fs)), + agentcontainers.WithManifestInfo("owner", "workspace", "parent-agent", "/home/coder"), + agentcontainers.WithContainerCLI(&fakeContainerCLI{}), + agentcontainers.WithDevcontainerCLI(mDCCLI), + agentcontainers.WithProjectDiscovery(true), + agentcontainers.WithDiscoveryAutostart(false), + ) + api.Start() + defer api.Close() + r.Mount("/", api.Routes()) + + // When: All expected dev containers have been found. + require.Eventuallyf(t, func() bool { + req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + + got := codersdk.WorkspaceAgentListContainersResponse{} + err := json.NewDecoder(rec.Body).Decode(&got) + require.NoError(t, err) + + return len(got.Devcontainers) >= 1 + }, testutil.WaitShort, testutil.IntervalFast, "dev containers never found") + + // Then: We expect the mock infra to not fail. + }) }) } diff --git a/cli/agent.go b/cli/agent.go index 4f50fbfe88942..c192d4429ccaf 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -40,23 +40,24 @@ import ( func (r *RootCmd) workspaceAgent() *serpent.Command { var ( - auth string - logDir string - scriptDataDir string - pprofAddress string - noReap bool - sshMaxTimeout time.Duration - tailnetListenPort int64 - prometheusAddress string - debugAddress string - slogHumanPath string - slogJSONPath string - slogStackdriverPath string - blockFileTransfer bool - agentHeaderCommand string - agentHeader []string - devcontainers bool - devcontainerProjectDiscovery bool + auth string + logDir string + scriptDataDir string + pprofAddress string + noReap bool + sshMaxTimeout time.Duration + tailnetListenPort int64 + prometheusAddress string + debugAddress string + slogHumanPath string + slogJSONPath string + slogStackdriverPath string + blockFileTransfer bool + agentHeaderCommand string + agentHeader []string + devcontainers bool + devcontainerProjectDiscovery bool + devcontainerDiscoveryAutostart bool ) cmd := &serpent.Command{ Use: "agent", @@ -366,6 +367,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { DevcontainerAPIOptions: []agentcontainers.Option{ agentcontainers.WithSubAgentURL(r.agentURL.String()), agentcontainers.WithProjectDiscovery(devcontainerProjectDiscovery), + agentcontainers.WithDiscoveryAutostart(devcontainerDiscoveryAutostart), }, }) @@ -519,6 +521,13 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { Description: "Allow the agent to search the filesystem for devcontainer projects.", Value: serpent.BoolOf(&devcontainerProjectDiscovery), }, + { + Flag: "devcontainers-discovery-autostart-enable", + Default: "false", + Env: "CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE", + Description: "Allow the agent to autostart devcontainer projects it discovers based on their configuration.", + Value: serpent.BoolOf(&devcontainerDiscoveryAutostart), + }, } return cmd diff --git a/cli/testdata/coder_agent_--help.golden b/cli/testdata/coder_agent_--help.golden index 0627016855e08..c6d75705a6eb4 100644 --- a/cli/testdata/coder_agent_--help.golden +++ b/cli/testdata/coder_agent_--help.golden @@ -33,6 +33,10 @@ OPTIONS: --debug-address string, $CODER_AGENT_DEBUG_ADDRESS (default: 127.0.0.1:2113) The bind address to serve a debug HTTP server. + --devcontainers-discovery-autostart-enable bool, $CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE (default: false) + Allow the agent to autostart devcontainer projects it discovers based + on their configuration. + --devcontainers-enable bool, $CODER_AGENT_DEVCONTAINERS_ENABLE (default: true) Allow the agent to automatically detect running devcontainers. From 267d9be477719643912be93de3885277b17028ce Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:01:24 +1000 Subject: [PATCH 3/7] ci: set valid xcode version in release script (#19143) 16.0.0 was yanked from the macOS runners, so this will likely need cherry picking into the upcoming release branch. We've already checked everything builds fine on #19125. In a few releases we'll stop building the dylib and also therefore remove xcode as a dependency on coder/coder altogether. (cherry picked from commit b95cf47f9998e21f8d97c913424f5ec052c4ab19) --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9feaf72b938ff..6ea28ad87a90c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -60,7 +60,7 @@ jobs: - name: Switch XCode Version uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: - xcode-version: "16.0.0" + xcode-version: "16.1.0" - name: Setup Go uses: ./.github/actions/setup-go @@ -655,7 +655,7 @@ jobs: detached_signature="${binary}.asc" gcloud storage cp "./site/out/bin/${binary}" "gs://releases.coder.com/coder-cli/${version}/${binary}" gcloud storage cp "./site/out/bin/${detached_signature}" "gs://releases.coder.com/coder-cli/${version}/${detached_signature}" - done + done - name: Publish release run: | From 37d038693496cf09688adfcd8b5eb34cd1369fa2 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Mon, 4 Aug 2025 18:53:12 +0200 Subject: [PATCH 4/7] docs: document how to start a remote MCP Coder server (#19150) Document how to start a remote MCP Coder server. Addresses https://github.com/coder/internal/issues/823. (cherry picked from commit a02d1c1e59e2c4c8efa8a802eba04a84bc4af22b) --- docs/ai-coder/mcp-server.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/ai-coder/mcp-server.md b/docs/ai-coder/mcp-server.md index 29d602030ab58..e4ce4c25d7501 100644 --- a/docs/ai-coder/mcp-server.md +++ b/docs/ai-coder/mcp-server.md @@ -1,6 +1,6 @@ # MCP Server -Power users can configure Claude Desktop, Cursor, or other external agents to interact with Coder in order to: +Power users can configure [claude.ai](https://claude.ai), Claude Desktop, Cursor, or other external agents to interact with Coder in order to: - List workspaces - Create/start/stop workspaces @@ -12,6 +12,8 @@ Power users can configure Claude Desktop, Cursor, or other external agents to in In this model, any custom agent could interact with a remote Coder workspace, or Coder can be used in a remote pipeline or a larger workflow. +## Local MCP server + The Coder CLI has options to automatically configure MCP servers for you. On your local machine, run the following command: ```sh @@ -30,4 +32,27 @@ coder exp mcp server ``` > [!NOTE] -> The MCP server is authenticated with the same identity as your Coder CLI and can perform any action on the user's behalf. Fine-grained permissions and a remote MCP server are in development. [Contact us](https://coder.com/contact) if this use case is important to you. +> The MCP server is authenticated with the same identity as your Coder CLI and can perform any action on the user's behalf. Fine-grained permissions are in development. [Contact us](https://coder.com/contact) if this use case is important to you. + +## Remote MCP server + +Coder can expose an MCP server via HTTP. This is useful for connecting web-based agents, like https://claude.ai/, to Coder. This is an experimental feature and is subject to change. + +To enable this feature, activate the `oauth2` and `mcp-server-http` experiments using an environment variable or a CLI flag: + +```sh +CODER_EXPERIMENTS="oauth2,mcp-server-http" coder server +# or +coder server --experiments=oauth2,mcp-server-http +``` + +The Coder server will expose the MCP server at: + +```txt +https://coder.example.com/api/experimental/mcp/http +``` + +> [!NOTE] +> At this time, the remote MCP server is not compatible with web-based ChatGPT. + +Users can authenticate applications to use the remote MCP server with OAuth2. An authenticated application can perform any action on the user's behalf. Fine-grained permissions are in development. From 2f9e272352f26bf334a1aaceb7badab74a9e2a06 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 4 Aug 2025 20:17:47 +0200 Subject: [PATCH 5/7] docs: add OAuth2 provider experimental feature documentation (#19165) # Add OAuth2 Provider Documentation This PR adds comprehensive documentation for the experimental OAuth2 Provider feature, which allows Coder to function as an OAuth2 authorization server. The documentation covers: - Feature overview and experimental status warning - Setup requirements and enabling the feature - Methods for creating OAuth2 applications (UI and API) - Integration patterns including standard OAuth2 and PKCE flows - Discovery endpoints and token management - Testing and development guidance - Troubleshooting common issues - Security considerations and current limitations The documentation is marked as experimental and includes appropriate warnings about production usage. Signed-off-by: Thomas Kosiewski (cherry picked from commit 247efc0dcc7da73cd2ae80ad08d1491c8825c163) --- docs/admin/integrations/oauth2-provider.md | 239 +++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 docs/admin/integrations/oauth2-provider.md diff --git a/docs/admin/integrations/oauth2-provider.md b/docs/admin/integrations/oauth2-provider.md new file mode 100644 index 0000000000000..fb98e1520b2dd --- /dev/null +++ b/docs/admin/integrations/oauth2-provider.md @@ -0,0 +1,239 @@ +# OAuth2 Provider (Experimental) + +> ⚠️ **Experimental Feature** +> +> 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 `oauth2` experiment flag to be enabled +> +> Use 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: + +```bash +coder server --experiments oauth2 +``` + +Or set the environment variable: + +```env +CODER_EXPERIMENTS=oauth2 +``` + +## Creating OAuth2 Applications + +### Method 1: Web UI + +1. Navigate to **Deployment Settings** → **OAuth2 Applications** +2. Click **Create Application** +3. Fill in the application details: + - **Name**: Your application name + - **Callback URL**: `https://yourapp.example.com/callback` + - **Icon**: Optional icon URL + +### Method 2: Management API + +Create an application using the Coder API: + +```bash +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: + +```bash +curl -X POST \ + -H "Authorization: Bearer $CODER_SESSION_TOKEN" \ + "$CODER_URL/api/v2/oauth2-provider/apps/$APP_ID/secrets" +``` + +## Integration Patterns + +### Standard OAuth2 Flow + +1. **Authorization Request**: Redirect users to Coder's authorization endpoint: + + ```url + https://coder.example.com/oauth2/authorize? + client_id=your-client-id& + response_type=code& + redirect_uri=https://yourapp.example.com/callback& + state=random-string + ``` + +2. **Token Exchange**: Exchange the authorization code for an access token: + + ```bash + 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" + ``` + +3. **API Access**: Use the access token to call Coder's API: + + ```bash + curl -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$CODER_URL/api/v2/users/me" + ``` + +### PKCE Flow (Public Clients) + +For mobile apps and single-page applications, use PKCE for enhanced security: + +1. Generate a code verifier and challenge: + + ```bash + 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) + ``` + +2. Include PKCE parameters in the authorization request: + + ```url + 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 + ``` + +3. Include the code verifier in the token exchange: + + ```bash + curl -X POST \ + -d "grant_type=authorization_code" \ + -d "code=$AUTH_CODE" \ + -d "client_id=$CLIENT_ID" \ + -d "code_verifier=$CODE_VERIFIER" \ + "$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](https://datatracker.ietf.org/doc/html/rfc8414) and [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728). + +## Token Management + +### Refresh Tokens + +Refresh an expired access token: + +```bash +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: + +```bash +curl -X DELETE \ + -H "Authorization: Bearer $CODER_SESSION_TOKEN" \ + "$CODER_URL/oauth2/tokens?client_id=$CLIENT_ID" +``` + +## Testing and Development + +Coder provides comprehensive test scripts for OAuth2 development: + +```bash +# 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](../../../scripts/oauth2/README.md). + +## 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. + +### "PKCE verification failed" + +Verify that the `code_verifier` used in the token request matches the one used to generate the `code_challenge`. + +## Security Considerations + +- **Use HTTPS**: Always use HTTPS in production to protect tokens in transit +- **Implement PKCE**: Use PKCE for all public clients (mobile apps, SPAs) +- **Validate redirect URLs**: Only register trusted redirect URIs for your applications +- **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 +- Limited to opaque access tokens (no JWT support) + +## Standards Compliance + +This implementation follows established OAuth2 standards including [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) (OAuth2 core), [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) (PKCE), and related specifications for discovery and client registration. + +## Next Steps + +- Review the [API Reference](../../reference/api/index.md) for complete endpoint documentation +- Check [External Authentication](../external-auth/index.md) for configuring Coder as an OAuth2 client +- See [Security Best Practices](../security/index.md) for deployment security guidance + +--- + +> 📝 **Feedback** +> +> This is an experimental feature under active development. Please report issues and feedback through [GitHub Issues](https://github.com/coder/coder/issues) with the `oauth2` label. From b8be8cd4941036b03af438f530e48445bdbf794b Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Mon, 4 Aug 2025 21:20:51 +0200 Subject: [PATCH 6/7] docs: oauth2-provider fixes (#19170) Adds the oauth2-provider doc page to the manifest so it's rendered in the docs, fixes formatting in the oauth2-provider doc, and links to it from the MCP doc. To see the formatting issues, visit https://coder.com/docs/@4bcf44a/admin/integrations/oauth2-provider. To see the doc after the fixes, visit https://coder.com/docs/@f05969a/admin/integrations/oauth2-provider. (cherry picked from commit c94333d9b5db165fcaf449b1e19159d98c308f17) --- docs/admin/integrations/oauth2-provider.md | 9 +++------ docs/ai-coder/mcp-server.md | 2 +- docs/manifest.json | 5 +++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/admin/integrations/oauth2-provider.md b/docs/admin/integrations/oauth2-provider.md index fb98e1520b2dd..e5264904293f7 100644 --- a/docs/admin/integrations/oauth2-provider.md +++ b/docs/admin/integrations/oauth2-provider.md @@ -1,7 +1,6 @@ # OAuth2 Provider (Experimental) -> ⚠️ **Experimental Feature** -> +> [!WARNING] > The OAuth2 provider functionality is currently **experimental and unstable**. This feature: > > - Is subject to breaking changes without notice @@ -232,8 +231,6 @@ This implementation follows established OAuth2 standards including [RFC 6749](ht - Check [External Authentication](../external-auth/index.md) for configuring Coder as an OAuth2 client - See [Security Best Practices](../security/index.md) for deployment security guidance ---- +## Feedback -> 📝 **Feedback** -> -> This is an experimental feature under active development. Please report issues and feedback through [GitHub Issues](https://github.com/coder/coder/issues) with the `oauth2` label. +This is an experimental feature under active development. Please report issues and feedback through [GitHub Issues](https://github.com/coder/coder/issues) with the `oauth2` label. diff --git a/docs/ai-coder/mcp-server.md b/docs/ai-coder/mcp-server.md index e4ce4c25d7501..fdfadb4117d36 100644 --- a/docs/ai-coder/mcp-server.md +++ b/docs/ai-coder/mcp-server.md @@ -55,4 +55,4 @@ https://coder.example.com/api/experimental/mcp/http > [!NOTE] > At this time, the remote MCP server is not compatible with web-based ChatGPT. -Users can authenticate applications to use the remote MCP server with OAuth2. An authenticated application can perform any action on the user's behalf. Fine-grained permissions are in development. +Users can authenticate applications to use the remote MCP server with [OAuth2](../admin/integrations/oauth2-provider.md). An authenticated application can perform any action on the user's behalf. Fine-grained permissions are in development. diff --git a/docs/manifest.json b/docs/manifest.json index 0305105c029fd..9959fab8f79c0 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -718,6 +718,11 @@ "title": "Hashicorp Vault", "description": "Integrate Coder with Hashicorp Vault", "path": "./admin/integrations/vault.md" + }, + { + "title": "OAuth2 Provider", + "description": "Use Coder as an OAuth2 provider", + "path": "./admin/integrations/oauth2-provider.md" } ] }, From f54c8fef25ed7af3f95ad58a688864f9d935679a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Sat, 2 Aug 2025 18:23:46 +0100 Subject: [PATCH 7/7] ci: conditionally disable spotlight indexing (#19124) Work around for following issue: ``` Run sudo mdutil -a -i off sudo mdutil -a -i off sudo mdutil -X / sudo launchctl bootout system /System/Library/LaunchDaemons/com.apple.metadata.mds.plist shell: /bin/bash -e {0} 4 files/directories removed Boot-out failed: 5: Input/output error ``` This can happen if spotlight has already been disabled. (cherry picked from commit 6a35400f67679578283def7fe8f906d3454041a2) --- .github/workflows/ci.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 06efc8d5afd64..a55bd137f7994 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -340,6 +340,11 @@ jobs: - name: Disable Spotlight Indexing if: runner.os == 'macOS' run: | + enabled=$(sudo mdutil -a -s | grep "Indexing enabled" | wc -l) + if [ $enabled -eq 0 ]; then + echo "Spotlight indexing is already disabled" + exit 0 + fi sudo mdutil -a -i off sudo mdutil -X / sudo launchctl bootout system /System/Library/LaunchDaemons/com.apple.metadata.mds.plist