From d4867b6353092334e346f22496b813fe40377379 Mon Sep 17 00:00:00 2001 From: Tom Fenech Date: Tue, 14 May 2024 16:42:21 +0200 Subject: [PATCH 1/5] Pass OIDC claims into post-login flow to include in web hook context The login flow doesn't trigger a refresh of the identity when the OIDC claims have changed. By passing the claims through to the web hook context, this means that an external handler can be configured to update the identity as appropriate, when there are changes. --- schema/handler_test.go | 1 - selfservice/flow/login/handler.go | 2 +- selfservice/flow/login/hook.go | 16 ++-- selfservice/flow/login/hook_test.go | 4 +- selfservice/hook/error.go | 3 +- selfservice/hook/require_verified_address.go | 3 +- .../hook/require_verified_address_test.go | 10 +-- selfservice/hook/session_destroyer.go | 3 +- selfservice/hook/session_destroyer_test.go | 1 + selfservice/hook/show_verification_ui.go | 3 +- selfservice/hook/show_verification_ui_test.go | 12 +-- selfservice/hook/stub/test_body.jsonnet | 10 ++- selfservice/hook/verification.go | 3 +- selfservice/hook/verification_test.go | 4 +- selfservice/hook/web_hook.go | 5 +- selfservice/hook/web_hook_integration_test.go | 49 ++++++++-- selfservice/strategy/oidc/claims/claims.go | 61 +++++++++++++ .../strategy/oidc/claims/claims_test.go | 18 ++++ selfservice/strategy/oidc/claims/locale.go | 29 ++++++ selfservice/strategy/oidc/export_test.go | 3 +- selfservice/strategy/oidc/provider.go | 90 ++----------------- selfservice/strategy/oidc/provider_amazon.go | 5 +- selfservice/strategy/oidc/provider_apple.go | 10 ++- .../strategy/oidc/provider_apple_test.go | 11 +-- selfservice/strategy/oidc/provider_auth0.go | 5 +- selfservice/strategy/oidc/provider_config.go | 3 +- .../strategy/oidc/provider_config_test.go | 23 ++--- .../strategy/oidc/provider_dingtalk.go | 5 +- selfservice/strategy/oidc/provider_discord.go | 7 +- .../strategy/oidc/provider_facebook.go | 5 +- .../strategy/oidc/provider_generic_oidc.go | 13 +-- selfservice/strategy/oidc/provider_github.go | 5 +- .../strategy/oidc/provider_github_app.go | 5 +- selfservice/strategy/oidc/provider_gitlab.go | 5 +- selfservice/strategy/oidc/provider_google.go | 6 +- selfservice/strategy/oidc/provider_lark.go | 5 +- .../strategy/oidc/provider_linkedin.go | 5 +- .../strategy/oidc/provider_linkedin_test.go | 5 +- .../strategy/oidc/provider_microsoft.go | 5 +- selfservice/strategy/oidc/provider_netid.go | 15 ++-- selfservice/strategy/oidc/provider_patreon.go | 5 +- .../strategy/oidc/provider_salesforce.go | 5 +- selfservice/strategy/oidc/provider_slack.go | 5 +- selfservice/strategy/oidc/provider_spotify.go | 5 +- selfservice/strategy/oidc/provider_test.go | 17 ++-- .../strategy/oidc/provider_test_fedcm.go | 6 +- selfservice/strategy/oidc/provider_uaepass.go | 5 +- .../strategy/oidc/provider_userinfo_test.go | 17 ++-- selfservice/strategy/oidc/provider_vk.go | 5 +- selfservice/strategy/oidc/provider_x.go | 5 +- selfservice/strategy/oidc/provider_yandex.go | 5 +- selfservice/strategy/oidc/strategy.go | 7 +- .../strategy/oidc/strategy_helper_test.go | 4 +- selfservice/strategy/oidc/strategy_login.go | 9 +- .../strategy/oidc/strategy_registration.go | 7 +- .../strategy/oidc/strategy_settings.go | 3 +- selfservice/strategy/oidc/strategy_test.go | 5 +- .../strategy/oidc/strategy_test_login.go | 5 +- .../strategy/oidc/strategy_test_login_test.go | 9 +- .../oidc/strategy_update_identity_test.go | 27 +++--- selfservice/strategy/oidc/token_verifier.go | 5 +- 61 files changed, 372 insertions(+), 262 deletions(-) create mode 100644 selfservice/strategy/oidc/claims/claims.go create mode 100644 selfservice/strategy/oidc/claims/claims_test.go create mode 100644 selfservice/strategy/oidc/claims/locale.go 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..66e66190857a 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,31 @@ 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 { + 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.sh/some_end_point", string(tp), oidcClaims.Nickname, oidcClaims.RawClaims["groups"].([]string)[0], oidcClaims.RawClaims["groups"].([]string)[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 +270,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 +512,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 +523,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 +1153,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 +1168,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 +1192,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 +1241,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..d88f9eba8c58 --- /dev/null +++ b/selfservice/strategy/oidc/claims/claims.go @@ -0,0 +1,61 @@ +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..47ada4a4695c --- /dev/null +++ b/selfservice/strategy/oidc/claims/claims_test.go @@ -0,0 +1,18 @@ +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..07fb8576becc --- /dev/null +++ b/selfservice/strategy/oidc/claims/locale.go @@ -0,0 +1,29 @@ +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 { From 4fe3281f19cb8b61552ebc1b5edb745c519d058a Mon Sep 17 00:00:00 2001 From: Tom Fenech Date: Tue, 14 May 2024 16:57:10 +0200 Subject: [PATCH 2/5] Add license comment to new files --- selfservice/strategy/oidc/claims/claims.go | 3 +++ selfservice/strategy/oidc/claims/claims_test.go | 3 +++ selfservice/strategy/oidc/claims/locale.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/selfservice/strategy/oidc/claims/claims.go b/selfservice/strategy/oidc/claims/claims.go index d88f9eba8c58..ff233a11b6f4 100644 --- a/selfservice/strategy/oidc/claims/claims.go +++ b/selfservice/strategy/oidc/claims/claims.go @@ -1,3 +1,6 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package claims import ( diff --git a/selfservice/strategy/oidc/claims/claims_test.go b/selfservice/strategy/oidc/claims/claims_test.go index 47ada4a4695c..8c532eab569c 100644 --- a/selfservice/strategy/oidc/claims/claims_test.go +++ b/selfservice/strategy/oidc/claims/claims_test.go @@ -1,3 +1,6 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package claims_test import ( diff --git a/selfservice/strategy/oidc/claims/locale.go b/selfservice/strategy/oidc/claims/locale.go index 07fb8576becc..226737505170 100644 --- a/selfservice/strategy/oidc/claims/locale.go +++ b/selfservice/strategy/oidc/claims/locale.go @@ -1,3 +1,6 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + package claims import ( From c8136e3c57831855d395c44ab2f62b046c09e03b Mon Sep 17 00:00:00 2001 From: Tom Fenech Date: Sun, 30 Nov 2025 12:35:02 +0100 Subject: [PATCH 3/5] chore: fix formatting issue --- oryx/dbal/testhelpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() From 66b5ae1eac55698cbc80dd01768e20ca0f4af1e9 Mon Sep 17 00:00:00 2001 From: Tom Fenech Date: Mon, 15 Jun 2026 14:49:38 +0200 Subject: [PATCH 4/5] test: fix claims in body (apply Code Rabbit suggestion) --- selfservice/hook/web_hook_integration_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go index 66e66190857a..fe661e383cda 100644 --- a/selfservice/hook/web_hook_integration_test.go +++ b/selfservice/hook/web_hook_integration_test.go @@ -226,6 +226,7 @@ func TestWebHooks(t *testing.T) { }) 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", @@ -240,7 +241,7 @@ func TestWebHooks(t *testing.T) { "transient_payload": %s, "nickname": "%s", "groups": ["%s", "%s"] - }`, f.GetID(), s.Identity.ID, s.ID, req.Method, "http://www.ory.sh/some_end_point", string(tp), oidcClaims.Nickname, oidcClaims.RawClaims["groups"].([]string)[0], oidcClaims.RawClaims["groups"].([]string)[1]) + }`, 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}) From 267f50f7c7dd063232dbde62f45ac55754778fd7 Mon Sep 17 00:00:00 2001 From: David Carrera Date: Wed, 26 Nov 2025 21:32:11 +0100 Subject: [PATCH 5/5] allow additional ranges of ips --- oryx/ipx/ssrf.go | 2 ++ 1 file changed, 2 insertions(+) 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)