Skip to content
Open
Prev Previous commit
Next Next commit
chore: sane defaults and make gen
  • Loading branch information
Emyrk committed Dec 11, 2025
commit ead92c8cba03f2ab95ddfd0d2bba9cea88cb1564
2 changes: 2 additions & 0 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2771,6 +2771,8 @@ func parseExternalAuthProvidersFromEnv(prefix string, environ []string) ([]coder
provider.MCPToolAllowRegex = v.Value
case "MCP_TOOL_DENY_REGEX":
provider.MCPToolDenyRegex = v.Value
case "PKCE_METHODS":
provider.CodeChallengeMethodsSupported = strings.Split(v.Value, " ")
}
providers[providerNum] = provider
}
Expand Down
2 changes: 1 addition & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -1298,7 +1298,7 @@ func New(options *Options) *API {
})
r.Route("/oidc/callback", func(r chi.Router) {
r.Use(
httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, oidcAuthURLParams, options.OIDCConfig.PKCEMethods),
httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, oidcAuthURLParams, options.OIDCConfig.PKCESupported()),
)
r.Get("/", api.userOIDC)
})
Expand Down
18 changes: 10 additions & 8 deletions coderd/externalauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/coder/coder/v2/coderd/externalauth"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
)

Expand Down Expand Up @@ -417,14 +418,15 @@ func ExternalAuthConfigs(auths []*externalauth.Config) []codersdk.ExternalAuthLi

func ExternalAuthConfig(cfg *externalauth.Config) codersdk.ExternalAuthLinkProvider {
return codersdk.ExternalAuthLinkProvider{
ID: cfg.ID,
Type: cfg.Type,
Device: cfg.DeviceAuth != nil,
DisplayName: cfg.DisplayName,
DisplayIcon: cfg.DisplayIcon,
AllowRefresh: !cfg.NoRefresh,
AllowValidate: cfg.ValidateURL != "",
SupportsRevocation: cfg.RevokeURL != "",
ID: cfg.ID,
Type: cfg.Type,
Device: cfg.DeviceAuth != nil,
DisplayName: cfg.DisplayName,
DisplayIcon: cfg.DisplayIcon,
AllowRefresh: !cfg.NoRefresh,
AllowValidate: cfg.ValidateURL != "",
SupportsRevocation: cfg.RevokeURL != "",
CodeChallengeMethodsSupported: slice.ToStrings(cfg.CodeChallengeMethodsSupported),
}
}

Expand Down
50 changes: 26 additions & 24 deletions coderd/externalauth/externalauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/promoauth"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/retry"
)
Expand Down Expand Up @@ -724,24 +725,25 @@ func ConvertConfig(instrument *promoauth.Factory, entries []codersdk.ExternalAut
}

cfg := &Config{
InstrumentedOAuth2Config: instrumented,
ID: entry.ID,
ClientID: entry.ClientID,
ClientSecret: entry.ClientSecret,
Regex: regex,
Type: entry.Type,
NoRefresh: entry.NoRefresh,
ValidateURL: entry.ValidateURL,
RevokeURL: entry.RevokeURL,
RevokeTimeout: tokenRevocationTimeout,
AppInstallationsURL: entry.AppInstallationsURL,
AppInstallURL: entry.AppInstallURL,
DisplayName: entry.DisplayName,
DisplayIcon: entry.DisplayIcon,
ExtraTokenKeys: entry.ExtraTokenKeys,
MCPURL: entry.MCPURL,
MCPToolAllowRegex: mcpToolAllow,
MCPToolDenyRegex: mcpToolDeny,
InstrumentedOAuth2Config: instrumented,
ID: entry.ID,
ClientID: entry.ClientID,
ClientSecret: entry.ClientSecret,
Regex: regex,
Type: entry.Type,
NoRefresh: entry.NoRefresh,
ValidateURL: entry.ValidateURL,
RevokeURL: entry.RevokeURL,
RevokeTimeout: tokenRevocationTimeout,
AppInstallationsURL: entry.AppInstallationsURL,
AppInstallURL: entry.AppInstallURL,
DisplayName: entry.DisplayName,
DisplayIcon: entry.DisplayIcon,
ExtraTokenKeys: entry.ExtraTokenKeys,
MCPURL: entry.MCPURL,
MCPToolAllowRegex: mcpToolAllow,
MCPToolDenyRegex: mcpToolDeny,
CodeChallengeMethodsSupported: slice.StringEnums[promoauth.Oauth2PKCEChallengeMethod](entry.CodeChallengeMethodsSupported),
}

