Skip to content

Commit 5eb6e9f

Browse files
committed
chore: pkce defaults
1 parent 40283ea commit 5eb6e9f

File tree

8 files changed

+157
-93
lines changed

8 files changed

+157
-93
lines changed

cli/server.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ func createOIDCConfig(ctx context.Context, logger slog.Logger, vals *codersdk.De
186186
secondaryClaimsSrc = coderd.MergedClaimsSourceAccessToken
187187
}
188188

189+
var pkceSupport struct {
190+
CodeChallengeMethodsSupported []promoauth.Oauth2PKCEChallengeMethod `json:"code_challenge_methods_supported"`
191+
}
192+
err = oidcProvider.Claims(&pkceSupport)
193+
if err != nil {
194+
return nil, xerrors.Errorf("pkce detect in claims: %w", err)
195+
}
196+
189197
return &coderd.OIDCConfig{
190198
OAuth2Config: useCfg,
191199
Provider: oidcProvider,
@@ -206,6 +214,8 @@ func createOIDCConfig(ctx context.Context, logger slog.Logger, vals *codersdk.De
206214
SignupsDisabledText: vals.OIDC.SignupsDisabledText.String(),
207215
IconURL: vals.OIDC.IconURL.String(),
208216
IgnoreEmailVerified: vals.OIDC.IgnoreEmailVerified.Value(),
217+
// We only support S256 PKCE challenge method.
218+
PKCEMethods: pkceSupport.CodeChallengeMethodsSupported,
209219
}, nil
210220
}
211221

coderd/coderd.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/coder/coder/v2/coderd/oauth2provider"
2525
"github.com/coder/coder/v2/coderd/pproflabel"
2626
"github.com/coder/coder/v2/coderd/prebuilds"
27+
"github.com/coder/coder/v2/coderd/promoauth"
2728
"github.com/coder/coder/v2/coderd/usage"
2829
"github.com/coder/coder/v2/coderd/wsbuilder"
2930

@@ -940,7 +941,7 @@ func New(options *Options) *API {
940941
r.Route(fmt.Sprintf("/%s/callback", externalAuthConfig.ID), func(r chi.Router) {
941942
r.Use(
942943
apiKeyMiddlewareRedirect,
943-
httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil),
944+
httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil, externalAuthConfig.CodeChallengeMethodsSupported),
944945
)
945946
r.Get("/", api.externalAuthCallback(externalAuthConfig))
946947
})
@@ -1289,14 +1290,15 @@ func New(options *Options) *API {
12891290
r.Get("/github/device", api.userOAuth2GithubDevice)
12901291
r.Route("/github", func(r chi.Router) {
12911292
r.Use(
1292-
httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil),
1293+
// Github supports PKCE S256
1294+
httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil, []promoauth.Oauth2PKCEChallengeMethod{promoauth.PKCEChallengeMethodSha256}),
12931295
)
12941296
r.Get("/callback", api.userOAuth2Github)
12951297
})
12961298
})
12971299
r.Route("/oidc/callback", func(r chi.Router) {
12981300
r.Use(
1299-
httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, oidcAuthURLParams),
1301+
httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, oidcAuthURLParams, options.OIDCConfig.PKCEMethods),
13001302
)
13011303
r.Get("/", api.userOIDC)
13021304
})

coderd/externalauth/externalauth.go

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ type Config struct {
102102
// injected into Coder AI Bridge upstream requests.
103103
// In the case of conflicts, items evaluated by this list override [MCPToolAllowRegex].
104104
// This field can be nil if unspecified in the config.
105-
MCPToolDenyRegex *regexp.Regexp
105+
MCPToolDenyRegex *regexp.Regexp
106+
CodeChallengeMethodsSupported []promoauth.Oauth2PKCEChallengeMethod
106107
}
107108

