-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
PKCE implementation #1784
PKCE implementation #1784
Conversation
Signed-off-by: Tadeusz Magura-Witkowski <[email protected]>
…cret In PKCE flow, no client_secret is used, so the check for a valid client_secret would always fail. Signed-off-by: Bernd Eckstein <[email protected]>
Signed-off-by: Bernd Eckstein <[email protected]>
Also dissallow PKCE on /token, when PKCE flow was not started on /auth Signed-off-by: Bernd Eckstein <[email protected]>
Signed-off-by: Bernd Eckstein <[email protected]>
* Added test for invalid_grant, when wrong code_verifier is sent * Added test for mixed PKCE / no PKCE auth flows. Signed-off-by: Bernd Eckstein <[email protected]>
* fixed connector being overwritten Signed-off-by: Bernd Eckstein <[email protected]>
…authorization_code with PKCE extension Signed-off-by: Bernd Eckstein <[email protected]>
Signed-off-by: Bernd Eckstein <[email protected]>
* Adds "Authorization" to the default CORS headers{"Accept", "Accept-Language", "Content-Language", "Origin"} Signed-off-by: Bernd Eckstein <[email protected]>
Hi, good job! 👍 |
discovery endpoint /dex/.well-known/openid-configuration now has the following entry: "code_challenge_methods_supported": [ "S256", "plain" ] Signed-off-by: Bernd Eckstein <[email protected]>
Signed-off-by: Bernd Eckstein <[email protected]>
Faro upstream/feature/pkce
@sagikazarmark could you take a look at it? That would be great! |
Is it necessary to support the plain code challenge method? https://tools.ietf.org/html/rfc7636#section-7.2
https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-4.1.1.2
|
@asoorm Reading the RFC, it sounds to me that the server has to support the
However the client should not use it. It should use
Other Auth Providers also support both (e.g. auth0). |
* @asoorm added test that checks if downgrade to "plain" on /token endpoint Signed-off-by: Bernd Eckstein <[email protected]>
Updated tests (mixed-up comments), added a PKCE test
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.
Would be nice to also add some tests to oauth2_test.go
Signed-off-by: Bernd Eckstein <[email protected]> Signed-off-by: Bernd Eckstein <[email protected]>
b23b855
to
a1aab00
Compare
Signed-off-by: Bernd Eckstein <[email protected]>
In authorization_code flow with PKCE, allow empty client_secret on /auth and /token endpoints. But check the client_secret when it is given. Signed-off-by: Bernd Eckstein <[email protected]>
…onfigured" This reverts commit b6e297b. Signed-off-by: Martin Heide <[email protected]>
Revert "Allow public clients (e.g. with PKCE) to have redirect URIs configured"
@bonifaido @sagikazarmark it seems that all requested changes have been implemented in 46c6d9d . |
I could be doing something wrong but I just tried using this PR with a oidc-client-js implementation. The handleToken method would still always respond with unauthorized at the checking of client secret step. Any ideas why this would be happening? Is the if statement even required if the handleAuthCode does the verifiying step. |
@trentis It was decided to still use the client_secret with PKCE. So there are two ways to solve this issue for you:
staticClients:
- id: example-app
name: 'Example App'
public: true Note that configuring Actually i was thinking about implementing a different error message in case of PKCE used with empty client_secret on the /token endpoint, as i was aware of that pitfall. Something like: |
Cheers for the assistance @HEllRZA . Yeah an error might be good took me a while to realize it wasn't oidc-client-js as they do the code challenge automatically. So it particularly could help others. I'll probably end up using a public client and merge in the other PRs to get what I need. Hopefully it gets merged soon |
* When connecting to the token endpoint with PKCE without client_secret, but the client is configured with a client_secret, generate a special error message. Signed-off-by: Bernd Eckstein <[email protected]>
PKCE on client_secret client error message
server/handlers.go
Outdated
@@ -749,6 +759,10 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) { | |||
} | |||
return | |||
} | |||
if clientSecret == "" && client.Secret != "" && r.PostFormValue("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.
Seems inconsistent that we want to specifically call out "missing credentials" in PKCE case and not in general case.
Also I'm wondering whether not specifying client authentication error details but just general "Invalid client credentials." is for security reasons. E.g. the error you introduced would allow to determine whether the client having a specific client_id is registered on the server.
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 understand. Should we instead just log it?
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 would say it's a good idea, many such log entries could indicate that some attack is in place. We could also log when client_id is invalid. Not sure if we would like to specifically call-out "missing credentials" case, but I'm not against it. And let's do it regardless of PKCE.
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.
Ok, Now, there is a log output instead.
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 was thinking more in lines of:
if client.Secret != clientSecret {
if clientSecret != "" {
s.logger.Info("missing client secret")
} else {
s.logger.Info("invalid client secret")
}
s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
return
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.
Written text has so much potential for misunderstanding ;o)
…ial client * removes the special error message Signed-off-by: Bernd Eckstein <[email protected]>
Output info message when PKCE without client_secret used on confident…
Signed-off-by: Bernd Eckstein <[email protected]>
General missing/invalid client_secret message on token endpoint
@sagikazarmark Yes, I believe so. |
@tkleczek @sagikazarmark |
@@ -750,6 +760,11 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) { | |||
return | |||
} | |||
if client.Secret != clientSecret { | |||
if clientSecret == "" { | |||
s.logger.Infof("missing client_secret on token request for client: %s", client.ID) |
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.
Shouldn't this case be OK for PKCE? The whole idea is that the client doesn't have to provide the client secret. The role of the IdP in this case is just to verify Code verifier and Code challenge, which happens in handleAuthCode
.
Ref: https://auth0.com/docs/flows/authorization-code-flow-with-proof-key-for-code-exchange-pkce
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 tried rewriting this locally to move the clientSecret validation into handleAuthCode
, handleRefreshToken
and handlePasswordGrant
respectively and it works. I think we have to remove it from this function for PKCE to work.
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.
Here's a rought sketch of the changes I made, though I'd suggest extracting the client secret validation into its own function instead:
diff --git a/server/handlers.go b/server/handlers.go
index 342849ee..1141dffc 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -765,24 +765,15 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) {
}
return
}
- if client.Secret != clientSecret {
- if clientSecret == "" {
- s.logger.Infof("missing client_secret on token request for client: %s", client.ID)
- } else {
- s.logger.Infof("invalid client_secret on token request for client: %s", client.ID)
- }
- s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
- return
- }
grantType := r.PostFormValue("grant_type")
switch grantType {
case grantTypeAuthorizationCode:
- s.handleAuthCode(w, r, client)
+ s.handleAuthCode(w, r, client, clientSecret)
case grantTypeRefreshToken:
- s.handleRefreshToken(w, r, client)
+ s.handleRefreshToken(w, r, client, clientSecret)
case grantTypePassword:
- s.handlePasswordGrant(w, r, client)
+ s.handlePasswordGrant(w, r, client, clientSecret)
default:
s.tokenErrHelper(w, errInvalidGrant, "", http.StatusBadRequest)
}
@@ -801,7 +792,7 @@ func (s *Server) calculateCodeChallenge(codeVerifier, codeChallengeMethod string
}
// handle an access token request https://tools.ietf.org/html/rfc6749#section-4.1.3
-func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client storage.Client) {
+func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client storage.Client, clientSecret string) {
code := r.PostFormValue("code")
redirectURI := r.PostFormValue("redirect_uri")
@@ -839,6 +830,16 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
// Received PKCE request on /auth, but no code_verifier on /token
s.tokenErrHelper(w, errInvalidGrant, "Expecting parameter code_verifier in PKCE flow.", http.StatusBadRequest)
return
+ } else {
+ if client.Secret != clientSecret {
+ if clientSecret == "" {
+ s.logger.Infof("missing client_secret on token request for client: %s", client.ID)
+ } else {
+ s.logger.Infof("invalid client_secret on token request for client: %s", client.ID)
+ }
+ s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
+ return
+ }
}
if authCode.RedirectURI != redirectURI {
@@ -1000,7 +1001,17 @@ func (s *Server) exchangeAuthCode(w http.ResponseWriter, authCode storage.AuthCo
}
// handle a refresh token request https://tools.ietf.org/html/rfc6749#section-6
-func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, client storage.Client) {
+func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, client storage.Client, clientSecret string) {
+ if client.Secret != clientSecret {
+ if clientSecret == "" {
+ s.logger.Infof("missing client_secret on token request for client: %s", client.ID)
+ } else {
+ s.logger.Infof("invalid client_secret on token request for client: %s", client.ID)
+ }
+ s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
+ return
+ }
+
code := r.PostFormValue("refresh_token")
scope := r.PostFormValue("scope")
if code == "" {
@@ -1227,7 +1238,17 @@ func (s *Server) handleUserInfo(w http.ResponseWriter, r *http.Request) {
w.Write(claims)
}
-func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, client storage.Client) {
+func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, client storage.Client, clientSecret string) {
+ if client.Secret != clientSecret {
+ if clientSecret == "" {
+ s.logger.Infof("missing client_secret on token request for client: %s", client.ID)
+ } else {
+ s.logger.Infof("invalid client_secret on token request for client: %s", client.ID)
+ }
+ s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
+ return
+ }
+
// Parse the fields
if err := r.ParseForm(); err != nil {
s.tokenErrHelper(w, errInvalidRequest, "Couldn't parse data", http.StatusBadRequest)
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 initially implemented it in a similar way by skipping the client_secret validation, if the request was PKCE.
We decided that, for that case of using PKCE, the client should be configured as public and have no client_secret. And we should not omit the client_secret if the client is confidential. In fact the current implementation checks the client secret either way. Public client just allows an empty secret.
Quote from @tkleczek:
I agree that PKCE was introduced to mitigate security concerns for public clients, but my reasoning is:
If client is configured as public, then the check should pass as both clientSecret and client.Secret should be empty.
But if for whatever reason sb configures confidential client with PKCE, why not validate client secret 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.
That makes sense, thanks!
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.
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.
LGTM, thank you very much!
* Basic implementation of PKCE Signed-off-by: Tadeusz Magura-Witkowski <[email protected]> * @mfmarche on 24 Feb: when code_verifier is set, don't check client_secret In PKCE flow, no client_secret is used, so the check for a valid client_secret would always fail. Signed-off-by: Bernd Eckstein <[email protected]> * @deric on 16 Jun: return invalid_grant when wrong code_verifier Signed-off-by: Bernd Eckstein <[email protected]> * Enforce PKCE flow on /token when PKCE flow was started on /auth Also dissallow PKCE on /token, when PKCE flow was not started on /auth Signed-off-by: Bernd Eckstein <[email protected]> * fixed error messages when mixed PKCE/no PKCE flow. Signed-off-by: Bernd Eckstein <[email protected]> * server_test.go: Added PKCE error cases on /token endpoint * Added test for invalid_grant, when wrong code_verifier is sent * Added test for mixed PKCE / no PKCE auth flows. Signed-off-by: Bernd Eckstein <[email protected]> * cleanup: extracted method checkErrorResponse and type TestDefinition * fixed connector being overwritten Signed-off-by: Bernd Eckstein <[email protected]> * /token endpoint: skip client_secret verification only for grand type authorization_code with PKCE extension Signed-off-by: Bernd Eckstein <[email protected]> * Allow "Authorization" header in CORS handlers * Adds "Authorization" to the default CORS headers{"Accept", "Accept-Language", "Content-Language", "Origin"} Signed-off-by: Bernd Eckstein <[email protected]> * Add "code_challenge_methods_supported" to discovery endpoint discovery endpoint /dex/.well-known/openid-configuration now has the following entry: "code_challenge_methods_supported": [ "S256", "plain" ] Signed-off-by: Bernd Eckstein <[email protected]> * Updated tests (mixed-up comments), added a PKCE test * @asoorm added test that checks if downgrade to "plain" on /token endpoint Signed-off-by: Bernd Eckstein <[email protected]> * remove redefinition of providedCodeVerifier, fixed spelling (#6) Signed-off-by: Bernd Eckstein <[email protected]> Signed-off-by: Bernd Eckstein <[email protected]> * Rename struct CodeChallenge to PKCE Signed-off-by: Bernd Eckstein <[email protected]> * PKCE: Check clientSecret when available In authorization_code flow with PKCE, allow empty client_secret on /auth and /token endpoints. But check the client_secret when it is given. Signed-off-by: Bernd Eckstein <[email protected]> * Enable PKCE with public: true dex configuration public on staticClients now enables the following behavior in PKCE: - Public: false, PKCE will always check client_secret. This means PKCE in it's natural form is disabled. - Public: true, PKCE is enabled. It will only check client_secret if the client has sent one. But it allows the code flow if the client didn't sent one. Signed-off-by: Bernd Eckstein <[email protected]> * Redirect error on unsupported code_challenge_method - Check for unsupported code_challenge_method after redirect uri is validated, and use newErr() to return the error. - Add PKCE tests to oauth2_test.go Signed-off-by: Bernd Eckstein <[email protected]> * Reverted go.mod and go.sum to the state of master Signed-off-by: Bernd Eckstein <[email protected]> * Don't omit client secret check for PKCE Signed-off-by: Bernd Eckstein <[email protected]> * Allow public clients (e.g. with PKCE) to have redirect URIs configured Signed-off-by: Martin Heide <[email protected]> * Remove "Authorization" as Accepted Headers on CORS, small fixes Signed-off-by: Bernd Eckstein <[email protected]> * Revert "Allow public clients (e.g. with PKCE) to have redirect URIs configured" This reverts commit b6e297b. Signed-off-by: Martin Heide <[email protected]> * PKCE on client_secret client error message * When connecting to the token endpoint with PKCE without client_secret, but the client is configured with a client_secret, generate a special error message. Signed-off-by: Bernd Eckstein <[email protected]> * Output info message when PKCE without client_secret used on confidential client * removes the special error message Signed-off-by: Bernd Eckstein <[email protected]> * General missing/invalid client_secret message on token endpoint Signed-off-by: Bernd Eckstein <[email protected]> Co-authored-by: Tadeusz Magura-Witkowski <[email protected]> Co-authored-by: Martin Heide <[email protected]> Co-authored-by: M. Heide <[email protected]>
The PRs dexidp/dex#1784 and dexidp/dex#1822 are super useful, but the documentation can be confusing. First, it does not make sense to specify a secret in a public client because it will require you to pass the secret to the public client, which will make the secret "not secret". Also, as of dexidp/dex#1822, it is possible to use `redirectURIs` in a public client. Signed-off-by: leonnicolas <[email protected]>
The PRs dexidp/dex#1784 and dexidp/dex#1822 are super useful, but the documentation can be confusing. First, it does not make sense to specify a secret in a public client because it will require you to pass the secret to the public client, which will make the secret "not secret". Also, as of dexidp/dex#1822, it is possible to use `redirectURIs` in a public client. Signed-off-by: leonnicolas <[email protected]>
This pull request adds the PKCE (Proof Key for Code Exchange) implementation. (RFC 7636), Closes #1114.
Based on the implementation of @Teeed (PR #1652)
I am lookin forward to your review.
Test PKCE
You should use a public client (without secret), so PKCE is not stopped by a missing client_secret
Note
loadUserInfo: true
, it will not work until fix: allow Authorization header when doing CORS #1819 is merged.Thanks to @Teeed and i hope this PR is of interest to @mfmarche, @deric, @justin-slowik