-
-
Notifications
You must be signed in to change notification settings - Fork 962
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 support during OIDC and generic redirect_uri #4059
Conversation
a784ec2
to
872581e
Compare
ceff2da
to
fb86964
Compare
Why is that? |
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.
Good stuff! I'm primarily concerned:
- That the internal context gets correctly passed around and we don't lose track of the PKCE challenge or provider ID
- That the organization flag is still working correctly
- That we're returning the wrong flow in one instance (which could lead to some errors)
- That the generic callback keeps track of the currently active provider ID across multiple login / sign up attempts
Please also add e2e tests verifying that PKCE works end-to-end, and that the generic callback also works correctly.
// IMPORTANT: If you set this to `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 the missing <provider> path segment and no trailing slash). | ||
PKCE string `json:"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.
You could make this an enum, in which case this would also end up correctly in swagger API definitions and so on.
f.EnsureInternalContext() | ||
bytes, err := sjson.SetBytes( | ||
f.GetInternalContext(), | ||
"oidc_provider", |
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.
The internal context is prone to problems when retrying flows. I believe this is/was a problem for several MFA methods. Relying on the internal context implies that we probably need to test if multiple oidc flows (same provider / different provider) work when this feature is used. It's quite possible that we already have these tests.
@@ -173,8 +173,11 @@ func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlo | |||
} | |||
|
|||
sess := session.NewInactiveSession() | |||
sess.CompletedLoginForWithProvider(s.ID(), identity.AuthenticatorAssuranceLevel1, provider.Config().ID, | |||
httprouter.ParamsFromContext(r.Context()).ByName("organization")) | |||
orgID := "" |
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.
This change looks incorrect? Previously we fetched this from the http route and now we use it from the loginFlow (but do we know if it is correctly set there?)
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 ended up simplifying the original code and take the OrgID from the provider config directly, instead of looking at the current URL.
@@ -245,7 +259,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat | |||
return errors.WithStack(flow.ErrCompletedByStrategy) | |||
} | |||
|
|||
func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, rf *registration.Flow, providerID string) (*login.Flow, error) { | |||
func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, rf *registration.Flow) (*login.Flow, error) { |
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 method keep working with PKCE or do we somehow lose track of PKCE? To test it, you could create an e2e test:
- For an PKCE oauth2 provider with pkce force
- Sign in with account A which does not exit
- Kratos asks to sign up
- Sign up with account A
- Be logged in
- Sign out
- Perform sign up with account A
- Expect log in
- Log in with account A
- Be signed in
If you want to be extra sure add validation errors in between (e.g. incomplete profile data from oidc)
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.
All this should continue working as before, because it's orthogonal to PKCE from what I can see in the code.
Whenever we move between flows, the OAuth2 token exchange has already taken place, so all PKCE-related stuff is already done.
Essentially, we store the verifier directly before redirecting the end user, and verify it right after he comes back (during token exchange). There are no cross-flow (registrationToLogin
or the other way around) shenanigans to get in the way, I don't think.
if orgID := httprouter.ParamsFromContext(r.Context()).ByName("organization"); orgID != "" { | ||
i.OrganizationID = uuid.NullUUID{UUID: x.ParseUUID(orgID), Valid: true} | ||
} | ||
i.OrganizationID = a.OrganizationID |
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.
How are you certain that this is not a regression?
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 did end up changing the code to get the correct OrgID directly from the provider configuration.
return s.handleError(w, r, ctxUpdate.Flow, p.Link, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) | ||
} | ||
|
||
codeURL, err := getAuthRedirectURL(ctx, provider, ctxUpdate.Flow, state, up) |
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.
ctxUpdate.Flow
does not represent the most up-to-date version of the flow. Use the return value of _, err = s.validateFlow(ctx, r, ctxUpdate.Flow.ID)
instead (like it was before)
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.
done
@@ -470,6 +479,67 @@ func TestStrategy(t *testing.T) { | |||
return id | |||
} | |||
|
|||
t.Run("case=force PKCE", func(t *testing.T) { |
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 you can conveniently add tests that check if more complex flows also pass:
- Sign in with wrong provider, then try again with another
- Does PKCE actually get validated? Expect an error case
- Does the automatic detection of whether to sign in or up with PKCE work 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.
- We already have a bunch of these tests in the context of login hints/account linking. I've added a test which checks a mismatch between providers in URL and state is correctly detected and the flow aborted.
- Added tests.
- Test is already there.
Since a few other changes landed, there are a few conflicts now. Probably mostly beacuse a lot of functions got contexts now. |
Yep, I'm rebasing. |
Would the regular callback with provider id in url work too? |
Having the generic redirect URI forces PKCE for security reasons. If you want to keep your redirect URI as-is, you'll still benefic from automatic PKCE if the provider advertises it. |
# Conflicts: # selfservice/strategy/oidc/strategy.go # selfservice/strategy/oidc/strategy_login.go # selfservice/strategy/oidc/strategy_registration.go # selfservice/strategy/oidc/strategy_settings.go # Conflicts: # selfservice/strategy/oidc/strategy.go
fd56e48
to
5195727
Compare
6b39809
to
e56f142
Compare
Superseeded by #4078 |
This change introduces a new configuration for OIDC providers:
pkce
with valuesauto
(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 theissuer_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 individualredirect_uri
s for each Kratos provider config.Supersedes #4033
Closes #4009
Ref https://github.com/ory-corp/cloud/issues/6613