108109
// GenerateTokenExtra generates the extra token data to store in the database.
@@ -800,9 +801,11 @@ func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) {
800801
copyDefaultSettings(config, azureDevopsEntraDefaults(config))
801802
return
802803
default:
803-
// No defaults for this type. We still want to run this apply with
804-
// an empty set of defaults.
805-
copyDefaultSettings(config, codersdk.ExternalAuthConfig{})
804+
// Defaults apply to any provider that doesn't have specific defaults.
805+
copyDefaultSettings(config, codersdk.ExternalAuthConfig{
806+
// PKCE should always be enabled by default.
807+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodSha256)},
808+
})
806809
return
807810
}
808811
}
@@ -856,6 +859,9 @@ func copyDefaultSettings(config *codersdk.ExternalAuthConfig, defaults codersdk.
856859
// This is a key emoji.
857860
config.DisplayIcon = "/emojis/1f511.png"
858861
}
862+
if config.CodeChallengeMethodsSupported == nil {
863+
config.CodeChallengeMethodsSupported = defaults.CodeChallengeMethodsSupported
864+
}
859865
}
860866

861867
// gitHubDefaults returns default config values for GitHub.
@@ -869,9 +875,10 @@ func gitHubDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthCo
869875
DisplayIcon: "/icon/github.svg",
870876
Regex: `^(https?://)?github\.com(/.*)?$`,
871877
// "workflow" is required for managing GitHub Actions in a repository.
872-
Scopes: []string{"repo", "workflow"},
873-
DeviceCodeURL: "https://github.com/login/device/code",
874-
AppInstallationsURL: "https://api.github.com/user/installations",
878+
Scopes: []string{"repo", "workflow"},
879+
DeviceCodeURL: "https://github.com/login/device/code",
880+
AppInstallationsURL: "https://api.github.com/user/installations",
881+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodSha256)},
875882
}
876883

877884
if config.RevokeURL == "" && config.ClientID != "" {
@@ -886,6 +893,8 @@ func bitbucketServerDefaults(config *codersdk.ExternalAuthConfig) codersdk.Exter
886893
DisplayName: "Bitbucket Server",
887894
Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"},
888895
DisplayIcon: "/icon/bitbucket.svg",
896+
// TODO: PKCE support? Test 'S256' as the string value
897+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodNone)},
889898
}
890899
// Bitbucket servers will have some base url, e.g. https://bitbucket.coder.com.
891900
// We will grab this from the Auth URL. This choice is a bit arbitrary,
@@ -923,14 +932,15 @@ func bitbucketServerDefaults(config *codersdk.ExternalAuthConfig) codersdk.Exter
923932
// Any user specific fields will override this if provided.
924933
func gitlabDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthConfig {
925934
cloud := codersdk.ExternalAuthConfig{
926-
AuthURL: "https://gitlab.com/oauth/authorize",
927-
TokenURL: "https://gitlab.com/oauth/token",
928-
ValidateURL: "https://gitlab.com/oauth/token/info",
929-
RevokeURL: "https://gitlab.com/oauth/revoke",
930-
DisplayName: "GitLab",
931-
DisplayIcon: "/icon/gitlab.svg",
932-
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
933-
Scopes: []string{"write_repository"},
935+
AuthURL: "https://gitlab.com/oauth/authorize",
936+
TokenURL: "https://gitlab.com/oauth/token",
937+
ValidateURL: "https://gitlab.com/oauth/token/info",
938+
RevokeURL: "https://gitlab.com/oauth/revoke",
939+
DisplayName: "GitLab",
940+
DisplayIcon: "/icon/gitlab.svg",
941+
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
942+
Scopes: []string{"write_repository"},
943+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodSha256)},
934944
}
935945