if entry.DeviceFlow {
Expand Down Expand Up @@ -801,11 +803,8 @@ func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) {
copyDefaultSettings(config, azureDevopsEntraDefaults(config))
return
default:
// Defaults apply to any provider that doesn't have specific defaults.
copyDefaultSettings(config, codersdk.ExternalAuthConfig{
// PKCE should always be enabled by default.
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodSha256)},
})
// Global defaults are specified at the end of the `copyDefaultSettings` function.
copyDefaultSettings(config, codersdk.ExternalAuthConfig{})
return
}
}
Expand Down Expand Up @@ -847,6 +846,9 @@ func copyDefaultSettings(config *codersdk.ExternalAuthConfig, defaults codersdk.
if len(config.ExtraTokenKeys) == 0 {
config.ExtraTokenKeys = defaults.ExtraTokenKeys
}
if config.CodeChallengeMethodsSupported == nil {
config.CodeChallengeMethodsSupported = defaults.CodeChallengeMethodsSupported
}

// Apply defaults if it's still empty...
if config.ID == "" {
Expand All @@ -860,7 +862,7 @@ func copyDefaultSettings(config *codersdk.ExternalAuthConfig, defaults codersdk.
config.DisplayIcon = "/emojis/1f511.png"
}
if config.CodeChallengeMethodsSupported == nil {
config.CodeChallengeMethodsSupported = defaults.CodeChallengeMethodsSupported
config.CodeChallengeMethodsSupported = []string{string(promoauth.PKCEChallengeMethodSha256)}
}
}

Expand Down
42 changes: 42 additions & 0 deletions coderd/externalauth/externalauth_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,45 @@ func Test_bitbucketServerConfigDefaults(t *testing.T) {
})
}
}

