-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat!: support PKCE in the oauth2 client's auth/exchange flow #21215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Used in coder primary auth
📚 Documentation Check✅ Updates Needed
📝 ReasoningThis PR adds PKCE support for external OAuth2 authentication, which is a user-facing feature that requires administrator configuration. Based on my analysis: What Changed:
Why Documentation is Needed:
What's Already Done:
This comment was generated by an AI Agent through Coder Tasks |
mafredri
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nits/suggestions, but code changes look good otherwise 👍🏻. I agree with doc check that it would be good to have some added admin documentation.
| // clientID to be used by coderd | ||
| clientID string | ||
| clientSecret string | ||
| pkce bool // TODO: Implement for refresh token flow as well |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: TODO(name):, or perhaps track via GH ticket.
| if f.pkce { | ||
| challenge, ok := f.codeToChallengeMap.Load(code) | ||
| if !ok { | ||
| httpError(rw, http.StatusBadRequest, xerrors.New("code challenge not found for code")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| httpError(rw, http.StatusBadRequest, xerrors.New("code challenge not found for code")) | |
| httpError(rw, http.StatusBadRequest, xerrors.New("challenge not found for code")) |
This read a bit clearer to me, but if the original is more correct, keep it. Should we mention pkce here to give a hint pkce: challenge ...?
| codeVerifier := values.Get("code_verifier") | ||
| expecter := oauth2.S256ChallengeFromVerifier(codeVerifier) | ||
| if challenge != expecter { | ||
| httpError(rw, http.StatusBadRequest, xerrors.New("invalid code verifier")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| httpError(rw, http.StatusBadRequest, xerrors.New("invalid code verifier")) | |
| httpError(rw, http.StatusBadRequest, xerrors.New("pkce: invalid code verifier")) |
?
| httpError(rw, http.StatusBadRequest, xerrors.New("code challenge not found for code")) | ||
| return | ||
| } | ||
| codeVerifier := values.Get("code_verifier") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty handling?
| DisplayName: "Bitbucket Server", | ||
| Scopes: []string{"PUBLIC_REPOS", "REPO_READ", "REPO_WRITE"}, | ||
| DisplayIcon: "/icon/bitbucket.svg", | ||
| // TODO: PKCE support? Test 'S256' as the string value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get the gist, but these TODOs could be a bit clearer, if someone else needs to look at it in the future.
| r.Route("/github", func(r chi.Router) { | ||
| r.Use( | ||
| httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil), | ||
| // Github supports PKCE S256 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to call this out, link to docs?
| r.Use( | ||
| httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil), | ||
| // Github supports PKCE S256 | ||
| httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil, []promoauth.Oauth2PKCEChallengeMethod{promoauth.PKCEChallengeMethodSha256}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: PKCESupported() on GithubOAuth2Config?
| // OAuth2StateCookie is the name of the cookie that stores the oauth2 state. | ||
| OAuth2StateCookie = "oauth_state" | ||
|
|
||
| OAuth2PKCEChallenge = "oauth_pkce_challenge" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, since this is storing the verifier, not the challenge. Is the terminology correct?
| // DisplayIcon is a URL to an icon to display in the UI. | ||
| DisplayIcon string `json:"display_icon" yaml:"display_icon"` | ||
| DisplayIcon string `json:"display_icon" yaml:"display_icon"` | ||
| CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported" yaml:"code_challenge_methods_supported"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Not sure if these docs get generated or not, but could add a description?
| const providerID = "test-idp" | ||
| fake := oidctest.NewFakeIDP(t, | ||
| append([]oidctest.FakeIDPOpt{}, settings.FakeIDPOpts...)..., | ||
| append([]oidctest.FakeIDPOpt{oidctest.WithPKCE()}, settings.FakeIDPOpts...)..., |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this make it impossible to disable PKCE for tests when using setupOauth2Test?
| } | ||
|
|
||
| if f.pkce { | ||
| challenge, ok := f.codeToChallengeMap.Load(code) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also clean up this map after use.
Breaking Change: Existing oauth apps might now use PKCE. If an unknown type was being used, and it does not support PKCE, it will break.
To fix, set the PKCE methods on the external auth to
nonePKCE challenge used in primary coderd oauth2 flow.
Closes #21213
Implementation from: golang/oauth2@55cd552
What this does
This adds PKCE support when Coder is the OAuth client to an external IdP.
OIDC
PKCE support is automatically detected from the
well-knownconfiguration endpoint. If it is supported, Coder will use it.coder/cli/server.go
Lines 189 to 195 in 53fc864
OAuth (github login & external auth).
Known Oauth types have defaults set based on manual testing. We need to manually test to expand coverage.
We assume PKCE is supported by all unknown oauth IdP's. This can fixed in the configuration
Example to disable:
Manual tests
Tested to work on:
{"message":"Failed exchanging Oauth code.","detail":"oauth2: \"bad_verification_code\" \"The code passed is incorrect or expired.\" \"https://docs.github.com/apps/managing-oauth-apps/troubleshooting-oauth-app-access-token-request-errors/#bad-verification-code\""}{"message":"Failed exchanging Oauth code.","detail":"oauth2: \"invalid_grant\" \"The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.\""}{"message":"Failed exchanging Oauth code.","detail":"oauth2: \"unauthorized_client\" \"failed PKCE code challenge\""}{"message":"Failed exchanging Oauth code.","detail":"oauth2: \"invalid_grant\" \"Invalid authorization code\""}