936946
if config.AuthURL == "" || config.AuthURL == cloud.AuthURL {
@@ -946,14 +956,15 @@ func gitlabDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthCo
946956

947957
// At this point, assume it is self-hosted and use the AuthURL
948958
return codersdk.ExternalAuthConfig{
949-
DisplayName: cloud.DisplayName,
950-
Scopes: cloud.Scopes,
951-
DisplayIcon: cloud.DisplayIcon,
952-
AuthURL: au.ResolveReference(&url.URL{Path: "/oauth/authorize"}).String(),
953-
TokenURL: au.ResolveReference(&url.URL{Path: "/oauth/token"}).String(),
954-
ValidateURL: au.ResolveReference(&url.URL{Path: "/oauth/token/info"}).String(),
955-
RevokeURL: au.ResolveReference(&url.URL{Path: "/oauth/revoke"}).String(),
956-
Regex: fmt.Sprintf(`^(https?://)?%s(/.*)?$`, strings.ReplaceAll(au.Host, ".", `\.`)),
959+
DisplayName: cloud.DisplayName,
960+
Scopes: cloud.Scopes,
961+
DisplayIcon: cloud.DisplayIcon,
962+
AuthURL: au.ResolveReference(&url.URL{Path: "/oauth/authorize"}).String(),
963+
TokenURL: au.ResolveReference(&url.URL{Path: "/oauth/token"}).String(),
964+
ValidateURL: au.ResolveReference(&url.URL{Path: "/oauth/token/info"}).String(),
965+
RevokeURL: au.ResolveReference(&url.URL{Path: "/oauth/revoke"}).String(),
966+
Regex: fmt.Sprintf(`^(https?://)?%s(/.*)?$`, strings.ReplaceAll(au.Host, ".", `\.`)),
967+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodSha256)},
957968
}
958969
}
959970

@@ -962,6 +973,8 @@ func jfrogArtifactoryDefaults(config *codersdk.ExternalAuthConfig) codersdk.Exte
962973
DisplayName: "JFrog Artifactory",
963974
Scopes: []string{"applied-permissions/user"},
964975
DisplayIcon: "/icon/jfrog.svg",
976+
// TODO: PKCE support? Test 'S256' as the string value
977+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodNone)},
965978
}
966979
// Artifactory servers will have some base url, e.g. https://jfrog.coder.com.
967980
// We will grab this from the Auth URL. This choice is not arbitrary. It is a
@@ -997,9 +1010,10 @@ func jfrogArtifactoryDefaults(config *codersdk.ExternalAuthConfig) codersdk.Exte
9971010

9981011
func giteaDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthConfig {
9991012
defaults := codersdk.ExternalAuthConfig{
1000-
DisplayName: "Gitea",
1001-
Scopes: []string{"read:repository", " write:repository", "read:user"},
1002-
DisplayIcon: "/icon/gitea.svg",
1013+
DisplayName: "Gitea",
1014+
Scopes: []string{"read:repository", " write:repository", "read:user"},
1015+
DisplayIcon: "/icon/gitea.svg",
1016+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodSha256)},
10031017
}
10041018
// Gitea's servers will have some base url, e.g: https://gitea.coder.com.
10051019
// If an auth url is not set, we will assume they are using the default
@@ -1031,6 +1045,8 @@ func azureDevopsEntraDefaults(config *codersdk.ExternalAuthConfig) codersdk.Exte
10311045
DisplayName: "Azure DevOps (Entra)",
10321046
DisplayIcon: "/icon/azure-devops.svg",
10331047
Regex: `^(https?://)?dev\.azure\.com(/.*)?$`,
1048+
// TODO: PKCE support? Test 'S256' as the string value
1049+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodNone)},
10341050
}
10351051
// The tenant ID is required for urls and is in the auth url.
10361052
if config.AuthURL == "" {
@@ -1069,6 +1085,8 @@ var staticDefaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.External
10691085
DisplayIcon: "/icon/azure-devops.svg",
10701086
Regex: `^(https?://)?dev\.azure\.com(/.*)?$`,
10711087
Scopes: []string{"vso.code_write"},
1088+
// TODO: PKCE support? Test 'S256' as the string value
1089+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodNone)},
10721090
},
10731091
codersdk.EnhancedExternalAuthProviderBitBucketCloud: {
10741092
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
@@ -1078,6 +1096,8 @@ var staticDefaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.External
10781096
DisplayIcon: "/icon/bitbucket.svg",
10791097
Regex: `^(https?://)?bitbucket\.org(/.*)?$`,
10801098
Scopes: []string{"account", "repository:write"},
1099+
// TODO: PKCE support? Test 'S256' as the string value
1100+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodNone)},
10811101
},
10821102
codersdk.EnhancedExternalAuthProviderSlack: {
10831103
AuthURL: "https://slack.com/oauth/v2/authorize",
@@ -1087,6 +1107,8 @@ var staticDefaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.External
10871107
DisplayIcon: "/icon/slack.svg",
10881108
// See: https://api.slack.com/authentication/oauth-v2#exchanging
10891109
ExtraTokenKeys: []string{"authed_user"},
1110+
// TODO: PKCE support? Test 'S256' as the string value
1111+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodNone)},
10901112
},
10911113
}
10921114

