Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions coderd/oauth2_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,19 @@ func TestOAuth2PrivilegeEscalation(t *testing.T) {
ClientName: fmt.Sprintf("native-app-3-%d", time.Now().UnixNano()),
TokenEndpointAuthMethod: "none", // Required for public clients
},
{
// Bare custom schemes (no reverse-domain notation) are the
// schemes real native apps register with the OS, and PKCE,
// not the scheme's spelling, is what secures the redirect.
RedirectURIs: []string{"vscode://coder.authenticate"},
ClientName: fmt.Sprintf("native-app-vscode-%d", time.Now().UnixNano()),
TokenEndpointAuthMethod: "none",
},
{
RedirectURIs: []string{"jetbrains://coder-callback"},
ClientName: fmt.Sprintf("native-app-jetbrains-%d", time.Now().UnixNano()),
TokenEndpointAuthMethod: "none",
},
}

for i, req := range validCustomSchemeRequests {
Expand Down Expand Up @@ -312,6 +325,54 @@ func TestOAuth2PrivilegeEscalation(t *testing.T) {
require.Contains(t, err.Error(), "dangerous scheme")
})
}

// mailto, tel, and sms are not in the dangerous-scheme blocklist
// above: they hand off to a mail client, dialer, or SMS app rather
// than injecting content, so they are harmless for a confidential
// client's redirect. A public client has no secret, so the redirect
// URI's scheme is its only mechanism for regaining control, and
// none of these three return control to it the way a real redirect
// scheme does. They are rejected for public clients specifically,
// with a distinct error from the dangerous-scheme case above.
publicClientDisallowedSchemeRequests := []struct {
req codersdk.OAuth2ClientRegistrationRequest
scheme string
}{
{
req: codersdk.OAuth2ClientRegistrationRequest{
RedirectURIs: []string{"mailto:user@example.com"},
ClientName: fmt.Sprintf("native-app-mailto-%d", time.Now().UnixNano()),
TokenEndpointAuthMethod: "none",
},
scheme: "mailto",
},
{
req: codersdk.OAuth2ClientRegistrationRequest{
RedirectURIs: []string{"tel:+15555550100"},
ClientName: fmt.Sprintf("native-app-tel-%d", time.Now().UnixNano()),
TokenEndpointAuthMethod: "none",
},
scheme: "tel",
},
{
req: codersdk.OAuth2ClientRegistrationRequest{
RedirectURIs: []string{"sms:+15555550100"},
ClientName: fmt.Sprintf("native-app-sms-%d", time.Now().UnixNano()),
TokenEndpointAuthMethod: "none",
},
scheme: "sms",
},
}

for _, test := range publicClientDisallowedSchemeRequests {
t.Run(fmt.Sprintf("PublicClientDisallowedScheme_%s", test.scheme), func(t *testing.T) {
t.Parallel()

_, err := client.PostOAuth2ClientRegistration(ctx, test.req)
require.Error(t, err)
require.Contains(t, err.Error(), "public clients may not use the "+test.scheme+" scheme")
})
}
})
}

