Skip to content
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

Merged
merged 35 commits into from
Oct 26, 2020
Merged

PKCE implementation #1784

merged 35 commits into from
Oct 26, 2020

Conversation

HEllRZA
Copy link
Contributor

@HEllRZA HEllRZA commented Aug 26, 2020

This pull request adds the PKCE (Proof Key for Code Exchange) implementation. (RFC 7636), Closes #1114.

Based on the implementation of @Teeed (PR #1652)

  • add the remarks of @deric (return invalid_grand when wrong code_verifier)
  • fix the issue, @mfmarche found.
  • Enforce PKCE flow on /token when PKCE flow was started on /auth
  • made sure, the correct errors are returned
  • add Tests for the error cases
  • make sure, the client_secret is only omitted, when code_verifier is received on /token, and grant_type is correct.
  • add "Authorization" to allowed CORS headers

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

staticClients:
- id: example-app
  name: 'Example App'
  public: true

Note

Thanks to @Teeed and i hope this PR is of interest to @mfmarche, @deric, @justin-slowik

Teeed and others added 10 commits August 17, 2020 12:40
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]>
Also dissallow PKCE on /token, when PKCE flow was not started on /auth

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]>
* Adds "Authorization" to the default CORS headers{"Accept", "Accept-Language", "Content-Language", "Origin"}

Signed-off-by: Bernd Eckstein <[email protected]>
@Teeed
Copy link
Contributor

Teeed commented Aug 26, 2020

Hi,

good job! 👍

HEllRZA and others added 3 commits August 31, 2020 10:50
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]>
@HEllRZA
Copy link
Contributor Author

HEllRZA commented Sep 10, 2020

@sagikazarmark could you take a look at it? That would be great!

@asoorm
Copy link

asoorm commented Sep 10, 2020

Is it necessary to support the plain code challenge method?

https://tools.ietf.org/html/rfc7636#section-7.2

Because of this, "plain" SHOULD NOT be used and exists only for
compatibility with deployed implementations where the request path is
already protected. The "plain" method SHOULD NOT be used in new
implementations, unless they cannot support "S256" for some technical
reason.

The "S256" code challenge method or other cryptographically secure
code challenge method extension SHOULD be used. The "plain" code
challenge method relies on the operating system and transport
security not to disclose the request to an attacker.

https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-4.1.1.2

The plain transformation is for compatibility with existing
deployments and for constrained environments that can't use the
"S256" transformation.

@HEllRZA
Copy link
Contributor Author

HEllRZA commented Sep 10, 2020

@asoorm
Thanks for your input! I also dislike "plain".

Reading the RFC, it sounds to me that the server has to support the code_challenge_method "plain". It is even the default, when no code_challenge_method is specified. The passages you quote discourage the use of "plain" in the client that starts the PKCE request.

Client Sends the Code Challenge with the Authorization Request

The client sends the code challenge as part of the OAuth 2.0
Authorization Request (Section 4.1.1 of [RFC6749]) using the
following additional parameters:

code_challenge
REQUIRED. Code challenge.

code_challenge_method
OPTIONAL, defaults to "plain" if not present in the request. Code
verifier transformation method is "S256" or "plain".

However the client should not use it. It should use "S256" and never downgrade to "plain". "S256" is required on the server.

Clients MUST NOT downgrade to "plain" after trying the "S256" method.
Servers that support PKCE are required to support "S256", and servers
that do not support PKCE will simply ignore the unknown
"code_verifier".

Other Auth Providers also support both (e.g. auth0).
Attacks should be prevented by disallowing to use "plain" on the /token endpoint, when "S256" was used as code_challenge_method on the /auth endpoint to start the flow. I will make sure that i have a test for that!

HEllRZA and others added 2 commits September 10, 2020 16:09
* @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
server/handlers.go Outdated Show resolved Hide resolved
server/oauth2.go Outdated Show resolved Hide resolved
server/handlers.go Outdated Show resolved Hide resolved
server/server_test.go Outdated Show resolved Hide resolved
Copy link
Contributor

@tkleczek tkleczek left a 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

storage/storage.go Outdated Show resolved Hide resolved
Signed-off-by: Bernd Eckstein <[email protected]>
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]>
heidemn-faro and others added 2 commits October 5, 2020 18:26
…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"
@heidemn-faro
Copy link
Contributor

heidemn-faro commented Oct 8, 2020

@bonifaido @sagikazarmark it seems that all requested changes have been implemented in 46c6d9d .
Anything else that needs to be addressed before this can be merged?

@trentis
Copy link

trentis commented Oct 13, 2020

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.

@HEllRZA
Copy link
Contributor Author

HEllRZA commented Oct 13, 2020

@trentis It was decided to still use the client_secret with PKCE.

So there are two ways to solve this issue for you:

  1. Use a client secret with oidc-client-js (that's probably not what you want to do)
  2. Configure your dex-client as public client with no client secret.
staticClients:
- id: example-app
  name: 'Example App'
  public: true

Note that configuring redirectURIs does currently not work with public clients (see #1822) but public clients allow all localhost callback URIs (e.g. http://localhost:5556/dex/callback) (IIRC 127.0.0.1 does not work!)

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:
"Missing client_secret. Please configure your dex client as "public" if you want to use PKCE without client_secret"

@HEllRZA
Copy link
Contributor Author

HEllRZA commented Oct 13, 2020

@trentis Another pitfall:
If you use loadUserInfo: true in oidc-client-js, it will not work until #1819 is merged.

@trentis
Copy link

trentis commented Oct 13, 2020

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

HEllRZA and others added 2 commits October 14, 2020 11:24
* 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
@@ -749,6 +759,10 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) {
}
return
}
if clientSecret == "" && client.Secret != "" && r.PostFormValue("code_verifier") != "" {
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

@tkleczek tkleczek Oct 15, 2020

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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)

HEllRZA and others added 4 commits October 15, 2020 09:07
…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…
General missing/invalid client_secret message on token endpoint
@sagikazarmark
Copy link
Member

@tkleczek @HEllRZA is this PR ready for another look?

@tkleczek
Copy link
Contributor

@sagikazarmark Yes, I believe so.

@HEllRZA
Copy link
Contributor Author

HEllRZA commented Oct 18, 2020

@tkleczek @sagikazarmark
Yes, it is ripe.

@@ -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)
Copy link

@johanbrandhorst johanbrandhorst Oct 21, 2020

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

Copy link

@johanbrandhorst johanbrandhorst Oct 21, 2020

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.

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)

Copy link
Contributor Author

@HEllRZA HEllRZA Oct 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johanbrandhorst

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, thanks!

Copy link
Member

@sagikazarmark sagikazarmark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks @HEllRZA and @tkleczek for your work! ❤️

Copy link
Member

@bonifaido bonifaido left a 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!

@sagikazarmark sagikazarmark merged commit b551969 into dexidp:master Oct 26, 2020
elffjs pushed a commit to DIMO-Network/dex that referenced this pull request Jun 27, 2022
* 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]>
leonnicolas added a commit to leonnicolas/website that referenced this pull request Feb 14, 2023
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]>
kerberos727 added a commit to kerberos727/dex-project that referenced this pull request Aug 23, 2024
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PKCE support for public clients
9 participants