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

feat: client-side PKCE take 3 #4078

Merged
merged 4 commits into from
Sep 12, 2024
Merged

feat: client-side PKCE take 3 #4078

merged 4 commits into from
Sep 12, 2024

Conversation

alnr
Copy link
Contributor

@alnr alnr commented Sep 4, 2024

This change introduces a new configuration for OIDC providers: pkce with values auto (default), never, force.

When auto is specified or the field is omitted, Kratos will perform autodiscovery and perform PKCE when the server advertises support for it. This requires the issuer_url to be set for the provider.

never completely disables PKCE support. This is only theoretically useful: when a provider advertises PKCE support but doesn't actually implement it.

force always sends a PKCE challenge in the initial redirect URL, regardless of what the provider advertises. This setting is useful when the provider offers PKCE but doesn't advertise it in his ./well-known/openid-configuration.

Important: When setting pkce: force, you must whitelist a different return URL for your OAuth2 client in the provider's configuration. Instead of <base-url>/self-service/methods/oidc/callback/<provider>, you must use <base-url>/self-service/methods/oidc/callback (note missing last path segment). This is to enable the use of the same OAuth client ID+secret when configuring several Kratos OIDC providers, without having to whitelist individual redirect_uris for each Kratos provider config.

Supersedes #4033
Supersedes #4059
Closes #4009
Ref https://github.com/ory-corp/cloud/issues/6613

@alnr alnr self-assigned this Sep 4, 2024
selfservice/strategy/oidc/strategy.go Dismissed Show resolved Hide resolved
@alnr alnr marked this pull request as draft September 4, 2024 15:05
@alnr alnr force-pushed the alnr/oidc-callback-url-2 branch 2 times, most recently from 79b76d6 to 03478f9 Compare September 9, 2024 15:38
@alnr alnr marked this pull request as ready for review September 9, 2024 17:16
Copy link

codecov bot commented Sep 9, 2024

Codecov Report

Attention: Patch coverage is 64.23841% with 108 lines in your changes missing coverage. Please review.

Project coverage is 78.46%. Comparing base (5592029) to head (91b5cd3).
Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
gen/oidc/v1/state.pb.go 57.53% 29 Missing and 2 partials ⚠️
selfservice/strategy/oidc/strategy.go 56.86% 20 Missing and 2 partials ⚠️
selfservice/strategy/oidc/strategy_registration.go 45.00% 19 Missing and 3 partials ⚠️
selfservice/strategy/oidc/strategy_login.go 44.00% 13 Missing and 1 partial ⚠️
selfservice/strategy/oidc/state.go 78.33% 6 Missing and 7 partials ⚠️
cipher/chacha20.go 0.00% 1 Missing and 1 partial ⚠️
selfservice/strategy/oidc/pkce.go 94.28% 1 Missing and 1 partial ⚠️
selfservice/strategy/oidc/strategy_settings.go 60.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4078      +/-   ##
==========================================
- Coverage   78.92%   78.46%   -0.46%     
==========================================
  Files         373      376       +3     
  Lines       27123    26728     -395     
==========================================
- Hits        21406    20973     -433     
- Misses       4129     4150      +21     
- Partials     1588     1605      +17     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@hperl hperl left a comment

Choose a reason for hiding this comment

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

Very nice and clean! The only thing that could suprise users is that pkce set to force will also change the callback URL, but at least it is well documented in the config schema.

Maybe we could set the issuer URL for more of the pre-configured providers, so that this will be an automatic security improvement for as many users as possible?

selfservice/strategy/oidc/pkce.go Show resolved Hide resolved
selfservice/strategy/oidc/state.go Show resolved Hide resolved
This change introduces a new configuration for OIDC providers: pkce with values auto (default), never, force.

When auto is specified or the field is omitted, Kratos will perform autodiscovery and perform PKCE when the server advertises support for it. This requires the issuer_url to be set for the provider.

never completely disables PKCE support. This is only theoretically useful: when a provider advertises PKCE support but doesn't actually implement it.

force always sends a PKCE challenge in the initial redirect URL, regardless of what the provider advertises. This setting is useful when the provider offers PKCE but doesn't advertise it in his ./well-known/openid-configuration.

Important: When setting pkce: force, you must whitelist a different return URL for your OAuth2 client in the provider's configuration. Instead of <base-url>/self-service/methods/oidc/callback/<provider>, you must use <base-url>/self-service/methods/oidc/callback (note missing last path segment). This is to enable the use of the same OAuth client ID+secret when configuring several Kratos OIDC providers, without having to whitelist individual redirect_uris for each Kratos provider config.
@alnr alnr merged commit f7c1024 into master Sep 12, 2024
28 of 29 checks passed
@alnr alnr deleted the alnr/oidc-callback-url-2 branch September 12, 2024 07:47
Copy link
Member

