Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bf6a00876 | ||
|
|
9eb5fc695e | ||
|
|
079328d874 | ||
|
|
e68ffe85b7 | ||
|
|
e6ec95757a | ||
|
|
f1cf81c10b |
2
.github/actions/setup-go/action.yaml
vendored
2
.github/actions/setup-go/action.yaml
vendored
@@ -4,7 +4,7 @@ description: |
|
||||
inputs:
|
||||
version:
|
||||
description: "The Go version to use."
|
||||
default: "1.24.4"
|
||||
default: "1.24.6"
|
||||
use-preinstalled-go:
|
||||
description: "Whether to use preinstalled Go."
|
||||
default: "false"
|
||||
|
||||
7
.github/workflows/ci.yaml
vendored
7
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
@@ -959,7 +964,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
|
||||
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
43
cli/agent.go
43
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
|
||||
|
||||
4
cli/testdata/coder_agent_--help.golden
vendored
4
cli/testdata/coder_agent_--help.golden
vendored
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
28
coderd/dynamicparameters/presets.go
Normal file
28
coderd/dynamicparameters/presets.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
236
docs/admin/integrations/oauth2-provider.md
Normal file
236
docs/admin/integrations/oauth2-provider.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# 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 `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.
|
||||
@@ -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](../admin/integrations/oauth2-provider.md). An authenticated application can perform any action on the user's behalf. Fine-grained permissions are in development.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ RUN cargo install jj-cli typos-cli watchexec-cli
|
||||
FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 AS go
|
||||
|
||||
# Install Go manually, so that we can control the version
|
||||
ARG GO_VERSION=1.24.4
|
||||
ARG GO_VERSION=1.24.6
|
||||
|
||||
# Boring Go is needed to build FIPS-compliant binaries.
|
||||
RUN apt-get update && \
|
||||
|
||||
4
go.mod
4
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/coder/coder/v2
|
||||
|
||||
go 1.24.4
|
||||
go 1.24.6
|
||||
|
||||
// Required until a v3 of chroma is created to lazily initialize all XML files.
|
||||
// None of our dependencies seem to use the registries anyways, so this
|
||||
@@ -58,7 +58,7 @@ replace github.com/imulab/go-scim/pkg/v2 => github.com/coder/go-scim/pkg/v2 v2.0
|
||||
// Adds support for a new Listener from a driver.Connector
|
||||
// This lets us use rotating authentication tokens for passwords in connection strings
|
||||
// which we use in the awsiamrds package.
|
||||
replace github.com/lib/pq => github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102
|
||||
replace github.com/lib/pq => github.com/coder/pq v1.10.5-0.20250807075151-6ad9b0a25151
|
||||
|
||||
// Removes an init() function that causes terminal sequences to be printed to the web terminal when
|
||||
// used in conjunction with agent-exec. See https://github.com/coder/coder/pull/15817
|
||||
|
||||
4
go.sum
4
go.sum
@@ -912,8 +912,8 @@ github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136 h1:0RgB61LcNs
|
||||
github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136/go.mod h1:VkD1P761nykiq75dz+4iFqIQIZka189tx1BQLOp0Skc=
|
||||
github.com/coder/guts v1.5.0 h1:a94apf7xMf5jDdg1bIHzncbRiTn3+BvBZgrFSDbUnyI=
|
||||
github.com/coder/guts v1.5.0/go.mod h1:0Sbv5Kp83u1Nl7MIQiV2zmacJ3o02I341bkWkjWXSUQ=
|
||||
github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102 h1:ahTJlTRmTogsubgRVGOUj40dg62WvqPQkzTQP7pyepI=
|
||||
github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/coder/pq v1.10.5-0.20250807075151-6ad9b0a25151 h1:YAxwg3lraGNRwoQ18H7R7n+wsCqNve7Brdvj0F1rDnU=
|
||||
github.com/coder/pq v1.10.5-0.20250807075151-6ad9b0a25151/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
|
||||
github.com/coder/preview v1.0.3-0.20250714153828-a737d4750448 h1:S86sFp4Dr4dUn++fXOMOTu6ClnEZ/NrGCYv7bxZjYYc=
|
||||
|
||||
@@ -156,6 +156,7 @@ export const defaultParametersForBuiltinIcons = new Map<string, string>([
|
||||
["/icon/kasmvnc.svg", "whiteWithColor"],
|
||||
["/icon/kiro.svg", "whiteWithColor"],
|
||||
["/icon/memory.svg", "monochrome"],
|
||||
["/icon/openai.svg", "monochrome"],
|
||||
["/icon/rust.svg", "monochrome"],
|
||||
["/icon/terminal.svg", "monochrome"],
|
||||
["/icon/widgets.svg", "monochrome"],
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"nomad.svg",
|
||||
"novnc.svg",
|
||||
"okta.svg",
|
||||
"openai.svg",
|
||||
"personalize.svg",
|
||||
"php.svg",
|
||||
"phpstorm.svg",
|
||||
|
||||
2
site/static/icon/openai.svg
Normal file
2
site/static/icon/openai.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>OpenAI icon</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
Reference in New Issue
Block a user