coderd/externalauth/externalauth_internal_test.go

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/stretchr/testify/require"
77

8+
"github.com/coder/coder/v2/coderd/promoauth"
89
"github.com/coder/coder/v2/codersdk"
910
)
1011

@@ -13,17 +14,20 @@ func TestGitlabDefaults(t *testing.T) {
1314

1415
// The default cloud setup. Copying this here as hard coded
1516
// values.
16-
cloud := codersdk.ExternalAuthConfig{
17-
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
18-
ID: string(codersdk.EnhancedExternalAuthProviderGitLab),
19-
AuthURL: "https://gitlab.com/oauth/authorize",
20-
TokenURL: "https://gitlab.com/oauth/token",
21-
ValidateURL: "https://gitlab.com/oauth/token/info",
22-
RevokeURL: "https://gitlab.com/oauth/revoke",
23-
DisplayName: "GitLab",
24-
DisplayIcon: "/icon/gitlab.svg",
25-
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
26-
Scopes: []string{"write_repository"},
17+
cloud := func() codersdk.ExternalAuthConfig {
18+
return codersdk.ExternalAuthConfig{
19+
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
20+
ID: string(codersdk.EnhancedExternalAuthProviderGitLab),
21+
AuthURL: "https://gitlab.com/oauth/authorize",
22+
TokenURL: "https://gitlab.com/oauth/token",
23+
ValidateURL: "https://gitlab.com/oauth/token/info",
24+
RevokeURL: "https://gitlab.com/oauth/revoke",
25+
DisplayName: "GitLab",
26+
DisplayIcon: "/icon/gitlab.svg",
27+
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
28+
Scopes: []string{"write_repository"},
29+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodSha256)},
30+
}
2731
}
2832