@aeneasr aeneasr left a comment

Choose a reason for hiding this comment

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

Good stuff! I'm mostly worried about regressions for existing OIDC providers. In particular I'm worried about redirects we do in Ory Network.

The easiest way to test this is to set up kratos before this patch with some OIDC connections, and then check if those work like they did before after those changes.

func encryptState(ctx context.Context, c cipher.Cipher, state *oidcv1.State) (ciphertext string, err error) {
m, err := proto.Marshal(state)
if err != nil {
return "", herodot.ErrInternalServerError.WithReasonf("Unable to marshal state: %s", err)
Copy link
Member

Choose a reason for hiding this comment

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

Add WithStack for trace information across all error returns, it makes debugging significantly easier :)


var newStyleState = false

func TestHookEnableNewStyleState(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

Please don't reference the testing library in production code, but instead move this to _test.go files. If you include it, the testing library adds flags and has other side effects on the production binary. If you need to include this function in another else (like we do with persister tests), move it to a separate package. If the package is not referenced by production code, it is not included.

For example, running go run . version -test.short no longer fails with an invalid argument (as opposed to go run . version -asdf), because the flag is now defined. This can have unintended consequences / side effects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code will be removed as soon as we have completed the two-step rollout on Ory Network.

@@ -43,6 +44,11 @@ func (c *XChaCha20Poly1305) Encrypt(ctx context.Context, message []byte) (string
return "", herodot.ErrInternalServerError.WithWrap(err).WithReason("Unable to generate key")
}

// Make sure the size calculation does not overflow.
if len(message) > math.MaxInt-aead.NonceSize()-aead.Overhead() {
Copy link
Member

Choose a reason for hiding this comment

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

This method is taken from cryptopasta, if I recall correctly. Could you point me to the documentation / issue this addresses? This method is being used in several places, and I am worried this will introduce regressions. Granted, MaxInt is pretty large.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The AES-GCM code comes from cryptopasta. The XChaCha code was hand-written from the start.

In an earlier revision of this PR, I touched this code but reverted it later to the original version. GitHub's security scanner then started complaining, correctly, that the size calculation below could overflow. Even though the code was unchanged from master. So I added this assertion, which is the exact same as the one in Hydra.

@@ -63,10 +62,12 @@ func TestGetCmd(t *testing.T) {
return out
}
transform := func(token string) string {
if !encrypt {
return token
if encrypt {
Copy link
Member

Choose a reason for hiding this comment

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

I assume this changed because we now, somewhere, properly check if the token can be decrypted and then this test failed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exactly. I changed most tests across Kratos to actually use encryption, and this one was the only one to fail. Good sign :)

Comment on lines +94 to +95
config.ViperKeySecretsCookie: []string{randx.MustString(32, randx.AlphaNum)},
config.ViperKeySecretsDefault: []string{randx.MustString(32, randx.AlphaNum)},
Copy link
Member

Choose a reason for hiding this comment

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

Are these now always required? If so, have the quickstarts been adjusted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They are not strictly required, but I would consider it insecure to run Kratos without these even before this changeset.

@@ -5,6 +5,28 @@
"ui": {
"method": "POST",
"nodes": [
{
Copy link
Member

Choose a reason for hiding this comment

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

Is it expected for these existing OIDC snapshots to be updated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, because I configured additional OIDC providers (those with PKCE=force,auto,never).

return []oauth2.AuthCodeOption{oauth2.VerifierOption(s.GetPkceVerifier())}
}

func maybePKCE(ctx context.Context, d pkceDependencies, _p Provider) (verifier string) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: underscore is possible to use in Go, but uncommon since it's the throwaway variable name, e.g. _ context.Context. Better would be to use p Provider and oauth2Provider, ok := p.(OAuth2Provider)

}

func (s *State) String() string {
return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", s.FlowID, s.Data)))
Copy link
Member

Choose a reason for hiding this comment

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

We rely on this format in RedirectSocialAuthenticationFlows in our other repository. Changing this method will most likely cause regressions there. Are you aware of this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I am aware, that's why the toggle (which defaults to the old-style state generation) exists in the first place. I will adjust the code in cloud to work with both state generation versions before we activate the new one.

}

func ParseStateCompatiblity(ctx context.Context, c cipher.Cipher, s string) (*oidcv1.State, error) {
// new-style: encrypted
Copy link
Member

Choose a reason for hiding this comment

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

New style probably won't work with RedirectSocialAuthenticationFlows

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, we need a two-step rollout for this (hence the temporary toggle).

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.

Support for PKCE in OIDC social providers
3 participants