diff --git a/oryx/dbal/testhelpers.go b/oryx/dbal/testhelpers.go index 91df8af4e5ba..bf4481d91c73 100644 --- a/oryx/dbal/testhelpers.go +++ b/oryx/dbal/testhelpers.go @@ -31,7 +31,7 @@ func RestoreFromSchemaDump(t testing.TB, c *pop.Connection, migrations fs.FS, wr updateDump := func(t testing.TB) { dump := dockertest.DumpSchema(t, c) - f, err := os.OpenFile(dumpFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + f, err := os.OpenFile(dumpFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) require.NoError(t, err) defer f.Close() diff --git a/oryx/ipx/ssrf.go b/oryx/ipx/ssrf.go index 38ea4c898213..795b0f4a798a 100644 --- a/oryx/ipx/ssrf.go +++ b/oryx/ipx/ssrf.go @@ -24,10 +24,12 @@ var ( ssrf.WithNetworks("tcp4", "tcp6"), ssrf.WithAllowedV4Prefixes( netip.MustParsePrefix("10.0.0.0/8"), // Private-Use (RFC 1918) + netip.MustParsePrefix("100.64.0.0/10"), // Shared Address Space (RFC 6598) netip.MustParsePrefix("127.0.0.0/8"), // Loopback (RFC 1122, Section 3.2.1.3)) netip.MustParsePrefix("169.254.0.0/16"), // Link Local (RFC 3927) netip.MustParsePrefix("172.16.0.0/12"), // Private-Use (RFC 1918) netip.MustParsePrefix("192.168.0.0/16"), // Private-Use (RFC 1918) + netip.MustParsePrefix("198.18.0.0/15"), // Benchmarking (RFC 2544) ), ssrf.WithAllowedV6Prefixes( netip.MustParsePrefix("::1/128"), // Loopback (RFC 4193) diff --git a/schema/handler_test.go b/schema/handler_test.go index 164926079a75..a8631b98c497 100644 --- a/schema/handler_test.go +++ b/schema/handler_test.go @@ -135,7 +135,6 @@ func TestHandler(t *testing.T) { for id, s := range schemas { t.Run(fmt.Sprintf("case=get %s schema", id), func(t *testing.T) { - _, err := s.getRaw() actual := getReq(t.Context(), t, fmt.Sprintf("/schemas/%s", url.PathEscape(id)), s.expectedHttpResponseCode) require.True(t, json.Valid(actual), string(actual)) diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index 315196a156be..38b8f205b729 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -984,7 +984,7 @@ continueLogin: return } - if err := h.d.LoginHookExecutor().PostLoginHook(w, r, group, f, i, sess, ""); err != nil { + if err := h.d.LoginHookExecutor().PostLoginHook(w, r, group, f, i, sess, nil, ""); err != nil { if errors.Is(err, ErrAddressNotVerified()) { h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, ct, node.DefaultGroup, errors.WithStack(schema.NewAddressNotVerifiedError())) return diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index 0adaba114763..e1e4eb674261 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -22,6 +22,7 @@ import ( "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/sessiontokenexchange" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/session" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" @@ -38,7 +39,7 @@ type ( } PostHookExecutor interface { - ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, a *Flow, s *session.Session) error + ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, a *Flow, s *session.Session, c *claims.Claims) error } HooksProvider interface { @@ -132,6 +133,7 @@ func (e *HookExecutor) PostLoginHook( f *Flow, i *identity.Identity, s *session.Session, + c *claims.Claims, provider string, ) (err error) { ctx := r.Context() @@ -151,15 +153,15 @@ func (e *HookExecutor) PostLoginHook( return err } - c := e.d.Config() + cfg := e.d.Config() // Verify the redirect URL before we do any other processing. returnTo, err := redir.SecureRedirectTo(r, - c.SelfServiceBrowserDefaultReturnTo(ctx), + cfg.SelfServiceBrowserDefaultReturnTo(ctx), redir.SecureRedirectReturnTo(f.ReturnTo), redir.SecureRedirectUseSourceURL(f.RequestURL), - redir.SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(ctx)), - redir.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(ctx)), - redir.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowLoginReturnTo(ctx, f.Active.String())), + redir.SecureRedirectAllowURLs(cfg.SelfServiceBrowserAllowedReturnToDomains(ctx)), + redir.SecureRedirectAllowSelfServiceURLs(cfg.SelfPublicURL(ctx)), + redir.SecureRedirectOverrideDefaultReturnTo(cfg.SelfServiceFlowLoginReturnTo(ctx, f.Active.String())), ) if err != nil { return err @@ -187,7 +189,7 @@ func (e *HookExecutor) PostLoginHook( return err } for k, executor := range hooks { - if err := executor.ExecuteLoginPostHook(w, r, g, f, s); err != nil { + if err := executor.ExecuteLoginPostHook(w, r, g, f, s, c); err != nil { if errors.Is(err, ErrHookAbortFlow) { e.d.Logger(). WithRequest(r). diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go index c8c564947ce2..30b706535c01 100644 --- a/selfservice/flow/login/hook_test.go +++ b/selfservice/flow/login/hook_test.go @@ -75,7 +75,7 @@ func TestLoginExecutor(t *testing.T) { } testhelpers.SelfServiceHookLoginErrorHandler(t, w, r, - reg.LoginHookExecutor().PostLoginHook(w, r, strategy.ToUiNodeGroup(), loginFlow, useIdentity, sess, "")) + reg.LoginHookExecutor().PostLoginHook(w, r, strategy.ToUiNodeGroup(), loginFlow, useIdentity, sess, nil, "")) }) router.HandleFunc("GET /login/post2fa", func(w http.ResponseWriter, r *http.Request) { @@ -95,7 +95,7 @@ func TestLoginExecutor(t *testing.T) { } testhelpers.SelfServiceHookLoginErrorHandler(t, w, r, - reg.LoginHookExecutor().PostLoginHook(w, r, strategy.ToUiNodeGroup(), loginFlow, useIdentity, sess, "")) + reg.LoginHookExecutor().PostLoginHook(w, r, strategy.ToUiNodeGroup(), loginFlow, useIdentity, sess, nil, "")) }) ts := httptest.NewServer(router) diff --git a/selfservice/hook/error.go b/selfservice/hook/error.go index 58a87665c7e8..eb27a7db2894 100644 --- a/selfservice/hook/error.go +++ b/selfservice/hook/error.go @@ -12,6 +12,7 @@ import ( "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/identity" @@ -64,7 +65,7 @@ func (e Error) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Req return e.err("ExecuteSettingsPostPersistHook", settings.ErrHookAbortFlow) } -func (e Error) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, a *login.Flow, s *session.Session) error { +func (e Error) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, a *login.Flow, s *session.Session, c *claims.Claims) error { return e.err("ExecuteLoginPostHook", login.ErrHookAbortFlow) } diff --git a/selfservice/hook/require_verified_address.go b/selfservice/hook/require_verified_address.go index f166f36ca97f..29494b74fefc 100644 --- a/selfservice/hook/require_verified_address.go +++ b/selfservice/hook/require_verified_address.go @@ -11,6 +11,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/text" "github.com/ory/kratos/x" "github.com/ory/kratos/x/nosurfx" @@ -56,7 +57,7 @@ func NewAddressVerifier(r addressVerifierDependencies) *AddressVerifier { } } -func (e *AddressVerifier) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, f *login.Flow, s *session.Session) (err error) { +func (e *AddressVerifier) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, f *login.Flow, s *session.Session, _ *claims.Claims) (err error) { ctx, span := e.r.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.hook.Verifier.do") r = r.WithContext(ctx) defer otelx.End(span, &err) diff --git a/selfservice/hook/require_verified_address_test.go b/selfservice/hook/require_verified_address_test.go index 11afd1158289..68e84402d430 100644 --- a/selfservice/hook/require_verified_address_test.go +++ b/selfservice/hook/require_verified_address_test.go @@ -101,7 +101,7 @@ func TestAddressVerifier(t *testing.T) { ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID(), VerifiableAddresses: uc.verifiableAddresses}, } - err := verifier.ExecuteLoginPostHook(nil, httptest.NewRequest("GET", "http://example.com", nil), node.DefaultGroup, tc.flow, sessions) + err := verifier.ExecuteLoginPostHook(nil, httptest.NewRequest("GET", "http://example.com", nil), node.DefaultGroup, tc.flow, sessions, nil) if tc.neverError || uc.expectedError == nil { assert.NoError(t, err) } else { @@ -159,7 +159,7 @@ func TestAddressVerifier(t *testing.T) { } // Expect verification flow creation and ErrHookAbortFlow - err := verifier.ExecuteLoginPostHook(mockResponse, mockJSONReq, node.DefaultGroup, loginFlow, sessions) + err := verifier.ExecuteLoginPostHook(mockResponse, mockJSONReq, node.DefaultGroup, loginFlow, sessions, nil) assert.ErrorIs(t, err, login.ErrHookAbortFlow) // Verify response contains continueWith and ErrAddressNotVerified @@ -217,7 +217,7 @@ func TestAddressVerifier(t *testing.T) { } // Expect verification flow creation and redirect - err := verifier.ExecuteLoginPostHook(mockResponse, mockBrowserReq, node.DefaultGroup, browserFlow, sessions) + err := verifier.ExecuteLoginPostHook(mockResponse, mockBrowserReq, node.DefaultGroup, browserFlow, sessions, nil) assert.ErrorIs(t, err, login.ErrHookAbortFlow) // Verify redirect occurred @@ -255,7 +255,7 @@ func TestAddressVerifier(t *testing.T) { Identity: identity, } - err := verifier.ExecuteLoginPostHook(nil, mockRequest, node.DefaultGroup, verifiedFlow, sessions) + err := verifier.ExecuteLoginPostHook(nil, mockRequest, node.DefaultGroup, verifiedFlow, sessions, nil) assert.NoError(t, err) }) @@ -280,7 +280,7 @@ func TestAddressVerifier(t *testing.T) { Identity: identity, } - err := verifier.ExecuteLoginPostHook(nil, mockRequest, node.DefaultGroup, noAddressFlow, sessions) + err := verifier.ExecuteLoginPostHook(nil, mockRequest, node.DefaultGroup, noAddressFlow, sessions, nil) assert.ErrorIs(t, err, herodot.ErrMisconfiguration()) }) }) diff --git a/selfservice/hook/session_destroyer.go b/selfservice/hook/session_destroyer.go index 7503a2a44ef6..1cb73a43a546 100644 --- a/selfservice/hook/session_destroyer.go +++ b/selfservice/hook/session_destroyer.go @@ -10,6 +10,7 @@ import ( "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/session" "github.com/ory/kratos/ui/node" "github.com/ory/x/otelx" @@ -35,7 +36,7 @@ func NewSessionDestroyer(r sessionDestroyerDependencies) *SessionDestroyer { return &SessionDestroyer{r: r} } -func (e *SessionDestroyer) ExecuteLoginPostHook(_ http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, _ *login.Flow, s *session.Session) error { +func (e *SessionDestroyer) ExecuteLoginPostHook(_ http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, _ *login.Flow, s *session.Session, _ *claims.Claims) error { return otelx.WithSpan(r.Context(), "selfservice.hook.SessionDestroyer.ExecuteLoginPostHook", func(ctx context.Context) error { if _, err := e.r.SessionPersister().RevokeSessionsIdentityExcept(ctx, s.Identity.ID, s.ID); err != nil { return err diff --git a/selfservice/hook/session_destroyer_test.go b/selfservice/hook/session_destroyer_test.go index 66123a614127..a11adcbc97b3 100644 --- a/selfservice/hook/session_destroyer_test.go +++ b/selfservice/hook/session_destroyer_test.go @@ -53,6 +53,7 @@ func TestSessionDestroyer(t *testing.T) { node.DefaultGroup, nil, &session.Session{Identity: i}, + nil, ) }, }, diff --git a/selfservice/hook/show_verification_ui.go b/selfservice/hook/show_verification_ui.go index f533d5bca694..b8a856f4db90 100644 --- a/selfservice/hook/show_verification_ui.go +++ b/selfservice/hook/show_verification_ui.go @@ -16,6 +16,7 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/session" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" @@ -60,7 +61,7 @@ func (e *ShowVerificationUIHook) ExecutePostRegistrationPostPersistHook(_ http.R // ExecuteLoginPostHook adds redirect headers and status code if the request is a browser request. // If the request is not a browser request, this hook does nothing. -func (e *ShowVerificationUIHook) ExecuteLoginPostHook(_ http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, f *login.Flow, _ *session.Session) error { +func (e *ShowVerificationUIHook) ExecuteLoginPostHook(_ http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, f *login.Flow, _ *session.Session, _ *claims.Claims) error { return e.execute(r, f) } diff --git a/selfservice/hook/show_verification_ui_test.go b/selfservice/hook/show_verification_ui_test.go index fd216e4a9036..2dbf40225a85 100644 --- a/selfservice/hook/show_verification_ui_test.go +++ b/selfservice/hook/show_verification_ui_test.go @@ -85,7 +85,7 @@ func TestExecutePostRegistrationPostPersistHook(t *testing.T) { browserRequest := httptest.NewRequest("GET", "/", nil) f := &login.Flow{} rec := httptest.NewRecorder() - require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", f, nil)) + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", f, nil, nil)) require.Equal(t, 200, rec.Code) }) @@ -96,7 +96,7 @@ func TestExecutePostRegistrationPostPersistHook(t *testing.T) { browserRequest.Header.Add("Accept", "application/json") f := &login.Flow{} rec := httptest.NewRecorder() - require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", f, nil)) + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", f, nil, nil)) require.Equal(t, 200, rec.Code) }) @@ -113,7 +113,7 @@ func TestExecutePostRegistrationPostPersistHook(t *testing.T) { flow.NewContinueWithVerificationUI(vf.ID, "some@ory.sh", ""), } rec := httptest.NewRecorder() - require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", rf, nil)) + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", rf, nil, nil)) assert.Equal(t, 200, rec.Code) assert.Equal(t, "/verification?flow="+vf.ID.String(), rf.ReturnToVerification) }) @@ -128,7 +128,7 @@ func TestExecutePostRegistrationPostPersistHook(t *testing.T) { flow.NewContinueWithSetToken("token"), } rec := httptest.NewRecorder() - require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", rf, nil)) + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", rf, nil, nil)) assert.Equal(t, 200, rec.Code) }) }) @@ -201,7 +201,7 @@ func TestExecutePostRegistrationPostPersistHook(t *testing.T) { lf.InternalContext = internalContext rec := httptest.NewRecorder() - require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", lf, nil)) + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", lf, nil, nil)) assert.Equal(t, 200, rec.Code) assert.Equal(t, "/verification?flow="+vfID.String(), lf.ReturnToVerification) }) @@ -220,7 +220,7 @@ func TestExecutePostRegistrationPostPersistHook(t *testing.T) { lf.InternalContext = internalContext rec := httptest.NewRecorder() - err = h.ExecuteLoginPostHook(rec, browserRequest, "", lf, nil) + err = h.ExecuteLoginPostHook(rec, browserRequest, "", lf, nil, nil) require.Error(t, err) }) }) diff --git a/selfservice/hook/stub/test_body.jsonnet b/selfservice/hook/stub/test_body.jsonnet index 117ecc587707..f406ad51076e 100644 --- a/selfservice/hook/stub/test_body.jsonnet +++ b/selfservice/hook/stub/test_body.jsonnet @@ -1,10 +1,14 @@ function(ctx) std.prune({ flow_id: ctx.flow.id, - identity_id: if std.objectHas(ctx, "identity") then ctx.identity.id, - session_id: if std.objectHas(ctx, "session") then ctx.session.id, + identity_id: if std.objectHas(ctx, 'identity') then ctx.identity.id, + session_id: if std.objectHas(ctx, 'session') then ctx.session.id, headers: ctx.request_headers, url: ctx.request_url, method: ctx.request_method, cookies: ctx.request_cookies, - transient_payload: if std.objectHas(ctx.flow, "transient_payload") then ctx.flow.transient_payload, + transient_payload: if std.objectHas(ctx.flow, 'transient_payload') then ctx.flow.transient_payload, + nickname: if std.objectHas(ctx, 'claims') then ctx.claims.nickname, + groups: if std.objectHas(ctx, 'claims') && + std.objectHas(ctx.claims, 'raw_claims') && + std.objectHas(ctx.claims.raw_claims, 'groups') then ctx.claims.raw_claims.groups, }) diff --git a/selfservice/hook/verification.go b/selfservice/hook/verification.go index 1f5e563e02e4..50bf4894fa66 100644 --- a/selfservice/hook/verification.go +++ b/selfservice/hook/verification.go @@ -23,6 +23,7 @@ import ( "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/session" "github.com/ory/kratos/text" "github.com/ory/kratos/ui/node" @@ -74,7 +75,7 @@ func (e *Verifier) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http }) } -func (e *Verifier) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, f *login.Flow, s *session.Session) (err error) { +func (e *Verifier) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, f *login.Flow, s *session.Session, c *claims.Claims) (err error) { ctx, span := e.r.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.hook.Verifier.ExecuteLoginPostHook") r = r.WithContext(ctx) defer otelx.End(span, &err) diff --git a/selfservice/hook/verification_test.go b/selfservice/hook/verification_test.go index 4cfec6cf8e0b..a610847eebbb 100644 --- a/selfservice/hook/verification_test.go +++ b/selfservice/hook/verification_test.go @@ -52,7 +52,7 @@ func TestVerifier(t *testing.T) { name: "login", execHook: func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error { return h.ExecuteLoginPostHook( - httptest.NewRecorder(), u, node.CodeGroup, f.(*login.Flow), &session.Session{ID: x.NewUUID(), Identity: i}) + httptest.NewRecorder(), u, node.CodeGroup, f.(*login.Flow), &session.Session{ID: x.NewUUID(), Identity: i}, nil) }, originalFlow: func() interface { flow.InternalContexter @@ -159,7 +159,7 @@ func TestVerifier(t *testing.T) { h := hook.NewVerifier(reg) i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) f := &login.Flow{RequestedAAL: "aal2"} - require.NoError(t, h.ExecuteLoginPostHook(httptest.NewRecorder(), u, node.CodeGroup, f, &session.Session{ID: x.NewUUID(), Identity: i})) + require.NoError(t, h.ExecuteLoginPostHook(httptest.NewRecorder(), u, node.CodeGroup, f, &session.Session{ID: x.NewUUID(), Identity: i}, nil)) messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) require.EqualError(t, err, "queue is empty") diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go index d0eccdf40a97..3b907e897916 100644 --- a/selfservice/hook/web_hook.go +++ b/selfservice/hook/web_hook.go @@ -34,6 +34,7 @@ import ( "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/session" "github.com/ory/kratos/text" "github.com/ory/kratos/ui/node" @@ -88,6 +89,7 @@ type ( RequestCookies map[string]string `json:"request_cookies"` Identity *identity.Identity `json:"identity,omitempty"` Session *session.Session `json:"session,omitempty"` + Claims *claims.Claims `json:"claims,omitempty"` } WebHook struct { @@ -138,7 +140,7 @@ func (e *WebHook) ExecuteLoginPreHook(_ http.ResponseWriter, req *http.Request, }) } -func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, _ node.UiNodeGroup, flow *login.Flow, session *session.Session) error { +func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, _ node.UiNodeGroup, flow *login.Flow, session *session.Session, claims *claims.Claims) error { return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteLoginPostHook", func(ctx context.Context) error { return e.execute(ctx, &templateContext{ Flow: flow, @@ -148,6 +150,7 @@ func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, RequestCookies: cookies(req), Identity: session.Identity, Session: session, + Claims: claims, }) }) } diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go index f9aaf2c94c9c..fe661e383cda 100644 --- a/selfservice/hook/web_hook_integration_test.go +++ b/selfservice/hook/web_hook_integration_test.go @@ -43,6 +43,7 @@ import ( "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/session" "github.com/ory/kratos/text" "github.com/ory/kratos/ui/node" @@ -77,6 +78,13 @@ func newWebHookDeps(t *testing.T, logger *logrusx.Logger, reg *driver.RegistryDe } } +var oidcClaims = claims.Claims{ + Nickname: "nicky", + RawClaims: map[string]interface{}{ + "groups": []string{"first", "second"}, + }, +} + func TestWebHooks(t *testing.T) { logger := logrusx.New("kratos", "test") @@ -217,6 +225,32 @@ func TestWebHooks(t *testing.T) { "Valid-Header", }) + bodyWithFlowAndIdentityAndSessionAndClaimsAndTransientPayload := func(req *http.Request, f flow.Flow, s *session.Session, c *claims.Claims, tp json.RawMessage) string { + groups := c.RawClaims["groups"].([]string) + body := fmt.Sprintf(`{ + "flow_id": "%s", + "identity_id": "%s", + "session_id": "%s", + "method": "%s", + "url": "%s", + "cookies": { + "Some-Cookie-1": "Some-Cookie-Value", + "Some-Cookie-2": "Some-other-Cookie-Value", + "Some-Cookie-3": "Third-Cookie-Value" + }, + "transient_payload": %s, + "nickname": "%s", + "groups": ["%s", "%s"] + }`, f.GetID(), s.Identity.ID, s.ID, req.Method, "http://www.ory.com/some_end_point", string(tp), oidcClaims.Nickname, groups[0], groups[1]) + if len(req.Header) != 0 { + if ua := req.Header.Get("User-Agent"); ua != "" { + body, _ = sjson.Set(body, "headers.User-Agent", []string{ua}) + } + } + + return body + } + for _, tc := range []struct { uc string callWebHook func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error @@ -237,10 +271,10 @@ func TestWebHooks(t *testing.T) { uc: "Post Login Hook", createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} }, callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { - return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s) + return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s, &oidcClaims) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentityAndSessionAndTransientPayload(req, f, s, transientPayload) + return bodyWithFlowAndIdentityAndSessionAndClaimsAndTransientPayload(req, f, s, &oidcClaims, transientPayload) }, }, { @@ -479,7 +513,7 @@ func TestWebHooks(t *testing.T) { uc: "Post Login Hook - no block", createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { - return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s) + return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s, nil) }, webHookResponse: func() (int, []byte) { return http.StatusOK, []byte{} @@ -490,7 +524,7 @@ func TestWebHooks(t *testing.T) { uc: "Post Login Hook - block", createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { - return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s) + return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s, nil) }, webHookResponse: func() (int, []byte) { return http.StatusBadRequest, webHookResponse @@ -1120,7 +1154,7 @@ func TestDisallowPrivateIPRanges(t *testing.T) { Method: "GET", TemplateURI: "file://stub/test_body.jsonnet", }) - err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s) + err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s, nil) assert.ErrorContains(t, err, "no route to host") }) @@ -1135,7 +1169,7 @@ func TestDisallowPrivateIPRanges(t *testing.T) { Method: "GET", TemplateURI: "file://stub/test_body.jsonnet", }) - err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s) + err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s, nil) require.NoError(t, err) }) @@ -1159,7 +1193,7 @@ func TestDisallowPrivateIPRanges(t *testing.T) { Method: "GET", TemplateURI: "http://192.168.178.0/test_body.jsonnet", }) - err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s) + err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s, nil) require.ErrorContains(t, err, "no route to host") }) } @@ -1208,7 +1242,7 @@ func TestAsyncWebhook(t *testing.T) { Ignore: true, }, }) - err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s) + err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s, nil) require.NoError(t, err) // execution returns immediately for async webhook select { case <-time.After(5 * time.Second): diff --git a/selfservice/strategy/oidc/claims/claims.go b/selfservice/strategy/oidc/claims/claims.go new file mode 100644 index 000000000000..ff233a11b6f4 --- /dev/null +++ b/selfservice/strategy/oidc/claims/claims.go @@ -0,0 +1,64 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package claims + +import ( + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/strategy/oidc/oidcerr" + "github.com/ory/kratos/x" +) + +type Claims struct { + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Object string `json:"oid,omitempty"` + Name string `json:"name,omitempty"` + GivenName string `json:"given_name,omitempty"` + FamilyName string `json:"family_name,omitempty"` + LastName string `json:"last_name,omitempty"` + MiddleName string `json:"middle_name,omitempty"` + Nickname string `json:"nickname,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + Profile string `json:"profile,omitempty"` + Picture string `json:"picture,omitempty"` + Website string `json:"website,omitempty"` + Email string `json:"email,omitempty"` + // ConvertibleBoolean is used as Apple casually sends the email_verified field as a string. + EmailVerified x.ConvertibleBoolean `json:"email_verified,omitempty"` + Gender string `json:"gender,omitempty"` + Birthdate string `json:"birthdate,omitempty"` + Zoneinfo string `json:"zoneinfo,omitempty"` + Locale Locale `json:"locale,omitempty"` + PhoneNumber string `json:"phone_number,omitempty"` + PhoneNumberVerified bool `json:"phone_number_verified,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + HD string `json:"hd,omitempty"` + Team string `json:"team,omitempty"` + Nonce string `json:"nonce,omitempty"` + NonceSupported bool `json:"nonce_supported,omitempty"` + + // ACR is the Authentication Context Class Reference reported by the + // upstream OIDC provider. See OpenID Connect Core 1.0, Section 2. + ACR string `json:"acr,omitempty"` + + // AMR is the list of Authentication Methods References reported by the + // upstream OIDC provider. See OpenID Connect Core 1.0, Section 2 and + // RFC 8176. + AMR []string `json:"amr,omitempty"` + + RawClaims map[string]any `json:"raw_claims,omitempty"` +} + +// Validate checks if the claims are valid. +func (c *Claims) Validate() error { + if c.Subject == "" { + return oidcerr.Wrap(oidcerr.StepClaimsDecode, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("provider did not return a subject"))) + } + if c.Issuer == "" { + return oidcerr.Wrap(oidcerr.StepClaimsDecode, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("issuer not set in claims"))) + } + return nil +} diff --git a/selfservice/strategy/oidc/claims/claims_test.go b/selfservice/strategy/oidc/claims/claims_test.go new file mode 100644 index 000000000000..8c532eab569c --- /dev/null +++ b/selfservice/strategy/oidc/claims/claims_test.go @@ -0,0 +1,21 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package claims_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/selfservice/strategy/oidc/claims" +) + +func TestClaimsValidate(t *testing.T) { + require.Error(t, new(claims.Claims).Validate()) + require.Error(t, (&claims.Claims{Issuer: "not-empty"}).Validate()) + require.Error(t, (&claims.Claims{Issuer: "not-empty"}).Validate()) + require.Error(t, (&claims.Claims{Subject: "not-empty"}).Validate()) + require.Error(t, (&claims.Claims{Subject: "not-empty"}).Validate()) + require.NoError(t, (&claims.Claims{Issuer: "not-empty", Subject: "not-empty"}).Validate()) +} diff --git a/selfservice/strategy/oidc/claims/locale.go b/selfservice/strategy/oidc/claims/locale.go new file mode 100644 index 000000000000..226737505170 --- /dev/null +++ b/selfservice/strategy/oidc/claims/locale.go @@ -0,0 +1,32 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package claims + +import ( + "encoding/json" + "strings" +) + +type Locale string + +func (l *Locale) UnmarshalJSON(data []byte) error { + var linkedInLocale struct { + Language string `json:"language"` + Country string `json:"country"` + } + if err := json.Unmarshal(data, &linkedInLocale); err == nil { + switch { + case linkedInLocale.Language == "": + *l = Locale(linkedInLocale.Country) + case linkedInLocale.Country == "": + *l = Locale(linkedInLocale.Language) + default: + *l = Locale(strings.Join([]string{linkedInLocale.Language, linkedInLocale.Country}, "-")) + } + + return nil + } + + return json.Unmarshal(data, (*string)(l)) +} diff --git a/selfservice/strategy/oidc/export_test.go b/selfservice/strategy/oidc/export_test.go index 947d14047fb6..43b6d19b9a05 100644 --- a/selfservice/strategy/oidc/export_test.go +++ b/selfservice/strategy/oidc/export_test.go @@ -12,6 +12,7 @@ import ( oidcv1 "github.com/ory/kratos/gen/oidc/v1" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" ) // ValidateFlowForTest exposes the unexported flow resolver so tests in the @@ -31,7 +32,7 @@ func (s *Strategy) ForwardErrorForTest(ctx context.Context, w http.ResponseWrite // ProcessTestLoginForTest exposes processTestLogin so external tests can // exercise the dry-run OIDC callback path without routing through the full // callback HTTP pipeline. -func (s *Strategy) ProcessTestLoginForTest(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, claims *Claims, provider Provider) error { +func (s *Strategy) ProcessTestLoginForTest(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, claims *claims.Claims, provider Provider) error { return s.processTestLogin(ctx, w, r, f, claims, provider) } diff --git a/selfservice/strategy/oidc/provider.go b/selfservice/strategy/oidc/provider.go index f6e84e18e90c..f3434488e71e 100644 --- a/selfservice/strategy/oidc/provider.go +++ b/selfservice/strategy/oidc/provider.go @@ -5,18 +5,13 @@ package oidc import ( "context" - "encoding/json" "net/http" "net/url" - "strings" "github.com/dghubble/oauth1" - "github.com/pkg/errors" "golang.org/x/oauth2" - "github.com/ory/herodot" - "github.com/ory/kratos/selfservice/strategy/oidc/oidcerr" - "github.com/ory/kratos/x" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" ) type ( @@ -27,13 +22,13 @@ type ( Provider AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption OAuth2(ctx context.Context) (*oauth2.Config, error) - Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) + Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) } OAuth1Provider interface { Provider OAuth1(ctx context.Context) *oauth1.Config AuthURL(ctx context.Context, state string) (string, error) - Claims(ctx context.Context, token *oauth1.Token) (*Claims, error) + Claims(ctx context.Context, token *oauth1.Token) (*claims.Claims, error) ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error) } ) @@ -43,86 +38,11 @@ type OAuth2TokenExchanger interface { } type IDTokenVerifier interface { - Verify(ctx context.Context, rawIDToken string) (*Claims, error) + Verify(ctx context.Context, rawIDToken string) (*claims.Claims, error) } type NonceValidationSkipper interface { - CanSkipNonce(*Claims) bool -} - -type Claims struct { - Issuer string `json:"iss,omitempty"` - Subject string `json:"sub,omitempty"` - Object string `json:"oid,omitempty"` - Name string `json:"name,omitempty"` - GivenName string `json:"given_name,omitempty"` - FamilyName string `json:"family_name,omitempty"` - LastName string `json:"last_name,omitempty"` - MiddleName string `json:"middle_name,omitempty"` - Nickname string `json:"nickname,omitempty"` - PreferredUsername string `json:"preferred_username,omitempty"` - Profile string `json:"profile,omitempty"` - Picture string `json:"picture,omitempty"` - Website string `json:"website,omitempty"` - Email string `json:"email,omitempty"` - // ConvertibleBoolean is used as Apple casually sends the email_verified field as a string. - EmailVerified x.ConvertibleBoolean `json:"email_verified,omitempty"` - Gender string `json:"gender,omitempty"` - Birthdate string `json:"birthdate,omitempty"` - Zoneinfo string `json:"zoneinfo,omitempty"` - Locale Locale `json:"locale,omitempty"` - PhoneNumber string `json:"phone_number,omitempty"` - PhoneNumberVerified bool `json:"phone_number_verified,omitempty"` - UpdatedAt int64 `json:"updated_at,omitempty"` - HD string `json:"hd,omitempty"` - Team string `json:"team,omitempty"` - Nonce string `json:"nonce,omitempty"` - NonceSupported bool `json:"nonce_supported,omitempty"` - - // ACR is the Authentication Context Class Reference reported by the - // upstream OIDC provider. See OpenID Connect Core 1.0, Section 2. - ACR string `json:"acr,omitempty"` - - // AMR is the list of Authentication Methods References reported by the - // upstream OIDC provider. See OpenID Connect Core 1.0, Section 2 and - // RFC 8176. - AMR []string `json:"amr,omitempty"` - - RawClaims map[string]any `json:"raw_claims,omitempty"` -} - -type Locale string - -func (l *Locale) UnmarshalJSON(data []byte) error { - var linkedInLocale struct { - Language string `json:"language"` - Country string `json:"country"` - } - if err := json.Unmarshal(data, &linkedInLocale); err == nil { - switch { - case linkedInLocale.Language == "": - *l = Locale(linkedInLocale.Country) - case linkedInLocale.Country == "": - *l = Locale(linkedInLocale.Language) - default: - *l = Locale(strings.Join([]string{linkedInLocale.Language, linkedInLocale.Country}, "-")) - } - - return nil - } - - return json.Unmarshal(data, (*string)(l)) -} - -// Validate checks if the claims are valid. -func (c *Claims) Validate() error { - if c.Subject == "" { - return oidcerr.Wrap(oidcerr.StepClaimsDecode, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("provider did not return a subject"))) - } - if c.Issuer == "" { - return oidcerr.Wrap(oidcerr.StepClaimsDecode, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("issuer not set in claims"))) - } - return nil + CanSkipNonce(*claims.Claims) bool } // UpstreamParameters returns a list of oauth2.AuthCodeOption based on the upstream parameters. diff --git a/selfservice/strategy/oidc/provider_amazon.go b/selfservice/strategy/oidc/provider_amazon.go index 5d07f059c136..b24106bc1d6f 100644 --- a/selfservice/strategy/oidc/provider_amazon.go +++ b/selfservice/strategy/oidc/provider_amazon.go @@ -18,6 +18,7 @@ import ( "golang.org/x/oauth2/amazon" "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/ory/x/otelx" ) @@ -99,7 +100,7 @@ func (p *ProviderAmazon) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{} } -func (p *ProviderAmazon) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) { +func (p *ProviderAmazon) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *claims.Claims, err error) { ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderAmazon.Claims") defer otelx.End(span, &err) @@ -129,7 +130,7 @@ func (p *ProviderAmazon) Claims(ctx context.Context, exchange *oauth2.Token, que return nil, errors.WithStack(herodot.ErrUpstreamError().WithDetail("url", p.amazonProfileURL).WithDetail("raw_response", rawResponse).WithError(err.Error())) } - claims := &Claims{ + claims := &claims.Claims{ Subject: profile.UserID, Issuer: amazon.Endpoint.TokenURL, Name: profile.Name, diff --git a/selfservice/strategy/oidc/provider_apple.go b/selfservice/strategy/oidc/provider_apple.go index 2e287dc77ac3..5d00f6d2467a 100644 --- a/selfservice/strategy/oidc/provider_apple.go +++ b/selfservice/strategy/oidc/provider_apple.go @@ -15,6 +15,8 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/golang-jwt/jwt/v4" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" + "github.com/pkg/errors" "golang.org/x/oauth2" @@ -115,7 +117,7 @@ func (a *ProviderApple) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { return options } -func (a *ProviderApple) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (a *ProviderApple) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { claims, err := a.ProviderGenericOIDC.Claims(ctx, exchange, query) if err != nil { return claims, err @@ -129,7 +131,7 @@ func (a *ProviderApple) Claims(ctx context.Context, exchange *oauth2.Token, quer // The info is sent as an extra query parameter to the redirect URL. // See https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292 // Note that there's no way to make sure the info hasn't been tampered with. -func (a *ProviderApple) DecodeQuery(query url.Values, claims *Claims) { +func (a *ProviderApple) DecodeQuery(query url.Values, claims *claims.Claims) { var user struct { Name *struct { FirstName *string `json:"firstName"` @@ -159,7 +161,7 @@ var _ IDTokenVerifier = new(ProviderApple) const issuerURLApple = "https://appleid.apple.com" -func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, error) { +func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*claims.Claims, error) { keySet := oidc.NewRemoteKeySet(ctx, a.JWKSUrl) ctx = oidc.ClientContext(ctx, a.reg.HTTPClient(ctx).HTTPClient) @@ -168,6 +170,6 @@ func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, var _ NonceValidationSkipper = new(ProviderApple) -func (a *ProviderApple) CanSkipNonce(c *Claims) bool { +func (a *ProviderApple) CanSkipNonce(c *claims.Claims) bool { return c.NonceSupported } diff --git a/selfservice/strategy/oidc/provider_apple_test.go b/selfservice/strategy/oidc/provider_apple_test.go index a7e58b88cd1d..52c6af3035bd 100644 --- a/selfservice/strategy/oidc/provider_apple_test.go +++ b/selfservice/strategy/oidc/provider_apple_test.go @@ -20,6 +20,7 @@ import ( "github.com/ory/kratos/pkg" "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" ) func TestDecodeQuery(t *testing.T) { @@ -28,15 +29,15 @@ func TestDecodeQuery(t *testing.T) { } for k, tc := range []struct { - claims *oidc.Claims + claims *claims.Claims familyName string givenName string lastName string }{ - {claims: &oidc.Claims{}, familyName: "last", givenName: "first", lastName: "last"}, - {claims: &oidc.Claims{FamilyName: "fam"}, familyName: "fam", givenName: "first", lastName: "last"}, - {claims: &oidc.Claims{FamilyName: "fam", GivenName: "giv"}, familyName: "fam", givenName: "giv", lastName: "last"}, - {claims: &oidc.Claims{FamilyName: "fam", GivenName: "giv", LastName: "las"}, familyName: "fam", givenName: "giv", lastName: "las"}, + {claims: &claims.Claims{}, familyName: "last", givenName: "first", lastName: "last"}, + {claims: &claims.Claims{FamilyName: "fam"}, familyName: "fam", givenName: "first", lastName: "last"}, + {claims: &claims.Claims{FamilyName: "fam", GivenName: "giv"}, familyName: "fam", givenName: "giv", lastName: "last"}, + {claims: &claims.Claims{FamilyName: "fam", GivenName: "giv", LastName: "las"}, familyName: "fam", givenName: "giv", lastName: "las"}, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { a := oidc.NewProviderApple(&oidc.Configuration{}, nil).(*oidc.ProviderApple) diff --git a/selfservice/strategy/oidc/provider_auth0.go b/selfservice/strategy/oidc/provider_auth0.go index a6c4680e8296..e9de109c1ac4 100644 --- a/selfservice/strategy/oidc/provider_auth0.go +++ b/selfservice/strategy/oidc/provider_auth0.go @@ -12,6 +12,7 @@ import ( "path" "time" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/tidwall/sjson" @@ -73,7 +74,7 @@ func (g *ProviderAuth0) OAuth2(ctx context.Context) (*oauth2.Config, error) { return g.oauth2(ctx) } -func (g *ProviderAuth0) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderAuth0) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { o, err := g.OAuth2(ctx) if err != nil { return nil, err @@ -115,7 +116,7 @@ func (g *ProviderAuth0) Claims(ctx context.Context, exchange *oauth2.Token, quer } // Once we get here, we know that if there is an updated_at field in the json, it is the correct type. - var claims Claims + var claims claims.Claims if err := json.Unmarshal(b, &claims); err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) } diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index 4a1432917527..aec537908248 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -14,6 +14,7 @@ import ( "github.com/ory/herodot" "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/urlx" ) @@ -197,7 +198,7 @@ type Configuration struct { // one of the upstream `amr` entries matches one of the configured // AAL2AMRValues. An empty `acr` never matches, even if the allowlist // contains an empty string. -func (c *Configuration) AALForClaims(claims *Claims) identity.AuthenticatorAssuranceLevel { +func (c *Configuration) AALForClaims(claims *claims.Claims) identity.AuthenticatorAssuranceLevel { if claims == nil { return identity.AuthenticatorAssuranceLevel1 } diff --git a/selfservice/strategy/oidc/provider_config_test.go b/selfservice/strategy/oidc/provider_config_test.go index b78ab64787d7..072fd1716100 100644 --- a/selfservice/strategy/oidc/provider_config_test.go +++ b/selfservice/strategy/oidc/provider_config_test.go @@ -16,6 +16,7 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/pkg" "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" ) func TestConfig(t *testing.T) { @@ -41,7 +42,7 @@ func TestConfiguration_AALForClaims(t *testing.T) { for _, tc := range []struct { name string config oidc.Configuration - claims *oidc.Claims + claims *claims.Claims want identity.AuthenticatorAssuranceLevel }{ { @@ -53,61 +54,61 @@ func TestConfiguration_AALForClaims(t *testing.T) { { name: "empty config with claims stays aal1", config: oidc.Configuration{}, - claims: &oidc.Claims{ACR: "urn:mfa", AMR: []string{"mfa", "pwd"}}, + claims: &claims.Claims{ACR: "urn:mfa", AMR: []string{"mfa", "pwd"}}, want: identity.AuthenticatorAssuranceLevel1, }, { name: "acr match elevates to aal2", config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa", "urn:strong"}}, - claims: &oidc.Claims{ACR: "urn:mfa"}, + claims: &claims.Claims{ACR: "urn:mfa"}, want: identity.AuthenticatorAssuranceLevel2, }, { name: "acr mismatch stays aal1", config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}}, - claims: &oidc.Claims{ACR: "urn:basic"}, + claims: &claims.Claims{ACR: "urn:basic"}, want: identity.AuthenticatorAssuranceLevel1, }, { name: "empty acr does not accidentally match empty configured value", config: oidc.Configuration{AAL2ACRValues: []string{""}}, - claims: &oidc.Claims{ACR: ""}, + claims: &claims.Claims{ACR: ""}, want: identity.AuthenticatorAssuranceLevel1, }, { name: "amr match elevates to aal2", config: oidc.Configuration{AAL2AMRValues: []string{"mfa"}}, - claims: &oidc.Claims{AMR: []string{"pwd", "mfa"}}, + claims: &claims.Claims{AMR: []string{"pwd", "mfa"}}, want: identity.AuthenticatorAssuranceLevel2, }, { name: "amr mismatch stays aal1", config: oidc.Configuration{AAL2AMRValues: []string{"mfa"}}, - claims: &oidc.Claims{AMR: []string{"pwd"}}, + claims: &claims.Claims{AMR: []string{"pwd"}}, want: identity.AuthenticatorAssuranceLevel1, }, { name: "any configured amr value is sufficient", config: oidc.Configuration{AAL2AMRValues: []string{"otp", "hwk", "mfa"}}, - claims: &oidc.Claims{AMR: []string{"pwd", "hwk"}}, + claims: &claims.Claims{AMR: []string{"pwd", "hwk"}}, want: identity.AuthenticatorAssuranceLevel2, }, { name: "both acr and amr configured, acr matches", config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}}, - claims: &oidc.Claims{ACR: "urn:mfa", AMR: []string{"pwd"}}, + claims: &claims.Claims{ACR: "urn:mfa", AMR: []string{"pwd"}}, want: identity.AuthenticatorAssuranceLevel2, }, { name: "both acr and amr configured, amr matches", config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}}, - claims: &oidc.Claims{ACR: "urn:basic", AMR: []string{"pwd", "mfa"}}, + claims: &claims.Claims{ACR: "urn:basic", AMR: []string{"pwd", "mfa"}}, want: identity.AuthenticatorAssuranceLevel2, }, { name: "both configured, neither matches", config: oidc.Configuration{AAL2ACRValues: []string{"urn:mfa"}, AAL2AMRValues: []string{"mfa"}}, - claims: &oidc.Claims{ACR: "urn:basic", AMR: []string{"pwd"}}, + claims: &claims.Claims{ACR: "urn:basic", AMR: []string{"pwd"}}, want: identity.AuthenticatorAssuranceLevel1, }, } { diff --git a/selfservice/strategy/oidc/provider_dingtalk.go b/selfservice/strategy/oidc/provider_dingtalk.go index 5b19fb1c5fc0..71d679e18bbd 100644 --- a/selfservice/strategy/oidc/provider_dingtalk.go +++ b/selfservice/strategy/oidc/provider_dingtalk.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "golang.org/x/oauth2" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/hashicorp/go-retryablehttp" @@ -124,7 +125,7 @@ func (g *ProviderDingTalk) ExchangeOAuth2Token(ctx context.Context, code string, return token, nil } -func (g *ProviderDingTalk) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) { +func (g *ProviderDingTalk) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*claims.Claims, error) { userInfoURL := "https://api.dingtalk.com/v1.0/contact/users/me" accessToken := exchange.AccessToken @@ -162,7 +163,7 @@ func (g *ProviderDingTalk) Claims(ctx context.Context, exchange *oauth2.Token, _ return nil, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("userResp.ErrCode = %s, userResp.ErrMsg = %s", user.ErrCode, user.ErrMsg)) } - return &Claims{ + return &claims.Claims{ Issuer: userInfoURL, Subject: user.OpenID, Nickname: user.Nick, diff --git a/selfservice/strategy/oidc/provider_discord.go b/selfservice/strategy/oidc/provider_discord.go index 5141862af0b8..9040bd8aed61 100644 --- a/selfservice/strategy/oidc/provider_discord.go +++ b/selfservice/strategy/oidc/provider_discord.go @@ -9,6 +9,7 @@ import ( "net/url" "slices" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/x" "github.com/bwmarrin/discordgo" @@ -69,7 +70,7 @@ func (d *ProviderDiscord) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { } } -func (d *ProviderDiscord) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (d *ProviderDiscord) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), " ") for _, check := range d.Config().Scope { if !slices.Contains(grantedScopes, check) { @@ -87,7 +88,7 @@ func (d *ProviderDiscord) Claims(ctx context.Context, exchange *oauth2.Token, qu return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) } - claims := &Claims{ + claims := &claims.Claims{ Issuer: discordgo.EndpointOauth2, Subject: user.ID, Name: fmt.Sprintf("%s#%s", user.Username, user.Discriminator), @@ -96,7 +97,7 @@ func (d *ProviderDiscord) Claims(ctx context.Context, exchange *oauth2.Token, qu Picture: user.AvatarURL(""), Email: user.Email, EmailVerified: x.ConvertibleBoolean(user.Verified), - Locale: Locale(user.Locale), + Locale: claims.Locale(user.Locale), } return claims, nil diff --git a/selfservice/strategy/oidc/provider_facebook.go b/selfservice/strategy/oidc/provider_facebook.go index 8f8b9a1b0dfb..424102a0adb1 100644 --- a/selfservice/strategy/oidc/provider_facebook.go +++ b/selfservice/strategy/oidc/provider_facebook.go @@ -16,6 +16,7 @@ import ( "github.com/ory/x/httpx" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/x" "github.com/pkg/errors" @@ -64,7 +65,7 @@ func (g *ProviderFacebook) OAuth2(ctx context.Context) (*oauth2.Config, error) { return g.oauth2ConfigFromEndpoint(ctx, endpoint), nil } -func (g *ProviderFacebook) Claims(ctx context.Context, token *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderFacebook) Claims(ctx context.Context, token *oauth2.Token, query url.Values) (*claims.Claims, error) { o, err := g.OAuth2(ctx) if err != nil { return nil, err @@ -121,7 +122,7 @@ func (g *ProviderFacebook) Claims(ctx context.Context, token *oauth2.Token, quer user.EmailVerified = true } - return &Claims{ + return &claims.Claims{ Issuer: u.String(), Subject: user.ID, Name: user.Name, diff --git a/selfservice/strategy/oidc/provider_generic_oidc.go b/selfservice/strategy/oidc/provider_generic_oidc.go index 7e1ee78a6e2f..32787b0342a7 100644 --- a/selfservice/strategy/oidc/provider_generic_oidc.go +++ b/selfservice/strategy/oidc/provider_generic_oidc.go @@ -14,6 +14,7 @@ import ( "golang.org/x/oauth2" "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/reqlog" ) @@ -97,13 +98,13 @@ func (g *ProviderGenericOIDC) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption return options } -func (g *ProviderGenericOIDC) verifyAndDecodeClaimsWithProvider(ctx context.Context, provider *gooidc.Provider, raw string) (*Claims, error) { +func (g *ProviderGenericOIDC) verifyAndDecodeClaimsWithProvider(ctx context.Context, provider *gooidc.Provider, raw string) (*claims.Claims, error) { token, err := provider.VerifierContext(g.withHTTPClientContext(ctx), &gooidc.Config{ClientID: g.config.ClientID}).Verify(ctx, raw) if err != nil { return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) } - var claims Claims + var claims claims.Claims if err := token.Claims(&claims); err != nil { return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("%s", err)) } @@ -117,7 +118,7 @@ func (g *ProviderGenericOIDC) verifyAndDecodeClaimsWithProvider(ctx context.Cont return &claims, nil } -func (g *ProviderGenericOIDC) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) { +func (g *ProviderGenericOIDC) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*claims.Claims, error) { switch g.config.ClaimsSource { case ClaimsSourceIDToken, "": return g.claimsFromIDToken(ctx, exchange) @@ -129,7 +130,7 @@ func (g *ProviderGenericOIDC) Claims(ctx context.Context, exchange *oauth2.Token WithReasonf("Unknown claims source: %q", g.config.ClaimsSource)) } -func (g *ProviderGenericOIDC) claimsFromUserInfo(ctx context.Context, exchange *oauth2.Token) (*Claims, error) { +func (g *ProviderGenericOIDC) claimsFromUserInfo(ctx context.Context, exchange *oauth2.Token) (*claims.Claims, error) { p, err := g.provider(ctx) if err != nil { return nil, err @@ -142,7 +143,7 @@ func (g *ProviderGenericOIDC) claimsFromUserInfo(ctx context.Context, exchange * return nil, err } - var claims Claims + var claims claims.Claims if err = userInfo.Claims(&claims); err != nil { return nil, err } @@ -181,7 +182,7 @@ func (g *ProviderGenericOIDC) claimsFromUserInfo(ctx context.Context, exchange * return &claims, nil } -func (g *ProviderGenericOIDC) claimsFromIDToken(ctx context.Context, exchange *oauth2.Token) (*Claims, error) { +func (g *ProviderGenericOIDC) claimsFromIDToken(ctx context.Context, exchange *oauth2.Token) (*claims.Claims, error) { p, raw, err := g.idTokenAndProvider(ctx, exchange) if err != nil { return nil, err diff --git a/selfservice/strategy/oidc/provider_github.go b/selfservice/strategy/oidc/provider_github.go index 8eab1caee367..3eedc8ec49f0 100644 --- a/selfservice/strategy/oidc/provider_github.go +++ b/selfservice/strategy/oidc/provider_github.go @@ -9,6 +9,7 @@ import ( "net/url" "slices" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/x" "github.com/pkg/errors" @@ -62,7 +63,7 @@ func (g *ProviderGitHub) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{} } -func (g *ProviderGitHub) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderGitHub) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), ",") for _, check := range g.Config().Scope { if !slices.Contains(grantedScopes, check) { @@ -78,7 +79,7 @@ func (g *ProviderGitHub) Claims(ctx context.Context, exchange *oauth2.Token, que return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) } - claims := &Claims{ + claims := &claims.Claims{ Subject: fmt.Sprintf("%d", user.GetID()), Issuer: github.Endpoint.TokenURL, Name: user.GetName(), diff --git a/selfservice/strategy/oidc/provider_github_app.go b/selfservice/strategy/oidc/provider_github_app.go index 56383d255188..29b1bcb34f5b 100644 --- a/selfservice/strategy/oidc/provider_github_app.go +++ b/selfservice/strategy/oidc/provider_github_app.go @@ -8,6 +8,7 @@ import ( "fmt" "net/url" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/x" "github.com/ory/x/httpx" @@ -57,7 +58,7 @@ func (g *ProviderGitHubApp) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{} } -func (g *ProviderGitHubApp) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderGitHubApp) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), g.oauth2(ctx), exchange) gh := ghapi.NewClient(client.HTTPClient) @@ -66,7 +67,7 @@ func (g *ProviderGitHubApp) Claims(ctx context.Context, exchange *oauth2.Token, return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) } - claims := &Claims{ + claims := &claims.Claims{ Subject: fmt.Sprintf("%d", user.GetID()), Issuer: github.Endpoint.TokenURL, Name: user.GetName(), diff --git a/selfservice/strategy/oidc/provider_gitlab.go b/selfservice/strategy/oidc/provider_gitlab.go index 8128506ba98d..4cb1b2a00415 100644 --- a/selfservice/strategy/oidc/provider_gitlab.go +++ b/selfservice/strategy/oidc/provider_gitlab.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/go-retryablehttp" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/pkg/errors" @@ -70,7 +71,7 @@ func (g *ProviderGitLab) OAuth2(ctx context.Context) (*oauth2.Config, error) { return g.oauth2(ctx) } -func (g *ProviderGitLab) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderGitLab) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { o, err := g.OAuth2(ctx) if err != nil { return nil, err @@ -99,7 +100,7 @@ func (g *ProviderGitLab) Claims(ctx context.Context, exchange *oauth2.Token, que return nil, err } - var claims Claims + var claims claims.Claims if err := json.NewDecoder(resp.Body).Decode(&claims); err != nil { return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) } diff --git a/selfservice/strategy/oidc/provider_google.go b/selfservice/strategy/oidc/provider_google.go index b339a2274281..06c226c0c359 100644 --- a/selfservice/strategy/oidc/provider_google.go +++ b/selfservice/strategy/oidc/provider_google.go @@ -9,6 +9,8 @@ import ( gooidc "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" + + "github.com/ory/kratos/selfservice/strategy/oidc/claims" ) var _ OAuth2Provider = (*ProviderGoogle)(nil) @@ -74,7 +76,7 @@ var _ IDTokenVerifier = new(ProviderGoogle) const issuerURLGoogle = "https://accounts.google.com" -func (g *ProviderGoogle) Verify(ctx context.Context, rawIDToken string) (*Claims, error) { +func (g *ProviderGoogle) Verify(ctx context.Context, rawIDToken string) (*claims.Claims, error) { keySet := gooidc.NewRemoteKeySet(ctx, g.JWKSUrl) ctx = gooidc.ClientContext(ctx, g.reg.HTTPClient(ctx).HTTPClient) @@ -83,7 +85,7 @@ func (g *ProviderGoogle) Verify(ctx context.Context, rawIDToken string) (*Claims var _ NonceValidationSkipper = new(ProviderGoogle) -func (g *ProviderGoogle) CanSkipNonce(c *Claims) bool { +func (g *ProviderGoogle) CanSkipNonce(c *claims.Claims) bool { // Not all SDKs support nonce validation, so we skip it if no nonce is present in the claims of the ID Token. return c.Nonce == "" } diff --git a/selfservice/strategy/oidc/provider_lark.go b/selfservice/strategy/oidc/provider_lark.go index f8e62d5f120c..38882b197c79 100644 --- a/selfservice/strategy/oidc/provider_lark.go +++ b/selfservice/strategy/oidc/provider_lark.go @@ -13,6 +13,7 @@ import ( "golang.org/x/oauth2" "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" ) @@ -58,7 +59,7 @@ func (pl *ProviderLark) OAuth2(ctx context.Context) (*oauth2.Config, error) { }, nil } -func (pl *ProviderLark) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (pl *ProviderLark) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { // larkClaim is defined in the https://open.feishu.cn/document/common-capabilities/sso/api/get-user-info type larkClaim struct { Sub string `json:"sub"` @@ -101,7 +102,7 @@ func (pl *ProviderLark) Claims(ctx context.Context, exchange *oauth2.Token, quer return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) } - return &Claims{ + return &claims.Claims{ Issuer: larkUserEndpoint, Subject: user.OpenID, Name: user.Name, diff --git a/selfservice/strategy/oidc/provider_linkedin.go b/selfservice/strategy/oidc/provider_linkedin.go index 60dcaadd6e4e..b3d625126b40 100644 --- a/selfservice/strategy/oidc/provider_linkedin.go +++ b/selfservice/strategy/oidc/provider_linkedin.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/otelx" "github.com/hashicorp/go-retryablehttp" @@ -167,7 +168,7 @@ func (l *ProviderLinkedIn) ProfilePicture(profile *LinkedInProfile) string { return identifiers[0].Identifier } -func (l *ProviderLinkedIn) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) { +func (l *ProviderLinkedIn) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *claims.Claims, err error) { ctx, span := l.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.Claims") defer otelx.End(span, &err) @@ -187,7 +188,7 @@ func (l *ProviderLinkedIn) Claims(ctx context.Context, exchange *oauth2.Token, q return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) } - claims := &Claims{ + claims := &claims.Claims{ Subject: profile.ID, Issuer: "https://login.linkedin.com/", Email: email.Elements[0].Handle.EmailAddress, diff --git a/selfservice/strategy/oidc/provider_linkedin_test.go b/selfservice/strategy/oidc/provider_linkedin_test.go index c1f24971f2f2..9f0879f5fa52 100644 --- a/selfservice/strategy/oidc/provider_linkedin_test.go +++ b/selfservice/strategy/oidc/provider_linkedin_test.go @@ -19,6 +19,7 @@ import ( "github.com/ory/kratos/pkg" "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" ) func TestProviderLinkedin_Claims(t *testing.T) { @@ -122,7 +123,7 @@ func TestProviderLinkedin_Claims(t *testing.T) { ) require.NoError(t, err) - assert.Equal(t, &oidc.Claims{ + assert.Equal(t, &claims.Claims{ Issuer: "https://login.linkedin.com/", Subject: "5foOWOiYXD", GivenName: "John", @@ -198,7 +199,7 @@ func TestProviderLinkedin_No_Picture(t *testing.T) { ) require.NoError(t, err) - assert.Equal(t, &oidc.Claims{ + assert.Equal(t, &claims.Claims{ Issuer: "https://login.linkedin.com/", Subject: "5foOWOiYXD", GivenName: "John", diff --git a/selfservice/strategy/oidc/provider_microsoft.go b/selfservice/strategy/oidc/provider_microsoft.go index 5a7d0395e3c5..acfe1c01f918 100644 --- a/selfservice/strategy/oidc/provider_microsoft.go +++ b/selfservice/strategy/oidc/provider_microsoft.go @@ -17,6 +17,7 @@ import ( "golang.org/x/oauth2" "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" ) @@ -52,7 +53,7 @@ func (m *ProviderMicrosoft) OAuth2(ctx context.Context) (*oauth2.Config, error) return m.oauth2ConfigFromEndpoint(ctx, endpoint), nil } -func (m *ProviderMicrosoft) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) { +func (m *ProviderMicrosoft) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*claims.Claims, error) { raw, ok := exchange.Extra("id_token").(string) if !ok || len(raw) == 0 { return nil, errors.WithStack(ErrIDTokenMissing()) @@ -83,7 +84,7 @@ func (m *ProviderMicrosoft) Claims(ctx context.Context, exchange *oauth2.Token, return m.updateSubject(ctx, claims, exchange) } -func (m *ProviderMicrosoft) updateSubject(ctx context.Context, claims *Claims, exchange *oauth2.Token) (*Claims, error) { +func (m *ProviderMicrosoft) updateSubject(ctx context.Context, claims *claims.Claims, exchange *oauth2.Token) (*claims.Claims, error) { if m.config.SubjectSource == "me" { o, err := m.OAuth2(ctx) if err != nil { diff --git a/selfservice/strategy/oidc/provider_netid.go b/selfservice/strategy/oidc/provider_netid.go index eab9394d2cce..eaa852959f09 100644 --- a/selfservice/strategy/oidc/provider_netid.go +++ b/selfservice/strategy/oidc/provider_netid.go @@ -17,6 +17,7 @@ import ( "golang.org/x/oauth2" "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/ory/x/urlx" ) @@ -71,7 +72,7 @@ func (n *ProviderNetID) oAuth2(ctx context.Context) (*oauth2.Config, error) { }, nil } -func (n *ProviderNetID) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*Claims, error) { +func (n *ProviderNetID) Claims(ctx context.Context, exchange *oauth2.Token, _ url.Values) (*claims.Claims, error) { o, err := n.OAuth2(ctx) if err != nil { return nil, err @@ -103,21 +104,21 @@ func (n *ProviderNetID) Claims(ctx context.Context, exchange *oauth2.Token, _ ur return nil, errors.WithStack(ErrIDTokenMissing()) } - claims, err := n.verifyAndDecodeClaimsWithProvider(ctx, p, raw) + dec, err := n.verifyAndDecodeClaimsWithProvider(ctx, p, raw) if err != nil { return nil, err } - var userinfo Claims + var userinfo claims.Claims if err := json.NewDecoder(resp.Body).Decode(&userinfo); err != nil { return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) } - userinfo.Issuer = claims.Issuer - userinfo.Subject = claims.Subject + userinfo.Issuer = dec.Issuer + userinfo.Subject = dec.Subject return &userinfo, nil } -func (n *ProviderNetID) Verify(ctx context.Context, rawIDToken string) (*Claims, error) { +func (n *ProviderNetID) Verify(ctx context.Context, rawIDToken string) (*claims.Claims, error) { provider, err := n.provider(ctx) if err != nil { return nil, err @@ -154,7 +155,7 @@ func (n *ProviderNetID) Verify(ctx context.Context, rawIDToken string) (*Claims, } var ( - claims Claims + claims claims.Claims rawClaims map[string]any ) diff --git a/selfservice/strategy/oidc/provider_patreon.go b/selfservice/strategy/oidc/provider_patreon.go index 8f4242fd573f..415ea64a1460 100644 --- a/selfservice/strategy/oidc/provider_patreon.go +++ b/selfservice/strategy/oidc/provider_patreon.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/go-retryablehttp" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/pkg/errors" @@ -81,7 +82,7 @@ func (d *ProviderPatreon) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { } } -func (d *ProviderPatreon) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (d *ProviderPatreon) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { identityURL := "https://www.patreon.com/api/oauth2/v2/identity?fields%5Buser%5D=first_name,last_name,url,full_name,email,image_url" o := d.oauth2(ctx) @@ -109,7 +110,7 @@ func (d *ProviderPatreon) Claims(ctx context.Context, exchange *oauth2.Token, qu return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(jsonErr).WithReasonf("%s", jsonErr)) } - claims := &Claims{ + claims := &claims.Claims{ Issuer: "https://www.patreon.com/", Subject: data.Data.ID, Name: data.Data.Attributes.FullName, diff --git a/selfservice/strategy/oidc/provider_salesforce.go b/selfservice/strategy/oidc/provider_salesforce.go index 79da3555a124..4b5713d419b9 100644 --- a/selfservice/strategy/oidc/provider_salesforce.go +++ b/selfservice/strategy/oidc/provider_salesforce.go @@ -12,6 +12,7 @@ import ( "path" "time" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/tidwall/sjson" @@ -73,7 +74,7 @@ func (g *ProviderSalesforce) OAuth2(ctx context.Context) (*oauth2.Config, error) return g.oauth2(ctx) } -func (g *ProviderSalesforce) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderSalesforce) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { o, err := g.OAuth2(ctx) if err != nil { return nil, err @@ -115,7 +116,7 @@ func (g *ProviderSalesforce) Claims(ctx context.Context, exchange *oauth2.Token, } // Once we get here, we know that if there is an updated_at field in the json, it is the correct type. - var claims Claims + var claims claims.Claims if err := json.Unmarshal(b, &claims); err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("%s", err)) } diff --git a/selfservice/strategy/oidc/provider_slack.go b/selfservice/strategy/oidc/provider_slack.go index 685592e2671c..4950bbcc53c2 100644 --- a/selfservice/strategy/oidc/provider_slack.go +++ b/selfservice/strategy/oidc/provider_slack.go @@ -10,6 +10,7 @@ import ( "slices" "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/pkg/errors" "golang.org/x/oauth2" @@ -63,7 +64,7 @@ func (d *ProviderSlack) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{} } -func (d *ProviderSlack) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (d *ProviderSlack) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), ",") for _, check := range d.Config().Scope { if !slices.Contains(grantedScopes, check) { @@ -77,7 +78,7 @@ func (d *ProviderSlack) Claims(ctx context.Context, exchange *oauth2.Token, quer return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) } - claims := &Claims{ + claims := &claims.Claims{ Issuer: "https://slack.com/oauth/", Subject: identity.User.ID, Name: identity.User.Name, diff --git a/selfservice/strategy/oidc/provider_spotify.go b/selfservice/strategy/oidc/provider_spotify.go index 0909abdbe3fd..b680d8223d30 100644 --- a/selfservice/strategy/oidc/provider_spotify.go +++ b/selfservice/strategy/oidc/provider_spotify.go @@ -14,6 +14,7 @@ import ( "github.com/pkg/errors" "golang.org/x/oauth2" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/ory/x/stringsx" @@ -63,7 +64,7 @@ func (g *ProviderSpotify) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{} } -func (g *ProviderSpotify) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderSpotify) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), " ") for _, check := range g.Config().Scope { if !slices.Contains(grantedScopes, check) { @@ -88,7 +89,7 @@ func (g *ProviderSpotify) Claims(ctx context.Context, exchange *oauth2.Token, qu userPicture = user.Images[0].URL } - claims := &Claims{ + claims := &claims.Claims{ Subject: user.ID, Issuer: spotify.Endpoint.TokenURL, Name: user.DisplayName, diff --git a/selfservice/strategy/oidc/provider_test.go b/selfservice/strategy/oidc/provider_test.go index 2d3b1729f244..71a9e8d56ae7 100644 --- a/selfservice/strategy/oidc/provider_test.go +++ b/selfservice/strategy/oidc/provider_test.go @@ -11,16 +11,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" -) -func TestClaimsValidate(t *testing.T) { - require.Error(t, new(Claims).Validate()) - require.Error(t, (&Claims{Issuer: "not-empty"}).Validate()) - require.Error(t, (&Claims{Issuer: "not-empty"}).Validate()) - require.Error(t, (&Claims{Subject: "not-empty"}).Validate()) - require.Error(t, (&Claims{Subject: "not-empty"}).Validate()) - require.NoError(t, (&Claims{Issuer: "not-empty", Subject: "not-empty"}).Validate()) -} + "github.com/ory/kratos/selfservice/strategy/oidc/claims" +) type TestProvider struct { *ProviderGenericOIDC @@ -43,11 +36,11 @@ func RegisterTestProvider(t *testing.T, id string) { var _ IDTokenVerifier = new(TestProvider) -func (t *TestProvider) Verify(_ context.Context, token string) (*Claims, error) { +func (t *TestProvider) Verify(_ context.Context, token string) (*claims.Claims, error) { if token == "error" { return nil, fmt.Errorf("stub error") } - c := Claims{} + c := claims.Claims{} if err := json.Unmarshal([]byte(token), &c); err != nil { return nil, err } @@ -101,7 +94,7 @@ func TestLocale(t *testing.T) { expected: "", }} { t.Run(tc.name, func(t *testing.T) { - var c Claims + var c claims.Claims err := json.Unmarshal([]byte(tc.json), &c) if tc.assertErr != nil { tc.assertErr(t, err) diff --git a/selfservice/strategy/oidc/provider_test_fedcm.go b/selfservice/strategy/oidc/provider_test_fedcm.go index 518b8fa4b3f3..e5d22afb6202 100644 --- a/selfservice/strategy/oidc/provider_test_fedcm.go +++ b/selfservice/strategy/oidc/provider_test_fedcm.go @@ -7,6 +7,8 @@ import ( "context" "github.com/golang-jwt/jwt/v5" + + "github.com/ory/kratos/selfservice/strategy/oidc/claims" ) // ProviderTestFedcm is a mock provider to test FedCM. @@ -28,9 +30,9 @@ func NewProviderTestFedcm( } } -func (g *ProviderTestFedcm) Verify(_ context.Context, rawIDToken string) (claims *Claims, err error) { +func (g *ProviderTestFedcm) Verify(_ context.Context, rawIDToken string) (cl *claims.Claims, err error) { rawClaims := &struct { - Claims + claims.Claims jwt.MapClaims }{} _, err = jwt.ParseWithClaims(rawIDToken, rawClaims, func(token *jwt.Token) (interface{}, error) { diff --git a/selfservice/strategy/oidc/provider_uaepass.go b/selfservice/strategy/oidc/provider_uaepass.go index 12f7e2af3ddb..462aab76629b 100644 --- a/selfservice/strategy/oidc/provider_uaepass.go +++ b/selfservice/strategy/oidc/provider_uaepass.go @@ -16,6 +16,7 @@ import ( "golang.org/x/oauth2" "github.com/ory/herodot" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/ory/x/otelx" ) @@ -115,7 +116,7 @@ func (p *ProviderUAEPass) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { // Claims fetches user claims from the UAE PASS userinfo endpoint. // UAE PASS requires the access token to be passed as a Bearer token in the Authorization header. -func (p *ProviderUAEPass) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) { +func (p *ProviderUAEPass) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *claims.Claims, err error) { ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderUAEPass.Claims") defer otelx.End(span, &err) @@ -168,7 +169,7 @@ func (p *ProviderUAEPass) Claims(ctx context.Context, exchange *oauth2.Token, qu } // Map UAE PASS profile to standard OIDC claims - claims := &Claims{ + claims := &claims.Claims{ Subject: str("sub"), Issuer: p.baseURL, Email: str("email"), diff --git a/selfservice/strategy/oidc/provider_userinfo_test.go b/selfservice/strategy/oidc/provider_userinfo_test.go index cd40fff011a1..f83213020556 100644 --- a/selfservice/strategy/oidc/provider_userinfo_test.go +++ b/selfservice/strategy/oidc/provider_userinfo_test.go @@ -20,6 +20,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/pkg" "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/ory/x/otelx" @@ -45,7 +46,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { ctx := context.Background() token := &oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)} - expectedClaims := &oidc.Claims{ + expectedClaims := &claims.Claims{ Issuer: "ignore-me", Subject: "123456789012345", Name: "John Doe", @@ -75,7 +76,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { config *oidc.Configuration provider oidc.Provider userInfoHandler func(req *http.Request) (*http.Response, error) - expectedClaims *oidc.Claims + expectedClaims *claims.Claims useToken *oauth2.Token hook func(t *testing.T) }{ @@ -135,7 +136,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { }, ) }, - expectedClaims: &oidc.Claims{Issuer: "https://broker.netid.de/", Subject: "1234567890", Name: "John Doe", GivenName: "John", FamilyName: "Doe", LastName: "", MiddleName: "", Nickname: "John Doe", PreferredUsername: "John Doe", Profile: "", Picture: "", Website: "", Email: "john.doe@example.com", EmailVerified: true, Gender: "", Birthdate: "01/01/1990", Zoneinfo: "", Locale: "", PhoneNumber: "", PhoneNumberVerified: false, UpdatedAt: 0, HD: "", Team: ""}, + expectedClaims: &claims.Claims{Issuer: "https://broker.netid.de/", Subject: "1234567890", Name: "John Doe", GivenName: "John", FamilyName: "Doe", LastName: "", MiddleName: "", Nickname: "John Doe", PreferredUsername: "John Doe", Profile: "", Picture: "", Website: "", Email: "john.doe@example.com", EmailVerified: true, Gender: "", Birthdate: "01/01/1990", Zoneinfo: "", Locale: "", PhoneNumber: "", PhoneNumberVerified: false, UpdatedAt: 0, HD: "", Team: ""}, }, { name: "vk", @@ -158,7 +159,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { return resp, err }, - expectedClaims: &oidc.Claims{ + expectedClaims: &claims.Claims{ Issuer: "https://api.vk.com/method/users.get", Subject: "123456789012345", Email: "john.doe@example.com", @@ -186,7 +187,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { return resp, err }, - expectedClaims: &oidc.Claims{ + expectedClaims: &claims.Claims{ Issuer: "https://login.yandex.ru/info", Subject: "123456789012345", Email: "john.doe@example.com", @@ -231,7 +232,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { }) return resp, err }, - expectedClaims: &oidc.Claims{ + expectedClaims: &claims.Claims{ Issuer: "https://graph.facebook.com/me?fields=id,name,first_name,last_name,middle_name,email,picture,birthday,gender&appsecret_proof=0c0d98f7e3d9d45e72e8877bc1b104327efb9c07b18f2ffeced76d81307f1fff", Subject: "123456789012345", Name: "John Doe", @@ -302,7 +303,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { }, ) }, - expectedClaims: &oidc.Claims{ + expectedClaims: &claims.Claims{ Issuer: "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0", Subject: "new-id", Name: "John Doe", Email: "john.doe@example.com", RawClaims: map[string]interface{}{"aud": []interface{}{"foo"}, "exp": 4.071728504e+09, "iat": 1.516239022e+09, "iss": "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0", "email": "john.doe@example.com", "name": "John Doe", "sub": "1234567890", "tid": "a9b86385-f32c-4803-afc8-4b2312fbdf24"}, }, @@ -327,7 +328,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { ID: "dingtalk", Provider: "dingtalk", }, reg), - expectedClaims: &oidc.Claims{ + expectedClaims: &claims.Claims{ Issuer: "https://api.dingtalk.com/v1.0/contact/users/me", Subject: "123456789012345", Email: "john.doe@example.com", diff --git a/selfservice/strategy/oidc/provider_vk.go b/selfservice/strategy/oidc/provider_vk.go index 282be109f2b9..f2b1ba29f6b6 100644 --- a/selfservice/strategy/oidc/provider_vk.go +++ b/selfservice/strategy/oidc/provider_vk.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/go-retryablehttp" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/pkg/errors" @@ -61,7 +62,7 @@ func (g *ProviderVK) OAuth2(ctx context.Context) (*oauth2.Config, error) { return g.oauth2(ctx), nil } -func (g *ProviderVK) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderVK) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { o, err := g.OAuth2(ctx) if err != nil { return nil, err @@ -120,7 +121,7 @@ func (g *ProviderVK) Claims(ctx context.Context, exchange *oauth2.Token, query u gender = "male" } - return &Claims{ + return &claims.Claims{ Issuer: "https://api.vk.com/method/users.get", Subject: strconv.Itoa(user.ID), GivenName: user.FirstName, diff --git a/selfservice/strategy/oidc/provider_x.go b/selfservice/strategy/oidc/provider_x.go index d71f4433b2d0..89db7c2956ef 100644 --- a/selfservice/strategy/oidc/provider_x.go +++ b/selfservice/strategy/oidc/provider_x.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/otelx" "github.com/dghubble/oauth1" @@ -109,7 +110,7 @@ func (p *ProviderX) userInfoEndpoint() string { return xUserInfoBase } -func (p *ProviderX) Claims(ctx context.Context, token *oauth1.Token) (*Claims, error) { +func (p *ProviderX) Claims(ctx context.Context, token *oauth1.Token) (*claims.Claims, error) { ctx = context.WithValue(ctx, oauth1.HTTPClient, p.reg.HTTPClient(ctx).HTTPClient) c := p.OAuth1(ctx) @@ -136,7 +137,7 @@ func (p *ProviderX) Claims(ctx context.Context, token *oauth1.Token) (*Claims, e website = *user.URL } - return &Claims{ + return &claims.Claims{ Issuer: endpoint, Subject: user.IDStr, Name: user.Name, diff --git a/selfservice/strategy/oidc/provider_yandex.go b/selfservice/strategy/oidc/provider_yandex.go index e522b73070e9..39e2ab1e06b1 100644 --- a/selfservice/strategy/oidc/provider_yandex.go +++ b/selfservice/strategy/oidc/provider_yandex.go @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" "golang.org/x/oauth2" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/httpx" "github.com/ory/herodot" @@ -59,7 +60,7 @@ func (g *ProviderYandex) OAuth2(ctx context.Context) (*oauth2.Config, error) { return g.oauth2(ctx), nil } -func (g *ProviderYandex) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderYandex) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*claims.Claims, error) { o, err := g.OAuth2(ctx) if err != nil { return nil, err @@ -102,7 +103,7 @@ func (g *ProviderYandex) Claims(ctx context.Context, exchange *oauth2.Token, que user.Picture = "" } - return &Claims{ + return &claims.Claims{ Issuer: "https://login.yandex.ru/info", Subject: user.ID, GivenName: user.FirstName, diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 95f49c211ff5..db86ec1761eb 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -38,6 +38,7 @@ import ( "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/kratos/selfservice/strategy" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/selfservice/strategy/oidc/oidcerr" "github.com/ory/kratos/session" "github.com/ory/kratos/text" @@ -156,7 +157,7 @@ type Strategy struct { conflictingIdentityPolicy ConflictingIdentityPolicy } -type ConflictingIdentityPolicy func(ctx context.Context, existingIdentity, newIdentity *identity.Identity, provider Provider, claims *Claims) ConflictingIdentityVerdict +type ConflictingIdentityPolicy func(ctx context.Context, existingIdentity, newIdentity *identity.Identity, provider Provider, claims *claims.Claims) ConflictingIdentityVerdict type AuthCodeContainer struct { FlowID string `json:"flow_id"` @@ -503,7 +504,7 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request) { return } - var claims *Claims + var claims *claims.Claims var et *identity.CredentialsOIDCEncryptedTokens switch p := provider.(type) { case OAuth2Provider: @@ -863,7 +864,7 @@ func (s *Strategy) CompletedAuthenticationMethod(context.Context) session.Authen } } -func (s *Strategy) ProcessIDToken(r *http.Request, provider Provider, idToken, idTokenNonce string) (*Claims, error) { +func (s *Strategy) ProcessIDToken(r *http.Request, provider Provider, idToken, idTokenNonce string) (*claims.Claims, error) { verifier, ok := provider.(IDTokenVerifier) if !ok { return nil, oidcerr.Wrap(oidcerr.StepIDTokenVerify, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("The provider %s does not support id_token verification", provider.Config().Provider))) diff --git a/selfservice/strategy/oidc/strategy_helper_test.go b/selfservice/strategy/oidc/strategy_helper_test.go index b26b03cde0f4..54c2f24ed0d2 100644 --- a/selfservice/strategy/oidc/strategy_helper_test.go +++ b/selfservice/strategy/oidc/strategy_helper_test.go @@ -393,7 +393,7 @@ var publicJWKS []byte //go:embed stub/jwks_public2.json var publicJWKS2 []byte -type claims struct { +type jwtClaims struct { *jwt.RegisteredClaims Email string `json:"email"` } @@ -401,7 +401,7 @@ type claims struct { func createIDToken(t *testing.T, cl jwt.RegisteredClaims) string { key := &jwk.KeySpec{} require.NoError(t, json.Unmarshal(rawKey, key)) - token := jwt.NewWithClaims(jwt.SigningMethodRS256, &claims{ + token := jwt.NewWithClaims(jwt.SigningMethodRS256, &jwtClaims{ RegisteredClaims: &cl, Email: "acme@ory.sh", }) diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index dd7e00a39d53..6f912f4cac35 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -28,6 +28,7 @@ import ( "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flowhelpers" "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/session" "github.com/ory/kratos/text" "github.com/ory/kratos/ui/node" @@ -102,7 +103,7 @@ type UpdateLoginFlowWithOidcMethod struct { TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } -func (s *Strategy) handleConflictingIdentity(ctx context.Context, loginFlow *login.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer) (verdict ConflictingIdentityVerdict, id *identity.Identity, credentials *identity.Credentials, err error) { +func (s *Strategy) handleConflictingIdentity(ctx context.Context, loginFlow *login.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *claims.Claims, provider Provider, container *AuthCodeContainer) (verdict ConflictingIdentityVerdict, id *identity.Identity, credentials *identity.Credentials, err error) { if s.conflictingIdentityPolicy == nil { return ConflictingIdentityVerdictReject, nil, nil, nil } @@ -201,7 +202,7 @@ func jsonEqual(a, b json.RawMessage) bool { // result to an existing identity. It returns true if traits or metadata changed. // Unlike registration, user-supplied form traits are not merged — only the // mapper output is applied. -func (s *Strategy) UpdateIdentityFromClaims(ctx context.Context, claims *Claims, provider Provider, i *identity.Identity) (changed bool, err error) { +func (s *Strategy) UpdateIdentityFromClaims(ctx context.Context, claims *claims.Claims, provider Provider, i *identity.Identity) (changed bool, err error) { evaluated, _, err := s.EvaluateClaimsMapper(ctx, claims, provider, i) if err != nil { return false, err @@ -317,7 +318,7 @@ func (s *Strategy) UpdateIdentityFromClaims(ctx context.Context, claims *Claims, return changed, nil } -func (s *Strategy) ProcessLogin(ctx context.Context, w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer) (_ *registration.Flow, err error) { +func (s *Strategy) ProcessLogin(ctx context.Context, w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *claims.Claims, provider Provider, container *AuthCodeContainer) (_ *registration.Flow, err error) { ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.Strategy.processLogin") defer otelx.End(span, &err) @@ -425,7 +426,7 @@ func (s *Strategy) ProcessLogin(ctx context.Context, w http.ResponseWriter, r *h } } - if err = s.d.LoginHookExecutor().PostLoginHook(w, r, node.OpenIDConnectGroup, loginFlow, i, sess, provider.Config().ID); err != nil { + if err = s.d.LoginHookExecutor().PostLoginHook(w, r, node.OpenIDConnectGroup, loginFlow, i, sess, claims, provider.Config().ID); err != nil { return nil, x.WrapWithIdentityIDError(s.HandleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err), i.ID) } return nil, nil diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index 6943e41288ac..149b57e2bb4b 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -26,6 +26,7 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/session" "github.com/ory/kratos/text" "github.com/ory/kratos/x" @@ -323,7 +324,7 @@ func (s *Strategy) registrationToLogin(ctx context.Context, w http.ResponseWrite return lf, nil } -func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWriter, r *http.Request, rf *registration.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer) (_ *login.Flow, err error) { +func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWriter, r *http.Request, rf *registration.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *claims.Claims, provider Provider, container *AuthCodeContainer) (_ *login.Flow, err error) { ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.Strategy.processRegistration") defer otelx.End(span, &err) @@ -398,7 +399,7 @@ func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWrite // EvaluateClaimsMapper runs the Jsonnet mapper for the given claims and returns // the evaluated JSON string. If currentIdentity is non-nil, its traits and // metadata are made available in the Jsonnet context as std.extVar('identity'). -func (s *Strategy) EvaluateClaimsMapper(ctx context.Context, claims *Claims, provider Provider, currentIdentity *identity.Identity) (evaluated string, claimsJSON []byte, err error) { +func (s *Strategy) EvaluateClaimsMapper(ctx context.Context, claims *claims.Claims, provider Provider, currentIdentity *identity.Identity) (evaluated string, claimsJSON []byte, err error) { fetch := fetcher.NewFetcher(fetcher.WithClient(s.d.HTTPClient(ctx)), fetcher.WithCache(jsonnetCache, 60*time.Minute)) jsonnetSnippet, err := fetch.FetchContext(ctx, provider.Config().Mapper) if err != nil { @@ -456,7 +457,7 @@ func (s *Strategy) EvaluateClaimsMapper(ctx context.Context, claims *Claims, pro return evaluated, jsonClaims.Bytes(), nil } -func (s *Strategy) newIdentityFromClaims(ctx context.Context, claims *Claims, provider Provider, container *AuthCodeContainer, schema flow.IdentitySchema) (_ *identity.Identity, _ []VerifiedAddress, err error) { +func (s *Strategy) newIdentityFromClaims(ctx context.Context, claims *claims.Claims, provider Provider, container *AuthCodeContainer, schema flow.IdentitySchema) (_ *identity.Identity, _ []VerifiedAddress, err error) { evaluated, _, err := s.EvaluateClaimsMapper(ctx, claims, provider, nil) if err != nil { return nil, nil, err diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go index 615c4a678e9d..aba352b8d974 100644 --- a/selfservice/strategy/oidc/strategy_settings.go +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -31,6 +31,7 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/strategy" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/session" "github.com/ory/kratos/text" "github.com/ory/kratos/ui/node" @@ -490,7 +491,7 @@ func (s *Strategy) initLinkProvider(ctx context.Context, w http.ResponseWriter, return errors.WithStack(flow.ErrCompletedByStrategy) } -func (s *Strategy) linkProvider(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider) error { +func (s *Strategy) linkProvider(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, token *identity.CredentialsOIDCEncryptedTokens, claims *claims.Claims, provider Provider) error { p := &updateSettingsFlowWithOidcMethod{ Link: provider.Config().ID, FlowID: ctxUpdate.Flow.ID.String(), } diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 22e71f05e91e..b240c4f88f30 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -39,6 +39,7 @@ import ( "github.com/ory/kratos/selfservice/hook/hooktest" "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/kratos/selfservice/strategy/oidc" + cl "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/session" "github.com/ory/kratos/text" "github.com/ory/kratos/ui/container" @@ -1991,7 +1992,7 @@ func TestStrategy(t *testing.T) { scope = []string{"openid"} reg.AllLoginStrategies().MustStrategy("oidc").(*oidc.Strategy).SetOnConflictingIdentity(t, - func(ctx context.Context, existingIdentity, newIdentity *identity.Identity, _ oidc.Provider, _ *oidc.Claims) oidc.ConflictingIdentityVerdict { + func(ctx context.Context, existingIdentity, newIdentity *identity.Identity, _ oidc.Provider, _ *cl.Claims) oidc.ConflictingIdentityVerdict { return oidc.ConflictingIdentityVerdictMerge }) @@ -2028,7 +2029,7 @@ func TestStrategy(t *testing.T) { scope = []string{"openid"} reg.AllLoginStrategies().MustStrategy("oidc").(*oidc.Strategy).SetOnConflictingIdentity(t, - func(ctx context.Context, existingIdentity, newIdentity *identity.Identity, _ oidc.Provider, _ *oidc.Claims) oidc.ConflictingIdentityVerdict { + func(ctx context.Context, existingIdentity, newIdentity *identity.Identity, _ oidc.Provider, _ *cl.Claims) oidc.ConflictingIdentityVerdict { return oidc.ConflictingIdentityVerdictMerge }) diff --git a/selfservice/strategy/oidc/strategy_test_login.go b/selfservice/strategy/oidc/strategy_test_login.go index 4f328e07d3c2..a3ccbbc55c78 100644 --- a/selfservice/strategy/oidc/strategy_test_login.go +++ b/selfservice/strategy/oidc/strategy_test_login.go @@ -20,6 +20,7 @@ import ( "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/selfservice/strategy/oidc/oidcerr" "github.com/ory/kratos/text" ) @@ -64,7 +65,7 @@ func (s *Strategy) processTestLogin( w http.ResponseWriter, r *http.Request, f *login.Flow, - claims *Claims, + claims *claims.Claims, provider Provider, ) error { if f.State != flow.StateChooseMethod { @@ -234,7 +235,7 @@ func collectLeafValidationErrorsForLogin(v *jsonschema.ValidationError) []login. // claimsToMap serializes OIDC id_token claims to a generic map so they can be // recorded verbatim on DebugPayload.IDTokenClaims for the admin debug view. -func claimsToMap(claims *Claims) (map[string]any, error) { +func claimsToMap(claims *claims.Claims) (map[string]any, error) { raw, err := json.Marshal(claims) if err != nil { return nil, errors.WithStack(err) diff --git a/selfservice/strategy/oidc/strategy_test_login_test.go b/selfservice/strategy/oidc/strategy_test_login_test.go index 1f6de56c295e..d536a9fe7d62 100644 --- a/selfservice/strategy/oidc/strategy_test_login_test.go +++ b/selfservice/strategy/oidc/strategy_test_login_test.go @@ -24,6 +24,7 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/kratos/selfservice/strategy/oidc/oidcerr" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" @@ -206,7 +207,7 @@ func TestStrategy_ProcessTestLogin_HappyPath(t *testing.T) { f := newTestLoginFlow(t, ctx, reg, "providerID") - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", Email: "alice@example.com", Website: "https://example.com", @@ -296,7 +297,7 @@ func TestStrategy_ProcessTestLogin_AlreadyCaptured(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/self-service/methods/oidc/callback?code=x&state=y", nil) - err = strat.ProcessTestLoginForTest(ctx, w, r, f, &oidc.Claims{Subject: "alice"}, provider) + err = strat.ProcessTestLoginForTest(ctx, w, r, f, &claims.Claims{Subject: "alice"}, provider) require.Error(t, err) var herr *herodot.DefaultError require.ErrorAs(t, err, &herr) @@ -321,7 +322,7 @@ func TestStrategy_ProcessTestLogin_RawTokensNotStored(t *testing.T) { f := newTestLoginFlow(t, ctx, reg, "providerID") - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", RawClaims: map[string]any{ "groups": []any{"admin"}, @@ -416,7 +417,7 @@ func TestStrategy_ProcessTestLogin_MapperEvaluationFailure(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/self-service/methods/oidc/callback?code=x&state=y", nil) - require.NoError(t, strat.ProcessTestLoginForTest(ctx, w, r, f, &oidc.Claims{Subject: "alice"}, provider)) + require.NoError(t, strat.ProcessTestLoginForTest(ctx, w, r, f, &claims.Claims{Subject: "alice"}, provider)) got, err := reg.LoginFlowPersister().GetLoginFlow(ctx, f.ID) require.NoError(t, err) diff --git a/selfservice/strategy/oidc/strategy_update_identity_test.go b/selfservice/strategy/oidc/strategy_update_identity_test.go index 7494b3474e0c..f2fa2a99c57a 100644 --- a/selfservice/strategy/oidc/strategy_update_identity_test.go +++ b/selfservice/strategy/oidc/strategy_update_identity_test.go @@ -19,6 +19,7 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/pkg" "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" ) type testUpdateProvider struct { @@ -40,7 +41,7 @@ func TestEvaluateClaimsMapper(t *testing.T) { Mapper: "file://./stub/oidc.hydra.jsonnet", }} - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", Email: "alice@example.com", Website: "https://example.com", @@ -106,7 +107,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { MetadataAdmin: []byte(`{}`), } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", Website: "https://new.example.com", RawClaims: map[string]any{ @@ -132,7 +133,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { MetadataAdmin: []byte(`{}`), } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", Picture: "https://example.com/new-pic.png", PhoneNumber: "+1234567890", @@ -156,7 +157,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { MetadataAdmin: []byte(`{"phone_number":"+1234567890"}`), } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", Website: "https://example.com", Picture: "https://example.com/alice.png", @@ -184,7 +185,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { Credentials: originalCreds, } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", Website: "https://new.example.com", RawClaims: map[string]any{"_placeholder": true}, @@ -212,7 +213,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { MetadataAdmin: []byte(`{}`), } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", Website: "https://new.example.com", Picture: "https://example.com/new.png", @@ -245,7 +246,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { MetadataAdmin: []byte(`{}`), } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", Website: "https://new.example.com", RawClaims: map[string]any{"_placeholder": true}, @@ -276,7 +277,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { Mapper: "file://./stub/oidc.update-identity.jsonnet", }} - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", Website: "https://example.com", RawClaims: map[string]any{"_placeholder": true}, @@ -306,7 +307,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { MetadataAdmin: []byte(`{}`), } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", RawClaims: map[string]any{ "groups": []any{"admin"}, @@ -366,7 +367,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { }, } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", RawClaims: map[string]any{"_placeholder": true}, } @@ -398,7 +399,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { MetadataAdmin: []byte(`{"secret":"admin-data"}`), } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", Website: "https://new.example.com", RawClaims: map[string]any{"_placeholder": true}, @@ -438,7 +439,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { }, } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", RawClaims: map[string]any{"_placeholder": true}, } @@ -494,7 +495,7 @@ func TestUpdateIdentityFromClaims(t *testing.T) { }, } - claims := &oidc.Claims{ + claims := &claims.Claims{ Subject: "alice@example.com", RawClaims: map[string]any{"_placeholder": true}, } diff --git a/selfservice/strategy/oidc/token_verifier.go b/selfservice/strategy/oidc/token_verifier.go index 408d28cb5615..e14198a3a185 100644 --- a/selfservice/strategy/oidc/token_verifier.go +++ b/selfservice/strategy/oidc/token_verifier.go @@ -11,10 +11,11 @@ import ( "github.com/coreos/go-oidc/v3/oidc" + "github.com/ory/kratos/selfservice/strategy/oidc/claims" "github.com/ory/x/reqlog" ) -func verifyToken(ctx context.Context, keySet oidc.KeySet, config *Configuration, rawIDToken, issuerURL string) (*Claims, error) { +func verifyToken(ctx context.Context, keySet oidc.KeySet, config *Configuration, rawIDToken, issuerURL string) (*claims.Claims, error) { tokenAudiences := append([]string{config.ClientID}, config.AdditionalIDTokenAudiences...) var token *oidc.IDToken err := fmt.Errorf("no audience matched the token's audience") @@ -39,7 +40,7 @@ func verifyToken(ctx context.Context, keySet oidc.KeySet, config *Configuration, // None of the allowed audiences matched the audience in the token return nil, fmt.Errorf("token audience didn't match allowed audiences: %+v %w", tokenAudiences, err) } - claims := &Claims{} + claims := &claims.Claims{} var rawClaims map[string]any if token == nil {