-
-
Notifications
You must be signed in to change notification settings - Fork 964
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: add support for Proof Key For Code Exchange (PKCE) in OIDC social providers #4033
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,10 +16,10 @@ import ( | |
"strings" | ||
"time" | ||
|
||
"github.com/ory/x/sqlxx" | ||
|
||
"golang.org/x/exp/maps" | ||
|
||
"github.com/ory/x/sqlxx" | ||
|
||
"github.com/ory/x/urlx" | ||
|
||
"go.opentelemetry.io/otel/attribute" | ||
|
@@ -426,7 +426,7 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt | |
var et *identity.CredentialsOIDCEncryptedTokens | ||
switch p := provider.(type) { | ||
case OAuth2Provider: | ||
token, err := s.ExchangeCode(r.Context(), provider, code) | ||
token, err := s.ExchangeCode(r.Context(), provider, code, req) | ||
if err != nil { | ||
s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) | ||
return | ||
|
@@ -510,7 +510,7 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt | |
} | ||
} | ||
|
||
func (s *Strategy) ExchangeCode(ctx context.Context, provider Provider, code string) (token *oauth2.Token, err error) { | ||
func (s *Strategy) ExchangeCode(ctx context.Context, provider Provider, code string, flow flow.Flow) (token *oauth2.Token, err error) { | ||
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "strategy.oidc.ExchangeCode") | ||
defer otelx.End(span, &err) | ||
span.SetAttributes(attribute.String("provider_id", provider.Config().ID)) | ||
|
@@ -525,11 +525,23 @@ func (s *Strategy) ExchangeCode(ctx context.Context, provider Provider, code str | |
return nil, err | ||
} | ||
} | ||
|
||
client := s.d.HTTPClient(ctx) | ||
ctx = context.WithValue(ctx, oauth2.HTTPClient, client.HTTPClient) | ||
token, err = te.Exchange(ctx, code) | ||
return token, err | ||
switch loginFlow := flow.(type) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this not work for registration flows? |
||
case *login.Flow: | ||
if provider.Config().PKCSMethod != "" { | ||
pkcsContext, err := GetPKCSContext(loginFlow) | ||
if err != nil { | ||
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode PKCS context: %s", err)) | ||
} | ||
if pkcsContext.Verifier != "" && (pkcsContext.Method == "S256" || pkcsContext.Method == "plain") { | ||
return te.Exchange(ctx, code, oauth2.VerifierOption(pkcsContext.Verifier)) | ||
} else { | ||
return nil, errors.Errorf("Invalid PKCS method: %s or empty verifier: %s", pkcsContext.Method, pkcsContext.Verifier) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a |
||
} | ||
} | ||
} | ||
return te.Exchange(ctx, code) | ||
default: | ||
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The chosen provider is not capable of exchanging an OAuth 2.0 code for an access token.")) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,22 +10,22 @@ import ( | |
"strings" | ||
"time" | ||
|
||
"github.com/ory/kratos/selfservice/strategy/idfirst" | ||
"github.com/ory/x/stringsx" | ||
|
||
"github.com/ory/kratos/selfservice/flowhelpers" | ||
|
||
"github.com/julienschmidt/httprouter" | ||
"github.com/tidwall/gjson" | ||
"github.com/tidwall/sjson" | ||
"golang.org/x/oauth2" | ||
|
||
"github.com/ory/kratos/session" | ||
"github.com/ory/kratos/text" | ||
|
||
"github.com/ory/kratos/ui/node" | ||
"github.com/ory/x/otelx" | ||
"github.com/ory/x/sqlcon" | ||
"github.com/ory/x/stringsx" | ||
|
||
"github.com/ory/kratos/selfservice/flow/registration" | ||
|
||
"github.com/ory/kratos/text" | ||
"github.com/ory/kratos/selfservice/flowhelpers" | ||
"github.com/ory/kratos/selfservice/strategy/idfirst" | ||
|
||
"github.com/ory/kratos/continuity" | ||
|
||
|
@@ -48,6 +48,13 @@ func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { | |
s.setRoutes(r) | ||
} | ||
|
||
const internalContextPKCSPath = "pkcs" | ||
|
||
type PkcsContext struct { | ||
Method string `json:"method"` | ||
Verifier string `json:"verifier"` | ||
} | ||
|
||
// Update Login Flow with OpenID Connect Method | ||
// | ||
// swagger:model updateLoginFlowWithOidcMethod | ||
|
@@ -255,6 +262,17 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, | |
} | ||
|
||
state := generateState(f.ID.String()) | ||
|
||
if provider.Config().PKCSMethod != "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also needs to be done for registration. And how about settings? |
||
err := SetPKCSContext(f, PkcsContext{ | ||
Method: provider.Config().PKCSMethod, | ||
Verifier: oauth2.GenerateVerifier(), | ||
}) | ||
if err != nil { | ||
return nil, s.handleError(w, r, f, pid, nil, err) | ||
} | ||
} | ||
|
||
if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(ctx, f.ID); hasCode { | ||
state.setCode(code.InitCode) | ||
} | ||
|
@@ -386,3 +404,34 @@ func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request | |
func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, f *login.Flow) error { | ||
return s.populateMethod(r, f, text.NewInfoLoginWith) | ||
} | ||
|
||
func SetPKCSContext(flow flow.InternalContexter, context PkcsContext) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd move this and the next method into the |
||
if flow.GetInternalContext() == nil { | ||
flow.EnsureInternalContext() | ||
} | ||
bytes, err := sjson.SetBytes( | ||
flow.GetInternalContext(), | ||
internalContextPKCSPath, | ||
context, | ||
) | ||
if err != nil { | ||
return err | ||
} | ||
flow.SetInternalContext(bytes) | ||
|
||
return nil | ||
} | ||
|
||
func GetPKCSContext(flow flow.InternalContexter) (*PkcsContext, error) { | ||
if flow.GetInternalContext() == nil { | ||
flow.EnsureInternalContext() | ||
} | ||
raw := gjson.GetBytes(flow.GetInternalContext(), internalContextPKCSPath) | ||
if !raw.IsObject() { | ||
return nil, nil | ||
} | ||
var context PkcsContext | ||
err := json.Unmarshal([]byte(raw.Raw), &context) | ||
|
||
return &context, err | ||
} |
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.
r
could also be a registration flow, correct? In that case, we won't find a login flow. Also, the context needs to be bound to the user's request context, notcontext.Background()
.I think you can just add
flow.InternalContexter
to theider
interface, and user
directly.