Expand Down
51 changes: 23 additions & 28 deletions codersdk/oauth2_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,31 @@ func validateRedirectURIs(uris []string, tokenEndpointAuthMethod OAuth2TokenEndp
}
}
}
} else {
// Custom scheme validation for public clients (RFC 8252 section 7.1)
if isPublicClient {
// For public clients, custom schemes should follow RFC 8252 recommendations
// Should be reverse domain notation based on domain under their control
if !isValidCustomScheme(uri.Scheme) {
return xerrors.Errorf("redirect URI at index %d: custom scheme %s should use reverse domain notation (e.g. com.example.app)", i, uri.Scheme)
}
} else if isPublicClient {
// mailto, tel, and sms hand off to a mail client, dialer, or SMS
// app rather than returning control to the application that
// started the flow. A public client has no other way to obtain
// its authorization code, so registering one of these would
// produce a client that can never complete authorization.
//
// This check runs only for public clients because that is how
// custom-scheme validation was scoped before this change, not
// because these three schemes are known to be safe for a
// confidential client's redirect; confidential clients were
// never subject to any scheme-shape check beyond validateScheme
// and remain so here.
switch uri.Scheme {
case "mailto", "tel", "sms":
return xerrors.Errorf("redirect URI at index %d: public clients may not use the %s scheme", i, uri.Scheme)
}
// For confidential clients, custom schemes are less common but allowed
}
// Beyond that, custom schemes need no further check: validateScheme
// already blocked the ones that are dangerous in a redirect context,
// and RFC 8252 §7.1 only recommends reverse-domain notation rather
// than requiring it. Rejecting bare schemes such as vscode:// or
// jetbrains:// would penalize the native and CLI apps this client
// type exists for; PKCE, not the scheme's spelling, is what secures
// the redirect.

// Prevent URI fragments (RFC 6749 section 3.1.2)
if uri.Fragment != "" || strings.Contains(uriStr, "#") {
Expand Down Expand Up @@ -295,22 +309,3 @@ func isLoopbackAddress(hostname string) bool {
hostname == "127.0.0.1" ||
hostname == "::1"
}

// isValidCustomScheme validates custom schemes for public clients (RFC 8252)
func isValidCustomScheme(scheme string) bool {
// For security and RFC compliance, require reverse domain notation
// Should contain at least one period and not be a well-known scheme
if !strings.Contains(scheme, ".") {
return false
}

// Block schemes that look like well-known protocols
wellKnownSchemes := []string{"http", "https", "ftp", "mailto", "tel", "sms"}
for _, wellKnown := range wellKnownSchemes {
if strings.EqualFold(scheme, wellKnown) {
return false
}
}

return true
}
16 changes: 15 additions & 1 deletion docs/admin/integrations/oauth2-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
CODER_EXPERIMENTS=oauth2
```

## Creating OAuth2 Applications

Check warning on line 35 in docs/admin/integrations/oauth2-provider.md

View workflow job for this annotation

GitHub Actions / lint-docs

Coder.GerundHeading

Heading starts with an -ing word ('Creating'); prefer the imperative ('Install') or the noun ('Installation'). See capitalization-and-punctuation.md#no-gerund-leading-headings.

### Method 1: Web UI

Expand Down Expand Up @@ -289,7 +289,7 @@
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

Check warning on line 292 in docs/admin/integrations/oauth2-provider.md

View workflow job for this annotation

GitHub Actions / lint-docs

Coder.GerundHeading

Heading starts with an -ing word ('Testing'); prefer the imperative ('Install') or the noun ('Installation'). See capitalization-and-punctuation.md#no-gerund-leading-headings.

Coder provides comprehensive test scripts for OAuth2 development:

Expand Down Expand Up @@ -333,20 +333,34 @@

Verify that the `code_verifier` used in the token request matches the one used to generate the `code_challenge`.

### "public clients may not use the mailto/tel/sms scheme"

This error appears during client registration when a public client
(`token_endpoint_auth_method: none`) registers a redirect URI using the
`mailto:`, `tel:`, or `sms:` scheme. These schemes hand off to a mail
client, dialer, or SMS app instead of returning control to the
application that started the flow, so a public client registered with
one of them could never complete authorization. Register a redirect URI
the client can actually receive control on instead, such as a custom
scheme (`myapp://callback`) or a loopback HTTP address.

## 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:`.

Public clients (`token_endpoint_auth_method: none`) additionally cannot register `mailto:`, `tel:`, or `sms:` redirect URIs, since those schemes hand off to another app rather than returning an authorization code to the client. Confidential clients are not subject to this restriction.

## 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
custom URI schemes for native apps (`myapp://`) are permitted, and public
clients additionally cannot use `mailto:`, `tel:`, or `sms:`
- **Rotate secrets**: Periodically rotate client secrets using the management API

## Limitations
Expand Down
Loading