func TestUntyped(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input codersdk.ExternalAuthConfig
expected codersdk.ExternalAuthConfig
}{
{
// Unknown Type uses S256 by default.
name: "RandomValues",
input: codersdk.ExternalAuthConfig{
Type: "unknown",
AuthURL: "https://auth.com/auth",
ValidateURL: "https://validate.com/validate",
TokenURL: "https://token.com/token",
RevokeURL: "https://token.com/revoke",
Regex: "random",
},
expected: codersdk.ExternalAuthConfig{
ID: "unknown",
Type: "unknown",
DisplayName: "unknown",
DisplayIcon: "/emojis/1f511.png",
AuthURL: "https://auth.com/auth",
ValidateURL: "https://validate.com/validate",
TokenURL: "https://token.com/token",
RevokeURL: "https://token.com/revoke",
Regex: `random`,
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodSha256)},
},
},
}
for _, c := range tests {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
applyDefaultsToConfig(&c.input)
require.Equal(t, c.input, c.expected)
})
}
}
18 changes: 9 additions & 9 deletions coderd/httpmw/oauth2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ func TestOAuth2(t *testing.T) {
t.Parallel()
req := httptest.NewRequest("GET", "/", nil)
res := httptest.NewRecorder()
httpmw.ExtractOAuth2(nil, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req)
httpmw.ExtractOAuth2(nil, nil, codersdk.HTTPCookieConfig{}, nil, nil)(nil).ServeHTTP(res, req)
require.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
})
t.Run("RedirectWithoutCode", func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest("GET", "/?redirect="+url.QueryEscape("/dashboard"), nil)
res := httptest.NewRecorder()
tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil, nil)(nil).ServeHTTP(res, req)
location := res.Header().Get("Location")
if !assert.NotEmpty(t, location) {
return
Expand All @@ -82,7 +82,7 @@ func TestOAuth2(t *testing.T) {
req := httptest.NewRequest("GET", "/?redirect="+url.QueryEscape(uri.String()), nil)
res := httptest.NewRecorder()
tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil, nil)(nil).ServeHTTP(res, req)
location := res.Header().Get("Location")
if !assert.NotEmpty(t, location) {
return
Expand All @@ -97,15 +97,15 @@ func TestOAuth2(t *testing.T) {
req := httptest.NewRequest("GET", "/?code=something", nil)
res := httptest.NewRecorder()
tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil, nil)(nil).ServeHTTP(res, req)
require.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
})
t.Run("NoStateCookie", func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest("GET", "/?code=something&state=test", nil)
res := httptest.NewRecorder()
tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil, nil)(nil).ServeHTTP(res, req)
require.Equal(t, http.StatusUnauthorized, res.Result().StatusCode)
})
t.Run("MismatchedState", func(t *testing.T) {
Expand All @@ -117,7 +117,7 @@ func TestOAuth2(t *testing.T) {
})
res := httptest.NewRecorder()
tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil, nil)(nil).ServeHTTP(res, req)
require.Equal(t, http.StatusUnauthorized, res.Result().StatusCode)
})
t.Run("ExchangeCodeAndState", func(t *testing.T) {
Expand All @@ -133,7 +133,7 @@ func TestOAuth2(t *testing.T) {
})
res := httptest.NewRecorder()
tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil, nil)(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
state := httpmw.OAuth2(r)
require.Equal(t, "/dashboard", state.Redirect)
})).ServeHTTP(res, req)
Expand All @@ -144,7 +144,7 @@ func TestOAuth2(t *testing.T) {
res := httptest.NewRecorder()
tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("foo", "bar"))
authOpts := map[string]string{"foo": "bar"}
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, authOpts)(nil).ServeHTTP(res, req)
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, authOpts, nil)(nil).ServeHTTP(res, req)
location := res.Header().Get("Location")
// Ideally we would also assert that the location contains the query params
// we set in the auth URL but this would essentially be testing the oauth2 package.
Expand All @@ -160,7 +160,7 @@ func TestOAuth2(t *testing.T) {
httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{
Secure: true,
SameSite: "none",
}, nil)(nil).ServeHTTP(res, req)
}, nil, nil)(nil).ServeHTTP(res, req)

found := false
for _, cookie := range res.Result().Cookies() {
Expand Down
8 changes: 8 additions & 0 deletions coderd/userauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,14 @@ type OIDCConfig struct {
PKCEMethods []promoauth.Oauth2PKCEChallengeMethod
}

// PKCESupported is to prevent nil pointer dereference.
func (o *OIDCConfig) PKCESupported() []promoauth.Oauth2PKCEChallengeMethod {
if o == nil {
return nil
}
return o.PKCEMethods
}

// @Summary OpenID Connect Callback
// @ID openid-connect-callback
// @Security CoderSessionToken
Expand Down
17 changes: 9 additions & 8 deletions codersdk/externalauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,15 @@ type ExternalAuthLink struct {

// ExternalAuthLinkProvider are the static details of a provider.
type ExternalAuthLinkProvider struct {
ID string `json:"id"`
Type string `json:"type"`
Device bool `json:"device"`
DisplayName string `json:"display_name"`
DisplayIcon string `json:"display_icon"`
AllowRefresh bool `json:"allow_refresh"`
AllowValidate bool `json:"allow_validate"`
SupportsRevocation bool `json:"supports_revocation"`
ID string `json:"id"`
Type string `json:"type"`
Device bool `json:"device"`
DisplayName string `json:"display_name"`
DisplayIcon string `json:"display_icon"`
AllowRefresh bool `json:"allow_refresh"`
AllowValidate bool `json:"allow_validate"`
SupportsRevocation bool `json:"supports_revocation"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}

type ExternalAuthAppInstallation struct {
Expand Down
1 change: 1 addition & 0 deletions site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions site/src/testHelpers/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4502,6 +4502,7 @@ export const MockGithubExternalProvider: TypesGen.ExternalAuthLinkProvider = {
allow_refresh: true,
allow_validate: true,
supports_revocation: false,
code_challenge_methods_supported: ["S256"],
};

export const MockGithubAuthLink: TypesGen.ExternalAuthLink = {
Expand Down
Loading