2933
tests := []struct {
@@ -38,7 +42,7 @@ func TestGitlabDefaults(t *testing.T) {
3842
input: codersdk.ExternalAuthConfig{
3943
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
4044
},
41-
expected: cloud,
45+
expected: cloud(),
4246
},
4347
{
4448
// If someone was to manually configure the gitlab cli.
@@ -47,7 +51,7 @@ func TestGitlabDefaults(t *testing.T) {
4751
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
4852
AuthURL: "https://gitlab.com/oauth/authorize",
4953
},
50-
expected: cloud,
54+
expected: cloud(),
5155
},
5256
{
5357
// Changing some of the defaults of the cloud option
@@ -60,7 +64,7 @@ func TestGitlabDefaults(t *testing.T) {
6064
DisplayName: "custom",
6165
Regex: ".*",
6266
},
63-
expected: cloud,
67+
expected: cloud(),
6468
mutateExpected: func(config *codersdk.ExternalAuthConfig) {
6569
config.AuthURL = "https://gitlab.com/oauth/authorize?foo=bar"
6670
config.DisplayName = "custom"
@@ -75,7 +79,7 @@ func TestGitlabDefaults(t *testing.T) {
7579
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
7680
AuthURL: "https://gitlab.company.org/oauth/authorize?foo=bar",
7781
},
78-
expected: cloud,
82+
expected: cloud(),
7983
mutateExpected: func(config *codersdk.ExternalAuthConfig) {
8084
config.AuthURL = "https://gitlab.company.org/oauth/authorize?foo=bar"
8185
config.ValidateURL = "https://gitlab.company.org/oauth/token/info"
@@ -88,20 +92,22 @@ func TestGitlabDefaults(t *testing.T) {
8892
// Strange values
8993
name: "RandomValues",
9094
input: codersdk.ExternalAuthConfig{
91-
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
92-
AuthURL: "https://auth.com/auth",
93-
ValidateURL: "https://validate.com/validate",
94-
TokenURL: "https://token.com/token",
95-
RevokeURL: "https://token.com/revoke",
96-
Regex: "random",
95+
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
96+
AuthURL: "https://auth.com/auth",
97+
ValidateURL: "https://validate.com/validate",
98+
TokenURL: "https://token.com/token",
99+
RevokeURL: "https://token.com/revoke",
100+
Regex: "random",
101+
CodeChallengeMethodsSupported: []string{"random"},
97102
},
98-
expected: cloud,
103+
expected: cloud(),
99104
mutateExpected: func(config *codersdk.ExternalAuthConfig) {
100105
config.AuthURL = "https://auth.com/auth"
101106
config.ValidateURL = "https://validate.com/validate"
102107
config.TokenURL = "https://token.com/token"
103108
config.RevokeURL = "https://token.com/revoke"
104109
config.Regex = `random`
110+
config.CodeChallengeMethodsSupported = []string{"random"}
105111
},
106112
},
107113
}
@@ -133,11 +139,12 @@ func Test_bitbucketServerConfigDefaults(t *testing.T) {
133139
Type: bbType,
134140
},
135141
expected: codersdk.ExternalAuthConfig{
136-
Type: bbType,
137-
ID: bbType,
138-
DisplayName: "Bitbucket Server",
139-
Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"},
140-
DisplayIcon: "/icon/bitbucket.svg",
142+
Type: bbType,
143+
ID: bbType,
144+
DisplayName: "Bitbucket Server",
145+
Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"},
146+
DisplayIcon: "/icon/bitbucket.svg",
147+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodNone)},
141148
},
142149
},
143150
{
@@ -148,15 +155,16 @@ func Test_bitbucketServerConfigDefaults(t *testing.T) {
148155
AuthURL: "https://bitbucket.example.com/login/oauth/authorize",
149156
},
150157
expected: codersdk.ExternalAuthConfig{
151-
Type: bbType,
152-
ID: bbType,
153-
AuthURL: "https://bitbucket.example.com/login/oauth/authorize",
154-
TokenURL: "https://bitbucket.example.com/rest/oauth2/latest/token",
155-
ValidateURL: "https://bitbucket.example.com/rest/api/latest/inbox/pull-requests/count",
156-
Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"},
157-
Regex: `^(https?://)?bitbucket\.example\.com(/.*)?$`,
158-
DisplayName: "Bitbucket Server",
159-
DisplayIcon: "/icon/bitbucket.svg",
158+
Type: bbType,
159+
ID: bbType,
160+
AuthURL: "https://bitbucket.example.com/login/oauth/authorize",
161+
TokenURL: "https://bitbucket.example.com/rest/oauth2/latest/token",
162+
ValidateURL: "https://bitbucket.example.com/rest/api/latest/inbox/pull-requests/count",
163+
Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"},
164+
Regex: `^(https?://)?bitbucket\.example\.com(/.*)?$`,
165+
DisplayName: "Bitbucket Server",
166+
DisplayIcon: "/icon/bitbucket.svg",
167+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodNone)},
160168
},
161169
},
162170
{
@@ -167,15 +175,16 @@ func Test_bitbucketServerConfigDefaults(t *testing.T) {
167175
Type: "bitbucket",
168176
},
169177
expected: codersdk.ExternalAuthConfig{
170-
Type: string(codersdk.EnhancedExternalAuthProviderBitBucketCloud),
171-
ID: "bitbucket", // Legacy ID remains unchanged
172-
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
173-
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
174-
ValidateURL: "https://api.bitbucket.org/2.0/user",
175-
DisplayName: "BitBucket",
176-
DisplayIcon: "/icon/bitbucket.svg",
177-
Regex: `^(https?://)?bitbucket\.org(/.*)?$`,
178-
Scopes: []string{"account", "repository:write"},
178+
Type: string(codersdk.EnhancedExternalAuthProviderBitBucketCloud),
179+
ID: "bitbucket", // Legacy ID remains unchanged
180+
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
181+
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
182+
ValidateURL: "https://api.bitbucket.org/2.0/user",
183+
DisplayName: "BitBucket",
184+
DisplayIcon: "/icon/bitbucket.svg",
185+
Regex: `^(https?://)?bitbucket\.org(/.*)?$`,
186+
Scopes: []string{"account", "repository:write"},
187+
CodeChallengeMethodsSupported: []string{string(promoauth.PKCEChallengeMethodNone)},
179188
},
180189
},
181190
}

0 commit comments

Comments
 (0)