diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 11b0dddfae49..a99b8f5b2e80 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -77,7 +77,7 @@ jobs: fetch-depth: 2 - uses: actions/setup-go@v4 with: - go-version: "1.19" + go-version: "1.21" - run: go list -json > go.list - name: Run nancy uses: sonatype-nexus-community/nancy-github-action@v1.0.2 @@ -91,7 +91,7 @@ jobs: GOGC: 100 with: args: --timeout 10m0s - version: v1.50.1 + version: v1.54.2 skip-go-installation: true skip-pkg-cache: true - name: Build Kratos @@ -169,7 +169,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: "1.19" + go-version: "1.21" - name: Install selfservice-ui-react-native uses: actions/checkout@v3 @@ -272,7 +272,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: "1.19" + go-version: "1.21" - run: go build -tags sqlite,json1 . - name: Install selfservice-ui-react-native diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index a7a720ebc0a7..b59c85d31b22 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version: "1.21" - run: make format - name: Indicate formatting issues run: git diff HEAD --exit-code --color diff --git a/.github/workflows/licenses.yml b/.github/workflows/licenses.yml index a4592c63ceda..8871ccb2c542 100644 --- a/.github/workflows/licenses.yml +++ b/.github/workflows/licenses.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: - go-version: "1.18" + go-version: "1.21" - uses: actions/setup-node@v2 with: node-version: "18" diff --git a/CHANGELOG.md b/CHANGELOG.md index 636f28017155..6d63a3555b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,12 @@ **Table of Contents** -- [ (2023-10-19)](#2023-10-19) +- [ (2023-11-08)](#2023-11-08) - [Breaking Changes](#breaking-changes) - [Bug Fixes](#bug-fixes) - [Documentation](#documentation) - [Features](#features) + - [Reverts](#reverts) - [Tests](#tests) - [1.0.0 (2023-07-12)](#100-2023-07-12) - [Bug Fixes](#bug-fixes-1) @@ -44,7 +45,7 @@ - [Code Refactoring](#code-refactoring-1) - [Documentation](#documentation-4) - [Features](#features-5) - - [Reverts](#reverts) + - [Reverts](#reverts-1) - [Tests](#tests-4) - [Unclassified](#unclassified-2) - [0.10.1 (2022-06-01)](#0101-2022-06-01) @@ -113,7 +114,7 @@ - [Code Refactoring](#code-refactoring-5) - [Documentation](#documentation-12) - [Features](#features-11) - - [Reverts](#reverts-1) + - [Reverts](#reverts-2) - [Tests](#tests-10) - [Unclassified](#unclassified-5) - [0.7.6-alpha.1 (2021-09-12)](#076-alpha1-2021-09-12) @@ -313,7 +314,7 @@ -# [](https://github.com/ory/kratos/compare/v1.0.0...v) (2023-10-19) +# [](https://github.com/ory/kratos/compare/v1.0.0...v) (2023-11-08) ## Breaking Changes @@ -406,9 +407,27 @@ https://github.com/ory/kratos/pull/3480 - Change ListIdentities to keyset pagination ([e16fed1](https://github.com/ory/kratos/commit/e16fed1f8563509aac30886386668bb85e6dc797)) +- Change shebangs and makefile from /bin/bash to /usr/bin/env bash + ([#3597](https://github.com/ory/kratos/issues/3597)) + ([1343bbb](https://github.com/ory/kratos/commit/1343bbbfa11ff3e7fcbc0f233b858d13fd40c66d)): + + - makefile fix + + - shebangs changed to /usr/bin/env bash + + Signed-off-by: nxy7 + - Code method on registration and 2fa ([#3481](https://github.com/ory/kratos/issues/3481)) ([7aa2e29](https://github.com/ory/kratos/commit/7aa2e293175d0f4b6c13552cc3781f54f8caf3a0)) +- Consider OIDC registration flows errored with duplicate credential to be + completed by strategy ([#3525](https://github.com/ory/kratos/issues/3525)) + ([3e3c789](https://github.com/ory/kratos/commit/3e3c78967523676cbce9a227d574c2f7f4ea314d)): + + Returning anything else here may cause Kratos to respond with two concatenated + JSON objects: new login flow with actual error message as the first one and a + very confusing '500, aborted registration hook execution' as the second one. + - Data race in test ([ab6dc31](https://github.com/ory/kratos/commit/ab6dc3121535d27668fed58804a218b17b17ae43)) - Do not encode full config in multiple places @@ -471,6 +490,16 @@ https://github.com/ory/kratos/pull/3480 The identity is not always available in the session struct, for example when AAL2 is required. +- On verification required after registration, preserve return_to + ([#3589](https://github.com/ory/kratos/issues/3589)) + ([6a0a914](https://github.com/ory/kratos/commit/6a0a9149b9828ba994bec9b48a43f9d70245f43f)): + + - fix: on verification required after registration, preserve return_to + + - test: return_to on verification flow + + - chore: refactor + - Pass context ([#3452](https://github.com/ory/kratos/issues/3452)) ([c492bdc](https://github.com/ory/kratos/commit/c492bdcd0c5dbdf527ae523d879a6c1eeb9c4cdf)) - Properly normalize OIDC verified emails @@ -497,6 +526,28 @@ https://github.com/ory/kratos/pull/3480 - style: format +- Registration should accept hydra login + ([#3592](https://github.com/ory/kratos/issues/3592)) + ([7a47827](https://github.com/ory/kratos/commit/7a47827cfd58ef68ebfbbeaf5ed86c394ba2bd5e)): + + - fix: registration should accept hydra login + + - fix: oauth2 registration flow with session + + - wip: registration oauth flow tests + + - wip: refactor oauth flows test + + - wip: refactor op_registration_test + + - wip: oauth provider registration test + + - wip: refactor oauth flows test + + - fix(test): oauth provider login + + - style: format + - Registration with verification ([#3451](https://github.com/ory/kratos/issues/3451)) ([77c3196](https://github.com/ory/kratos/commit/77c3196fd60c5927b84e9a7f6546f80ac2d78ee5)) @@ -512,6 +563,9 @@ https://github.com/ory/kratos/pull/3480 - Remove slow queries from update identities ([#3553](https://github.com/ory/kratos/issues/3553)) ([d138abb](https://github.com/ory/kratos/commit/d138abb6278ebb232e120bee0fb956a0f2816b8d)) +- Respect gomail.SendError in mail queue + ([#3600](https://github.com/ory/kratos/issues/3600)) + ([9c608b9](https://github.com/ory/kratos/commit/9c608b991874d839782d9219f2fc27d0d4a398af)) - Respond with 422 when SPA identity requires AAL2 ([#3572](https://github.com/ory/kratos/issues/3572)) ([df18c09](https://github.com/ory/kratos/commit/df18c09e0089743e8aee17540d277b9572252e06)): @@ -527,8 +581,22 @@ https://github.com/ory/kratos/pull/3480 - Return 400 bad request for invalid login challenge ([#3404](https://github.com/ory/kratos/issues/3404)) ([ca34e9b](https://github.com/ory/kratos/commit/ca34e9b744482b41d65082f3bed52e9c4ebd7ba4)) +- Return HTTP 400 if key unmarshal fails + ([#3594](https://github.com/ory/kratos/issues/3594)) + ([fdf4956](https://github.com/ory/kratos/commit/fdf4956d9218cfa1d2227c4880e48f9bbdaeb95d)): + + - fix: return HTTP 400 if key unmarshal fails + + - fix: apply reviewer's suggestion, prepare for bump + + - fix: follow up reviewer suggestion from ory/x + + - chore: bump ory/x + - Schema test errors ([#3528](https://github.com/ory/kratos/issues/3528)) ([bee0341](https://github.com/ory/kratos/commit/bee0341c5bf5708a2210146fc59f050a1b9df663)) +- Specify correct minimum versions in migratest + ([18b89ea](https://github.com/ory/kratos/commit/18b89ea588d129fa88379f7b0d7f4fd00ec6023d)) - Tracing improvements ([c804cb2](https://github.com/ory/kratos/commit/c804cb2bebbefc97073cf3b8fa250c3eefc58894)) - Type-assert all interfaces that WebHook implements @@ -599,6 +667,8 @@ https://github.com/ory/kratos/pull/3480 - Add OpenTelemetry span for password hash comparison ([#3383](https://github.com/ory/kratos/issues/3383)) ([e3fcf0c](https://github.com/ory/kratos/commit/e3fcf0c31db9742ed61bcf783e37ee119ed19d42)) +- Add WebhookSucceeded event + ([aa8c936](https://github.com/ory/kratos/commit/aa8c93677a8f682f7693afe69f1baf1887355e0a)) - Added various new text messages ([ea91483](https://github.com/ory/kratos/commit/ea914834e6bb626de2977e228af2b40935ccc980)): @@ -738,6 +808,19 @@ https://github.com/ory/kratos/pull/3480 - Improve performance by computing password hashes while validating ([#3508](https://github.com/ory/kratos/issues/3508)) ([a9786c5](https://github.com/ory/kratos/commit/a9786c599d09f61e2e07df5066ce94feb2d99bac)) +- Link oidc credentials when login + ([#3563](https://github.com/ory/kratos/issues/3563)) + ([b784949](https://github.com/ory/kratos/commit/b784949d03b849d9d1d594977f75f5843b7b5da8)), + closes [#2727](https://github.com/ory/kratos/issues/2727) + [#3222](https://github.com/ory/kratos/issues/3222): + + When user tries to login with OIDC for the first time but has already + registered before with email/password a credentials identifier conflict may be + detected by Kratos. In this case user needs to login with email/password first + and then link OIDC credentials on a settings screen. This PR simplifies UX and + allows user to link OIDC credentials to existing account right in the login + flow, without switching to settings flow. + - Login with code on any credential type ([#3549](https://github.com/ory/kratos/issues/3549)) ([ceed7d5](https://github.com/ory/kratos/commit/ceed7d5478c5cca894587698c57f676dda100b27)): @@ -749,6 +832,13 @@ https://github.com/ory/kratos/pull/3480 - One-time code native flows ([#3516](https://github.com/ory/kratos/issues/3516)) ([9b0fee3](https://github.com/ory/kratos/commit/9b0fee30f980d860fd548e7589fa6a06e593537a)) +- Parametrize courier worker + ([#3601](https://github.com/ory/kratos/issues/3601)) + ([0e4be57](https://github.com/ory/kratos/commit/0e4be57e41e1152f4be22f490541c2c099cfe3fe)): + + Allows one to parametrize how many messages the courier will fetch and how + often it will fetch messages. + - Passwordless browser login and registration via code to email ([#3378](https://github.com/ory/kratos/issues/3378)) ([eaaf375](https://github.com/ory/kratos/commit/eaaf37519917612671238412a633847386d7c613)), @@ -811,6 +901,17 @@ https://github.com/ory/kratos/pull/3480 - fix: upgrade hydra in tests +- Webhook analytic events + ([9c8a25e](https://github.com/ory/kratos/commit/9c8a25eb0d3e06df182565d3d959d57e5dccfed8)) + +### Reverts + +- Revert "chore: simplify courier code (#3603)" + ([7c54c9f](https://github.com/ory/kratos/commit/7c54c9f36c86142c8e071a5359c71cf6213a1a69)), + closes [#3603](https://github.com/ory/kratos/issues/3603): + + This reverts commit 316cd4aacfe31efafa7d737a7c476e2c794e9c9b. + ### Tests - **e2e:** Logout return_to ([#3418](https://github.com/ory/kratos/issues/3418)) diff --git a/Makefile b/Makefile index 1b8572d3cfe0..59fd30158bdb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -SHELL=/bin/bash -o pipefail +SHELL=/usr/bin/env bash -o pipefail # EXECUTABLES = docker-compose docker node npm go # K := $(foreach exec,$(EXECUTABLES),\ @@ -49,7 +49,7 @@ docs/swagger: npx @redocly/openapi-cli preview-docs spec/swagger.json .bin/golangci-lint: Makefile - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.52.2 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.54.2 .bin/hydra: Makefile bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -d -b .bin hydra v2.2.0-rc.3 diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go index a9e96b909922..6a9ff0610017 100644 --- a/cmd/clidoc/main.go +++ b/cmd/clidoc/main.go @@ -119,10 +119,13 @@ func init() { "NewInfoLoginTOTPLabel": text.NewInfoLoginTOTPLabel(), "NewInfoLoginLookupLabel": text.NewInfoLoginLookupLabel(), "NewInfoLogin": text.NewInfoLogin(), + "NewInfoLoginAndLink": text.NewInfoLoginAndLink(), + "NewInfoLoginLinkMessage": text.NewInfoLoginLinkMessage("{duplicteIdentifier}", "{provider}", "{newLoginUrl}"), "NewInfoLoginTOTP": text.NewInfoLoginTOTP(), "NewInfoLoginLookup": text.NewInfoLoginLookup(), "NewInfoLoginVerify": text.NewInfoLoginVerify(), "NewInfoLoginWith": text.NewInfoLoginWith("{provider}"), + "NewInfoLoginWithAndLink": text.NewInfoLoginWithAndLink("{provider}"), "NewErrorValidationLoginFlowExpired": text.NewErrorValidationLoginFlowExpired(aSecondAgo), "NewErrorValidationLoginNoStrategyFound": text.NewErrorValidationLoginNoStrategyFound(), "NewErrorValidationRegistrationNoStrategyFound": text.NewErrorValidationRegistrationNoStrategyFound(), @@ -144,6 +147,7 @@ func init() { "NewErrorValidationRecoveryStateFailure": text.NewErrorValidationRecoveryStateFailure(), "NewInfoNodeInputEmail": text.NewInfoNodeInputEmail(), "NewInfoNodeResendOTP": text.NewInfoNodeResendOTP(), + "NewInfoNodeLoginAndLinkCredential": text.NewInfoNodeLoginAndLinkCredential(), "NewInfoNodeLabelContinue": text.NewInfoNodeLabelContinue(), "NewInfoSelfServiceSettingsRegisterWebAuthn": text.NewInfoSelfServiceSettingsRegisterWebAuthn(), "NewInfoLoginWebAuthnPasswordless": text.NewInfoLoginWebAuthnPasswordless(), @@ -163,6 +167,7 @@ func init() { "NewInfoSelfServiceLoginCode": text.NewInfoSelfServiceLoginCode(), "NewErrorValidationRegistrationRetrySuccessful": text.NewErrorValidationRegistrationRetrySuccessful(), "NewInfoSelfServiceRegistrationRegisterCode": text.NewInfoSelfServiceRegistrationRegisterCode(), + "NewErrorValidationLoginLinkedCredentialsDoNotMatch": text.NewErrorValidationLoginLinkedCredentialsDoNotMatch(), } } diff --git a/continuity/manager_test.go b/continuity/manager_test.go index 8bfad92d6815..6790137faa80 100644 --- a/continuity/manager_test.go +++ b/continuity/manager_test.go @@ -53,7 +53,7 @@ func TestManager(t *testing.T) { i := identity.NewIdentity("") require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) - var newServer = func(t *testing.T, p continuity.Manager, tc *persisterTestCase) *httptest.Server { + newServer := func(t *testing.T, p continuity.Manager, tc *persisterTestCase) *httptest.Server { writer := herodot.NewJSONWriter(logrusx.New("", "")) router := httprouter.New() router.PUT("/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { @@ -103,8 +103,8 @@ func TestManager(t *testing.T) { return ts } - var newClient = func() *http.Client { - return &http.Client{Jar: x.EasyCookieJar(t, nil)} + newClient := func() *http.Client { + return &http.Client{Jar: testhelpers.EasyCookieJar(t, nil)} } p := reg.ContinuityManager() @@ -114,12 +114,12 @@ func TestManager(t *testing.T) { ts := newServer(t, p, new(persisterTestCase)) href := ts.URL + "/" + x.NewUUID().String() - res, err := cl.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil)) require.NoError(t, err) require.NoError(t, res.Body.Close()) require.Equal(t, http.StatusNoContent, res.StatusCode) - req := x.NewTestHTTPRequest(t, "GET", href, nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", href, nil) require.Len(t, res.Cookies(), 1) for _, c := range res.Cookies() { // Change something in the string @@ -143,21 +143,21 @@ func TestManager(t *testing.T) { ts := newServer(t, p, tc) href := ts.URL + "/" + x.NewUUID().String() - res, err := http.DefaultClient.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + res, err := http.DefaultClient.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil)) require.NoError(t, err) require.NoError(t, res.Body.Close()) require.Equal(t, http.StatusNoContent, res.StatusCode) // We change the key to another one href = ts.URL + "/" + x.NewUUID().String() - req := x.NewTestHTTPRequest(t, "GET", href, nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", href, nil) require.Len(t, res.Cookies(), 1) for _, c := range res.Cookies() { req.AddCookie(c) } tc.ro = []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"})} - res, err = http.DefaultClient.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + res, err = http.DefaultClient.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil)) require.NoError(t, err) require.NoError(t, res.Body.Close()) require.Equal(t, http.StatusNoContent, res.StatusCode) @@ -196,13 +196,13 @@ func TestManager(t *testing.T) { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { cl := newClient() ts := newServer(t, p, &tc) - var genid = func() string { + genid := func() string { return ts.URL + "/" + x.NewUUID().String() } t.Run("case=resume non-existing session", func(t *testing.T) { href := genid() - res, err := cl.Do(x.NewTestHTTPRequest(t, "GET", href, nil)) + res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) @@ -213,12 +213,12 @@ func TestManager(t *testing.T) { t.Run("case=pause and resume session", func(t *testing.T) { href := genid() - res, err := cl.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil)) require.NoError(t, err) require.NoError(t, res.Body.Close()) require.Equal(t, http.StatusNoContent, res.StatusCode) - res, err = cl.Do(x.NewTestHTTPRequest(t, "GET", href, nil)) + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) @@ -238,16 +238,16 @@ func TestManager(t *testing.T) { t.Run("case=pause and retry session", func(t *testing.T) { href := genid() - res, err := cl.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil)) require.NoError(t, err) require.NoError(t, res.Body.Close()) require.Equal(t, http.StatusNoContent, res.StatusCode) - res, err = cl.Do(x.NewTestHTTPRequest(t, "GET", href, nil)) + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) - res, err = cl.Do(x.NewTestHTTPRequest(t, "GET", href, nil)) + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) require.NoError(t, err) require.Equal(t, http.StatusBadRequest, res.StatusCode) body := ioutilx.MustReadAll(res.Body) @@ -257,7 +257,7 @@ func TestManager(t *testing.T) { t.Run("case=pause and resume session in the same request", func(t *testing.T) { href := genid() - res, err := cl.Do(x.NewTestHTTPRequest(t, "POST", href, nil)) + res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "POST", href, nil)) require.NoError(t, err) require.Equal(t, http.StatusOK, res.StatusCode) t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) @@ -272,17 +272,17 @@ func TestManager(t *testing.T) { t.Run("case=pause, abort, and continue session with failure", func(t *testing.T) { href := genid() - res, err := cl.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil)) require.NoError(t, err) require.NoError(t, res.Body.Close()) require.Equal(t, http.StatusNoContent, res.StatusCode) - res, err = cl.Do(x.NewTestHTTPRequest(t, "DELETE", href, nil)) + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "DELETE", href, nil)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) require.Equal(t, http.StatusNoContent, res.StatusCode) - res, err = cl.Do(x.NewTestHTTPRequest(t, "GET", href, nil)) + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) diff --git a/corpx/faker.go b/corpx/faker.go index a633f956dd8f..e8fc4b0e388f 100644 --- a/corpx/faker.go +++ b/corpx/faker.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow" diff --git a/courier/courier.go b/courier/courier.go index a1eeed13e111..b42a580fc061 100644 --- a/courier/courier.go +++ b/courier/courier.go @@ -39,6 +39,7 @@ type ( SetGetEmailTemplateType(f func(t EmailTemplate) (TemplateType, error)) SetNewEmailTemplateFromMessage(f func(d template.Dependencies, msg Message) (EmailTemplate, error)) UseBackoff(b backoff.BackOff) + FailOnDispatchError() } Provider interface { @@ -50,12 +51,12 @@ type ( } courier struct { - smsClient *smsClient - smtpClient *smtpClient - httpClient *httpClient - deps Dependencies - failOnError bool - backoff backoff.BackOff + smsClient *smsClient + smtpClient *smtpClient + httpClient *httpClient + deps Dependencies + failOnDispatchError bool + backoff backoff.BackOff } ) @@ -74,7 +75,7 @@ func NewCourier(ctx context.Context, deps Dependencies) (Courier, error) { } func (c *courier) FailOnDispatchError() { - c.failOnError = true + c.failOnDispatchError = true } func (c *courier) Work(ctx context.Context) error { @@ -99,6 +100,7 @@ func (c *courier) UseBackoff(b backoff.BackOff) { } func (c *courier) watchMessages(ctx context.Context, errChan chan error) { + wait := c.deps.CourierConfig().CourierWorkerPullWait(ctx) c.backoff.Reset() for { if err := backoff.Retry(func() error { @@ -107,6 +109,6 @@ func (c *courier) watchMessages(ctx context.Context, errChan chan error) { errChan <- err return } - time.Sleep(time.Second) + time.Sleep(wait) } } diff --git a/courier/courier_dispatcher.go b/courier/courier_dispatcher.go index dbf3bfb7e795..8470c024fca4 100644 --- a/courier/courier_dispatcher.go +++ b/courier/courier_dispatcher.go @@ -54,8 +54,9 @@ func (c *courier) DispatchMessage(ctx context.Context, msg Message) error { func (c *courier) DispatchQueue(ctx context.Context) error { maxRetries := c.deps.CourierConfig().CourierMessageRetries(ctx) + pullCount := c.deps.CourierConfig().CourierWorkerPullCount(ctx) - messages, err := c.deps.CourierPersister().NextMessages(ctx, 10) + messages, err := c.deps.CourierPersister().NextMessages(ctx, uint8(pullCount)) if err != nil { if errors.Is(err, ErrQueueEmpty) { return nil @@ -73,36 +74,40 @@ func (c *courier) DispatchQueue(ctx context.Context) error { Error(`Unable to set the retried message's status to "abandoned".`) return err } + // Skip the message c.deps.Logger(). WithField("message_id", msg.ID). WithField("message_nid", msg.NID). Warnf(`Message was abandoned because it did not deliver after %d attempts`, msg.SendCount) - } else if err := c.DispatchMessage(ctx, msg); err != nil { - if err := c.deps.CourierPersister().RecordDispatch(ctx, msg.ID, CourierMessageDispatchStatusFailed, err); err != nil { c.deps.Logger(). WithError(err). WithField("message_id", msg.ID). WithField("message_nid", msg.NID). Error(`Unable to record failure log entry.`) + if c.failOnDispatchError { + return err + } } for _, replace := range messages[k:] { if err := c.deps.CourierPersister().SetMessageStatus(ctx, replace.ID, MessageStatusQueued); err != nil { - if c.failOnError { - return err - } c.deps.Logger(). WithError(err). WithField("message_id", replace.ID). WithField("message_nid", replace.NID). Error(`Unable to reset the failed message's status to "queued".`) + if c.failOnDispatchError { + return err + } } } - return err + if c.failOnDispatchError { + return err + } } else if err := c.deps.CourierPersister().RecordDispatch(ctx, msg.ID, CourierMessageDispatchStatusSuccess, nil); err != nil { c.deps.Logger(). WithError(err). diff --git a/courier/courier_dispatcher_test.go b/courier/courier_dispatcher_test.go index e0ad2b61b371..528badf2de02 100644 --- a/courier/courier_dispatcher_test.go +++ b/courier/courier_dispatcher_test.go @@ -66,6 +66,7 @@ func TestDispatchQueue(t *testing.T) { c, err := reg.Courier(ctx) require.NoError(t, err) + c.FailOnDispatchError() ctx, cancel := context.WithCancel(ctx) defer cancel() diff --git a/courier/handler_test.go b/courier/handler_test.go index b8920a5bc5f9..28a7ec55d8b2 100644 --- a/courier/handler_test.go +++ b/courier/handler_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/tidwall/gjson" diff --git a/courier/smtp.go b/courier/smtp.go index 23bb60700d3d..3de84b00384c 100644 --- a/courier/smtp.go +++ b/courier/smtp.go @@ -215,7 +215,12 @@ func (c *courier) dispatchEmail(ctx context.Context, msg Message) error { Error("Unable to send email using SMTP connection.") var protoErr *textproto.Error - if containsProtoErr := errors.As(err, &protoErr); containsProtoErr && protoErr.Code >= 500 { + var mailErr *gomail.SendError + + switch { + case errors.As(err, &mailErr) && mailErr.Index >= 500: + fallthrough + case errors.As(err, &protoErr) && protoErr.Code >= 500: // See https://en.wikipedia.org/wiki/List_of_SMTP_server_return_codes // If the SMTP server responds with 5xx, sending the message should not be retried (without changing something about the request) if err := c.deps.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusAbandoned); err != nil { @@ -227,6 +232,7 @@ func (c *courier) dispatchEmail(ctx context.Context, msg Message) error { return err } } + return errors.WithStack(herodot.ErrInternalServerError. WithError(err.Error()).WithReason("failed to send email via smtp")) } diff --git a/courier/test/persistence.go b/courier/test/persistence.go index fad80efe1742..dddd8adb2cbf 100644 --- a/courier/test/persistence.go +++ b/courier/test/persistence.go @@ -14,7 +14,7 @@ import ( "github.com/gofrs/uuid" "github.com/tidwall/gjson" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/driver/config/config.go b/driver/config/config.go index 950152f6d088..edec657e10ae 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -77,6 +77,8 @@ const ( ViperKeyCourierSMSEnabled = "courier.sms.enabled" ViperKeyCourierSMSFrom = "courier.sms.from" ViperKeyCourierMessageRetries = "courier.message_retries" + ViperKeyCourierWorkerPullCount = "courier.worker.pull_count" + ViperKeyCourierWorkerPullWait = "courier.worker.pull_wait" ViperKeySecretsDefault = "secrets.default" ViperKeySecretsCookie = "secrets.cookie" ViperKeySecretsCipher = "secrets.cipher" @@ -289,6 +291,8 @@ type ( CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate CourierMessageRetries(ctx context.Context) int + CourierWorkerPullCount(ctx context.Context) int + CourierWorkerPullWait(ctx context.Context) time.Duration } ) @@ -1110,6 +1114,14 @@ func (p *Config) CourierMessageRetries(ctx context.Context) int { return p.GetProvider(ctx).IntF(ViperKeyCourierMessageRetries, 5) } +func (p *Config) CourierWorkerPullCount(ctx context.Context) int { + return p.GetProvider(ctx).Int(ViperKeyCourierWorkerPullCount) +} + +func (p *Config) CourierWorkerPullWait(ctx context.Context) time.Duration { + return p.GetProvider(ctx).Duration(ViperKeyCourierWorkerPullWait) +} + func (p *Config) CourierSMTPHeaders(ctx context.Context) map[string]string { return p.GetProvider(ctx).StringMap(ViperKeyCourierSMTPHeaders) } diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 03898eec012b..0f1cfd03546d 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -280,6 +280,11 @@ "default": false, "description": "Deprecated, please use `response.parse` instead. If enabled allows the web hook to interrupt / abort the self-service flow. It only applies to certain flows (registration/verification/login/settings) and requires a valid response format." }, + "emit_analytics_event": { + "type": "boolean", + "default": true, + "description": "Emit tracing events for this webhook on delivery or error" + }, "auth": { "type": "object", "title": "Auth mechanisms", @@ -1776,6 +1781,23 @@ "default": 5, "examples": [10, 60] }, + "worker": { + "description": "Configures the dispatch worker.", + "type": "object", + "properties": { + "pull_count": { + "description": "Defines how many messages are pulled from the queue at once.", + "type": "integer", + "default": 10 + }, + "pull_wait": { + "description": "Defines how long the worker waits before pulling messages from the queue again.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1s" + } + } + }, "delivery_strategy": { "title": "Delivery Strategy", "description": "Defines how emails will be sent, either through SMTP (default) or HTTP.", diff --git a/go.mod b/go.mod index 1b52289fb58f..262ebd0103e5 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,12 @@ module github.com/ory/kratos -go 1.19 +go 1.21 replace ( - github.com/bradleyjkemp/cupaloy/v2 => github.com/aeneasr/cupaloy/v2 v2.6.1-0.20210924214125-3dfdd01210a3 - github.com/go-sql-driver/mysql => github.com/go-sql-driver/mysql v1.7.2-0.20231005084435-37980127edfb github.com/gorilla/sessions => github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 - github.com/mattn/go-sqlite3 => github.com/mattn/go-sqlite3 v1.14.7-0.20210414154423-1157a4212dcb + github.com/mattn/go-sqlite3 => github.com/mattn/go-sqlite3 v1.14.16 // Use the internal httpclient which can be generated in this codebase but mark it as the // official SDK, allowing for the Ory CLI to consume Ory Kratos' CLI commands. @@ -21,7 +19,7 @@ require ( github.com/avast/retry-go/v3 v3.1.1 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 github.com/bwmarrin/discordgo v0.23.0 - github.com/bxcodec/faker/v3 v3.3.1 + github.com/go-faker/faker/v4 v4.2.0 github.com/cenkalti/backoff v2.2.1+incompatible github.com/coreos/go-oidc v2.2.1+incompatible github.com/cortesi/modd v0.0.0-20210323234521-b35eddab86cc @@ -77,7 +75,7 @@ require ( github.com/ory/jsonschema/v3 v3.0.8 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.595 + github.com/ory/x v0.0.597 github.com/peterhellberg/link v1.2.0 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 @@ -95,19 +93,21 @@ require ( github.com/tidwall/sjson v1.2.5 github.com/urfave/negroni v1.0.0 github.com/zmb3/spotify/v2 v2.0.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.4 - go.opentelemetry.io/otel v1.14.0 - go.opentelemetry.io/otel/trace v1.14.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 + go.opentelemetry.io/otel v1.19.0 + go.opentelemetry.io/otel/trace v1.19.0 golang.org/x/crypto v0.14.0 golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 golang.org/x/net v0.17.0 - golang.org/x/oauth2 v0.11.0 - golang.org/x/sync v0.2.0 + golang.org/x/oauth2 v0.12.0 + golang.org/x/sync v0.3.0 golang.org/x/text v0.13.0 golang.org/x/tools/cmd/cover v0.1.0-deprecated - google.golang.org/grpc v1.55.0 + google.golang.org/grpc v1.59.0 ) +require go.opentelemetry.io/otel/sdk v1.19.0 + require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -125,7 +125,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/boombuler/barcode v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cockroachdb/cockroach-go/v2 v2.2.16 // indirect github.com/containerd/continuity v0.3.0 // indirect @@ -147,7 +147,7 @@ require ( github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-crypt/x v0.2.1 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.4 // indirect @@ -176,14 +176,14 @@ require ( github.com/goccy/go-yaml v1.9.6 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.1.0 // indirect + github.com/golang/glog v1.1.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-querystring v1.0.0 // indirect github.com/google/go-tpm v0.9.0 // indirect github.com/google/pprof v0.0.0-20221010195024-131d412537ea // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.1 // indirect github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/handlers v1.5.1 // indirect @@ -192,7 +192,7 @@ require ( github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.12.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.2.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect @@ -258,7 +258,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc2 // indirect github.com/opencontainers/runc v1.1.5 // indirect - github.com/openzipkin/zipkin-go v0.4.1 // indirect + github.com/openzipkin/zipkin-go v0.4.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/philhofer/fwd v1.1.2 // indirect @@ -270,7 +270,7 @@ require ( github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect github.com/segmentio/backo-go v1.0.1 // indirect github.com/sergi/go-diff v1.2.0 // indirect @@ -297,27 +297,25 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect go.mongodb.org/mongo-driver v1.11.3 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.36.4 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.11.1 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.11.1 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.5.2 // indirect - go.opentelemetry.io/otel/exporters/jaeger v1.11.1 // indirect - go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 // indirect; / indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.14.0 // indirect; / indirect - go.opentelemetry.io/otel/exporters/zipkin v1.14.0 // indirect; / indirect - go.opentelemetry.io/otel/metric v0.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.14.0 // indirect - go.opentelemetry.io/proto/otlp v0.19.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.20.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect; / indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect; / indirect + go.opentelemetry.io/otel/exporters/zipkin v1.19.0 // indirect; / indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/tools v0.9.3 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index 76c7ba81b427..c5d1fc506475 100644 --- a/go.sum +++ b/go.sum @@ -54,13 +54,10 @@ github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2y github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/a8m/envsubst v1.3.0 h1:GmXKmVssap0YtlU3E230W98RWtWCyIZzjtf1apWWyAg= github.com/a8m/envsubst v1.3.0/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= -github.com/aeneasr/cupaloy/v2 v2.6.1-0.20210924214125-3dfdd01210a3 h1:/SkiUr3JJzun9QN9cpUVCPri2ZwOFJ3ani+F3vdoCiY= -github.com/aeneasr/cupaloy/v2 v2.6.1-0.20210924214125-3dfdd01210a3/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -69,7 +66,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -99,16 +95,15 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bwmarrin/discordgo v0.23.0 h1://ARp8qUrRZvDGMkfAjtcC20WOvsMtTgi+KrdKnl6eY= github.com/bwmarrin/discordgo v0.23.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= -github.com/bxcodec/faker/v3 v3.3.1 h1:G7uldFk+iO/ES7W4v7JlI/WU9FQ6op9VJ15YZlDEhGQ= -github.com/bxcodec/faker/v3 v3.3.1/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= -github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -124,11 +119,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go/v2 v2.2.16 h1:t9dmZuC9J2W8IDQDSIGXmP+fBuEJSsrGXxWQz4cYqBY= @@ -186,8 +176,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= @@ -206,6 +194,7 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= @@ -219,6 +208,8 @@ github.com/go-crypt/x v0.2.1 h1:OGw78Bswme9lffCOX6tyuC280ouU5391glsvThMtM5U= github.com/go-crypt/x v0.2.1/go.mod h1:Q/y9rms7yw4/1CavBlNGn0Itg4HqwNpe1N9FX0TxXrc= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-faker/faker/v4 v4.2.0 h1:dGebOupKwssrODV51E0zbMrv5e2gO9VWSLNC1WDCpWg= +github.com/go-faker/faker/v4 v4.2.0/go.mod h1:F/bBy8GH9NxOxMInug5Gx4WYeG6fHJZ8Ol/dhcpRub4= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -233,8 +224,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= @@ -291,6 +282,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-swagger/go-swagger v0.30.5 h1:SQ2+xSonWjjoEMOV5tcOnZJVlfyUfCBhGQGArS1b9+U= github.com/go-swagger/go-swagger v0.30.5/go.mod h1:cWUhSyCNqV7J1wkkxfr5QmbcnCewetCdvEXqgPvbc/Q= github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l9rI6sNaZgNC0LnF3MiE+qTmyBA/tZAg1rtyrGbUMK0= +github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-webauthn/webauthn v0.8.4 h1:/emQ9b9Rj4flWO94Fo8KJeYvZ6VzPywXsmqyDA/WicY= @@ -327,6 +319,7 @@ github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+ github.com/gobuffalo/helpers v0.6.7 h1:C9CedoRSfgWg2ZoIkVXgjI5kgmSpL34Z3qdnzpfNVd8= github.com/gobuffalo/helpers v0.6.7/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA= github.com/gobuffalo/here v0.6.7 h1:hpfhh+kt2y9JLDfhYUxxCRxQol540jsVfKUZzjlbp8o= +github.com/gobuffalo/here v0.6.7/go.mod h1:vuCfanjqckTuRlqAitJz6QC4ABNnS27wLb816UhsPcc= github.com/gobuffalo/httptest v1.5.2 h1:GpGy520SfY1QEmyPvaqmznTpG4gEQqQ82HtHqyNEreM= github.com/gobuffalo/httptest v1.5.2/go.mod h1:FA23yjsWLGj92mVV74Qtc8eqluc11VqcWr8/C1vxt4g= github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= @@ -374,9 +367,8 @@ github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 h1:xisWqjiKEff2B0KfFYGpCqc3M3zdTz+OHQHRc09FeYk= github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -424,6 +416,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v27 v27.0.1 h1:sSMFSShNn4VnqCqs+qhab6TS3uQc+uVR6TD1bW6MavM= github.com/google/go-github/v27 v27.0.1/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0= github.com/google/go-github/v38 v38.1.0 h1:C6h1FkaITcBFK7gAmq4eFzt6gbhEhk7L5z6R3Uva+po= @@ -456,8 +449,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -481,17 +474,17 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.12.0 h1:kr3j8iIMR4ywO/O0rvksXaJvauGGCMg2zAZIiNZ9uIQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.12.0/go.mod h1:ummNFgdgLhhX7aIiy35vVmQNS0rWXknfPE0qe6fmFXg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 h1:7xsUJsB2NrdcttQPa7JLEaGzvdbk7KvfrjgHZXOQRo0= github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69/go.mod h1:YLEMZOtU+AZ7dhN9T/IpGhXVGly2bvkJQ+zxj3WeVQo= github.com/hashicorp/consul/api v1.20.0 h1:9IHTjNVSZ7MIwjlW3N3a7iGiykCMDpxZu8jsxFJh0yc= github.com/hashicorp/consul/api v1.20.0/go.mod h1:nR64eD44KQ59Of/ECwt2vUmIK2DKsDzAwTmwmLl8Wpo= github.com/hashicorp/consul/sdk v0.13.1 h1:EygWVWWMczTzXGpO93awkHFzfUka6hLYJ0qhETd+6lY= +github.com/hashicorp/consul/sdk v0.13.1/go.mod h1:SW/mM4LbKfqmMvcFu8v+eiQQ7oitXEFeiBe9StxERb0= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= @@ -506,6 +499,7 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= @@ -513,11 +507,14 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= @@ -530,6 +527,7 @@ github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -658,6 +656,7 @@ github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPucc github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= +github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g= github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -726,6 +725,7 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= +github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= @@ -747,8 +747,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-sqlite3 v1.14.7-0.20210414154423-1157a4212dcb h1:ax2vG2unlxsjwS7PMRo4FECIfAdQLowd6ejWYwPQhBo= -github.com/mattn/go-sqlite3 v1.14.7-0.20210414154423-1157a4212dcb/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/goveralls v0.0.7 h1:vzy0i4a2iDzEFMdXIxcanRadkr0FBvSBKUmj0P8SPlQ= github.com/mattn/goveralls v0.0.7/go.mod h1:h8b4ow6FxSPMQHF6o2ve3qsclnffZjYTNEKmLesRwqw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -789,11 +789,11 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nyaruka/phonenumbers v1.1.6 h1:DcueYq7QrOArAprAYNoQfDgp0KetO4LqtnBtQC6Wyes= github.com/nyaruka/phonenumbers v1.1.6/go.mod h1:yShPJHDSH3aTKzCbXyVxNpbl2kA+F+Ne5Pun/MvFRos= github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= @@ -801,11 +801,12 @@ github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFP github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= @@ -814,8 +815,8 @@ github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/ github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= -github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A= -github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= +github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= +github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBpXmAM= github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0= github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= @@ -837,8 +838,8 @@ github.com/ory/nosurf v1.2.7 h1:YrHrbSensQyU6r6HT/V5+HPdVEgrOTMJiLoJABSBOp4= github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OUxA= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/ory/x v0.0.595 h1:oh2/wLyyQ6hMaFblj9u0EGzrR5tEOmnp+2as+XkER9g= -github.com/ory/x v0.0.595/go.mod h1:ksLBEd6iW6czGpE6eNA0gCIxO1FFeqIxCZgsgwNrzMM= +github.com/ory/x v0.0.597 h1:msBfbEE5Ps8MXR3VxxIVUvei+f1o7cE/XKoIytuTqVQ= +github.com/ory/x v0.0.597/go.mod h1:ksLBEd6iW6czGpE6eNA0gCIxO1FFeqIxCZgsgwNrzMM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -904,14 +905,14 @@ github.com/rakutentech/jwk-go v1.1.3 h1:PiLwepKyUaW+QFG3ki78DIO2+b4IVK3nMhlxM70z github.com/rakutentech/jwk-go v1.1.3/go.mod h1:LtzSv4/+Iti1nnNeVQiP6l5cI74GBStbhyXCYvgPZFk= github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 h1:FLWDC+iIP9BWgYKvWKKtOUZux35LIQNAuIzp/63RQJU= github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -963,7 +964,6 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9 github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= @@ -1067,42 +1067,40 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.36.4 h1:toN8e0U4RWQL4f8H+1eFtaeWe/IkSM3+81qJEDOgShs= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.36.4/go.mod h1:u4OeI4ujQmFbpZOOysLUfYrRWOmEVmvzkM2zExVorXM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.4 h1:aUEBEdCa6iamGzg6fuYxDA8ThxvOG240mAvWDU+XLio= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.4/go.mod h1:l2MdsbKTocpPS5nQZscqTR9jd8u96VYZdcpF8Sye7mA= -go.opentelemetry.io/contrib/propagators/b3 v1.11.1 h1:icQ6ttRV+r/2fnU46BIo/g/mPu6Rs5Ug8Rtohe3KqzI= -go.opentelemetry.io/contrib/propagators/b3 v1.11.1/go.mod h1:ECIveyMXgnl4gorxFcA7RYjJY/Ql9n20ubhbfDc3QfA= -go.opentelemetry.io/contrib/propagators/jaeger v1.11.1 h1:Gw+P9NQzw4bjNGZXsoDhwwDWLnk4Y1waF8MQZAq/eYM= -go.opentelemetry.io/contrib/propagators/jaeger v1.11.1/go.mod h1:dP/N3ZFADH8azBcZfGXEFNBXpEmPTXYcNj9rkw1+2Oc= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.5.2 h1:Izp9RqrioK/y7J/RXy2c7zd83iKQ4N3td3AMNKNzHiI= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.5.2/go.mod h1:Z0aRlRERn9v/3J2K+ATa6ffKyb8/i+/My/gTzFr3dII= -go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= -go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= -go.opentelemetry.io/otel/exporters/jaeger v1.11.1 h1:F9Io8lqWdGyIbY3/SOGki34LX/l+7OL0gXNxjqwcbuQ= -go.opentelemetry.io/otel/exporters/jaeger v1.11.1/go.mod h1:lRa2w3bQ4R4QN6zYsDgy7tEezgoKEu7Ow2g35Y75+KI= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 h1:/fXHZHGvro6MVqV34fJzDhi7sHGpX3Ej/Qjmfn003ho= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0/go.mod h1:UFG7EBMRdXyFstOwH028U0sVf+AvukSGhF0g8+dmNG8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 h1:TKf2uAs2ueguzLaxOCBXNpHxfO/aC7PAdDsSH0IbeRQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0/go.mod h1:HrbCVv40OOLTABmOn1ZWty6CHXkU8DK/Urc43tHug70= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.14.0 h1:3jAYbRHQAqzLjd9I4tzxwJ8Pk/N6AqBcF6m1ZHrxG94= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.14.0/go.mod h1:+N7zNjIJv4K+DeX67XXET0P+eIciESgaFDBqh+ZJFS4= -go.opentelemetry.io/otel/exporters/zipkin v1.14.0 h1:reEVE1upBF9tcujgvSqLJS0SrI7JQPaTKP4s4rymnSs= -go.opentelemetry.io/otel/exporters/zipkin v1.14.0/go.mod h1:RcjvOAcvhzcufQP8aHmzRw1gE9g/VEZufDdo2w+s4sk= -go.opentelemetry.io/otel/metric v0.33.0 h1:xQAyl7uGEYvrLAiV/09iTJlp1pZnQ9Wl793qbVvED1E= -go.opentelemetry.io/otel/metric v0.33.0/go.mod h1:QlTYc+EnYNq/M2mNk1qDDMRLpqCOj2f/r5c7Fd5FYaI= -go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY= -go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= -go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= -go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0 h1:2ea0IkZBsWH+HA2GkD+7+hRw2u97jzdFyRtXuO14a1s= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0/go.mod h1:4m3RnBBb+7dB9d21y510oO1pdB1V4J6smNf14WXcBFQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +go.opentelemetry.io/contrib/propagators/b3 v1.20.0 h1:Yty9Vs4F3D6/liF1o6FNt0PvN85h/BJJ6DQKJ3nrcM0= +go.opentelemetry.io/contrib/propagators/b3 v1.20.0/go.mod h1:On4VgbkqYL18kbJlWsa18+cMNe6rYpBnPi1ARI/BrsU= +go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 h1:iVhNKkMIpzyZqxk8jkDU2n4DFTD+FbpGacvooxEvyyc= +go.opentelemetry.io/contrib/propagators/jaeger v1.20.0/go.mod h1:cpSABr0cm/AH/HhbJjn+AudBVUMgZWdfN3Gb+ZqxSZc= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0 h1:Xg9iU9DF9V9zC6NI8sJthYqHlSWsWAQMTXM8QIErKlc= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0/go.mod h1:ExRuq62/gYluX5fzTTZif5WujyG51ail4APTbBUu+S4= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/exporters/zipkin v1.19.0 h1:EGY0h5mGliP9o/nIkVuLI0vRiQqmsYOcbwCuotksO1o= +go.opentelemetry.io/otel/exporters/zipkin v1.19.0/go.mod h1:JQgTGJP11yi3o4GHzIWYodhPisxANdqxF1eHwDSnJrI= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -1245,10 +1243,9 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= -golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1264,8 +1261,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1389,6 +1386,7 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -1517,7 +1515,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -1531,13 +1528,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1551,16 +1547,13 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99 h1:qA8rMbz1wQ4DOFfM2ouD29DG9aHWBm6ZOy9BGxiUMmY= +google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1587,6 +1580,7 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/mold.v2 v2.2.0/go.mod h1:XMyyRsGtakkDPbxXbrA5VODo6bUXyvoDjLd5l3T0XoA= @@ -1605,7 +1599,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1625,6 +1618,7 @@ gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= +gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/hydra/fake.go b/hydra/fake.go index 9050833bb64b..ef27af19932e 100644 --- a/hydra/fake.go +++ b/hydra/fake.go @@ -19,12 +19,17 @@ const ( var ErrFakeAcceptLoginRequestFailed = errors.New("failed to accept login request") -type FakeHydra struct{} +type FakeHydra struct { + Skip bool + RequestURL string +} var _ Hydra = &FakeHydra{} func NewFake() *FakeHydra { - return &FakeHydra{} + return &FakeHydra{ + RequestURL: "https://www.ory.sh", + } } func (h *FakeHydra) AcceptLoginRequest(_ context.Context, params AcceptLoginRequestParams) (string, error) { @@ -47,7 +52,8 @@ func (h *FakeHydra) GetLoginRequest(_ context.Context, loginChallenge string) (* return nil, herodot.ErrBadRequest.WithReasonf("Unable to get OAuth 2.0 Login Challenge.") case FakeValidLoginChallenge: return &hydraclientgo.OAuth2LoginRequest{ - RequestUrl: "https://www.ory.sh", + RequestUrl: h.RequestURL, + Skip: h.Skip, }, nil default: panic("unknown fake login_challenge " + loginChallenge) diff --git a/identity/handler_test.go b/identity/handler_test.go index 43d432b55eb4..2e38de8db126 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -18,7 +18,7 @@ import ( "testing" "time" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/peterhellberg/link" "github.com/stretchr/testify/assert" diff --git a/identity/manager.go b/identity/manager.go index 11220ec15f12..9bd02ce7451c 100644 --- a/identity/manager.go +++ b/identity/manager.go @@ -110,12 +110,7 @@ func (m *Manager) Create(ctx context.Context, i *Identity, opts ...ManagerOption return nil } -func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identity) (err error) { - if !m.r.Config().SelfServiceFlowRegistrationLoginHints(ctx) { - return &ErrDuplicateCredentials{error: e} - } - // First we try to find the conflict in the identifiers table. This is most likely to have a conflict. - var found *Identity +func (m *Manager) ConflictingIdentity(ctx context.Context, i *Identity) (found *Identity, foundConflictAddress string, err error) { for ct, cred := range i.Credentials { for _, id := range cred.Identifiers { found, _, err = m.r.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, ct, id) @@ -125,53 +120,65 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi // FindByCredentialsIdentifier does not expand identity credentials. if err = m.r.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, found, ExpandCredentials); err != nil { - return err + return nil, "", err } + + return found, id, nil } } // If the conflict is not in the identifiers table, it is coming from the verifiable or recovery address. - var foundConflictAddress string - if found == nil { - for _, va := range i.VerifiableAddresses { - conflictingAddress, err := m.r.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, va.Via, va.Value) - if errors.Is(err, sqlcon.ErrNoRows) { - continue - } else if err != nil { - return err - } + for _, va := range i.VerifiableAddresses { + conflictingAddress, err := m.r.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, va.Via, va.Value) + if errors.Is(err, sqlcon.ErrNoRows) { + continue + } else if err != nil { + return nil, "", err + } - foundConflictAddress = conflictingAddress.Value - found, err = m.r.PrivilegedIdentityPool().GetIdentity(ctx, conflictingAddress.IdentityID, ExpandCredentials) - if err != nil { - return err - } + foundConflictAddress = conflictingAddress.Value + found, err = m.r.PrivilegedIdentityPool().GetIdentity(ctx, conflictingAddress.IdentityID, ExpandCredentials) + if err != nil { + return nil, "", err } + + return found, foundConflictAddress, nil } // Last option: check the recovery address - if found == nil { - for _, va := range i.RecoveryAddresses { - conflictingAddress, err := m.r.PrivilegedIdentityPool().FindRecoveryAddressByValue(ctx, va.Via, va.Value) - if errors.Is(err, sqlcon.ErrNoRows) { - continue - } else if err != nil { - return err - } + for _, va := range i.RecoveryAddresses { + conflictingAddress, err := m.r.PrivilegedIdentityPool().FindRecoveryAddressByValue(ctx, va.Via, va.Value) + if errors.Is(err, sqlcon.ErrNoRows) { + continue + } else if err != nil { + return nil, "", err + } - foundConflictAddress = conflictingAddress.Value - found, err = m.r.PrivilegedIdentityPool().GetIdentity(ctx, conflictingAddress.IdentityID, ExpandCredentials) - if err != nil { - return err - } + foundConflictAddress = conflictingAddress.Value + found, err = m.r.PrivilegedIdentityPool().GetIdentity(ctx, conflictingAddress.IdentityID, ExpandCredentials) + if err != nil { + return nil, "", err } + + return found, foundConflictAddress, nil } - // Still not found? Return generic error. - if found == nil { + return nil, "", sqlcon.ErrNoRows +} + +func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identity) (err error) { + if !m.r.Config().SelfServiceFlowRegistrationLoginHints(ctx) { return &ErrDuplicateCredentials{error: e} } + found, foundConflictAddress, err := m.ConflictingIdentity(ctx, i) + if err != nil { + if errors.Is(err, sqlcon.ErrNoRows) { + return &ErrDuplicateCredentials{error: e} + } + return err + } + // We need to sort the credentials for the error message to be deterministic. var creds []Credentials for _, cred := range found.Credentials { diff --git a/identity/manager_test.go b/identity/manager_test.go index 800d84590470..81001c9ba9c6 100644 --- a/identity/manager_test.go +++ b/identity/manager_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/ory/x/pointerx" + "github.com/ory/x/sqlcon" "github.com/gofrs/uuid" @@ -556,6 +557,68 @@ func TestManager(t *testing.T) { checkExtensionFields(fromStore, "email-updatetraits-1@ory.sh")(t) }) }) + + t.Run("method=ConflictingIdentity", func(t *testing.T) { + ctx := context.Background() + + conflicOnIdentifier := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + conflicOnIdentifier.Traits = identity.Traits(`{"email":"conflict-on-identifier@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, conflicOnIdentifier)) + + conflicOnVerifiableAddress := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + conflicOnVerifiableAddress.Traits = identity.Traits(`{"email":"user-va@example.com", "email_verify":"conflict-on-va@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, conflicOnVerifiableAddress)) + + conflicOnRecoveryAddress := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + conflicOnRecoveryAddress.Traits = identity.Traits(`{"email":"user-ra@example.com", "email_recovery":"conflict-on-ra@example.com"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, conflicOnRecoveryAddress)) + + t.Run("case=returns not found if no conflict", func(t *testing.T) { + found, foundConflictAddress, err := reg.IdentityManager().ConflictingIdentity(ctx, &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Identifiers: []string{"no-conflict@example.com"}}, + }, + }) + assert.ErrorIs(t, err, sqlcon.ErrNoRows) + assert.Nil(t, found) + assert.Empty(t, foundConflictAddress) + }) + + t.Run("case=conflict on identifier", func(t *testing.T) { + found, foundConflictAddress, err := reg.IdentityManager().ConflictingIdentity(ctx, &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Identifiers: []string{"conflict-on-identifier@example.com"}}, + }, + }) + require.NoError(t, err) + assert.Equal(t, conflicOnIdentifier.ID, found.ID) + assert.Equal(t, "conflict-on-identifier@example.com", foundConflictAddress) + }) + + t.Run("case=conflict on verifiable address", func(t *testing.T) { + found, foundConflictAddress, err := reg.IdentityManager().ConflictingIdentity(ctx, &identity.Identity{ + VerifiableAddresses: []identity.VerifiableAddress{{ + Value: "conflict-on-va@example.com", + Via: "email", + }}, + }) + require.NoError(t, err) + assert.Equal(t, conflicOnVerifiableAddress.ID, found.ID) + assert.Equal(t, "conflict-on-va@example.com", foundConflictAddress) + }) + + t.Run("case=conflict on recovery address", func(t *testing.T) { + found, foundConflictAddress, err := reg.IdentityManager().ConflictingIdentity(ctx, &identity.Identity{ + RecoveryAddresses: []identity.RecoveryAddress{{ + Value: "conflict-on-ra@example.com", + Via: "email", + }}, + }) + require.NoError(t, err) + assert.Equal(t, conflicOnRecoveryAddress.ID, found.ID) + assert.Equal(t, "conflict-on-ra@example.com", foundConflictAddress) + }) + }) } func TestManagerNoDefaultNamedSchema(t *testing.T) { diff --git a/identity/test/pool.go b/identity/test/pool.go index f20a3f36af6d..5b4008231f70 100644 --- a/identity/test/pool.go +++ b/identity/test/pool.go @@ -15,7 +15,7 @@ import ( "github.com/ory/x/crdbx" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/internal/testhelpers/handler_mock.go b/internal/testhelpers/handler_mock.go index 11031abbcca3..bcc68a1e61c1 100644 --- a/internal/testhelpers/handler_mock.go +++ b/internal/testhelpers/handler_mock.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/julienschmidt/httprouter" "github.com/pkg/errors" diff --git a/internal/testhelpers/http.go b/internal/testhelpers/http.go index 444a3360cfed..5523de9d5368 100644 --- a/internal/testhelpers/http.go +++ b/internal/testhelpers/http.go @@ -117,3 +117,41 @@ func HTTPPostForm(t *testing.T, client *http.Client, remote string, in *url.Valu return payload, res } + +func NewTestHTTPRequest(t *testing.T, method, url string, body io.Reader) *http.Request { + req, err := http.NewRequest(method, url, body) + require.NoError(t, err) + return req +} + +func EasyGet(t *testing.T, c *http.Client, url string) (*http.Response, []byte) { + res, err := c.Get(url) + require.NoError(t, err) + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body +} + +func EasyGetJSON(t *testing.T, c *http.Client, url string) (*http.Response, []byte) { + req, err := http.NewRequest("GET", url, nil) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + res, err := c.Do(req) + require.NoError(t, err) + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return res, body +} + +func EasyGetBody(t *testing.T, c *http.Client, url string) []byte { + _, body := EasyGet(t, c, url) // nolint: bodyclose + return body +} + +func EasyCookieJar(t *testing.T, o *cookiejar.Options) *cookiejar.Jar { + cj, err := cookiejar.New(o) + require.NoError(t, err) + return cj +} diff --git a/internal/testhelpers/identity.go b/internal/testhelpers/identity.go index 7029e376d58c..5c7cdd5be692 100644 --- a/internal/testhelpers/identity.go +++ b/internal/testhelpers/identity.go @@ -7,8 +7,6 @@ import ( "testing" "time" - "github.com/ory/kratos/x" - "github.com/stretchr/testify/require" "github.com/ory/kratos/driver" @@ -18,7 +16,7 @@ import ( ) func CreateSession(t *testing.T, reg driver.Registry) *session.Session { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(req.Context(), i)) sess, err := session.NewActiveSession(req, i, reg.Config(), time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) diff --git a/internal/testhelpers/selfservice.go b/internal/testhelpers/selfservice.go index a827aad0a6de..8c7b4c588d78 100644 --- a/internal/testhelpers/selfservice.go +++ b/internal/testhelpers/selfservice.go @@ -11,7 +11,7 @@ import ( "net/url" "testing" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/internal/testhelpers/selfservice_login.go b/internal/testhelpers/selfservice_login.go index f9766d7bb905..bedba03fee16 100644 --- a/internal/testhelpers/selfservice_login.go +++ b/internal/testhelpers/selfservice_login.go @@ -193,6 +193,13 @@ func LoginMakeRequest( return string(ioutilx.MustReadAll(res.Body)), res } +func GetLoginFlow(t *testing.T, client *http.Client, ts *httptest.Server, flowID string) *kratos.LoginFlow { + publicClient := NewSDKCustomClient(ts, client) + rs, _, err := publicClient.FrontendApi.GetLoginFlow(context.Background()).Id(flowID).Execute() + require.NoError(t, err) + return rs +} + // SubmitLoginForm initiates a login flow (for Browser and API!), fills out the form and modifies // the form values with `withValues`, and submits the form. Returns the body and checks for expectedStatusCode and // expectedURL on completion diff --git a/internal/testhelpers/selfservice_registration.go b/internal/testhelpers/selfservice_registration.go index e285f8f8b2bd..0cab8517e541 100644 --- a/internal/testhelpers/selfservice_registration.go +++ b/internal/testhelpers/selfservice_registration.go @@ -83,6 +83,13 @@ func InitializeRegistrationFlowViaAPI(t *testing.T, client *http.Client, ts *htt return rs } +func GetRegistrationFlow(t *testing.T, client *http.Client, ts *httptest.Server, flowID string) *kratos.RegistrationFlow { + rs, _, err := NewSDKCustomClient(ts, client).FrontendApi.GetRegistrationFlow(context.Background()).Id(flowID).Execute() + require.NoError(t, err) + assert.Empty(t, rs.Active) + return rs +} + func RegistrationMakeRequest( t *testing.T, isAPI bool, diff --git a/internal/testhelpers/selfservice_verification.go b/internal/testhelpers/selfservice_verification.go index c1b84f3d5430..757f21adb30a 100644 --- a/internal/testhelpers/selfservice_verification.go +++ b/internal/testhelpers/selfservice_verification.go @@ -150,7 +150,7 @@ func SubmitRecoveryForm( func PersistNewRecoveryFlow(t *testing.T, strategy recovery.Strategy, conf *config.Config, reg *driver.RegistryDefault) *recovery.Flow { t.Helper() - req := x.NewTestHTTPRequest(t, "GET", conf.SelfPublicURL(context.Background()).String()+"/test", nil) + req := NewTestHTTPRequest(t, "GET", conf.SelfPublicURL(context.Background()).String()+"/test", nil) f, err := recovery.NewFlow(conf, conf.SelfServiceFlowRecoveryRequestLifespan(context.Background()), reg.GenerateCSRFToken(req), req, strategy, flow.TypeBrowser) require.NoError(t, err, "Expected no error when creating a new recovery flow: %s", err) diff --git a/internal/testhelpers/session.go b/internal/testhelpers/session.go index b96c5ef47f00..91a2da5abda2 100644 --- a/internal/testhelpers/session.go +++ b/internal/testhelpers/session.go @@ -134,14 +134,14 @@ func NewHTTPClientWithSessionToken(t *testing.T, reg *driver.RegistryDefault, se maybePersistSession(t, reg, sess) return &http.Client{ - Transport: x.NewTransportWithHeader(http.Header{ + Transport: NewTransportWithHeader(t, http.Header{ "Authorization": {"Bearer " + sess.Token}, }), } } func NewHTTPClientWithArbitrarySessionToken(t *testing.T, reg *driver.RegistryDefault) *http.Client { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) s, err := session.NewActiveSession(req, &identity.Identity{ID: x.NewUUID(), State: identity.StateActive}, NewSessionLifespanProvider(time.Hour), @@ -155,7 +155,7 @@ func NewHTTPClientWithArbitrarySessionToken(t *testing.T, reg *driver.RegistryDe } func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryDefault) *http.Client { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) s, err := session.NewActiveSession(req, &identity.Identity{ID: x.NewUUID(), State: identity.StateActive, Traits: []byte("{}")}, NewSessionLifespanProvider(time.Hour), @@ -169,7 +169,7 @@ func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryD } func NewNoRedirectHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryDefault) *http.Client { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) s, err := session.NewActiveSession(req, &identity.Identity{ID: x.NewUUID(), State: identity.StateActive}, NewSessionLifespanProvider(time.Hour), @@ -183,7 +183,7 @@ func NewNoRedirectHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver } func NewHTTPClientWithIdentitySessionCookie(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) s, err := session.NewActiveSession(req, id, NewSessionLifespanProvider(time.Hour), @@ -197,7 +197,7 @@ func NewHTTPClientWithIdentitySessionCookie(t *testing.T, reg *driver.RegistryDe } func NewHTTPClientWithIdentitySessionToken(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) s, err := session.NewActiveSession(req, id, NewSessionLifespanProvider(time.Hour), @@ -222,10 +222,32 @@ func EnsureAAL(t *testing.T, c *http.Client, ts *httptest.Server, aal string, me assert.Len(t, gjson.GetBytes(sess, "authentication_methods").Array(), 1+len(methods)) } -func NewAuthorizedTransport(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *x.TransportWithHeader { +func NewAuthorizedTransport(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *TransportWithHeader { maybePersistSession(t, reg, sess) - return x.NewTransportWithHeader(http.Header{ + return NewTransportWithHeader(t, http.Header{ "Authorization": {"Bearer " + sess.Token}, }) } + +func NewTransportWithHeader(t *testing.T, h http.Header) *TransportWithHeader { + if t == nil { + panic("This function is for testing use only.") + } + return &TransportWithHeader{ + RoundTripper: http.DefaultTransport, + h: h, + } +} + +type TransportWithHeader struct { + http.RoundTripper + h http.Header +} + +func (ct *TransportWithHeader) RoundTrip(req *http.Request) (*http.Response, error) { + for k := range ct.h { + req.Header.Set(k, ct.h.Get(k)) + } + return ct.RoundTripper.RoundTrip(req) +} diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index afa101987742..93513badc4c7 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -87,7 +87,7 @@ func TestMigrations_Postgres(t *testing.T) { t.Skip("skipping testing in short mode") } t.Parallel() - testDatabase(t, "postgres", dockertest.ConnectToTestPostgreSQLPop(t)) + testDatabase(t, "postgres", dockertest.ConnectPop(t, dockertest.RunTestPostgreSQLWithVersion(t, "11.8"))) } func TestMigrations_Mysql(t *testing.T) { @@ -95,7 +95,7 @@ func TestMigrations_Mysql(t *testing.T) { t.Skip("skipping testing in short mode") } t.Parallel() - testDatabase(t, "mysql", dockertest.ConnectToTestMySQLPop(t)) + testDatabase(t, "mysql", dockertest.ConnectPop(t, dockertest.RunTestMySQLWithVersion(t, "8.0.34"))) } func TestMigrations_Cockroach(t *testing.T) { @@ -103,7 +103,7 @@ func TestMigrations_Cockroach(t *testing.T) { t.Skip("skipping testing in short mode") } t.Parallel() - testDatabase(t, "cockroach", dockertest.ConnectToTestCockroachDBPop(t)) + testDatabase(t, "cockroach", dockertest.ConnectPop(t, dockertest.RunTestCockroachDBWithVersion(t, "latest-v23.1"))) } func testDatabase(t *testing.T, db string, c *pop.Connection) { diff --git a/persistence/sql/persister_registration_code.go b/persistence/sql/persister_registration_code.go index 5c9ac909838c..29d1af549467 100644 --- a/persistence/sql/persister_registration_code.go +++ b/persistence/sql/persister_registration_code.go @@ -7,7 +7,7 @@ import ( "context" "time" - "github.com/bxcodec/faker/v3/support/slice" + "github.com/go-faker/faker/v4/pkg/slice" "github.com/gofrs/uuid" "github.com/pkg/errors" diff --git a/schema/errors.go b/schema/errors.go index 55957a10460c..d72e02af7d11 100644 --- a/schema/errors.go +++ b/schema/errors.go @@ -349,3 +349,13 @@ func NewLoginCodeInvalid() error { Messages: new(text.Messages).Add(text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed()), }) } + +func NewLinkedCredentialsDoNotMatch() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: `linked credentials do not match; please start a new flow`, + InstancePtr: "#/", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationLoginLinkedCredentialsDoNotMatch()), + }) +} diff --git a/script/add-down-migrations.sh b/script/add-down-migrations.sh index 57bf58254f66..d2339af7879b 100755 --- a/script/add-down-migrations.sh +++ b/script/add-down-migrations.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # This script adds empty down migrations for any migration that misses them. # Adding them is necessary because if the down migration is missing, the @@ -17,4 +17,4 @@ for f in $(find . -name "*.up.sql"); do echo "Adding empty down migration for $f" touch $dir/$migra_name.down.sql fi -done \ No newline at end of file +done diff --git a/script/debug-entrypoint.sh b/script/debug-entrypoint.sh index 28b0e15c1eab..7fd87d1a730f 100755 --- a/script/debug-entrypoint.sh +++ b/script/debug-entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash FILE_CHANGE_LOG_FILE=/tmp/changes.log SERVICE_ARGS="$@" diff --git a/script/test-envs.sh b/script/test-envs.sh index 426034b99a62..28ba4e3d584e 100755 --- a/script/test-envs.sh +++ b/script/test-envs.sh @@ -1,4 +1,4 @@ -#! /bin/bash +#!/usr/bin/env bash export TEST_DATABASE_MYSQL="mysql://root:secret@(127.0.0.1:3444)/mysql?parseTime=true&multiStatements=true" export TEST_DATABASE_POSTGRESQL="postgres://postgres:secret@127.0.0.1:3445/postgres?sslmode=disable" diff --git a/script/testenv.sh b/script/testenv.sh index 671c5abc1b43..15977c10fc8b 100755 --- a/script/testenv.sh +++ b/script/testenv.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash docker rm -f kratos_test_database_mysql kratos_test_database_postgres kratos_test_database_cockroach kratos_test_hydra || true docker run --platform linux/amd64 --name kratos_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0.34 diff --git a/selfservice/flow/continue_with.go b/selfservice/flow/continue_with.go index 5d6eb6ff1619..a54af5abef5a 100644 --- a/selfservice/flow/continue_with.go +++ b/selfservice/flow/continue_with.go @@ -17,9 +17,8 @@ type ContinueWith any // swagger:enum ContinueWithActionSetOrySessionToken type ContinueWithActionSetOrySessionToken string -// #nosec G101 -- only a key constant const ( - ContinueWithActionSetOrySessionTokenString ContinueWithActionSetOrySessionToken = "set_ory_session_token" + ContinueWithActionSetOrySessionTokenString ContinueWithActionSetOrySessionToken = "set_ory_session_token" // #nosec G101 -- only a key constant ) var _ ContinueWith = new(ContinueWithSetOrySessionToken) diff --git a/selfservice/flow/duplicate_credentials.go b/selfservice/flow/duplicate_credentials.go new file mode 100644 index 000000000000..ddba57251ed2 --- /dev/null +++ b/selfservice/flow/duplicate_credentials.go @@ -0,0 +1,61 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flow + +import ( + "encoding/json" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/identity" + "github.com/ory/x/sqlxx" +) + +const internalContextDuplicateCredentialsPath = "registration_duplicate_credentials" + +type DuplicateCredentialsData struct { + CredentialsType identity.CredentialsType + CredentialsConfig sqlxx.JSONRawMessage + DuplicateIdentifier string +} + +type InternalContexter interface { + EnsureInternalContext() + GetInternalContext() sqlxx.JSONRawMessage + SetInternalContext(sqlxx.JSONRawMessage) +} + +// SetDuplicateCredentials sets the duplicate credentials data in the flow's internal context. +func SetDuplicateCredentials(flow InternalContexter, creds DuplicateCredentialsData) error { + if flow.GetInternalContext() == nil { + flow.EnsureInternalContext() + } + bytes, err := sjson.SetBytes( + flow.GetInternalContext(), + internalContextDuplicateCredentialsPath, + creds, + ) + if err != nil { + return err + } + flow.SetInternalContext(bytes) + + return nil +} + +// DuplicateCredentials returns the duplicate credentials data from the flow's internal context. +func DuplicateCredentials(flow InternalContexter) (*DuplicateCredentialsData, error) { + if flow.GetInternalContext() == nil { + flow.EnsureInternalContext() + } + raw := gjson.GetBytes(flow.GetInternalContext(), internalContextDuplicateCredentialsPath) + if !raw.IsObject() { + return nil, nil + } + var creds DuplicateCredentialsData + err := json.Unmarshal([]byte(raw.Raw), &creds) + + return &creds, err +} diff --git a/selfservice/flow/flow.go b/selfservice/flow/flow.go index be486e3723ae..ee9fbba6638d 100644 --- a/selfservice/flow/flow.go +++ b/selfservice/flow/flow.go @@ -8,15 +8,13 @@ import ( "net/http" "net/url" + "github.com/gofrs/uuid" "github.com/pkg/errors" "github.com/ory/herodot" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/x" - - "github.com/gofrs/uuid" - "github.com/ory/x/urlx" ) diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index 1bd95c7b1fad..5dd8422cdb84 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -231,6 +231,14 @@ func (f *Flow) EnsureInternalContext() { } } +func (f *Flow) GetInternalContext() sqlxx.JSONRawMessage { + return f.InternalContext +} + +func (f *Flow) SetInternalContext(bytes sqlxx.JSONRawMessage) { + f.InternalContext = bytes +} + func (f Flow) MarshalJSON() ([]byte, error) { type local Flow f.SetReturnTo() diff --git a/selfservice/flow/login/flow_test.go b/selfservice/flow/login/flow_test.go index 1c7f1e200a53..685d2604c43d 100644 --- a/selfservice/flow/login/flow_test.go +++ b/selfservice/flow/login/flow_test.go @@ -17,13 +17,14 @@ import ( "github.com/tidwall/gjson" "github.com/ory/x/jsonx" + "github.com/ory/x/sqlxx" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -35,6 +36,7 @@ import ( ) func TestFakeFlow(t *testing.T) { + t.Parallel() var r login.Flow require.NoError(t, faker.FakeData(&r)) @@ -47,6 +49,7 @@ func TestFakeFlow(t *testing.T) { } func TestNewFlow(t *testing.T) { + t.Parallel() ctx := context.Background() conf, _ := internal.NewFastRegistryWithMocks(t) @@ -130,6 +133,7 @@ func TestNewFlow(t *testing.T) { } func TestFlow(t *testing.T) { + t.Parallel() r := &login.Flow{ID: x.NewUUID()} assert.Equal(t, r.ID, r.GetID()) @@ -154,6 +158,7 @@ func TestFlow(t *testing.T) { } func TestGetType(t *testing.T) { + t.Parallel() for _, ft := range []flow.Type{ flow.TypeAPI, flow.TypeBrowser, @@ -166,18 +171,21 @@ func TestGetType(t *testing.T) { } func TestGetRequestURL(t *testing.T) { + t.Parallel() expectedURL := "http://foo/bar/baz" f := &login.Flow{RequestURL: expectedURL} assert.Equal(t, expectedURL, f.GetRequestURL()) } func TestFlowEncodeJSON(t *testing.T) { + t.Parallel() assert.EqualValues(t, "", gjson.Get(jsonx.TestMarshalJSONString(t, &login.Flow{RequestURL: "https://foo.bar?foo=bar"}), "return_to").String()) assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, &login.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) assert.EqualValues(t, "/bar", gjson.Get(jsonx.TestMarshalJSONString(t, login.Flow{RequestURL: "https://foo.bar?return_to=/bar"}), "return_to").String()) } func TestFlowDontOverrideReturnTo(t *testing.T) { + t.Parallel() f := &login.Flow{ReturnTo: "/foo"} f.SetReturnTo() assert.Equal(t, "/foo", f.ReturnTo) @@ -186,3 +194,29 @@ func TestFlowDontOverrideReturnTo(t *testing.T) { f.SetReturnTo() assert.Equal(t, "/bar", f.ReturnTo) } + +func TestDuplicateCredentials(t *testing.T) { + t.Parallel() + t.Run("case=returns previous data", func(t *testing.T) { + t.Parallel() + f := new(login.Flow) + dc := flow.DuplicateCredentialsData{ + CredentialsType: "foo", + CredentialsConfig: sqlxx.JSONRawMessage(`{"bar":"baz"}`), + DuplicateIdentifier: "bar", + } + + require.NoError(t, flow.SetDuplicateCredentials(f, dc)) + actual, err := flow.DuplicateCredentials(f) + require.NoError(t, err) + assert.Equal(t, dc, *actual) + }) + + t.Run("case=returns nil data", func(t *testing.T) { + t.Parallel() + f := new(login.Flow) + actual, err := flow.DuplicateCredentials(f) + require.NoError(t, err) + assert.Nil(t, actual) + }) +} diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index 9efb5f0844db..096a657c0869 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -100,6 +100,12 @@ func WithFlowReturnTo(returnTo string) FlowOption { } } +func WithInternalContext(internalContext []byte) FlowOption { + return func(f *Flow) { + f.InternalContext = internalContext + } +} + func WithFormErrorMessage(messages []text.Message) FlowOption { return func(f *Flow) { for i := range messages { @@ -793,7 +799,7 @@ continueLogin: } method := ss.CompletedAuthenticationMethod(r.Context()) - sess.CompletedLoginFor(method.Method, method.AAL) + sess.CompletedLoginForMethod(method) i = interim break } diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go index ada0814aaf1d..85daee4e65a9 100644 --- a/selfservice/flow/login/handler_test.go +++ b/selfservice/flow/login/handler_test.go @@ -85,7 +85,7 @@ func TestFlowLifecycle(t *testing.T) { if isAPI { route = login.RouteInitAPIFlow } - req := x.NewTestHTTPRequest(t, "GET", ts.URL+route, nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+route, nil) req.URL.RawQuery = extQuery.Encode() body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, req) if isAPI { @@ -100,7 +100,7 @@ func TestFlowLifecycle(t *testing.T) { route = login.RouteInitAPIFlow } client := ts.Client() - req := x.NewTestHTTPRequest(t, "GET", ts.URL+route, nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+route, nil) req.URL.RawQuery = extQuery.Encode() res, err := client.Do(req) @@ -147,7 +147,8 @@ func TestFlowLifecycle(t *testing.T) { Type: identity.CredentialsTypePassword, Identifiers: []string{id1mail}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), // foobar - }}, + }, + }, State: identity.StateActive, Traits: identity.Traits(`{"username":"` + id1mail + `"}`), } @@ -157,7 +158,8 @@ func TestFlowLifecycle(t *testing.T) { Type: identity.CredentialsTypePassword, Identifiers: []string{id2mail}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), // foobar - }}, + }, + }, State: identity.StateActive, Traits: identity.Traits(`{"username":"` + id2mail + `"}`), } @@ -168,8 +170,10 @@ func TestFlowLifecycle(t *testing.T) { t.Run("lifecycle=submit", func(t *testing.T) { t.Run("interaction=unauthenticated", func(t *testing.T) { run := func(t *testing.T, tt flow.Type, aal string, values url.Values) (string, *http.Response) { - f := login.Flow{Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), - UI: container.New(""), Refresh: false, RequestedAAL: identity.AuthenticatorAssuranceLevel(aal)} + f := login.Flow{ + Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), + UI: container.New(""), Refresh: false, RequestedAAL: identity.AuthenticatorAssuranceLevel(aal), + } require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f)) res, err := http.PostForm(ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), values) @@ -339,8 +343,10 @@ func TestFlowLifecycle(t *testing.T) { t.Run("case=ensure aal is checked for upgradeability on session", func(t *testing.T) { run := func(t *testing.T, tt flow.Type, values url.Values) (string, *http.Response) { - f := login.Flow{Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), - UI: container.New(""), Refresh: false, RequestedAAL: "aal1"} + f := login.Flow{ + Type: tt, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), + UI: container.New(""), Refresh: false, RequestedAAL: "aal1", + } require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f)) req, err := http.NewRequest("GET", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(values.Encode())) @@ -371,8 +377,10 @@ func TestFlowLifecycle(t *testing.T) { expired := time.Now().Add(-time.Minute) run := func(t *testing.T, tt flow.Type, aal string, values string, isSPA bool) (string, *http.Response) { - f := login.Flow{Type: tt, ExpiresAt: expired, IssuedAt: time.Now(), - UI: container.New(""), Refresh: false, RequestedAAL: identity.AuthenticatorAssuranceLevel(aal)} + f := login.Flow{ + Type: tt, ExpiresAt: expired, IssuedAt: time.Now(), + UI: container.New(""), Refresh: false, RequestedAAL: identity.AuthenticatorAssuranceLevel(aal), + } require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f)) req, err := http.NewRequest("POST", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(values)) @@ -428,7 +436,7 @@ func TestFlowLifecycle(t *testing.T) { key, err := totp.NewKey(context.Background(), "foo", reg) require.NoError(t, err) email := testhelpers.RandomEmail() - var id = &identity.Identity{ + id := &identity.Identity{ Credentials: map[identity.CredentialsType]identity.Credentials{ "password": { Type: "password", @@ -737,7 +745,7 @@ func TestFlowLifecycle(t *testing.T) { require.Contains(t, res.Request.URL.String(), loginTS.URL) c := ts.Client() - req := x.NewTestHTTPRequest(t, "GET", ts.URL+login.RouteGetFlow, nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+login.RouteGetFlow, nil) req.URL.RawQuery = url.Values{"id": {res.Request.URL.Query().Get("flow")}}.Encode() res, err := c.Do(req) @@ -792,7 +800,7 @@ func TestGetFlow(t *testing.T) { setupLoginUI := func(t *testing.T, c *http.Client) *httptest.Server { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // It is important that we use a HTTP request to fetch the flow because that will show us if CSRF works or not - _, err := w.Write(x.EasyGetBody(t, c, public.URL+login.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + _, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+login.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) require.NoError(t, err) })) conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, ts.URL) @@ -803,12 +811,13 @@ func TestGetFlow(t *testing.T) { _ = testhelpers.NewLoginUIFlowEchoServer(t, reg) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{ - "enabled": true}) + "enabled": true, + }) t.Run("case=fetching successful", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) setupLoginUI(t, client) - body := x.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow) assert.NotEmpty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", body) assert.NotEmpty(t, gjson.GetBytes(body, "id").String(), "%s", body) @@ -820,7 +829,7 @@ func TestGetFlow(t *testing.T) { t.Run("case=csrf cookie missing", func(t *testing.T) { client := http.DefaultClient setupLoginUI(t, client) - body := x.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow) assert.EqualValues(t, x.ErrInvalidCSRFToken.ReasonField, gjson.GetBytes(body, "error.reason").String(), "%s", body) }) @@ -828,7 +837,7 @@ func TestGetFlow(t *testing.T) { t.Run("case=expired", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) setupLoginUI(t, client) - body := x.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow) // Expire the flow f, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) @@ -837,7 +846,7 @@ func TestGetFlow(t *testing.T) { require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), f)) // Try the flow but it is expired - res, body := x.EasyGet(t, client, public.URL+login.RouteGetFlow+"?id="+f.ID.String()) + res, body := testhelpers.EasyGet(t, client, public.URL+login.RouteGetFlow+"?id="+f.ID.String()) assert.EqualValues(t, http.StatusGone, res.StatusCode) assert.Equal(t, public.URL+login.RouteInitBrowserFlow, gjson.GetBytes(body, "error.details.redirect_to").String(), "%s", body) }) @@ -848,7 +857,7 @@ func TestGetFlow(t *testing.T) { client := testhelpers.NewClientWithCookies(t) setupLoginUI(t, client) - body := x.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow+"?return_to="+returnTo) + body := testhelpers.EasyGetBody(t, client, public.URL+login.RouteInitBrowserFlow+"?return_to="+returnTo) // Expire the flow f, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) @@ -858,7 +867,7 @@ func TestGetFlow(t *testing.T) { // Retrieve the flow and verify that return_to is in the response getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", public.URL, login.RouteGetFlow, f.ID, returnTo) - getBody := x.EasyGetBody(t, client, getURL) + getBody := testhelpers.EasyGetBody(t, client, getURL) assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo) // submit the flow but it is expired @@ -878,7 +887,7 @@ func TestGetFlow(t *testing.T) { client := testhelpers.NewClientWithCookies(t) setupLoginUI(t, client) - res, _ := x.EasyGet(t, client, public.URL+login.RouteGetFlow+"?id="+x.NewUUID().String()) + res, _ := testhelpers.EasyGet(t, client, public.URL+login.RouteGetFlow+"?id="+x.NewUUID().String()) assert.EqualValues(t, http.StatusNotFound, res.StatusCode) }) } diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index 1bff4122ad1f..2af0c3daf1fc 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -9,22 +9,22 @@ import ( "net/http" "time" + "github.com/gofrs/uuid" + "github.com/pkg/errors" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "github.com/ory/kratos/x/events" - - "github.com/pkg/errors" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/hydra" "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/kratos/session" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" "github.com/ory/x/otelx" ) @@ -47,6 +47,7 @@ type ( executorDependencies interface { config.Provider hydra.Provider + identity.PrivilegedPoolProvider session.ManagementProvider session.PersistenceProvider x.CSRFTokenGeneratorProvider @@ -55,7 +56,9 @@ type ( x.TracingProvider sessiontokenexchange.PersistenceProvider + FlowPersistenceProvider HooksProvider + StrategyProvider } HookExecutor struct { d executorDependencies @@ -129,6 +132,10 @@ func (e *HookExecutor) PostLoginHook( r = r.WithContext(ctx) defer otelx.End(span, &err) + if err := e.maybeLinkCredentials(r.Context(), s, i, a); err != nil { + return err + } + if err := s.Activate(r, i, e.d.Config(), time.Now().UTC()); err != nil { return err } @@ -321,3 +328,51 @@ func (e *HookExecutor) PreLoginHook(w http.ResponseWriter, r *http.Request, a *F return nil } + +// maybeLinkCredentials links the identity with the credentials of the inner context of the login flow. +func (e *HookExecutor) maybeLinkCredentials(ctx context.Context, sess *session.Session, ident *identity.Identity, loginFlow *Flow) error { + lc, err := flow.DuplicateCredentials(loginFlow) + if err != nil { + return err + } else if lc == nil { + return nil + } + + if err := e.checkDuplicateCredentialsIdentifierMatch(ctx, ident.ID, lc.DuplicateIdentifier); err != nil { + return err + } + strategy, err := e.d.AllLoginStrategies().Strategy(lc.CredentialsType) + if err != nil { + return err + } + + linkableStrategy, ok := strategy.(LinkableStrategy) + if !ok { + // This should never happen because we check for this in the registration flow. + return errors.Errorf("strategy is not linkable: %T", linkableStrategy) + } + + if err := linkableStrategy.Link(ctx, ident, lc.CredentialsConfig); err != nil { + return err + } + + method := strategy.CompletedAuthenticationMethod(ctx) + sess.CompletedLoginForMethod(method) + + return nil +} + +func (e *HookExecutor) checkDuplicateCredentialsIdentifierMatch(ctx context.Context, identityID uuid.UUID, match string) error { + i, err := e.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, identityID) + if err != nil { + return err + } + for _, credentials := range i.Credentials { + for _, identifier := range credentials.Identifiers { + if identifier == match { + return nil + } + } + } + return schema.NewLinkedCredentialsDoNotMatch() +} diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go index ea3bd84bac72..052973317ca2 100644 --- a/selfservice/flow/login/hook_test.go +++ b/selfservice/flow/login/hook_test.go @@ -12,14 +12,15 @@ import ( "github.com/stretchr/testify/require" - "github.com/ory/kratos/hydra" - "github.com/ory/kratos/session" - "github.com/gobuffalo/httptest" "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" "github.com/tidwall/gjson" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/session" + "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" @@ -256,6 +257,58 @@ func TestLoginExecutor(t *testing.T) { assert.Contains(t, gjson.Get(body, "redirect_browser_to").String(), "/self-service/login/browser?aal=aal2", "%s", body) }) }) + + }) + t.Run("case=maybe links credential", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + email := testhelpers.RandomEmail() + useIdentity := &identity.Identity{Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Config: []byte(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), + Identifiers: []string{email}, + }, + }} + require.NoError(t, reg.Persister().CreateIdentity(context.Background(), useIdentity)) + + credsOIDC, err := identity.NewCredentialsOIDC( + "id-token", + "access-token", + "refresh-token", + "my-provider", + email, + "", + ) + require.NoError(t, err) + + t.Run("sub-case=links matching identity", func(t *testing.T) { + res, _ := makeRequestPost(t, newServer(t, flow.TypeBrowser, useIdentity, func(l *login.Flow) { + require.NoError(t, flow.SetDuplicateCredentials(l, flow.DuplicateCredentialsData{ + CredentialsType: identity.CredentialsTypeOIDC, + CredentialsConfig: credsOIDC.Config, + DuplicateIdentifier: email, + })) + }), false, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String()) + + ident, err := reg.Persister().GetIdentity(ctx, useIdentity.ID, identity.ExpandCredentials) + require.NoError(t, err) + assert.Equal(t, 2, len(ident.Credentials)) + }) + + t.Run("sub-case=errors on non-matching identity", func(t *testing.T) { + res, body := makeRequestPost(t, newServer(t, flow.TypeBrowser, useIdentity, func(l *login.Flow) { + require.NoError(t, flow.SetDuplicateCredentials(l, flow.DuplicateCredentialsData{ + CredentialsType: identity.CredentialsTypeOIDC, + CredentialsConfig: credsOIDC.Config, + DuplicateIdentifier: "wrong@example.com", + })) + }), false, url.Values{}) + assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode) + assert.Equal(t, schema.NewLinkedCredentialsDoNotMatch().Error(), body, "%s", body) + }) }) t.Run("type=api", func(t *testing.T) { diff --git a/selfservice/flow/login/strategy.go b/selfservice/flow/login/strategy.go index 818ecfea9cf0..c8ad84986a55 100644 --- a/selfservice/flow/login/strategy.go +++ b/selfservice/flow/login/strategy.go @@ -8,14 +8,13 @@ import ( "net/http" "github.com/gofrs/uuid" - - "github.com/ory/kratos/session" - "github.com/pkg/errors" "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/x/sqlxx" ) type Strategy interface { @@ -29,6 +28,10 @@ type Strategy interface { type Strategies []Strategy +type LinkableStrategy interface { + Link(ctx context.Context, i *identity.Identity, credentials sqlxx.JSONRawMessage) error +} + func (s Strategies) Strategy(id identity.CredentialsType) (Strategy, error) { ids := make([]identity.CredentialsType, len(s)) for k, ss := range s { diff --git a/selfservice/flow/login/test/persistence.go b/selfservice/flow/login/test/persistence.go index 8cfbc3b5a8eb..5dbbed75fc6d 100644 --- a/selfservice/flow/login/test/persistence.go +++ b/selfservice/flow/login/test/persistence.go @@ -11,7 +11,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/selfservice/flow/recovery/handler_test.go b/selfservice/flow/recovery/handler_test.go index 7d204e3846f0..0d8bf26d42b3 100644 --- a/selfservice/flow/recovery/handler_test.go +++ b/selfservice/flow/recovery/handler_test.go @@ -48,13 +48,13 @@ func TestHandlerRedirectOnAuthenticated(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") t.Run("does redirect to default on authenticated request", func(t *testing.T) { - body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, x.NewTestHTTPRequest(t, "GET", ts.URL+recovery.RouteInitBrowserFlow, nil)) + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+recovery.RouteInitBrowserFlow, nil)) assert.Contains(t, res.Request.URL.String(), redirTS.URL, "%+v", res) assert.EqualValues(t, "already authenticated", string(body)) }) t.Run("does redirect to default on authenticated request", func(t *testing.T) { - body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, x.NewTestHTTPRequest(t, "GET", ts.URL+recovery.RouteInitAPIFlow, nil)) + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+recovery.RouteInitAPIFlow, nil)) assert.Contains(t, res.Request.URL.String(), recovery.RouteInitAPIFlow) assert.EqualValues(t, text.ErrIDAlreadyLoggedIn, gjson.GetBytes(body, "error.id").Str) assertx.EqualAsJSON(t, recovery.ErrAlreadyLoggedIn, json.RawMessage(gjson.GetBytes(body, "error").Raw)) @@ -90,7 +90,7 @@ func TestInitFlow(t *testing.T) { if isAPI { route = recovery.RouteInitAPIFlow } - req := x.NewTestHTTPRequest(t, "GET", publicTS.URL+route, nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", publicTS.URL+route, nil) if isSPA { req.Header.Set("Accept", "application/json") } @@ -118,7 +118,7 @@ func TestInitFlow(t *testing.T) { initSPAFlow := func(t *testing.T, hc *http.Client, isSPA bool) (*http.Response, []byte) { route := recovery.RouteInitBrowserFlow c := publicTS.Client() - req := x.NewTestHTTPRequest(t, "GET", publicTS.URL+route, nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", publicTS.URL+route, nil) if isSPA { req.Header.Set("Accept", "application/json") } @@ -215,7 +215,7 @@ func TestGetFlow(t *testing.T) { setupRecoveryTS := func(t *testing.T, c *http.Client) *httptest.Server { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(x.EasyGetBody(t, c, public.URL+recovery.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + _, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+recovery.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) require.NoError(t, err) })) t.Cleanup(ts.Close) @@ -226,7 +226,7 @@ func TestGetFlow(t *testing.T) { t.Run("case=csrf cookie missing", func(t *testing.T) { client := http.DefaultClient setupRecoveryTS(t, client) - body := x.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow) assert.EqualValues(t, x.ErrInvalidCSRFToken.ReasonField, gjson.GetBytes(body, "error.reason").String(), "%s", body) }) @@ -234,7 +234,7 @@ func TestGetFlow(t *testing.T) { t.Run("case=valid", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) setupRecoveryTS(t, client) - body := x.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow) assert.NotEmpty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", body) assert.NotEmpty(t, gjson.GetBytes(body, "id").String(), "%s", body) assert.Empty(t, gjson.GetBytes(body, "headers").Value(), "%s", body) @@ -245,7 +245,7 @@ func TestGetFlow(t *testing.T) { t.Run("case=expired", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) setupRecoveryTS(t, client) - body := x.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow) // Expire the flow f, err := reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) @@ -253,7 +253,7 @@ func TestGetFlow(t *testing.T) { f.ExpiresAt = time.Now().Add(-time.Second) require.NoError(t, reg.RecoveryFlowPersister().UpdateRecoveryFlow(context.Background(), f)) - res, body := x.EasyGet(t, client, public.URL+recovery.RouteGetFlow+"?id="+f.ID.String()) + res, body := testhelpers.EasyGet(t, client, public.URL+recovery.RouteGetFlow+"?id="+f.ID.String()) assert.EqualValues(t, http.StatusGone, res.StatusCode) assert.Equal(t, public.URL+recovery.RouteInitBrowserFlow, gjson.GetBytes(body, "error.details.redirect_to").String(), "%s", body) }) @@ -263,7 +263,7 @@ func TestGetFlow(t *testing.T) { conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) client := testhelpers.NewClientWithCookies(t) setupRecoveryTS(t, client) - body := x.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow+"?return_to="+returnTo) + body := testhelpers.EasyGetBody(t, client, public.URL+recovery.RouteInitBrowserFlow+"?return_to="+returnTo) // Expire the flow f, err := reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) @@ -273,7 +273,7 @@ func TestGetFlow(t *testing.T) { // Retrieve the flow and verify that return_to is in the response getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", public.URL, recovery.RouteGetFlow, f.ID, returnTo) - getBody := x.EasyGetBody(t, client, getURL) + getBody := testhelpers.EasyGetBody(t, client, getURL) assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo) // submit the flow but it is expired @@ -293,7 +293,7 @@ func TestGetFlow(t *testing.T) { client := testhelpers.NewClientWithCookies(t) setupRecoveryTS(t, client) - res, _ := x.EasyGet(t, client, public.URL+recovery.RouteGetFlow+"?id="+x.NewUUID().String()) + res, _ := testhelpers.EasyGet(t, client, public.URL+recovery.RouteGetFlow+"?id="+x.NewUUID().String()) assert.EqualValues(t, http.StatusNotFound, res.StatusCode) }) } diff --git a/selfservice/flow/recovery/test/persistence.go b/selfservice/flow/recovery/test/persistence.go index 665dc84b9130..8bc9efad88e0 100644 --- a/selfservice/flow/recovery/test/persistence.go +++ b/selfservice/flow/recovery/test/persistence.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/selfservice/flow/registration/flow.go b/selfservice/flow/registration/flow.go index 494bf72383af..b3dae8a6a1c5 100644 --- a/selfservice/flow/registration/flow.go +++ b/selfservice/flow/registration/flow.go @@ -11,25 +11,19 @@ import ( "time" "github.com/gobuffalo/pop/v6" - + "github.com/gofrs/uuid" + "github.com/pkg/errors" "github.com/tidwall/gjson" - "github.com/ory/x/sqlxx" - hydraclientgo "github.com/ory/hydra-client-go/v2" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/hydra" - "github.com/ory/kratos/ui/container" - - "github.com/gofrs/uuid" - "github.com/pkg/errors" - - "github.com/ory/x/urlx" - "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/ui/container" "github.com/ory/kratos/x" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" ) // swagger:model registrationFlow @@ -208,6 +202,14 @@ func (f *Flow) EnsureInternalContext() { } } +func (f *Flow) GetInternalContext() sqlxx.JSONRawMessage { + return f.InternalContext +} + +func (f *Flow) SetInternalContext(bytes sqlxx.JSONRawMessage) { + f.InternalContext = bytes +} + func (f Flow) MarshalJSON() ([]byte, error) { type local Flow f.SetReturnTo() diff --git a/selfservice/flow/registration/flow_test.go b/selfservice/flow/registration/flow_test.go index d72e0c437921..d5c13815bb13 100644 --- a/selfservice/flow/registration/flow_test.go +++ b/selfservice/flow/registration/flow_test.go @@ -21,7 +21,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -68,7 +68,8 @@ func TestNewFlow(t *testing.T) { t.Run("case=1", func(t *testing.T) { r, err := registration.NewFlow(conf, 0, "csrf", &http.Request{ URL: urlx.ParseOrPanic("/?refresh=true"), - Host: "ory.sh"}, flow.TypeAPI) + Host: "ory.sh", + }, flow.TypeAPI) require.NoError(t, err) assert.Equal(t, r.IssuedAt, r.ExpiresAt) assert.Equal(t, flow.TypeAPI, r.Type) @@ -78,7 +79,8 @@ func TestNewFlow(t *testing.T) { t.Run("case=2", func(t *testing.T) { r, err := registration.NewFlow(conf, 0, "csrf", &http.Request{ URL: urlx.ParseOrPanic("https://ory.sh/"), - Host: "ory.sh"}, flow.TypeBrowser) + Host: "ory.sh", + }, flow.TypeBrowser) require.NoError(t, err) assert.Equal(t, "https://ory.sh/", r.RequestURL) }) diff --git a/selfservice/flow/registration/handler.go b/selfservice/flow/registration/handler.go index d58ff4c9e735..8cfe59e4d6d9 100644 --- a/selfservice/flow/registration/handler.go +++ b/selfservice/flow/registration/handler.go @@ -9,30 +9,25 @@ import ( "time" "github.com/gofrs/uuid" - - "github.com/ory/herodot" - "github.com/ory/kratos/hydra" - "github.com/ory/kratos/selfservice/sessiontokenexchange" - "github.com/ory/kratos/text" - "github.com/ory/nosurf" - - "github.com/ory/kratos/schema" - - "github.com/ory/kratos/identity" - "github.com/ory/kratos/ui/node" - "github.com/julienschmidt/httprouter" "github.com/pkg/errors" - "github.com/ory/x/sqlxx" - "github.com/ory/x/urlx" - + "github.com/ory/herodot" + hydraclientgo "github.com/ory/hydra-client-go/v2" "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hydra" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/errorx" "github.com/ory/kratos/selfservice/flow" - "github.com/ory/kratos/selfservice/flow/logout" + "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/nosurf" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" ) const ( @@ -318,27 +313,78 @@ type createBrowserRegistrationFlow struct { // 303: emptyResponse // default: errorGeneric func (h *Handler) createBrowserRegistrationFlow(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + ctx := r.Context() + a, err := h.NewRegistrationFlow(w, r, flow.TypeBrowser) if err != nil { - h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) return } - if sess, err := h.d.SessionManager().FetchFromRequest(r.Context(), r); err == nil { - if r.URL.Query().Has("login_challenge") { - logoutUrl := urlx.AppendPaths(h.d.Config().SelfPublicURL(r.Context()), logout.RouteSubmitFlow) - self := urlx.CopyWithQuery( - urlx.AppendPaths(h.d.Config().SelfPublicURL(r.Context()), RouteInitBrowserFlow), - r.URL.Query(), - ).String() + var ( + hydraLoginRequest *hydraclientgo.OAuth2LoginRequest + hydraLoginChallenge sqlxx.NullString + ) + if r.URL.Query().Has("login_challenge") { + var err error + hydraLoginChallenge, err = hydra.GetLoginChallengeID(h.d.Config(), r) + if err != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + hydraLoginRequest, err = h.d.Hydra().GetLoginRequest(ctx, hydraLoginChallenge.String()) + if err != nil { + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) + return + } + + // on OAuth2 flows, we need to use the RequestURL + // as the ReturnTo URL. + // This is because a user might want to switch between + // different flows, such as login to registration and login to recovery. + // After completing a complex flow, such as recovery, we want the user + // to be redirected back to the original OAuth2 login flow. + if hydraLoginRequest.RequestUrl != "" && h.d.Config().OAuth2ProviderOverrideReturnTo(r.Context()) { + q := r.URL.Query() + // replace the return_to query parameter + q.Set("return_to", hydraLoginRequest.RequestUrl) + r.URL.RawQuery = q.Encode() + } + } + if sess, err := h.d.SessionManager().FetchFromRequest(ctx, r); err == nil { + if hydraLoginRequest != nil { + if hydraLoginRequest.GetSkip() { + rt, err := h.d.Hydra().AcceptLoginRequest(r.Context(), + hydra.AcceptLoginRequestParams{ + LoginChallenge: string(hydraLoginChallenge), + IdentityID: sess.IdentityID.String(), + SessionID: sess.ID.String(), + AuthenticationMethods: sess.AMR, + }) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + return + } + returnTo, err := url.Parse(rt) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to parse URL: %s", rt))) + return + } + x.AcceptToRedirectOrJSON(w, r, h.d.Writer(), err, returnTo.String()) + return + } + + // hydra indicates that we cannot skip the login request + // so we must perform the login flow. + // we directly go to the login handler from here + // copy over any query parameters, such as `return_to` and `login_challenge` + loginURL := urlx.CopyWithQuery(urlx.AppendPaths(h.d.Config().SelfPublicURL(ctx), "/self-service/login/browser"), x.RequestURL(r).Query()) http.Redirect( w, r, - urlx.CopyWithQuery(logoutUrl, url.Values{ - "token": {sess.LogoutToken}, - "return_to": {self}, - }).String(), + loginURL.String(), http.StatusFound, ) return @@ -349,12 +395,12 @@ func (h *Handler) createBrowserRegistrationFlow(w http.ResponseWriter, r *http.R return } - returnTo, redirErr := x.SecureRedirectTo(r, h.d.Config().SelfServiceBrowserDefaultReturnTo(r.Context()), - x.SecureRedirectAllowSelfServiceURLs(h.d.Config().SelfPublicURL(r.Context())), - x.SecureRedirectAllowURLs(h.d.Config().SelfServiceBrowserAllowedReturnToDomains(r.Context())), + returnTo, redirErr := x.SecureRedirectTo(r, h.d.Config().SelfServiceBrowserDefaultReturnTo(ctx), + x.SecureRedirectAllowSelfServiceURLs(h.d.Config().SelfPublicURL(ctx)), + x.SecureRedirectAllowURLs(h.d.Config().SelfServiceBrowserAllowedReturnToDomains(ctx)), ) if redirErr != nil { - h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, redirErr) + h.d.SelfServiceErrorManager().Forward(ctx, w, r, redirErr) return } @@ -362,7 +408,7 @@ func (h *Handler) createBrowserRegistrationFlow(w http.ResponseWriter, r *http.R return } - redirTo := a.AppendTo(h.d.Config().SelfServiceFlowRegistrationUI(r.Context())).String() + redirTo := a.AppendTo(h.d.Config().SelfServiceFlowRegistrationUI(ctx)).String() x.AcceptToRedirectOrJSON(w, r, h.d.Writer(), a, redirTo) } diff --git a/selfservice/flow/registration/handler_test.go b/selfservice/flow/registration/handler_test.go index 27df3e0868a5..eae66fb720f8 100644 --- a/selfservice/flow/registration/handler_test.go +++ b/selfservice/flow/registration/handler_test.go @@ -16,10 +16,11 @@ import ( "testing" "time" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/ory/kratos/corpx" + "github.com/ory/kratos/hydra" "github.com/ory/x/urlx" "github.com/stretchr/testify/assert" @@ -32,6 +33,7 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/strategy/oidc" "github.com/ory/kratos/selfservice/strategy/password" @@ -45,6 +47,8 @@ func init() { func TestHandlerRedirectOnAuthenticated(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) + fakeHydra := hydra.NewFake() + reg.WithHydra(fakeHydra) router := x.NewRouterPublic() ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, x.NewRouterAdmin()) @@ -58,22 +62,50 @@ func TestHandlerRedirectOnAuthenticated(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") t.Run("does redirect to default on authenticated request", func(t *testing.T) { - body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, x.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitBrowserFlow, nil)) + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitBrowserFlow, nil)) assert.Contains(t, res.Request.URL.String(), redirTS.URL) assert.EqualValues(t, "already authenticated", string(body)) }) t.Run("does redirect to default on authenticated request", func(t *testing.T) { - body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, x.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitAPIFlow, nil)) + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitAPIFlow, nil)) assert.Contains(t, res.Request.URL.String(), registration.RouteInitAPIFlow) assertx.EqualAsJSON(t, registration.ErrAlreadyLoggedIn, json.RawMessage(gjson.GetBytes(body, "error").Raw)) }) t.Run("does redirect to return_to url on authenticated request", func(t *testing.T) { - body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, x.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitBrowserFlow+"?return_to="+returnToTS.URL, nil)) + body, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitBrowserFlow+"?return_to="+returnToTS.URL, nil)) assert.Contains(t, res.Request.URL.String(), returnToTS.URL) assert.EqualValues(t, "return_to", string(body)) }) + + t.Run("oauth2 with session and skip=false is redirected to login", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://fake-hydra") + + fakeHydra.RequestURL = "https://www.ory.sh/oauth2/auth?audience=&client_id=foo&login_verifier=" + fakeHydra.Skip = false + + client := testhelpers.NewClientWithCookies(t) + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + _, res := testhelpers.MockMakeAuthenticatedRequestWithClient(t, reg, conf, router.Router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitBrowserFlow+"?login_challenge="+hydra.FakeValidLoginChallenge, nil), client) + assert.Contains(t, res.Header.Get("location"), login.RouteInitBrowserFlow) + }) + + t.Run("oauth2 with session and skip=true is accepted", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, "https://fake-hydra") + + fakeHydra.Skip = true + fakeHydra.RequestURL = "https://www.ory.sh/oauth2/auth?audience=&client_id=foo&login_verifier=" + + client := testhelpers.NewClientWithCookies(t) + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + _, res := testhelpers.MockMakeAuthenticatedRequestWithClient(t, reg, conf, router.Router, testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+registration.RouteInitBrowserFlow+"?login_challenge="+hydra.FakeValidLoginChallenge, nil), client) + assert.Contains(t, res.Header.Get("location"), hydra.FakePostLoginURL) + }) } func TestInitFlow(t *testing.T) { @@ -103,7 +135,7 @@ func TestInitFlow(t *testing.T) { if isAPI { route = registration.RouteInitAPIFlow } - req := x.NewTestHTTPRequest(t, "GET", publicTS.URL+route, nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", publicTS.URL+route, nil) if isSPA { req.Header.Set("Accept", "application/json") } @@ -297,7 +329,7 @@ func TestGetFlow(t *testing.T) { setupRegistrationUI := func(t *testing.T, c *http.Client) *httptest.Server { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(x.EasyGetBody(t, c, public.URL+registration.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + _, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+registration.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) require.NoError(t, err) })) t.Cleanup(ts.Close) @@ -308,7 +340,7 @@ func TestGetFlow(t *testing.T) { t.Run("case=valid", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) _ = setupRegistrationUI(t, client) - body := x.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) assert.NotEmpty(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", body) assert.NotEmpty(t, gjson.GetBytes(body, "id").String(), "%s", body) @@ -320,7 +352,7 @@ func TestGetFlow(t *testing.T) { t.Run("case=csrf cookie missing", func(t *testing.T) { client := http.DefaultClient _ = setupRegistrationUI(t, client) - body := x.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) assert.EqualValues(t, x.ErrInvalidCSRFToken.ReasonField, gjson.GetBytes(body, "error.reason").String(), "%s", body) }) @@ -328,7 +360,7 @@ func TestGetFlow(t *testing.T) { t.Run("case=expired", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) setupRegistrationUI(t, client) - body := x.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) // Expire the flow f, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) @@ -336,7 +368,7 @@ func TestGetFlow(t *testing.T) { f.ExpiresAt = time.Now().Add(-time.Second) require.NoError(t, reg.RegistrationFlowPersister().UpdateRegistrationFlow(context.Background(), f)) - res, body := x.EasyGet(t, client, public.URL+registration.RouteGetFlow+"?id="+f.ID.String()) + res, body := testhelpers.EasyGet(t, client, public.URL+registration.RouteGetFlow+"?id="+f.ID.String()) assert.EqualValues(t, http.StatusGone, res.StatusCode) assert.Equal(t, public.URL+registration.RouteInitBrowserFlow, gjson.GetBytes(body, "error.details.redirect_to").String(), "%s", body) }) @@ -347,7 +379,7 @@ func TestGetFlow(t *testing.T) { client := testhelpers.NewClientWithCookies(t) setupRegistrationUI(t, client) - body := x.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow+"?return_to="+returnTo) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow+"?return_to="+returnTo) // Expire the flow f, err := reg.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) @@ -357,7 +389,7 @@ func TestGetFlow(t *testing.T) { // Retrieve the flow and verify that return_to is in the response getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", public.URL, registration.RouteGetFlow, f.ID, returnTo) - getBody := x.EasyGetBody(t, client, getURL) + getBody := testhelpers.EasyGetBody(t, client, getURL) assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo) // submit the flow but it is expired @@ -377,7 +409,7 @@ func TestGetFlow(t *testing.T) { client := testhelpers.NewClientWithCookies(t) setupRegistrationUI(t, client) - res, _ := x.EasyGet(t, client, public.URL+registration.RouteGetFlow+"?id="+x.NewUUID().String()) + res, _ := testhelpers.EasyGet(t, client, public.URL+registration.RouteGetFlow+"?id="+x.NewUUID().String()) assert.EqualValues(t, http.StatusNotFound, res.StatusCode) }) } @@ -420,7 +452,7 @@ func TestOIDCStrategyOrder(t *testing.T) { setupRegistrationUI := func(t *testing.T, c *http.Client) *httptest.Server { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(x.EasyGetBody(t, c, public.URL+registration.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + _, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+registration.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) require.NoError(t, err) })) t.Cleanup(ts.Close) @@ -431,7 +463,7 @@ func TestOIDCStrategyOrder(t *testing.T) { t.Run("case=accept `password` method while including `provider:google`", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) _ = setupRegistrationUI(t, client) - body := x.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) flow := gjson.GetBytes(body, "id").String() @@ -464,7 +496,7 @@ func TestOIDCStrategyOrder(t *testing.T) { t.Run("case=accept oidc flow with just `provider:google`", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) _ = setupRegistrationUI(t, client) - body := x.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+registration.RouteInitBrowserFlow) flow := gjson.GetBytes(body, "id").String() @@ -473,6 +505,7 @@ func TestOIDCStrategyOrder(t *testing.T) { payload := json.RawMessage(`{"provider": "google","csrf_token": "` + csrfToken + `"}`) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, public.URL+registration.RouteSubmitFlow+"?flow="+flow, bytes.NewBuffer(payload)) + require.NoError(t, err) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go index f7a7fed1d99f..6a997009c1c5 100644 --- a/selfservice/flow/registration/hook.go +++ b/selfservice/flow/registration/hook.go @@ -9,23 +9,21 @@ import ( "net/http" "time" - "go.opentelemetry.io/otel/attribute" - - "github.com/ory/x/otelx" - "github.com/julienschmidt/httprouter" - - "github.com/ory/kratos/selfservice/sessiontokenexchange" - "github.com/ory/kratos/x/events" - "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/hydra" "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/kratos/session" "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" ) type ( @@ -75,10 +73,14 @@ type ( executorDependencies interface { config.Provider identity.ManagementProvider + identity.PrivilegedPoolProvider identity.ValidationProvider + login.FlowPersistenceProvider + login.StrategyProvider session.PersistenceProvider session.ManagementProvider HooksProvider + FlowPersistenceProvider hydra.Provider x.CSRFTokenGeneratorProvider x.HTTPClientProvider @@ -99,7 +101,7 @@ func NewHookExecutor(d executorDependencies) *HookExecutor { return &HookExecutor{d: d} } -func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Request, ct identity.CredentialsType, provider string, a *Flow, i *identity.Identity) (err error) { +func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Request, ct identity.CredentialsType, provider string, registrationFlow *Flow, i *identity.Identity) (err error) { ctx := r.Context() ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "HookExecutor.PostRegistrationHook") r = r.WithContext(ctx) @@ -111,7 +113,7 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque WithField("flow_method", ct). Debug("Running PostRegistrationPrePersistHooks.") for k, executor := range e.d.PostRegistrationPrePersistHooks(r.Context(), ct) { - if err := executor.ExecutePostRegistrationPrePersistHook(w, r, a, i); err != nil { + if err := executor.ExecutePostRegistrationPrePersistHook(w, r, registrationFlow, i); err != nil { if errors.Is(err, ErrHookAbortFlow) { e.d.Logger(). WithRequest(r). @@ -135,7 +137,7 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque Error("ExecutePostRegistrationPostPersistHook hook failed with an error.") traits := i.Traits - return flow.HandleHookError(w, r, a, traits, ct.ToUiNodeGroup(), err, e.d, e.d) + return flow.HandleHookError(w, r, registrationFlow, traits, ct.ToUiNodeGroup(), err, e.d, e.d) } e.d.Logger().WithRequest(r). @@ -153,14 +155,36 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque // We're now creating the identity because any of the hooks could trigger a "redirect" or a "session" which // would imply that the identity has to exist already. } else if err := e.d.IdentityManager().Create(r.Context(), i); err != nil { + if errors.Is(err, sqlcon.ErrUniqueViolation) { + strategy, err := e.d.AllLoginStrategies().Strategy(ct) + if err != nil { + return err + } + + if _, ok := strategy.(login.LinkableStrategy); ok { + duplicateIdentifier, err := e.getDuplicateIdentifier(r.Context(), i) + if err != nil { + return err + } + registrationDuplicateCredentials := flow.DuplicateCredentialsData{ + CredentialsType: ct, + CredentialsConfig: i.Credentials[ct].Config, + DuplicateIdentifier: duplicateIdentifier, + } + + if err := flow.SetDuplicateCredentials(registrationFlow, registrationDuplicateCredentials); err != nil { + return err + } + } + } return err } // Verify the redirect URL before we do any other processing. c := e.d.Config() returnTo, err := x.SecureRedirectTo(r, c.SelfServiceBrowserDefaultReturnTo(r.Context()), - x.SecureRedirectReturnTo(a.ReturnTo), - x.SecureRedirectUseSourceURL(a.RequestURL), + x.SecureRedirectReturnTo(registrationFlow.ReturnTo), + x.SecureRedirectUseSourceURL(registrationFlow.RequestURL), x.SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(r.Context())), x.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(r.Context())), x.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowRegistrationReturnTo(r.Context(), ct.String())), @@ -179,7 +203,7 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque WithField("identity_id", i.ID). Info("A new identity has registered using self-service registration.") - span.AddEvent(events.NewRegistrationSucceeded(r.Context(), i.ID, string(a.Type), a.Active.String(), provider)) + span.AddEvent(events.NewRegistrationSucceeded(r.Context(), i.ID, string(registrationFlow.Type), registrationFlow.Active.String(), provider)) s := session.NewInactiveSession() @@ -201,7 +225,7 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque WithField("flow_method", ct). Debug("Running PostRegistrationPostPersistHooks.") for k, executor := range e.d.PostRegistrationPostPersistHooks(r.Context(), ct) { - if err := executor.ExecutePostRegistrationPostPersistHook(w, r, a, s); err != nil { + if err := executor.ExecutePostRegistrationPostPersistHook(w, r, registrationFlow, s); err != nil { if errors.Is(err, ErrHookAbortFlow) { e.d.Logger(). WithRequest(r). @@ -230,7 +254,7 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque span.SetAttributes(attribute.String("redirect_reason", "hook error"), attribute.String("executor", fmt.Sprintf("%T", executor))) traits := i.Traits - return flow.HandleHookError(w, r, a, traits, ct.ToUiNodeGroup(), err, e.d, e.d) + return flow.HandleHookError(w, r, registrationFlow, traits, ct.ToUiNodeGroup(), err, e.d, e.d) } e.d.Logger().WithRequest(r). @@ -248,13 +272,13 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque WithField("identity_id", i.ID). Debug("Post registration execution hooks completed successfully.") - if a.Type == flow.TypeAPI || x.IsJSONRequest(r) { + if registrationFlow.Type == flow.TypeAPI || x.IsJSONRequest(r) { span.SetAttributes(attribute.String("flow_type", string(flow.TypeAPI))) - if a.IDToken != "" { + if registrationFlow.IDToken != "" { // We don't want to redirect with the code, if the flow was submitted with an ID token. // This is the case for Sign in with native Apple SDK or Google SDK. - } else if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, ct.ToUiNodeGroup()); err != nil { + } else if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, registrationFlow, s.ID, ct.ToUiNodeGroup()); err != nil { return errors.WithStack(err) } else if handled { return nil @@ -262,21 +286,21 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque e.d.Writer().Write(w, r, &APIFlowResponse{ Identity: i, - ContinueWith: a.ContinueWith(), + ContinueWith: registrationFlow.ContinueWith(), }) return nil } finalReturnTo := returnTo.String() - if a.OAuth2LoginChallenge != "" { - if a.ReturnToVerification != "" { + if registrationFlow.OAuth2LoginChallenge != "" { + if registrationFlow.ReturnToVerification != "" { // Special case: If Kratos is used as a login UI *and* we want to show the verification UI, // redirect to the verification URL first and then return to Hydra. - finalReturnTo = a.ReturnToVerification + finalReturnTo = registrationFlow.ReturnToVerification } else { callbackURL, err := e.d.Hydra().AcceptLoginRequest(r.Context(), hydra.AcceptLoginRequestParams{ - LoginChallenge: string(a.OAuth2LoginChallenge), + LoginChallenge: string(registrationFlow.OAuth2LoginChallenge), IdentityID: i.ID.String(), SessionID: s.ID.String(), AuthenticationMethods: s.AMR, @@ -287,8 +311,8 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque finalReturnTo = callbackURL } span.SetAttributes(attribute.String("redirect_reason", "oauth2 login challenge")) - } else if a.ReturnToVerification != "" { - finalReturnTo = a.ReturnToVerification + } else if registrationFlow.ReturnToVerification != "" { + finalReturnTo = registrationFlow.ReturnToVerification span.SetAttributes(attribute.String("redirect_reason", "verification requested")) } span.SetAttributes(attribute.String("return_to", finalReturnTo)) @@ -297,6 +321,14 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque return nil } +func (e *HookExecutor) getDuplicateIdentifier(ctx context.Context, i *identity.Identity) (string, error) { + _, id, err := e.d.IdentityManager().ConflictingIdentity(ctx, i) + if err != nil { + return "", err + } + return id, nil +} + func (e *HookExecutor) PreRegistrationHook(w http.ResponseWriter, r *http.Request, a *Flow) error { for _, executor := range e.d.PreRegistrationHooks(r.Context()) { if err := executor.ExecuteRegistrationPreHook(w, r, a); err != nil { diff --git a/selfservice/flow/registration/hook_test.go b/selfservice/flow/registration/hook_test.go index c6f396b64514..3761692e3f45 100644 --- a/selfservice/flow/registration/hook_test.go +++ b/selfservice/flow/registration/hook_test.go @@ -246,7 +246,7 @@ func TestRegistrationExecutor(t *testing.T) { i := testhelpers.SelfServiceHookFakeIdentity(t) i.Traits = identity.Traits(`{"email": "verifiable4@ory.sh"}`) - jar := x.EasyCookieJar(t, nil) + jar := testhelpers.EasyCookieJar(t, nil) s := newServer(t, i, flow.TypeBrowser) s.Client().Jar = jar res, _ := makeRequestPost(t, s, false, url.Values{}) diff --git a/selfservice/flow/registration/test/persistence.go b/selfservice/flow/registration/test/persistence.go index a35059c845bc..d382c23a29ba 100644 --- a/selfservice/flow/registration/test/persistence.go +++ b/selfservice/flow/registration/test/persistence.go @@ -18,7 +18,7 @@ import ( "github.com/ory/x/assertx" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/selfservice/flow/settings/error_test.go b/selfservice/flow/settings/error_test.go index 73116ba8a49a..5776cd2b6942 100644 --- a/selfservice/flow/settings/error_test.go +++ b/selfservice/flow/settings/error_test.go @@ -17,7 +17,7 @@ import ( "github.com/ory/kratos/ui/node" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gobuffalo/httptest" "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" diff --git a/selfservice/flow/settings/flow_test.go b/selfservice/flow/settings/flow_test.go index 0a4da77f323d..26a40b71245d 100644 --- a/selfservice/flow/settings/flow_test.go +++ b/selfservice/flow/settings/flow_test.go @@ -22,7 +22,7 @@ import ( "github.com/ory/kratos/internal" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/selfservice/flow/settings/handler_test.go b/selfservice/flow/settings/handler_test.go index 0a2789466fce..a24e598c64aa 100644 --- a/selfservice/flow/settings/handler_test.go +++ b/selfservice/flow/settings/handler_test.go @@ -110,7 +110,7 @@ func TestHandler(t *testing.T) { require.NoError(t, err) reqURL.RawQuery = op.query.Encode() - req := x.NewTestHTTPRequest(t, "GET", reqURL.String(), nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", reqURL.String(), nil) if isSPA || isAPI { req.Header.Set("Accept", "application/json") } @@ -357,7 +357,7 @@ func TestHandler(t *testing.T) { conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) client := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg) - body := x.EasyGetBody(t, client, publicTS.URL+settings.RouteInitBrowserFlow+"?return_to="+returnTo) + body := testhelpers.EasyGetBody(t, client, publicTS.URL+settings.RouteInitBrowserFlow+"?return_to="+returnTo) // Expire the flow f, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) @@ -367,7 +367,7 @@ func TestHandler(t *testing.T) { // Retrieve the flow and verify that return_to is in the response getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", publicTS.URL, settings.RouteGetFlow, f.ID, returnTo) - getBody := x.EasyGetBody(t, client, getURL) + getBody := testhelpers.EasyGetBody(t, client, getURL) assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo) // submit the flow but it is expired diff --git a/selfservice/flow/settings/test/persistence.go b/selfservice/flow/settings/test/persistence.go index 62bce9c547bf..85c80e49d74e 100644 --- a/selfservice/flow/settings/test/persistence.go +++ b/selfservice/flow/settings/test/persistence.go @@ -22,7 +22,7 @@ import ( "github.com/ory/kratos/ui/node" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/selfservice/flow/verification/flow.go b/selfservice/flow/verification/flow.go index 28a71e47a977..264e356da476 100644 --- a/selfservice/flow/verification/flow.go +++ b/selfservice/flow/verification/flow.go @@ -185,7 +185,11 @@ func NewPostHookFlow(conf *config.Config, exp time.Duration, csrf string, r *htt requestURL = new(url.URL) } query := requestURL.Query() - query.Set("return_to", query.Get("after_verification_return_to")) + // we need to keep the return_to in-tact if the `after_verification_return_to` is empty + // otherwise we take the `after_verification_return_to` query parameter over the current `return_to` + if afterVerificationReturn := query.Get("after_verification_return_to"); afterVerificationReturn != "" { + query.Set("return_to", afterVerificationReturn) + } query.Del("after_verification_return_to") requestURL.RawQuery = query.Encode() f.RequestURL = requestURL.String() diff --git a/selfservice/flow/verification/flow_test.go b/selfservice/flow/verification/flow_test.go index 3485f3454fc4..e05482ffa39d 100644 --- a/selfservice/flow/verification/flow_test.go +++ b/selfservice/flow/verification/flow_test.go @@ -108,7 +108,7 @@ func TestNewPostHookFlow(t *testing.T) { }) t.Run("case=return_to supplied", func(t *testing.T) { - expectReturnTo(t, url.Values{"return_to": {"http://foo.com/original_flow_callback"}}, "") + expectReturnTo(t, url.Values{"return_to": {"http://foo.com/original_flow_callback"}}, "http://foo.com/original_flow_callback") }) t.Run("case=return_to and after_verification_return_to supplied", func(t *testing.T) { diff --git a/selfservice/flow/verification/handler_test.go b/selfservice/flow/verification/handler_test.go index 266cc9022431..6f37956b6055 100644 --- a/selfservice/flow/verification/handler_test.go +++ b/selfservice/flow/verification/handler_test.go @@ -41,7 +41,7 @@ func TestGetFlow(t *testing.T) { setupVerificationUI := func(t *testing.T, c *http.Client) *httptest.Server { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(x.EasyGetBody(t, c, public.URL+verification.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + _, err := w.Write(testhelpers.EasyGetBody(t, c, public.URL+verification.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) require.NoError(t, err) })) t.Cleanup(ts.Close) @@ -68,7 +68,7 @@ func TestGetFlow(t *testing.T) { t.Run("type=browser", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) _ = setupVerificationUI(t, client) - res, body := x.EasyGet(t, client, public.URL+verification.RouteInitBrowserFlow) + res, body := testhelpers.EasyGet(t, client, public.URL+verification.RouteInitBrowserFlow) require.NotEqualValues(t, res.Request.URL.String(), public.URL+verification.RouteInitBrowserFlow) assertFlowPayload(t, body, false) }) @@ -76,7 +76,7 @@ func TestGetFlow(t *testing.T) { t.Run("type=spa", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) _ = setupVerificationUI(t, client) - res, body := x.EasyGetJSON(t, client, public.URL+verification.RouteInitBrowserFlow) + res, body := testhelpers.EasyGetJSON(t, client, public.URL+verification.RouteInitBrowserFlow) require.EqualValues(t, res.Request.URL.String(), public.URL+verification.RouteInitBrowserFlow) assertFlowPayload(t, body, false) }) @@ -84,7 +84,7 @@ func TestGetFlow(t *testing.T) { t.Run("type=api", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) _ = setupVerificationUI(t, client) - res, body := x.EasyGet(t, client, public.URL+verification.RouteInitAPIFlow) + res, body := testhelpers.EasyGet(t, client, public.URL+verification.RouteInitAPIFlow) assert.Len(t, res.Header.Get("Set-Cookie"), 0) assertFlowPayload(t, body, true) }) @@ -93,7 +93,7 @@ func TestGetFlow(t *testing.T) { t.Run("case=csrf cookie missing", func(t *testing.T) { client := http.DefaultClient _ = setupVerificationUI(t, client) - body := x.EasyGetBody(t, client, public.URL+verification.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+verification.RouteInitBrowserFlow) assert.EqualValues(t, x.ErrInvalidCSRFToken.ReasonField, gjson.GetBytes(body, "error.reason").String(), "%s", body) }) @@ -101,7 +101,7 @@ func TestGetFlow(t *testing.T) { t.Run("case=expired", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) _ = setupVerificationUI(t, client) - body := x.EasyGetBody(t, client, public.URL+verification.RouteInitBrowserFlow) + body := testhelpers.EasyGetBody(t, client, public.URL+verification.RouteInitBrowserFlow) // Expire the flow f, err := reg.VerificationFlowPersister().GetVerificationFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) @@ -109,7 +109,7 @@ func TestGetFlow(t *testing.T) { f.ExpiresAt = time.Now().Add(-time.Second) require.NoError(t, reg.VerificationFlowPersister().UpdateVerificationFlow(context.Background(), f)) - res, body := x.EasyGet(t, client, public.URL+verification.RouteGetFlow+"?id="+f.ID.String()) + res, body := testhelpers.EasyGet(t, client, public.URL+verification.RouteGetFlow+"?id="+f.ID.String()) assert.EqualValues(t, http.StatusGone, res.StatusCode) assert.Equal(t, public.URL+verification.RouteInitBrowserFlow, gjson.GetBytes(body, "error.details.redirect_to").String(), "%s", body) }) @@ -120,7 +120,7 @@ func TestGetFlow(t *testing.T) { client := testhelpers.NewClientWithCookies(t) _ = setupVerificationUI(t, client) - body := x.EasyGetBody(t, client, public.URL+verification.RouteInitBrowserFlow+"?return_to="+returnTo) + body := testhelpers.EasyGetBody(t, client, public.URL+verification.RouteInitBrowserFlow+"?return_to="+returnTo) // Expire the flow f, err := reg.VerificationFlowPersister().GetVerificationFlow(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "id").String())) @@ -130,7 +130,7 @@ func TestGetFlow(t *testing.T) { // Retrieve the flow and verify that return_to is in the response getURL := fmt.Sprintf("%s%s?id=%s&return_to=%s", public.URL, verification.RouteGetFlow, f.ID, returnTo) - getBody := x.EasyGetBody(t, client, getURL) + getBody := testhelpers.EasyGetBody(t, client, getURL) assert.Equal(t, gjson.GetBytes(getBody, "error.details.return_to").String(), returnTo) // submit the flow but it is expired @@ -161,7 +161,7 @@ func TestGetFlow(t *testing.T) { client := testhelpers.NewClientWithCookies(t) _ = setupVerificationUI(t, client) - res, _ := x.EasyGet(t, client, public.URL+verification.RouteGetFlow+"?id="+x.NewUUID().String()) + res, _ := testhelpers.EasyGet(t, client, public.URL+verification.RouteGetFlow+"?id="+x.NewUUID().String()) assert.EqualValues(t, http.StatusNotFound, res.StatusCode) }) @@ -245,7 +245,7 @@ func TestPostFlow(t *testing.T) { t.Run("case=fails without a session", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write(x.EasyGetBody(t, client, public.URL+verification.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) + _, err := w.Write(testhelpers.EasyGetBody(t, client, public.URL+verification.RouteGetFlow+"?id="+r.URL.Query().Get("flow"))) require.NoError(t, err) })) t.Cleanup(ts.Close) diff --git a/selfservice/flow/verification/test/persistence.go b/selfservice/flow/verification/test/persistence.go index 0358462029f7..57c35cba8d2e 100644 --- a/selfservice/flow/verification/test/persistence.go +++ b/selfservice/flow/verification/test/persistence.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/selfservice/hook/session_destroyer_test.go b/selfservice/hook/session_destroyer_test.go index 653f77111209..e2d0cc21c2e2 100644 --- a/selfservice/hook/session_destroyer_test.go +++ b/selfservice/hook/session_destroyer_test.go @@ -13,7 +13,7 @@ import ( "github.com/ory/kratos/corpx" "github.com/ory/kratos/ui/node" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gobuffalo/httptest" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" diff --git a/selfservice/hook/show_verification_ui.go b/selfservice/hook/show_verification_ui.go index 1f7e83ce5c12..51f29a2aa6ce 100644 --- a/selfservice/hook/show_verification_ui.go +++ b/selfservice/hook/show_verification_ui.go @@ -59,9 +59,10 @@ func (e *ShowVerificationUIHook) execute(r *http.Request, f *registration.Flow) } } + ctx := r.Context() if vf != nil { - redir := e.d.Config().SelfServiceFlowVerificationUI(r.Context()) - f.ReturnToVerification = vf.AppendTo(redir).String() + redirURL := e.d.Config().SelfServiceFlowVerificationUI(ctx) + f.ReturnToVerification = vf.AppendTo(redirURL).String() } return nil diff --git a/selfservice/hook/verification.go b/selfservice/hook/verification.go index e041164f372d..c75cf1ce073b 100644 --- a/selfservice/hook/verification.go +++ b/selfservice/hook/verification.go @@ -70,29 +70,37 @@ func (e *Verifier) do( ) error { // This is called after the identity has been created so we can safely assume that all addresses are available // already. + ctx := r.Context() - strategy, err := e.r.GetActiveVerificationStrategy(r.Context()) + strategy, err := e.r.GetActiveVerificationStrategy(ctx) if err != nil { return err } + isBrowserFlow := f.GetType() == flow.TypeBrowser + isRegistrationFlow := f.GetFlowName() == flow.RegistrationFlow + for k := range i.VerifiableAddresses { address := &i.VerifiableAddresses[k] if address.Status != identity.VerifiableAddressStatusPending { continue } - csrf := "" + + var csrf string + // TODO: this is pretty ugly, we should probably have a better way to handle CSRF tokens here. - if f.GetType() != flow.TypeBrowser { - } else if _, ok := f.(*registration.Flow); ok { - // If this hook is executed from a registration flow, we need to regenerate the CSRF token. - csrf = e.r.CSRFHandler().RegenerateToken(w, r) - } else { - // If it came from a settings flow, there already is a CSRF token, so we can just use that. - csrf = e.r.GenerateCSRFToken(r) + if isBrowserFlow { + if isRegistrationFlow { + // If this hook is executed from a registration flow, we need to regenerate the CSRF token. + csrf = e.r.CSRFHandler().RegenerateToken(w, r) + } else { + // If it came from a settings flow, there already is a CSRF token, so we can just use that. + csrf = e.r.GenerateCSRFToken(r) + } } + verificationFlow, err := verification.NewPostHookFlow(e.r.Config(), - e.r.Config().SelfServiceFlowVerificationRequestLifespan(r.Context()), + e.r.Config().SelfServiceFlowVerificationRequestLifespan(ctx), csrf, r, strategy, f) if err != nil { return err @@ -108,17 +116,17 @@ func (e *Verifier) do( return err } - if err := e.r.VerificationFlowPersister().CreateVerificationFlow(r.Context(), verificationFlow); err != nil { + if err := e.r.VerificationFlowPersister().CreateVerificationFlow(ctx, verificationFlow); err != nil { return err } - if err := strategy.SendVerificationEmail(r.Context(), verificationFlow, i, address); err != nil { + if err := strategy.SendVerificationEmail(ctx, verificationFlow, i, address); err != nil { return err } flowURL := "" if verificationFlow.Type == flow.TypeBrowser { - flowURL = verificationFlow.AppendTo(e.r.Config().SelfServiceFlowVerificationUI(r.Context())).String() + flowURL = verificationFlow.AppendTo(e.r.Config().SelfServiceFlowVerificationUI(ctx)).String() } f.AddContinueWith(flow.NewContinueWithVerificationUI(verificationFlow, address.Value, flowURL)) diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go index b5342ea7dbc1..c06d47a216ba 100644 --- a/selfservice/hook/web_hook.go +++ b/selfservice/hook/web_hook.go @@ -7,11 +7,13 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" + "net/http/httputil" "time" - "github.com/ory/herodot" - + "github.com/gofrs/uuid" + "github.com/hashicorp/go-retryablehttp" "github.com/pkg/errors" "github.com/tidwall/gjson" "go.opentelemetry.io/otel/attribute" @@ -20,10 +22,7 @@ import ( "go.opentelemetry.io/otel/trace" grpccodes "google.golang.org/grpc/codes" - "github.com/ory/kratos/ui/node" - "github.com/ory/x/jsonnetsecure" - "github.com/ory/x/otelx" - + "github.com/ory/herodot" "github.com/ory/kratos/identity" "github.com/ory/kratos/request" "github.com/ory/kratos/schema" @@ -35,7 +34,11 @@ import ( "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/session" "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/kratos/x/events" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/otelx" ) var _ interface { @@ -221,10 +224,7 @@ func (e *WebHook) ExecutePostRegistrationPostPersistHook(_ http.ResponseWriter, // We want to decouple the request from the hook execution, so that the hooks still execute even // if the request is canceled. - var cancel context.CancelFunc - ctx := trace.ContextWithSpan(context.Background(), trace.SpanFromContext(req.Context())) - ctx, cancel = context.WithTimeout(ctx, 5*time.Minute) - defer cancel() + ctx := context.WithoutCancel(req.Context()) return otelx.WithSpan(ctx, "selfservice.hook.WebHook.ExecutePostRegistrationPostPersistHook", func(ctx context.Context) error { return e.execute(ctx, &templateContext{ @@ -288,6 +288,7 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error { ignoreResponse = gjson.GetBytes(e.conf, "response.ignore").Bool() canInterrupt = gjson.GetBytes(e.conf, "can_interrupt").Bool() parseResponse = gjson.GetBytes(e.conf, "response.parse").Bool() + emitEvent = gjson.GetBytes(e.conf, "emit_analytics_event").Bool() || !gjson.GetBytes(e.conf, "emit_analytics_event").Exists() // default true tracer = trace.SpanFromContext(ctx).TracerProvider().Tracer("kratos-webhooks") ) if ignoreResponse && (parseResponse || canInterrupt) { @@ -296,27 +297,30 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error { makeRequest := func() (finalErr error) { if ignoreResponse { - // This is one of the few places where spawning a context.Background() is ok. We need to do this - // because the function runs asynchronously and we don't want to cancel the request if the - // incoming request context is cancelled. + // This means we want to run this closure asynchronously and not be + // canceled when the parent context is canceled. // - // The webhook will still cancel after 30 seconds as that is the configured timeout for the HTTP client. - var cancel context.CancelFunc - ctx = trace.ContextWithSpan(context.Background(), trace.SpanFromContext(ctx)) - ctx, cancel = context.WithTimeout(ctx, 5*time.Minute) - defer cancel() + // The webhook will still cancel after 30 seconds as that is the + // configured timeout for the HTTP client. + ctx = context.WithoutCancel(ctx) } ctx, span := tracer.Start(ctx, "selfservice.webhook") defer otelx.End(span, &finalErr) - startTime := time.Now() - defer func() { + if emitEvent { + instrumentHTTPClientForEvents(ctx, httpClient) + } + + defer func(startTime time.Time) { traceID, spanID := span.SpanContext().TraceID(), span.SpanContext().SpanID() logger := e.deps.Logger().WithField("otel", map[string]string{ "trace_id": traceID.String(), "span_id": spanID.String(), }).WithField("duration", time.Since(startTime)) if finalErr != nil { + if emitEvent && !errors.Is(finalErr, context.Canceled) { + span.AddEvent(events.NewWebhookFailed(ctx, finalErr)) + } if ignoreResponse { logger.WithError(finalErr).Warning("Webhook request failed but the error was ignored because the configuration indicated that the upstream response should be ignored") } else { @@ -324,8 +328,11 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error { } } else { logger.Info("Webhook request succeeded") + if emitEvent { + span.AddEvent(events.NewWebhookSucceeded(ctx)) + } } - }() + }(time.Now()) builder, err := request.NewBuilder(ctx, e.conf, e.deps) if err != nil { @@ -372,6 +379,7 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error { return errors.WithStack(err) } defer resp.Body.Close() + resp.Body = io.NopCloser(io.LimitReader(resp.Body, 5<<20)) // read at most 5 MB from the response span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...) if resp.StatusCode >= http.StatusBadRequest { @@ -499,3 +507,23 @@ func isTimeoutError(err error) bool { var te interface{ Timeout() bool } return errors.As(err, &te) && te.Timeout() || errors.Is(err, context.DeadlineExceeded) } + +func instrumentHTTPClientForEvents(ctx context.Context, httpClient *retryablehttp.Client) { + var ( + attempt = 0 + requestID uuid.UUID + reqBody []byte + ) + httpClient.RequestLogHook = func(_ retryablehttp.Logger, req *http.Request, retryNumber int) { + attempt = retryNumber + 1 + requestID = uuid.Must(uuid.NewV4()) + req.Header.Set("Ory-Webhook-Request-ID", requestID.String()) + reqBody, _ = httputil.DumpRequestOut(req, true) + } + httpClient.ResponseLogHook = func(_ retryablehttp.Logger, res *http.Response) { + res.Body = io.NopCloser(io.LimitReader(res.Body, 5<<20)) // read at most 5 MB from the response + resBody, _ := httputil.DumpResponse(res, true) + resBody = resBody[:min(len(resBody), 2<<10)] // truncate response body to 2 kB for event + trace.SpanFromContext(ctx).AddEvent(events.NewWebhookDelivered(ctx, res.Request.URL, reqBody, res.StatusCode, resBody, attempt, requestID)) + } +} diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go index ebff7305c437..447ddbea4ef8 100644 --- a/selfservice/hook/web_hook_integration_test.go +++ b/selfservice/hook/web_hook_integration_test.go @@ -19,39 +19,33 @@ import ( "testing" "time" - "github.com/ory/x/snapshotx" - + "github.com/julienschmidt/httprouter" "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/ory/kratos/schema" - "github.com/ory/kratos/text" - "github.com/ory/x/jsonnetsecure" - "github.com/ory/x/otelx" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + "golang.org/x/exp/slices" "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" - "github.com/ory/kratos/selfservice/hook" - "github.com/ory/kratos/ui/node" - "github.com/ory/x/logrusx" - + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/recovery" "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/flow" - - "github.com/julienschmidt/httprouter" - - "github.com/ory/kratos/identity" - "github.com/ory/kratos/x" - + "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/session" - - "github.com/ory/kratos/selfservice/flow/login" - - "github.com/stretchr/testify/assert" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/snapshotx" ) func TestWebHooks(t *testing.T) { @@ -1131,3 +1125,175 @@ func TestAsyncWebhook(t *testing.T) { } require.True(t, found) } + +func TestWebhookEvents(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) + logger := logrusx.New("kratos", "test") + whDeps := struct { + x.SimpleLoggerWithClient + *jsonnetsecure.TestProvider + }{ + x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})}, + jsonnetsecure.NewTestProvider(t), + } + + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + Host: "www.ory.sh", + TLS: new(tls.ConnectionState), + URL: &url.URL{Path: "/some_end_point"}, + Method: http.MethodPost, + } + s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}} + _ = s + f := &login.Flow{ID: x.NewUUID()} + + webhookReceiver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/ok" { + w.WriteHeader(200) + w.Write([]byte("ok")) + } else { + w.WriteHeader(400) + w.Write([]byte("fail")) + } + })) + t.Cleanup(webhookReceiver.Close) + + t.Run("success", func(t *testing.T) { + wh := hook.NewWebHook(&whDeps, json.RawMessage(fmt.Sprintf(` + { + "url": %q, + "method": "GET", + "body": "file://stub/test_body.jsonnet", + "response": { + "ignore": false, + "parse": false + } + }`, webhookReceiver.URL+"/ok"))) + + recorder := tracetest.NewSpanRecorder() + tracer := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)).Tracer("test") + ctx, span := tracer.Start(context.Background(), "parent") + defer span.End() + + r1 := req.Clone(ctx) + + require.NoError(t, wh.ExecuteLoginPreHook(nil, r1, f)) + + ended := recorder.Ended() + require.NotEmpty(t, ended) + + i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { + return sp.Name() == "selfservice.webhook" + }) + require.GreaterOrEqual(t, i, 0) + + events := ended[i].Events() + i = slices.IndexFunc(events, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookDelivered" + }) + require.GreaterOrEqual(t, i, 0) + + i = slices.IndexFunc(events, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookSucceeded" + }) + require.GreaterOrEqual(t, i, 0) + + i = slices.IndexFunc(events, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookFailed" + }) + require.Equal(t, -1, i) + }) + + t.Run("failed", func(t *testing.T) { + wh := hook.NewWebHook(&whDeps, json.RawMessage(fmt.Sprintf(` + { + "url": %q, + "method": "GET", + "body": "file://stub/test_body.jsonnet", + "response": { + "ignore": false, + "parse": false + } + }`, webhookReceiver.URL+"/fail"))) + + recorder := tracetest.NewSpanRecorder() + tracer := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)).Tracer("test") + ctx, span := tracer.Start(context.Background(), "parent") + defer span.End() + + r1 := req.Clone(ctx) + require.Error(t, wh.ExecuteLoginPreHook(nil, r1, f)) + + ended := recorder.Ended() + require.NotEmpty(t, ended) + + i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { + return sp.Name() == "selfservice.webhook" + }) + require.GreaterOrEqual(t, i, 0) + + events := ended[i].Events() + i = slices.IndexFunc(events, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookDelivered" + }) + require.GreaterOrEqual(t, i, 0) + + i = slices.IndexFunc(events, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookFailed" + }) + require.GreaterOrEqual(t, i, 0) + + i = slices.IndexFunc(events, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookSucceeded" + }) + require.Equal(t, i, -1) + }) + + t.Run("event disabled", func(t *testing.T) { + wh := hook.NewWebHook(&whDeps, json.RawMessage(fmt.Sprintf(` + { + "url": %q, + "method": "GET", + "body": "file://stub/test_body.jsonnet", + "response": { + "ignore": false, + "parse": false + }, + "emit_analytics_event": false + }`, webhookReceiver.URL+"/fail"))) + + recorder := tracetest.NewSpanRecorder() + tracer := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)).Tracer("test") + ctx, span := tracer.Start(context.Background(), "parent") + defer span.End() + + r1 := req.Clone(ctx) + require.Error(t, wh.ExecuteLoginPreHook(nil, r1, f)) + + ended := recorder.Ended() + require.NotEmpty(t, ended) + + i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { + return sp.Name() == "selfservice.webhook" + }) + require.GreaterOrEqual(t, i, 0) + + events := ended[i].Events() + i = slices.IndexFunc(events, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookDelivered" + }) + require.Equal(t, -1, i) + + i = slices.IndexFunc(events, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookFailed" + }) + require.Equal(t, -1, i) + + i = slices.IndexFunc(events, func(ev sdktrace.Event) bool { + return ev.Name == "WebhookSucceeded" + }) + require.Equal(t, i, -1) + }) +} diff --git a/selfservice/strategy/code/code_sender.go b/selfservice/strategy/code/code_sender.go index d3667aea3b07..3317fafdb2e4 100644 --- a/selfservice/strategy/code/code_sender.go +++ b/selfservice/strategy/code/code_sender.go @@ -252,7 +252,7 @@ func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, s.deps.Audit(). WithField("via", via). WithField("strategy", "code"). - WithSensitiveField("email_address", address). + WithSensitiveField("email_address", to). WithField("was_notified", notifyUnknownRecipients). Info("Address verification was requested for an unknown address.") if !notifyUnknownRecipients { diff --git a/selfservice/strategy/code/test/persistence.go b/selfservice/strategy/code/test/persistence.go index f505bcb9c185..f3c120402ddb 100644 --- a/selfservice/strategy/code/test/persistence.go +++ b/selfservice/strategy/code/test/persistence.go @@ -14,7 +14,7 @@ import ( "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/x/randx" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/selfservice/strategy/link/sender.go b/selfservice/strategy/link/sender.go index 99b6f22732fc..d58f167335d7 100644 --- a/selfservice/strategy/link/sender.go +++ b/selfservice/strategy/link/sender.go @@ -123,7 +123,7 @@ func (s *Sender) SendVerificationLink(ctx context.Context, f *verification.Flow, s.r.Audit(). WithField("via", via). WithField("strategy", "link"). - WithSensitiveField("email_address", address). + WithSensitiveField("email_address", to). WithField("was_notified", notifyUnknownRecipients). Info("Address verification was requested for an unknown address.") if !notifyUnknownRecipients { diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go index 71518cda33be..d470d8af6ce3 100644 --- a/selfservice/strategy/link/strategy_recovery_test.go +++ b/selfservice/strategy/link/strategy_recovery_test.go @@ -642,7 +642,7 @@ func TestRecovery(t *testing.T) { cl.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } - res, err := do(cl, x.NewTestHTTPRequest(t, "GET", recoveryLink, nil)) + res, err := do(cl, testhelpers.NewTestHTTPRequest(t, "GET", recoveryLink, nil)) require.NoError(t, err) require.NoError(t, res.Body.Close()) assert.Equal(t, http.StatusSeeOther, res.StatusCode) diff --git a/selfservice/strategy/link/test/persistence.go b/selfservice/strategy/link/test/persistence.go index 63abe6179f18..af5738eaae31 100644 --- a/selfservice/strategy/link/test/persistence.go +++ b/selfservice/strategy/link/test/persistence.go @@ -14,7 +14,7 @@ import ( "github.com/ory/kratos/selfservice/strategy/link" "github.com/ory/x/sqlcon" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json index 9a93294a6040..04eba3e43565 100644 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json @@ -36,6 +36,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010002, + "text": "Sign in with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, { "type": "input", "group": "oidc", diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json index 699d8d10ec62..e9b5dc03f8e6 100644 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json @@ -36,6 +36,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, { "type": "input", "group": "oidc", diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 70fa10585d18..8cf7b5069866 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -11,13 +11,17 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "path/filepath" "strings" + "github.com/ory/x/urlx" + "go.opentelemetry.io/otel/attribute" "golang.org/x/oauth2" "github.com/ory/kratos/cipher" + "github.com/ory/kratos/selfservice/flowhelpers" "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/x/jsonnetsecure" "github.com/ory/x/otelx" @@ -481,15 +485,44 @@ func (s *Strategy) ExchangeCode(ctx context.Context, provider Provider, code str return token, err } -func (s *Strategy) populateMethod(r *http.Request, c *container.Container, message func(provider string) *text.Message) error { +func (s *Strategy) populateMethod(r *http.Request, f flow.Flow, message func(provider string) *text.Message) error { conf, err := s.Config(r.Context()) if err != nil { return err } + providers := conf.Providers + + if lf, ok := f.(*login.Flow); ok && lf.IsForced() { + if _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID()); id != nil { + if c == nil { + // no OIDC credentials, don't add any providers + providers = nil + } else { + var credentials identity.CredentialsOIDC + if err := json.Unmarshal(c.Config, &credentials); err != nil { + // failed to read OIDC credentials, don't add any providers + providers = nil + } else { + // add only providers that can actually be used to log in as this identity + providers = make([]Configuration, 0, len(conf.Providers)) + for i := range conf.Providers { + for j := range credentials.Providers { + if conf.Providers[i].ID == credentials.Providers[j].Provider { + providers = append(providers, conf.Providers[i]) + break + } + } + } + } + } + } + } + // does not need sorting because there is only one field + c := f.GetUI() c.SetCSRF(s.d.GenerateCSRFToken(r)) - AddProviders(c, conf.Providers, message) + AddProviders(c, providers, message) return nil } @@ -544,15 +577,50 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl // This is kinda hacky and will probably need to be updated at some point. if dup := new(identity.ErrDuplicateCredentials); errors.As(err, &dup) { - rf.UI.Messages.Add(text.NewErrorValidationDuplicateCredentialsOnOIDCLink()) + err = schema.NewDuplicateCredentialsError(dup) + + if validationErr := new(schema.ValidationError); errors.As(err, &validationErr) { + for _, m := range validationErr.Messages { + m := m + rf.UI.Messages.Add(&m) + } + } else { + rf.UI.Messages.Add(text.NewErrorValidationDuplicateCredentialsOnOIDCLink()) + } + lf, err := s.registrationToLogin(w, r, rf, provider) if err != nil { return err } // return a new login flow with the error message embedded in the login flow. - x.AcceptToRedirectOrJSON(w, r, s.d.Writer(), lf, lf.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())).String()) + redirectURL := lf.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())) + if dc, err := flow.DuplicateCredentials(lf); err == nil && dc != nil { + redirectURL = urlx.CopyWithQuery(redirectURL, url.Values{"no_org_ui": {"true"}}) + + for i, n := range lf.UI.Nodes { + if n.Meta == nil || n.Meta.Label == nil { + continue + } + switch n.Meta.Label.ID { + case text.InfoSelfServiceLogin: + lf.UI.Nodes[i].Meta.Label = text.NewInfoLoginAndLink() + case text.InfoSelfServiceLoginWith: + p := gjson.GetBytes(n.Meta.Label.Context, "provider").String() + lf.UI.Nodes[i].Meta.Label = text.NewInfoLoginWithAndLink(p) + } + } + + newLoginURL := s.d.Config().SelfServiceFlowLoginUI(r.Context()).String() + lf.UI.Messages.Add(text.NewInfoLoginLinkMessage(dc.DuplicateIdentifier, provider, newLoginURL)) + + err := s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), lf) + if err != nil { + return err + } + } + x.AcceptToRedirectOrJSON(w, r, s.d.Writer(), lf, redirectURL.String()) // ensure the function does not continue to execute - return registration.ErrHookAbortFlow + return flow.ErrCompletedByStrategy } rf.UI.Nodes = node.Nodes{} @@ -630,3 +698,40 @@ func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provid return claims, nil } + +func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, idToken, accessToken, refreshToken, provider, subject, organization string) error { + if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, identity.ExpandCredentials); err != nil { + return err + } + var conf identity.CredentialsOIDC + creds, err := i.ParseCredentials(s.ID(), &conf) + if errors.Is(err, herodot.ErrNotFound) { + var err error + if creds, err = identity.NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, organization); err != nil { + return err + } + } else if err != nil { + return err + } else { + creds.Identifiers = append(creds.Identifiers, identity.OIDCUniqueID(provider, subject)) + conf.Providers = append(conf.Providers, identity.CredentialsOIDCProvider{ + Subject: subject, Provider: provider, + InitialAccessToken: accessToken, + InitialRefreshToken: refreshToken, + InitialIDToken: idToken, + Organization: organization, + }) + + creds.Config, err = json.Marshal(conf) + if err != nil { + return err + } + } + + i.Credentials[s.ID()] = *creds + if orgID, err := uuid.FromString(organization); err == nil { + i.OrganizationID = uuid.NullUUID{UUID: orgID, Valid: true} + } + + return nil +} diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index c81537015bc9..a3048df1e15b 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -48,7 +48,7 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.Au return nil } - return s.populateMethod(r, l.UI, text.NewInfoLoginWith) + return s.populateMethod(r, l, text.NewInfoLoginWith) } // Update Login Flow with OpenID Connect Method diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index b6747ef9ad0a..d3f3b217f760 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -62,7 +62,7 @@ func (s *Strategy) RegisterRegistrationRoutes(r *x.RouterPublic) { } func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { - return s.populateMethod(r, f.UI, text.NewInfoRegistrationWith) + return s.populateMethod(r, f, text.NewInfoRegistrationWith) } // Update Registration Flow with OpenID Connect Method @@ -261,6 +261,8 @@ func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, r opts = append(opts, login.WithFormErrorMessage(rf.UI.Messages)) } + opts = append(opts, login.WithInternalContext(rf.InternalContext)) + lf, _, err := s.d.LoginHandler().NewLoginFlow(w, r, rf.Type, opts...) if err != nil { return nil, err diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go index 513f5cccdabe..a38e4e0b40d9 100644 --- a/selfservice/strategy/oidc/strategy_settings.go +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -11,6 +11,8 @@ import ( "net/http" "time" + "github.com/ory/x/sqlxx" + "github.com/tidwall/sjson" "golang.org/x/oauth2" @@ -412,31 +414,10 @@ func (s *Strategy) linkProvider(w http.ResponseWriter, r *http.Request, ctxUpdat return s.handleSettingsError(w, r, ctxUpdate, p, err) } - var conf identity.CredentialsOIDC - creds, err := i.ParseCredentials(s.ID(), &conf) - if errors.Is(err, herodot.ErrNotFound) { - var err error - if creds, err = identity.NewCredentialsOIDC(it, cat, crt, provider.Config().ID, claims.Subject, ""); err != nil { - return s.handleSettingsError(w, r, ctxUpdate, p, err) - } - } else if err != nil { + if err := s.linkCredentials(r.Context(), i, it, cat, crt, provider.Config().ID, claims.Subject, provider.Config().OrganizationID); err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) - } else { - creds.Identifiers = append(creds.Identifiers, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)) - conf.Providers = append(conf.Providers, identity.CredentialsOIDCProvider{ - Subject: claims.Subject, Provider: provider.Config().ID, - InitialAccessToken: cat, - InitialRefreshToken: crt, - InitialIDToken: it, - }) - - creds.Config, err = json.Marshal(conf) - if err != nil { - return s.handleSettingsError(w, r, ctxUpdate, p, err) - } } - i.Credentials[s.ID()] = *creds if err := s.d.SettingsHookExecutor().PostSettingsHook(w, r, s.SettingsStrategyID(), ctxUpdate, i, settings.WithCallback(func(ctxUpdate *settings.UpdateContext) error { return s.PopulateSettingsMethod(r, ctxUpdate.Session.Identity, ctxUpdate.Flow) })); err != nil { @@ -533,3 +514,34 @@ func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, c return err } + +func (s *Strategy) Link(ctx context.Context, i *identity.Identity, credentialsConfig sqlxx.JSONRawMessage) error { + var credentialsOIDCConfig identity.CredentialsOIDC + if err := json.Unmarshal(credentialsConfig, &credentialsOIDCConfig); err != nil { + return err + } + if len(credentialsOIDCConfig.Providers) != 1 { + return errors.New("No oidc provider was set") + } + var credentialsOIDCProvider = credentialsOIDCConfig.Providers[0] + + if err := s.linkCredentials( + ctx, + i, + credentialsOIDCProvider.InitialIDToken, + credentialsOIDCProvider.InitialAccessToken, + credentialsOIDCProvider.InitialRefreshToken, + credentialsOIDCProvider.Provider, + credentialsOIDCProvider.Subject, + credentialsOIDCProvider.Organization, + ); err != nil { + return err + } + + options := []identity.ManagerOption{identity.ManagerAllowWriteProtectedTraits} + if err := s.d.IdentityManager().Update(ctx, i, options...); err != nil { + return err + } + + return nil +} diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go index 69f2bc03a560..a6b819c94202 100644 --- a/selfservice/strategy/oidc/strategy_settings_test.go +++ b/selfservice/strategy/oidc/strategy_settings_test.go @@ -327,7 +327,17 @@ func TestSettingsStrategy(t *testing.T) { _, res, req := unlink(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login") - rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() + fa := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi + lf, _, err := fa.GetLoginFlow(context.Background()).Id(res.Request.URL.Query()["flow"][0]).Execute() + require.NoError(t, err) + + for _, node := range lf.Ui.Nodes { + if node.Group == "oidc" && node.Attributes.UiNodeInputAttributes.Name == "provider" { + assert.Contains(t, []string{"ory", "github"}, node.Attributes.UiNodeInputAttributes.Value) + } + } + + rs, _, err := fa.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) require.EqualValues(t, flow.StateShowForm, rs.State) @@ -554,7 +564,17 @@ func TestSettingsStrategy(t *testing.T) { _, res, req := link(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login") - rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() + fa := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi + lf, _, err := fa.GetLoginFlow(context.Background()).Id(res.Request.URL.Query()["flow"][0]).Execute() + require.NoError(t, err) + + for _, node := range lf.Ui.Nodes { + if node.Group == "oidc" && node.Attributes.UiNodeInputAttributes.Name == "provider" { + assert.Contains(t, []string{"ory", "github"}, node.Attributes.UiNodeInputAttributes.Value) + } + } + + rs, _, err := fa.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) require.EqualValues(t, flow.StateShowForm, rs.State) diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 5d0a542ea7bd..cdbff20e0477 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -13,10 +13,13 @@ import ( "net/http/cookiejar" "net/http/httptest" "net/url" + "strconv" "strings" "testing" "time" + "github.com/ory/x/sqlxx" + "github.com/ory/kratos/hydra" "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/kratos/session" @@ -75,6 +78,7 @@ func TestStrategy(t *testing.T) { t, conf, newOIDCProvider(t, ts, remotePublic, remoteAdmin, "valid"), + newOIDCProvider(t, ts, remotePublic, remoteAdmin, "secondProvider"), oidc.Configuration{ Provider: "generic", ID: "invalid-issuer", @@ -217,8 +221,8 @@ func TestStrategy(t *testing.T) { var assertSystemErrorWithReason = func(t *testing.T, res *http.Response, body []byte, code int, reason string) { require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body) - assert.Equal(t, int64(code), gjson.GetBytes(body, "code").Int(), "%s", body) - assert.Contains(t, gjson.GetBytes(body, "reason").String(), reason, "%s", body) + assert.Equal(t, int64(code), gjson.GetBytes(body, "code").Int(), "%s", prettyJSON(t, body)) + assert.Contains(t, gjson.GetBytes(body, "reason").String(), reason, "%s", prettyJSON(t, body)) } // assert system error (redirect to error endpoint) @@ -232,15 +236,15 @@ func TestStrategy(t *testing.T) { // assert ui error (redirect to login/registration ui endpoint) var assertUIError = func(t *testing.T, res *http.Response, body []byte, reason string) { require.Contains(t, res.Request.URL.String(), uiTS.URL, "status: %d, body: %s", res.StatusCode, body) - assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), reason, "%s", body) + assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), reason, "%s", prettyJSON(t, body)) } // assert identity (success) var assertIdentity = func(t *testing.T, res *http.Response, body []byte) { - assert.Contains(t, res.Request.URL.String(), returnTS.URL) - assert.Equal(t, subject, gjson.GetBytes(body, "identity.traits.subject").String(), "%s", body) - assert.Equal(t, claims.traits.website, gjson.GetBytes(body, "identity.traits.website").String(), "%s", body) - assert.Equal(t, claims.metadataPublic.picture, gjson.GetBytes(body, "identity.metadata_public.picture").String(), "%s", body) + assert.Contains(t, res.Request.URL.String(), returnTS.URL, "%s", body) + assert.Equal(t, subject, gjson.GetBytes(body, "identity.traits.subject").String(), "%s", prettyJSON(t, body)) + assert.Equal(t, claims.traits.website, gjson.GetBytes(body, "identity.traits.website").String(), "%s", prettyJSON(t, body)) + assert.Equal(t, claims.metadataPublic.picture, gjson.GetBytes(body, "identity.metadata_public.picture").String(), "%s", prettyJSON(t, body)) } var newLoginFlow = func(t *testing.T, requestURL string, exp time.Duration, flowType flow.Type) (req *login.Flow) { @@ -433,23 +437,25 @@ func TestStrategy(t *testing.T) { }) }) + expectTokens := func(t *testing.T, provider string, body []byte) uuid.UUID { + id := uuid.FromStringOrNil(gjson.GetBytes(body, "identity.id").String()) + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), id) + require.NoError(t, err) + c := i.Credentials[identity.CredentialsTypeOIDC].Config + assert.NotEmpty(t, gjson.GetBytes(c, "providers.0.initial_access_token").String()) + assertx.EqualAsJSONExcept( + t, + json.RawMessage(fmt.Sprintf(`{"providers": [{"subject":"%s","provider":"%s"}]}`, subject, provider)), + json.RawMessage(c), + []string{"providers.0.initial_id_token", "providers.0.initial_access_token", "providers.0.initial_refresh_token"}, + ) + return id + } + t.Run("case=register and then login", func(t *testing.T) { subject = "register-then-login@ory.sh" scope = []string{"openid", "offline"} - expectTokens := func(t *testing.T, provider string, body []byte) { - i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), uuid.FromStringOrNil(gjson.GetBytes(body, "identity.id").String())) - require.NoError(t, err) - c := i.Credentials[identity.CredentialsTypeOIDC].Config - assert.NotEmpty(t, gjson.GetBytes(c, "providers.0.initial_access_token").String()) - assertx.EqualAsJSONExcept( - t, - json.RawMessage(fmt.Sprintf(`{"providers": [{"subject":"%s","provider":"%s"}]}`, subject, provider)), - json.RawMessage(c), - []string{"providers.0.initial_id_token", "providers.0.initial_access_token", "providers.0.initial_refresh_token"}, - ) - } - t.Run("case=should pass registration", func(t *testing.T) { r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") @@ -879,7 +885,7 @@ func TestStrategy(t *testing.T) { r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") res, body := makeRequest(t, "valid", action, url.Values{}) - assertUIError(t, res, body, "An account with the same identifier (email, phone, username, ...) exists already. Please sign in to your existing account and link your social profile in the settings page.") + assertUIError(t, res, body, "An account with the same identifier (email, phone, username, ...) exists already.") require.Contains(t, gjson.GetBytes(body, "ui.action").String(), "/self-service/login") }) @@ -1068,6 +1074,158 @@ func TestStrategy(t *testing.T) { }) }) + t.Run("case=registration should start new login flow if duplicate credentials detected", func(t *testing.T) { + require.NoError(t, reg.Config().Set(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true)) + loginWithOIDC := func(t *testing.T, c *http.Client, flowID uuid.UUID, provider string) (*http.Response, []byte) { + action := assertFormValues(t, flowID, provider) + res, err := c.PostForm(action, url.Values{"provider": {provider}}) + require.NoError(t, err, action) + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close()) + require.NoError(t, err) + return res, body + } + + checkCredentialsLinked := func(res *http.Response, body []byte, identityID uuid.UUID, provider string) { + assert.Contains(t, res.Request.URL.String(), returnTS.URL, "%s", body) + assert.Equal(t, strings.ToLower(subject), gjson.GetBytes(body, "identity.traits.subject").String(), "%s", body) + i, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, identityID) + require.NoError(t, err) + assert.NotEmpty(t, i.Credentials["oidc"], "%+v", i.Credentials) + assert.Equal(t, provider, gjson.GetBytes(i.Credentials["oidc"].Config, "providers.0.provider").String(), + "%s", string(i.Credentials["oidc"].Config[:])) + assert.Contains(t, gjson.GetBytes(body, "authentication_methods").String(), "oidc", "%s", body) + } + + t.Run("case=second login is password", func(t *testing.T) { + subject = "new-login-if-email-exist-with-password-strategy@ory.sh" + subject2 := "new-login-subject2@ory.sh" + scope = []string{"openid"} + password := "lwkj52sdkjf" + + var i *identity.Identity + t.Run("step=create password identity", func(t *testing.T) { + i = identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + p, err := reg.Hasher(ctx).Generate(ctx, []byte(password)) + require.NoError(t, err) + i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Identifiers: []string{subject}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }) + i.Traits = identity.Traits(`{"subject":"` + subject + `"}`) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + + i2 := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i2.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Identifiers: []string{subject2}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }) + i2.Traits = identity.Traits(`{"subject":"` + subject2 + `"}`) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i2)) + }) + + client := testhelpers.NewClientWithCookieJar(t, nil, false) + loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + + var linkingLoginFlow struct { + ID string + UIAction string + CSRFToken string + } + + // To test that the subject is normalized properly + subject = strings.ToUpper(subject) + + t.Run("step=should fail login and start a new flow", func(t *testing.T) { + res, body := loginWithOIDC(t, client, loginFlow.ID, "valid") + assert.True(t, res.Request.URL.Query().Has("no_org_ui")) + assertUIError(t, res, body, "You tried signing in with new-login-if-email-exist-with-password-strategy@ory.sh which is already in use by another account. You can sign in using your password.") + assert.Equal(t, "password", gjson.GetBytes(body, "ui.messages.#(id==4000028).context.available_credential_types.0").String()) + assert.Equal(t, "new-login-if-email-exist-with-password-strategy@ory.sh", gjson.GetBytes(body, "ui.messages.#(id==4000028).context.credential_identifier_hint").String()) + linkingLoginFlow.ID = gjson.GetBytes(body, "id").String() + linkingLoginFlow.UIAction = gjson.GetBytes(body, "ui.action").String() + linkingLoginFlow.CSRFToken = gjson.GetBytes(body, `ui.nodes.#(attributes.name=="csrf_token").attributes.value`).String() + assert.NotEqual(t, loginFlow.ID.String(), linkingLoginFlow.ID, "should have started a new flow") + }) + + t.Run("step=should fail login if existing identity identifier doesn't match", func(t *testing.T) { + res, err := client.PostForm(linkingLoginFlow.UIAction, url.Values{ + "csrf_token": {linkingLoginFlow.CSRFToken}, + "method": {"password"}, + "identifier": {subject2}, + "password": {password}}) + require.NoError(t, err, linkingLoginFlow.UIAction) + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close()) + require.NoError(t, err) + assert.Equal(t, + strconv.Itoa(int(text.ErrorValidationLoginLinkedCredentialsDoNotMatch)), + gjson.GetBytes(body, "ui.messages.0.id").String(), + prettyJSON(t, body), + ) + }) + + t.Run("step=should link oidc credentials to existing identity", func(t *testing.T) { + res, err := client.PostForm(linkingLoginFlow.UIAction, url.Values{ + "csrf_token": {linkingLoginFlow.CSRFToken}, + "method": {"password"}, + "identifier": {subject}, + "password": {password}}) + require.NoError(t, err, linkingLoginFlow.UIAction) + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close()) + require.NoError(t, err) + checkCredentialsLinked(res, body, i.ID, "valid") + }) + }) + + t.Run("case=second login is OIDC", func(t *testing.T) { + email1 := "existing-oidc-identity-1@ory.sh" + email2 := "existing-oidc-identity-2@ory.sh" + scope = []string{"openid", "offline"} + + var identityID uuid.UUID + t.Run("step=create OIDC identity", func(t *testing.T) { + subject = email1 + r := newRegistrationFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + action := assertFormValues(t, r.ID, "secondProvider") + res, body := makeRequest(t, "secondProvider", action, url.Values{}) + assertIdentity(t, res, body) + identityID = expectTokens(t, "secondProvider", body) + + subject = email2 + r = newRegistrationFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + action = assertFormValues(t, r.ID, "valid") + res, body = makeRequest(t, "valid", action, url.Values{}) + assertIdentity(t, res, body) + expectTokens(t, "valid", body) + }) + + subject = email1 + client := testhelpers.NewClientWithCookieJar(t, nil, false) + loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) + var linkingLoginFlow struct{ ID string } + t.Run("step=should fail login and start a new login", func(t *testing.T) { + res, body := loginWithOIDC(t, client, loginFlow.ID, "valid") + assertUIError(t, res, body, "You tried signing in with existing-oidc-identity-1@ory.sh which is already in use by another account. You can sign in using social sign in. You can sign in using one of the following social sign in providers: Secondprovider.") + linkingLoginFlow.ID = gjson.GetBytes(body, "id").String() + assert.NotEqual(t, loginFlow.ID.String(), linkingLoginFlow.ID, "should have started a new flow") + }) + + subject = email2 + t.Run("step=should fail login if existing identity identifier doesn't match", func(t *testing.T) { + res, body := loginWithOIDC(t, client, uuid.Must(uuid.FromString(linkingLoginFlow.ID)), "valid") + assertUIError(t, res, body, "Linked credentials do not match.") + }) + + subject = email1 + t.Run("step=should link oidc credentials to existing identity", func(t *testing.T) { + res, body := loginWithOIDC(t, client, uuid.Must(uuid.FromString(linkingLoginFlow.ID)), "secondProvider") + checkCredentialsLinked(res, body, identityID, "secondProvider") + }) + }) + }) + t.Run("method=TestPopulateSignUpMethod", func(t *testing.T) { conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://foo/") @@ -1089,6 +1247,13 @@ func TestStrategy(t *testing.T) { }) } +func prettyJSON(t *testing.T, body []byte) string { + var out bytes.Buffer + require.NoError(t, json.Indent(&out, body, "", "\t")) + + return out.String() +} + func TestCountActiveFirstFactorCredentials(t *testing.T) { _, reg := internal.NewFastRegistryWithMocks(t) strategy := oidc.NewStrategy(reg) diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index c6357ff6920f..8d8879cce91c 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -151,7 +151,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("should return an error because the request does not exist", func(t *testing.T) { - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.Equal(t, int64(http.StatusNotFound), gjson.Get(actual, "code").Int(), "%s", actual) assert.Equal(t, "Not Found", gjson.Get(actual, "status").String(), "%s", actual) assert.Contains(t, gjson.Get(actual, "message").String(), "Unable to locate the resource", "%s", actual) @@ -159,7 +159,8 @@ func TestCompleteLogin(t *testing.T) { fakeFlow := &kratos.LoginFlow{ Ui: kratos.UiContainer{ - Action: publicTS.URL + login.RouteSubmitFlow + "?flow=" + x.NewUUID().String()}, + Action: publicTS.URL + login.RouteSubmitFlow + "?flow=" + x.NewUUID().String(), + }, } t.Run("type=api", func(t *testing.T) { @@ -229,7 +230,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("case=should have correct CSRF behavior", func(t *testing.T) { - var values = url.Values{ + values := url.Values{ "method": {"password"}, "csrf_token": {"invalid_token"}, "identifier": {"login-identifier-csrf-browser"}, @@ -300,7 +301,7 @@ func TestCompleteLogin(t *testing.T) { }) }) - var expectValidationError = func(t *testing.T, isAPI, refresh, isSPA bool, values func(url.Values)) string { + expectValidationError := func(t *testing.T, isAPI, refresh, isSPA bool, values func(url.Values)) string { return testhelpers.SubmitLoginForm(t, isAPI, nil, publicTS, values, isSPA, refresh, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), @@ -308,7 +309,7 @@ func TestCompleteLogin(t *testing.T) { } t.Run("should return an error because the credentials are invalid (user does not exist)", func(t *testing.T) { - var check = func(t *testing.T, body string, start time.Time) { + check := func(t *testing.T, body string, start time.Time) { delay := time.Since(start) minConfiguredDelay := conf.HasherArgon2(ctx).ExpectedDuration - conf.HasherArgon2(ctx).ExpectedDeviation assert.GreaterOrEqual(t, delay, minConfiguredDelay) @@ -317,7 +318,7 @@ func TestCompleteLogin(t *testing.T) { assert.Equal(t, text.NewErrorValidationInvalidCredentials().Text, gjson.Get(body, "ui.messages.0.text").String(), body) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("identifier", "identifier") v.Set("password", "password") } @@ -339,7 +340,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("should return an error because no identifier is set", func(t *testing.T) { - var check = func(t *testing.T, body string) { + check := func(t *testing.T, body string) { assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, gjson.Get(body, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", body) @@ -351,7 +352,7 @@ func TestCompleteLogin(t *testing.T) { assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==password).attributes.value").String()) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Del("identifier") v.Set("method", identity.CredentialsTypePassword.String()) v.Set("password", "password") @@ -371,7 +372,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("should return an error because no password is set", func(t *testing.T) { - var check = func(t *testing.T, body string) { + check := func(t *testing.T, body string) { assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, gjson.Get(body, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", body) @@ -384,7 +385,7 @@ func TestCompleteLogin(t *testing.T) { assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==password).attributes.value").String()) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("identifier", "identifier") v.Del("password") } @@ -399,7 +400,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("should return an error both identifier and password are missing", func(t *testing.T) { - var check = func(t *testing.T, body string) { + check := func(t *testing.T, body string) { assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, gjson.Get(body, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", body) @@ -412,7 +413,7 @@ func TestCompleteLogin(t *testing.T) { assert.Empty(t, gjson.Get(body, "ui.nodes.#(attributes.name==password).attributes.value").String()) } - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("password", "") v.Set("identifier", "") } @@ -431,7 +432,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("should return an error because the credentials are invalid (password not correct)", func(t *testing.T) { - var check = func(t *testing.T, body string) { + check := func(t *testing.T, body string) { assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, gjson.Get(body, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", body) @@ -449,7 +450,7 @@ func TestCompleteLogin(t *testing.T) { identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("identifier", identifier) v.Set("password", "not-password") } @@ -470,7 +471,7 @@ func TestCompleteLogin(t *testing.T) { identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("identifier", identifier) v.Set("password", pwd) } @@ -596,7 +597,7 @@ func TestCompleteLogin(t *testing.T) { assert.NotEmpty(t, st, "%s", body) t.Run("retry with different refresh", func(t *testing.T) { - c := &http.Client{Transport: x.NewTransportWithHeader(http.Header{"Authorization": {"Bearer " + st}})} + c := &http.Client{Transport: testhelpers.NewTransportWithHeader(t, http.Header{"Authorization": {"Bearer " + st}})} t.Run("redirect to returnTS if refresh is missing", func(t *testing.T) { res, err := c.Do(testhelpers.NewHTTPGetJSONRequest(t, publicTS.URL+login.RouteInitAPIFlow)) @@ -650,17 +651,17 @@ func TestCompleteLogin(t *testing.T) { t.Run("case=should return an error because not passing validation and reset previous errors and values", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/login.schema.json") - var check = func(t *testing.T, actual string) { + check := func(t *testing.T, actual string) { assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", actual) } - var checkFirst = func(t *testing.T, actual string) { + checkFirst := func(t *testing.T, actual string) { check(t, actual) assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==identifier).messages.0").String(), "Property identifier is missing.", "%s", actual) } - var checkSecond = func(t *testing.T, actual string) { + checkSecond := func(t *testing.T, actual string) { check(t, actual) assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==identifier).attributes.error")) @@ -670,13 +671,13 @@ func TestCompleteLogin(t *testing.T) { assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==password).messages.0").String(), "Property password is missing.", "%s", actual) } - var valuesFirst = func(v url.Values) url.Values { + valuesFirst := func(v url.Values) url.Values { v.Del("identifier") v.Set("password", x.NewUUID().String()) return v } - var valuesSecond = func(v url.Values) url.Values { + valuesSecond := func(v url.Values) url.Values { v.Set("identifier", "identifier") v.Del("password") return v @@ -709,8 +710,10 @@ func TestCompleteLogin(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false) - values := url.Values{"method": {"password"}, "identifier": {identifier}, - "password": {pwd}, "csrf_token": {x.FakeCSRFToken}}.Encode() + values := url.Values{ + "method": {"password"}, "identifier": {identifier}, + "password": {pwd}, "csrf_token": {x.FakeCSRFToken}, + }.Encode() body1, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values) assert.EqualValues(t, http.StatusOK, res.StatusCode) @@ -776,13 +779,13 @@ func TestCompleteLogin(t *testing.T) { identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("method", "password") v.Set("identifier", identifier) v.Set("password", pwd) } - var check = func(t *testing.T, body string) { + check := func(t *testing.T, body string) { assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, gjson.Get(body, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", body) @@ -801,7 +804,6 @@ func TestCompleteLogin(t *testing.T) { t.Run("type=api", func(t *testing.T) { check(t, expectValidationError(t, true, false, false, values)) }) - }) t.Run("should upgrade password not primary hashing algorithm", func(t *testing.T) { @@ -836,7 +838,7 @@ func TestCompleteLogin(t *testing.T) { }, })) - var values = func(v url.Values) { + values := func(v url.Values) { v.Set("identifier", identifier) v.Set("method", identity.CredentialsTypePassword.String()) v.Set("password", pwd) diff --git a/selfservice/strategy/password/op_helpers_test.go b/selfservice/strategy/password/op_helpers_test.go new file mode 100644 index 000000000000..824de913be69 --- /dev/null +++ b/selfservice/strategy/password/op_helpers_test.go @@ -0,0 +1,221 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package password_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "golang.org/x/oauth2" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + hydraclientgo "github.com/ory/hydra-client-go/v2" + "github.com/ory/x/logrusx" + "github.com/ory/x/resilience" + "github.com/ory/x/urlx" + + "github.com/phayes/freeport" +) + +type clientAppConfig struct { + client *oauth2.Config + expectToken bool + state *clientAppState +} + +type clientAppState struct { + visits int64 + tokens int64 +} + +type callTrace string + +const ( + RegistrationUI callTrace = "registration-ui" + RegistrationWithOAuth2LoginChallenge = "registration-with-oauth2-login-challenge" + RegistrationWithFlowID = "registration-with-flow-id" + LoginUI = "login-ui" + LoginWithOAuth2LoginChallenge = "login-with-oauth2-login-challenge" + LoginWithFlowID = "login-with-flow-id" + Consent = "consent" + ConsentWithChallenge = "consent-with-challenge" + ConsentAccept = "consent-accept" + ConsentSkip = "consent-skip" + ConsentClientSkip = "consent-client-skip" + CodeExchange = "code-exchange" + CodeExchangeWithToken = "code-exchange-with-token" +) + +type testContextKey string + +const ( + TestUIConfig testContextKey = "test-ui-config" + TestOAuthClientState = "test-oauth-client-state" +) + +type testConfig struct { + identifier string + password string + browserClient *http.Client + kratosPublicTS *httptest.Server + clientAppTS *httptest.Server + hydraAdminClient hydraclientgo.OAuth2Api + consentRemember bool + requestedScope []string + callTrace *[]callTrace +} + +func createHydraOAuth2ApiClient(url string) hydraclientgo.OAuth2Api { + configuration := hydraclientgo.NewConfiguration() + configuration.Host = urlx.ParseOrPanic(url).Host + configuration.Servers = hydraclientgo.ServerConfigurations{{URL: url}} + + return hydraclientgo.NewAPIClient(configuration).OAuth2Api +} + +func createOAuth2Client(t *testing.T, ctx context.Context, hydraAdmin hydraclientgo.OAuth2Api, redirectURIs []string, scope string, skipConsent bool) string { + t.Helper() + + clientName := "kratos-hydra-integration-test-client-1" + tokenEndpointAuthMethod := "client_secret_post" + clientSecret := "client-secret" + + c, r, err := hydraAdmin.CreateOAuth2Client(ctx).OAuth2Client( + hydraclientgo.OAuth2Client{ + ClientName: &clientName, + RedirectUris: redirectURIs, + Scope: &scope, + TokenEndpointAuthMethod: &tokenEndpointAuthMethod, + ClientSecret: &clientSecret, + SkipConsent: &skipConsent, + }, + ).Execute() + require.NoError(t, err) + require.Equal(t, r.StatusCode, http.StatusCreated) + return *c.ClientId +} + +func makeAuthCodeURL(t *testing.T, c *oauth2.Config, requestedClaims string, isForced bool) string { + t.Helper() + + var options []oauth2.AuthCodeOption + + if isForced { + options = append(options, oauth2.SetAuthURLParam("prompt", "login")) + } + if requestedClaims != "" { + options = append(options, oauth2.SetAuthURLParam("claims", requestedClaims)) + } + + state := fmt.Sprintf("%x", uuid.Must(uuid.NewV4())) + return c.AuthCodeURL(state, options...) +} + +func newHydra(t *testing.T, loginUI string, consentUI string) (hydraAdmin string, hydraPublic string) { + publicPort, err := freeport.GetFreePort() + require.NoError(t, err) + adminPort, err := freeport.GetFreePort() + require.NoError(t, err) + + pool, err := dockertest.NewPool("") + require.NoError(t, err) + + hydraResource, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "oryd/hydra", + Tag: "v2.2.0", + Env: []string{ + "DSN=memory", + fmt.Sprintf("URLS_SELF_ISSUER=http://127.0.0.1:%d/", publicPort), + "URLS_LOGIN=" + loginUI, + "URLS_CONSENT=" + consentUI, + "LOG_LEAK_SENSITIVE_VALUES=true", + "SECRETS_SYSTEM=someverylongsecretthatis32byteslong", + }, + Cmd: []string{"serve", "all", "--dev"}, + ExposedPorts: []string{"4444/tcp", "4445/tcp"}, + PortBindings: map[docker.Port][]docker.PortBinding{ + "4444/tcp": {{HostPort: fmt.Sprintf("%d/tcp", publicPort)}}, + "4445/tcp": {{HostPort: fmt.Sprintf("%d/tcp", adminPort)}}, + }, + }) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, hydraResource.Close()) + }) + + require.NoError(t, hydraResource.Expire(uint(60*5))) + + require.NotEmpty(t, hydraResource.GetPort("4444/tcp"), "%+v", hydraResource.Container.NetworkSettings.Ports) + require.NotEmpty(t, hydraResource.GetPort("4445/tcp"), "%+v", hydraResource.Container) + + hydraPublic = "http://127.0.0.1:" + hydraResource.GetPort("4444/tcp") + hydraAdmin = "http://127.0.0.1:" + hydraResource.GetPort("4445/tcp") + + go pool.Client.Logs(docker.LogsOptions{ + ErrorStream: TestLogWriter{T: t, streamName: "hydra-stderr"}, + OutputStream: TestLogWriter{T: t, streamName: "hydra-stdout"}, + Stdout: false, + Stderr: true, + Follow: true, + Container: hydraResource.Container.ID, + }) + hl := logrusx.New("hydra-ready-check", "hydra-ready-check") + err = resilience.Retry(hl, time.Second*1, time.Second*5, func() error { + pr := hydraPublic + "/health/ready" + res, err := http.DefaultClient.Get(pr) + if err != nil || res.StatusCode != 200 { + return errors.Errorf("Hydra public is not ready at " + pr) + } + + ar := hydraAdmin + "/health/ready" + res, err = http.DefaultClient.Get(ar) + if err != nil && res.StatusCode != 200 { + return errors.Errorf("Hydra admin is not ready at " + ar) + } else { + return nil + } + }) + require.NoError(t, err) + + t.Logf("Ory Hydra running at: %s %s", hydraPublic, hydraAdmin) + + return hydraAdmin, hydraPublic +} + +type TestLogWriter struct { + streamName string + *testing.T +} + +func (t TestLogWriter) Write(p []byte) (int, error) { + t.Logf("[%d bytes @ %s]:\n\n%s\n", len(p), t.streamName, string(p)) + return len(p), nil +} + +func doOAuthFlow(t *testing.T, ctx context.Context, oauthClient *oauth2.Config, browserClient *http.Client) { + t.Helper() + + authCodeURL := makeAuthCodeURL(t, oauthClient, "", false) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, authCodeURL, nil) + require.NoError(t, err) + res, err := browserClient.Do(req) + require.NoError(t, err) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.NoError(t, res.Body.Close()) + require.Equal(t, "", string(body)) + require.Equal(t, http.StatusOK, res.StatusCode) +} diff --git a/selfservice/strategy/password/op_login_test.go b/selfservice/strategy/password/op_login_test.go index 21e81395d62d..64232072d736 100644 --- a/selfservice/strategy/password/op_login_test.go +++ b/selfservice/strategy/password/op_login_test.go @@ -7,27 +7,19 @@ import ( "context" _ "embed" "fmt" - "io" "net/http" "net/url" + "strings" "sync/atomic" "testing" - "time" - "github.com/phayes/freeport" - "github.com/pkg/errors" + "github.com/julienschmidt/httprouter" "github.com/tidwall/gjson" + "github.com/urfave/negroni" "golang.org/x/oauth2" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" - "github.com/gofrs/uuid" - "github.com/ory/x/logrusx" - "github.com/ory/x/resilience" - "github.com/ory/x/urlx" - hydraclientgo "github.com/ory/hydra-client-go/v2" "github.com/stretchr/testify/assert" @@ -38,131 +30,10 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/x" ) -func createHydraOAuth2ApiClient(url string) hydraclientgo.OAuth2Api { - configuration := hydraclientgo.NewConfiguration() - configuration.Host = urlx.ParseOrPanic(url).Host - configuration.Servers = hydraclientgo.ServerConfigurations{{URL: url}} - - return hydraclientgo.NewAPIClient(configuration).OAuth2Api -} - -func createOAuth2Client(t *testing.T, ctx context.Context, hydraAdmin hydraclientgo.OAuth2Api, redirectURIs []string, scope string) string { - clientName := "kratos-hydra-integration-test-client-1" - tokenEndpointAuthMethod := "client_secret_post" - clientSecret := "client-secret" - - c, r, err := hydraAdmin.CreateOAuth2Client(ctx).OAuth2Client( - hydraclientgo.OAuth2Client{ - ClientName: &clientName, - RedirectUris: redirectURIs, - Scope: &scope, - TokenEndpointAuthMethod: &tokenEndpointAuthMethod, - ClientSecret: &clientSecret, - }, - ).Execute() - require.NoError(t, err) - require.Equal(t, r.StatusCode, http.StatusCreated) - return *c.ClientId -} - -func makeAuthCodeURL(t *testing.T, c *oauth2.Config, requestedClaims string, isForced bool) string { - var options []oauth2.AuthCodeOption - - if isForced { - options = append(options, oauth2.SetAuthURLParam("prompt", "login")) - } - if requestedClaims != "" { - options = append(options, oauth2.SetAuthURLParam("claims", requestedClaims)) - } - - state := fmt.Sprintf("%x", uuid.Must(uuid.NewV4())) - return c.AuthCodeURL(state, options...) -} - -func newHydra(t *testing.T, loginUI string, consentUI string) (hydraAdmin string, hydraPublic string) { - publicPort, err := freeport.GetFreePort() - require.NoError(t, err) - adminPort, err := freeport.GetFreePort() - require.NoError(t, err) - - pool, err := dockertest.NewPool("") - require.NoError(t, err) - - hydraResource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "oryd/hydra", - Tag: "v2.2.0", - Env: []string{ - "DSN=memory", - fmt.Sprintf("URLS_SELF_ISSUER=http://127.0.0.1:%d/", publicPort), - "URLS_LOGIN=" + loginUI, - "URLS_CONSENT=" + consentUI, - "LOG_LEAK_SENSITIVE_VALUES=true", - "SECRETS_SYSTEM=someverylongsecretthatis32byteslong", - }, - Cmd: []string{"serve", "all", "--dev"}, - ExposedPorts: []string{"4444/tcp", "4445/tcp"}, - PortBindings: map[docker.Port][]docker.PortBinding{ - "4444/tcp": {{HostPort: fmt.Sprintf("%d/tcp", publicPort)}}, - "4445/tcp": {{HostPort: fmt.Sprintf("%d/tcp", adminPort)}}, - }, - }) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, hydraResource.Close()) - }) - - require.NoError(t, hydraResource.Expire(uint(60*5))) - - require.NotEmpty(t, hydraResource.GetPort("4444/tcp"), "%+v", hydraResource.Container.NetworkSettings.Ports) - require.NotEmpty(t, hydraResource.GetPort("4445/tcp"), "%+v", hydraResource.Container) - - hydraPublic = "http://127.0.0.1:" + hydraResource.GetPort("4444/tcp") - hydraAdmin = "http://127.0.0.1:" + hydraResource.GetPort("4445/tcp") - - go pool.Client.Logs(docker.LogsOptions{ - ErrorStream: TestLogWriter{T: t, streamName: "hydra-stderr"}, - OutputStream: TestLogWriter{T: t, streamName: "hydra-stdout"}, - Stdout: true, - Stderr: true, - Follow: true, - Container: hydraResource.Container.ID, - }) - hl := logrusx.New("hydra-ready-check", "hydra-ready-check") - err = resilience.Retry(hl, time.Second*1, time.Second*5, func() error { - pr := hydraPublic + "/health/ready" - res, err := http.DefaultClient.Get(pr) - if err != nil || res.StatusCode != 200 { - return errors.Errorf("Hydra public is not ready at " + pr) - } - - ar := hydraAdmin + "/health/ready" - res, err = http.DefaultClient.Get(ar) - if err != nil && res.StatusCode != 200 { - return errors.Errorf("Hydra admin is not ready at " + ar) - } else { - return nil - } - }) - require.NoError(t, err) - - t.Logf("Ory Hydra running at: %s %s", hydraPublic, hydraAdmin) - - return hydraAdmin, hydraPublic -} - -type TestLogWriter struct { - streamName string - *testing.T -} - -func (t TestLogWriter) Write(p []byte) (int, error) { - t.Logf("[%d bytes @ %s]:\n\n%s\n", len(p), t.streamName, string(p)) - return len(p), nil -} - func TestOAuth2Provider(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) @@ -172,87 +43,174 @@ func TestOAuth2Provider(t *testing.T) { map[string]interface{}{"enabled": true}, ) + var testRequireLogin atomic.Bool + testRequireLogin.Store(true) + router := x.NewRouterPublic() kratosPublicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, x.NewRouterAdmin()) - - browserClient := testhelpers.NewClientWithCookieJar(t, nil, true) - errTS := testhelpers.NewErrorTestServer(t, reg) - redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) - var oAuthSuccess atomic.Bool - var hydraAdminClient hydraclientgo.OAuth2Api - var clientAppOAuth2Config *oauth2.Config + router.GET("/login-ts", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + t.Log("[loginTS] navigated to the login ui") + c := r.Context().Value(TestUIConfig).(*testConfig) + *c.callTrace = append(*c.callTrace, LoginUI) - clientAppTS := testhelpers.NewHTTPTestServer(t, http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - t.Logf("[clientAppTS] handling a callback at client app %s", r.URL.String()) - if r.URL.Query().Has("code") { - token, err := clientAppOAuth2Config.Exchange(r.Context(), r.URL.Query().Get("code")) + q := r.URL.Query() + hlc := r.URL.Query().Get("login_challenge") + if hlc != "" { + *c.callTrace = append(*c.callTrace, LoginWithOAuth2LoginChallenge) + + loginUrl, err := url.Parse(c.kratosPublicTS.URL + login.RouteInitBrowserFlow) require.NoError(t, err) - require.NotNil(t, token) - require.NotEqual(t, "", token.AccessToken) - oAuthSuccess.Store(true) - t.Log("[clientAppTS] successfully exchanged code for token") - } else { - t.Error("[clientAppTS] code query parameter is missing") - } - })) - identifier, pwd := x.NewUUID().String(), "password" + q := loginUrl.Query() + q.Set("login_challenge", hlc) + loginUrl.RawQuery = q.Encode() - var testRequireLogin atomic.Bool - testRequireLogin.Store(true) + req, err := http.NewRequest("GET", loginUrl.String(), nil) + require.NoError(t, err) - uiTS := testhelpers.NewHTTPTestServer(t, http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - t.Logf("[uiTS] handling %s", r.URL) - q := r.URL.Query() + resp, err := c.browserClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.NoError(t, resp.Body.Close()) - if len(q) == 1 && !q.Has("flow") && q.Has("login_challenge") { - t.Log("[uiTS] initializing a new OpenID Provider flow") - hlc := r.URL.Query().Get("login_challenge") - f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, kratosPublicTS, false, false, false, !testRequireLogin.Load(), testhelpers.InitFlowWithOAuth2LoginChallenge(hlc)) - if testRequireLogin.Load() { - require.NotNil(t, f) + // if the registration page redirects us to the login page + // we will sign in which means we might have a session + var oryCookie *http.Cookie + currentURL, err := url.Parse(kratosPublicTS.URL) + require.NoError(t, err) - values := url.Values{"method": {"password"}, "identifier": {identifier}, "password": {pwd}, "csrf_token": {x.FakeCSRFToken}}.Encode() - _, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values) + for _, c := range c.browserClient.Jar.Cookies(currentURL) { + if c.Name == config.DefaultSessionCookieName { + oryCookie = c + break + } + } - assert.EqualValues(t, http.StatusOK, res.StatusCode) - } else { - require.Nil(t, f, "login flow should have been skipped and invalidated, but we successfully retrieved it") + // in some cases the initialize login flow already navigated to the consent page + // in which case no flow exists, but a session cookie does + if oryCookie != nil && !resp.Request.URL.Query().Has("flow") { + t.Logf("[loginTS] found a session cookie: %s", oryCookie.String()) + return } - return - } - if q.Has("consent_challenge") { - kratosUIHandleConsent(t, r, browserClient, hydraAdminClient, clientAppTS.URL) + flowID := resp.Request.URL.Query().Get("flow") + lf := testhelpers.GetLoginFlow(t, c.browserClient, c.kratosPublicTS, flowID) + require.NotNil(t, lf) + + values := url.Values{"method": {"password"}, "identifier": {c.identifier}, "password": {c.password}, "csrf_token": {x.FakeCSRFToken}}.Encode() + _, res := testhelpers.LoginMakeRequest(t, false, false, lf, c.browserClient, values) + assert.EqualValues(t, http.StatusOK, res.StatusCode) return } if q.Has("flow") { - t.Log("[uiTS] no operaton; the flow should be completed by the handler that initialized it") + *c.callTrace = append(*c.callTrace, LoginWithFlowID) + t.Log("[loginTS] login flow is ignored here since it will be handled by the code above, we just need to return") return } + }) + + router.GET("/consent", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + t.Log("[consentTS] navigated to the consent ui") + c := r.Context().Value(TestUIConfig).(*testConfig) + *c.callTrace = append(*c.callTrace, Consent) + + q := r.URL.Query() + consentChallenge := q.Get("consent_challenge") + assert.NotEmpty(t, consentChallenge) + + if consentChallenge != "" { + *c.callTrace = append(*c.callTrace, ConsentWithChallenge) + } + + cr, resp, err := c.hydraAdminClient.GetOAuth2ConsentRequest(ctx).ConsentChallenge(q.Get("consent_challenge")).Execute() + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.ElementsMatch(t, cr.RequestedScope, c.requestedScope) + + if cr.GetSkip() { + *c.callTrace = append(*c.callTrace, ConsentSkip) + } + + if cr.Client.GetSkipConsent() { + *c.callTrace = append(*c.callTrace, ConsentClientSkip) + } + + completedAcceptRequest, resp, err := c.hydraAdminClient.AcceptOAuth2ConsentRequest(r.Context()).AcceptOAuth2ConsentRequest(hydraclientgo.AcceptOAuth2ConsentRequest{ + Remember: &c.consentRemember, + GrantScope: c.requestedScope, + }).ConsentChallenge(q.Get("consent_challenge")).Execute() + + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + if completedAcceptRequest != nil { + *c.callTrace = append(*c.callTrace, ConsentAccept) + } + assert.NotNil(t, completedAcceptRequest) + + t.Logf("[consentTS] navigating to %s", completedAcceptRequest.RedirectTo) + resp, err = c.browserClient.Get(completedAcceptRequest.RedirectTo) + require.NoError(t, err) + require.Equal(t, c.clientAppTS.URL, fmt.Sprintf("%s://%s", resp.Request.URL.Scheme, resp.Request.URL.Host)) + }) + + kratosUIMiddleware := negroni.New() + kratosUIMiddleware.UseFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + // add the context from the global context to each request + next(rw, r.WithContext(ctx)) + }) + kratosUIMiddleware.UseHandler(router) + + kratosUITS := testhelpers.NewHTTPTestServer(t, kratosUIMiddleware) - t.Errorf("[uiTS] unexpected query %#v", q) - })) + clientAppTSMiddleware := negroni.New() + clientAppTSMiddleware.UseFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + // add the context from the global context to each request + next(rw, r.WithContext(ctx)) + }) + clientAppTSMiddleware.UseHandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c := ctx.Value(TestOAuthClientState).(*clientAppConfig) + kc := ctx.Value(TestUIConfig).(*testConfig) + *kc.callTrace = append(*kc.callTrace, CodeExchange) + + c.state.visits += 1 + t.Logf("[clientAppTS] handling a callback at client app %s", r.URL.String()) + if r.URL.Query().Has("code") { + token, err := c.client.Exchange(r.Context(), r.URL.Query().Get("code")) + require.NoError(t, err) + + if token != nil && token.AccessToken != "" { + t.Log("[clientAppTS] successfully exchanged code for token") + *kc.callTrace = append(*kc.callTrace, CodeExchangeWithToken) + c.state.tokens += 1 + } else { + t.Log("[clientAppTS] did not receive a token") + } + } else { + t.Error("[clientAppTS] code query parameter is missing") + } + }) + // A new OAuth client which will also function as the callback for the code exchange + clientAppTS := testhelpers.NewHTTPTestServer(t, clientAppTSMiddleware) conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) conf.MustSet(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts") - conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, kratosUITS.URL+"/login-ts") conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, redirTS.URL+"/return-ts") + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) testhelpers.SetDefaultIdentitySchemaFromRaw(conf, loginSchema) - createIdentity(ctx, reg, t, identifier, pwd) - hydraAdmin, hydraPublic := newHydra(t, uiTS.URL, uiTS.URL) + hydraAdmin, hydraPublic := newHydra(t, kratosUITS.URL+"/login-ts", kratosUITS.URL+"/consent") conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, hydraAdmin) - hydraAdminClient = createHydraOAuth2ApiClient(hydraAdmin) - clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, "profile email") + hydraAdminClient := createHydraOAuth2ApiClient(hydraAdmin) - t.Run("should sign in the user without OAuth2", func(t *testing.T) { + loginToAccount := func(t *testing.T, browserClient *http.Client, identifier, pwd string) { f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, kratosPublicTS, false, false, false, false) values := url.Values{"method": {"password"}, "identifier": {identifier}, "password": {pwd}, "csrf_token": {x.FakeCSRFToken}}.Encode() @@ -261,78 +219,594 @@ func TestOAuth2Provider(t *testing.T) { assert.EqualValues(t, http.StatusOK, res.StatusCode) assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) - }) - - clientAppOAuth2Config = &oauth2.Config{ - ClientID: clientID, - ClientSecret: "client-secret", - Endpoint: oauth2.Endpoint{ - AuthURL: hydraPublic + "/oauth2/auth", - TokenURL: hydraPublic + "/oauth2/token", - AuthStyle: oauth2.AuthStyleInParams, - }, - Scopes: []string{"profile", "email"}, - RedirectURL: clientAppTS.URL, } - conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) t.Run("should prompt the user for login and consent", func(t *testing.T) { - authCodeURL := makeAuthCodeURL(t, clientAppOAuth2Config, "", false) - res, err := browserClient.Get(authCodeURL) + // This is the default case, where the user is prompted for login and consent. + + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + }) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } - require.NoError(t, err, authCodeURL) - body, err := io.ReadAll(res.Body) - require.NoError(t, res.Body.Close()) - require.NoError(t, err) - require.Equal(t, "", string(body)) - require.Equal(t, http.StatusOK, res.StatusCode) - require.True(t, oAuthSuccess.Load()) - oAuthSuccess.Store(false) + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + ct := make([]callTrace, 0) + + tc := &testConfig{ + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + hydraAdminClient: hydraAdminClient, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + consentRemember: true, + identifier: identifier, + password: pwd, + } + ctx = context.WithValue(ctx, TestUIConfig, tc) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct) }) - conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) t.Run("should prompt the user for login and consent again", func(t *testing.T) { - authCodeURL := makeAuthCodeURL(t, clientAppOAuth2Config, "", false) - res, err := browserClient.Get(authCodeURL) + // This test verifies that when Kratos is set + // to SessionPersistentCookie=false, the user is not + // remembered from the previous OAuth2 flow. + // The user must then re-authenticate and re-consent. + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } - require.NoError(t, err, authCodeURL) - body, err := io.ReadAll(res.Body) - require.NoError(t, res.Body.Close()) - require.NoError(t, err) - require.Equal(t, "", string(body)) - require.Equal(t, http.StatusOK, res.StatusCode) - require.True(t, oAuthSuccess.Load()) - oAuthSuccess.Store(false) + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + ct := make([]callTrace, 0) + + tc := &testConfig{ + password: pwd, + identifier: identifier, + hydraAdminClient: hydraAdminClient, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + consentRemember: false, + } + ctx = context.WithValue(ctx, TestUIConfig, tc) + + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct) + + // Reset the call trace + ct = []callTrace{} + + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 2, + tokens: 2, + }, clientAS) + + expected = []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct) }) - testRequireLogin.Store(false) t.Run("should prompt the user for consent, but not for login", func(t *testing.T) { - authCodeURL := makeAuthCodeURL(t, clientAppOAuth2Config, "", false) - res, err := browserClient.Get(authCodeURL) + // This test verifies that when Kratos is set + // to SessionPersistentCookie=true, the user is + // remembered from the previous OAuth2 flow. + // The user must then only re-consent. + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + + identifier, pwd := x.NewUUID().String(), "password" + + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } - require.NoError(t, err, authCodeURL) - body, err := io.ReadAll(res.Body) - require.NoError(t, res.Body.Close()) - require.NoError(t, err) - require.Equal(t, "", string(body)) - require.Equal(t, http.StatusOK, res.StatusCode) - require.True(t, oAuthSuccess.Load()) - oAuthSuccess.Store(false) + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + ct := make([]callTrace, 0) + + tc := &testConfig{ + hydraAdminClient: hydraAdminClient, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + consentRemember: false, + password: pwd, + identifier: identifier, + } + ctx = context.WithValue(ctx, TestUIConfig, tc) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct) + + // reset the call trace + ct = []callTrace{} + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 2, + tokens: 2, + }, clientAS) + + expected = []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + + require.ElementsMatch(t, expected, ct) + }) + + t.Run("should prompt login even with session with OAuth flow", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + ct := make([]callTrace, 0) + + tc := &testConfig{ + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + hydraAdminClient: hydraAdminClient, + identifier: identifier, + password: pwd, + consentRemember: false, + } + + ctx = context.WithValue(ctx, TestUIConfig, tc) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + loginToAccount(t, browserClient, identifier, pwd) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, []callTrace{ + LoginUI, + LoginWithFlowID, + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + }, ct) + }) + + t.Run("first party clients can skip consent", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + clientSkipConsent := true + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), clientSkipConsent) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + ct := make([]callTrace, 0) + + tc := &testConfig{ + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + hydraAdminClient: hydraAdminClient, + identifier: identifier, + password: pwd, + consentRemember: false, + } + + ctx = context.WithValue(ctx, TestUIConfig, tc) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentClientSkip, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + }, ct) + }) + + t.Run("oauth flow with consent remember, skips consent", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + ct := make([]callTrace, 0) + + tc := &testConfig{ + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + hydraAdminClient: hydraAdminClient, + identifier: identifier, + password: pwd, + consentRemember: true, + } + + ctx = context.WithValue(ctx, TestUIConfig, tc) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: true, + }) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + }, ct) + + // reset the call trace + ct = []callTrace{} + clientAS = clientAppState{ + visits: 0, + tokens: 0, + } + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + Consent, + ConsentWithChallenge, + ConsentSkip, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + }, ct) }) - reg.WithHydra(&AcceptWrongSubject{h: reg.Hydra().(*hydra.DefaultHydra)}) t.Run("should fail when Hydra session subject doesn't match the subject authenticated by Kratos", func(t *testing.T) { - authCodeURL := makeAuthCodeURL(t, clientAppOAuth2Config, "", false) - res, err := browserClient.Get(authCodeURL) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } - require.NoError(t, err, authCodeURL) - body, err := io.ReadAll(res.Body) - require.NoError(t, res.Body.Close()) - require.NoError(t, err) - require.Equal(t, "", string(body)) - require.Equal(t, http.StatusOK, res.StatusCode) - require.False(t, oAuthSuccess.Load()) - oAuthSuccess.Store(false) + ct := make([]callTrace, 0) + + tc := &testConfig{ + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + callTrace: &ct, + requestedScope: scopes, + hydraAdminClient: hydraAdminClient, + identifier: identifier, + password: pwd, + consentRemember: false, + } + + ctx = context.WithValue(ctx, TestUIConfig, tc) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + state: &clientAS, + expectToken: false, + }) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, []callTrace{ + LoginUI, + LoginWithFlowID, + LoginUI, + LoginWithOAuth2LoginChallenge, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + }, ct) + + // reset the call trace + ct = []callTrace{} + clientAS = clientAppState{ + visits: 0, + tokens: 0, + } + + reg.WithHydra(&AcceptWrongSubject{h: reg.Hydra().(*hydra.DefaultHydra)}) + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 0, + tokens: 0, + }, clientAS) + + expected := []callTrace{ + LoginUI, + LoginWithOAuth2LoginChallenge, + } + require.ElementsMatch(t, expected, ct) }) } diff --git a/selfservice/strategy/password/op_registration_test.go b/selfservice/strategy/password/op_registration_test.go index f947e9a63007..55e9907aba07 100644 --- a/selfservice/strategy/password/op_registration_test.go +++ b/selfservice/strategy/password/op_registration_test.go @@ -9,11 +9,15 @@ import ( "fmt" "io" "net/http" - "net/http/httptest" + "net/url" + "strings" "testing" "golang.org/x/oauth2" + "github.com/julienschmidt/httprouter" + "github.com/urfave/negroni" + hydraclientgo "github.com/ory/hydra-client-go/v2" "github.com/stretchr/testify/assert" @@ -23,148 +27,216 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/x" ) -type clientAppConfig struct { - client *oauth2.Config - expectToken bool - state clientAppState -} +func TestOAuth2ProviderRegistration(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) -type clientAppState struct { - visits int64 - tokens int64 -} + kratosPublicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, x.NewRouterPublic(), x.NewRouterAdmin()) + errTS := testhelpers.NewErrorTestServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) -type kratosUIConfig struct { - expectLoginScreen bool - identifier string - password string - browserClient *http.Client - kratosPublicTS *httptest.Server - clientAppTS *httptest.Server - hydraAdminClient hydraclientgo.OAuth2Api -} + var hydraAdminClient hydraclientgo.OAuth2Api -func newClientAppTS(t *testing.T, c *clientAppConfig) *httptest.Server { - return testhelpers.NewHTTPTestServer(t, http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - c.state.visits += 1 - t.Logf("[clientAppTS] handling a callback at client app %s", r.URL.String()) - if r.URL.Query().Has("code") { - token, err := c.client.Exchange(r.Context(), r.URL.Query().Get("code")) - require.NoError(t, err) - require.NotNil(t, token) - require.NotEqual(t, "", token.AccessToken) - require.True(t, c.expectToken) - c.state.tokens += 1 - t.Log("[clientAppTS] successfully exchanged code for token") - } else { - t.Error("[clientAppTS] code query parameter is missing") - require.False(t, c.expectToken) + router := x.NewRouterPublic() + + const ( + TestUIConfig = "test-ui-config" + TestOAuthClientState = "test-oauth-client-state" + ) + + router.GET("/login-ts", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + t.Log("[loginTS] navigated to the login ui") + c := r.Context().Value(TestUIConfig).(*testConfig) + *c.callTrace = append(*c.callTrace, LoginUI) + + q := r.URL.Query() + hlc := r.URL.Query().Get("login_challenge") + + if hlc != "" { + *c.callTrace = append(*c.callTrace, LoginWithOAuth2LoginChallenge) + return } - })) -} -func kratosUIHandleConsent(t *testing.T, req *http.Request, client *http.Client, haa hydraclientgo.OAuth2Api, clientAppURL string) { - q := req.URL.Query() - cr, resp, err := haa.GetOAuth2ConsentRequest(req.Context()).ConsentChallenge(q.Get("consent_challenge")).Execute() - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - require.ElementsMatch(t, cr.RequestedScope, []string{"profile", "email"}) - - remember := true - completedAcceptRequest, resp, err := haa.AcceptOAuth2ConsentRequest(context.Background()).AcceptOAuth2ConsentRequest(hydraclientgo.AcceptOAuth2ConsentRequest{ - Remember: &remember, - }).ConsentChallenge(q.Get("consent_challenge")).Execute() - - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - require.NotNil(t, completedAcceptRequest) - - t.Logf("[uiTS] navigating to %s", completedAcceptRequest.RedirectTo) - resp, err = client.Get(completedAcceptRequest.RedirectTo) - require.NoError(t, err) - require.Equal(t, clientAppURL, fmt.Sprintf("%s://%s", resp.Request.URL.Scheme, resp.Request.URL.Host)) - require.True(t, resp.Request.URL.Query().Has("code")) -} + if q.Has("flow") { + *c.callTrace = append(*c.callTrace, LoginWithFlowID) + lf := testhelpers.GetLoginFlow(t, c.browserClient, c.kratosPublicTS, q.Get("flow")) + require.NotNil(t, lf) + values := testhelpers.SDKFormFieldsToURLValues(lf.Ui.Nodes) + values.Set("password", c.password) + + _, _ = testhelpers.LoginMakeRequest(t, false, false, lf, c.browserClient, values.Encode()) + t.Log("[loginTS] login flow is ignored here since it will be handled by the code above, we just need to return") + return + } + }) + + router.GET("/registration-ts", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + t.Log("[registrationTS] navigated to the registration ui") + c := r.Context().Value(TestUIConfig).(*testConfig) + *c.callTrace = append(*c.callTrace, RegistrationUI) -func newKratosUITS(t *testing.T, c *kratosUIConfig) *httptest.Server { - return testhelpers.NewHTTPTestServer(t, http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - t.Logf("[uiTS] handling %s", r.URL) q := r.URL.Query() + hlc := q.Get("login_challenge") - if len(q) == 1 && !q.Has("flow") && q.Has("login_challenge") { - t.Log("[uiTS] initializing a new OpenID Provider flow") - hlc := r.URL.Query().Get("login_challenge") - f := testhelpers.InitializeRegistrationFlowViaBrowser(t, c.browserClient, c.kratosPublicTS, false, false, !c.expectLoginScreen, testhelpers.InitFlowWithOAuth2LoginChallenge(hlc)) - if c.expectLoginScreen { - require.NotNil(t, f) + if hlc != "" { + *c.callTrace = append(*c.callTrace, RegistrationWithOAuth2LoginChallenge) + t.Log("[registrationTS] initializing a new OpenID Provider flow through the registration endpoint") + registrationUrl, err := url.Parse(c.kratosPublicTS.URL + registration.RouteInitBrowserFlow) + require.NoError(t, err) - values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) - values.Set("traits.foobar", c.identifier) - values.Set("traits.username", c.identifier) - values.Set("password", c.password) + q := registrationUrl.Query() + q.Set("login_challenge", hlc) + registrationUrl.RawQuery = q.Encode() - _, res := testhelpers.RegistrationMakeRequest(t, false, false, f, c.browserClient, values.Encode()) + req, err := http.NewRequest("GET", registrationUrl.String(), nil) + require.NoError(t, err) - assert.EqualValues(t, http.StatusOK, res.StatusCode) - } else { - require.Nil(t, f, "registration flow should have been skipped and invalidated, but we successfully retrieved it") + resp, err := c.browserClient.Do(req) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + require.NoError(t, resp.Body.Close()) + + // if the registration page redirects us to the login page + // we will sign in which means we might have a session + var oryCookie *http.Cookie + currentURL, err := url.Parse(kratosPublicTS.URL) + require.NoError(t, err) + + for _, c := range c.browserClient.Jar.Cookies(currentURL) { + if c.Name == config.DefaultSessionCookieName { + oryCookie = c + break + } } - return - } - if q.Has("consent_challenge") { - kratosUIHandleConsent(t, r, c.browserClient, c.hydraAdminClient, c.clientAppTS.URL) + if oryCookie != nil { + t.Log("[registrationTS] we expect to have been at the login screen and got an active flow. This means we have a session now") + return + } + + flowID := resp.Request.URL.Query().Get("flow") + assert.NotEmpty(t, flowID) + + f := testhelpers.GetRegistrationFlow(t, c.browserClient, c.kratosPublicTS, flowID) + require.NotNil(t, f) + + // continue the registration flow here + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("traits.foobar", c.identifier) + values.Set("traits.username", c.identifier) + values.Set("password", c.password) + + _, res := testhelpers.RegistrationMakeRequest(t, false, false, f, c.browserClient, values.Encode()) + assert.EqualValues(t, http.StatusOK, res.StatusCode) return } if q.Has("flow") { - t.Log("[uiTS] no operaton; the flow should be completed by the handler that initialized it") + *c.callTrace = append(*c.callTrace, RegistrationWithFlowID) + t.Log("[registrationTS] registration flow is ignored here since it will be handled by the code above, we just need to return") return } + }) - t.Errorf("[uiTS] unexpected query %#v", q) - })) -} + router.GET("/consent", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + t.Log("[consentTS] navigated to the consent ui") + c := r.Context().Value(TestUIConfig).(*testConfig) + *c.callTrace = append(*c.callTrace, Consent) -func TestOAuth2ProviderRegistration(t *testing.T) { - ctx := context.Background() - conf, reg := internal.NewFastRegistryWithMocks(t) - kratosPublicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, x.NewRouterPublic(), x.NewRouterAdmin()) - errTS := testhelpers.NewErrorTestServer(t, reg) - redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + q := r.URL.Query() + consentChallenge := q.Get("consent_challenge") + assert.NotEmpty(t, consentChallenge) - var hydraAdminClient hydraclientgo.OAuth2Api + if consentChallenge != "" { + *c.callTrace = append(*c.callTrace, ConsentWithChallenge) + } - cac := &clientAppConfig{} - clientAppTS := newClientAppTS(t, cac) + cr, resp, err := hydraAdminClient.GetOAuth2ConsentRequest(ctx).ConsentChallenge(q.Get("consent_challenge")).Execute() + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.ElementsMatch(t, cr.RequestedScope, c.requestedScope) - kuc := &kratosUIConfig{} - kratosUITS := newKratosUITS(t, kuc) + if cr.GetSkip() { + *c.callTrace = append(*c.callTrace, ConsentSkip) + } + + if cr.Client.GetSkipConsent() { + *c.callTrace = append(*c.callTrace, ConsentClientSkip) + } - hydraAdmin, hydraPublic := newHydra(t, kratosUITS.URL, kratosUITS.URL) + completedAcceptRequest, resp, err := hydraAdminClient.AcceptOAuth2ConsentRequest(r.Context()).AcceptOAuth2ConsentRequest(hydraclientgo.AcceptOAuth2ConsentRequest{ + Remember: &c.consentRemember, + GrantScope: c.requestedScope, + }).ConsentChallenge(q.Get("consent_challenge")).Execute() + + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + if completedAcceptRequest != nil { + *c.callTrace = append(*c.callTrace, ConsentAccept) + } + assert.NotNil(t, completedAcceptRequest) + + t.Logf("[consentTS] navigating to %s", completedAcceptRequest.RedirectTo) + resp, err = c.browserClient.Get(completedAcceptRequest.RedirectTo) + require.NoError(t, err) + require.Equal(t, c.clientAppTS.URL, fmt.Sprintf("%s://%s", resp.Request.URL.Scheme, resp.Request.URL.Host)) + }) + + kratosUIMiddleware := negroni.New() + kratosUIMiddleware.UseFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + // add the context from the global context to each request + next(rw, r.WithContext(ctx)) + }) + kratosUIMiddleware.UseHandler(router) + + kratosUITS := testhelpers.NewHTTPTestServer(t, kratosUIMiddleware) + + clientAppTSMiddleware := negroni.New() + clientAppTSMiddleware.UseFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + // add the context from the global context to each request + next(rw, r.WithContext(ctx)) + }) + clientAppTSMiddleware.UseHandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c := ctx.Value(TestOAuthClientState).(*clientAppConfig) + kc := ctx.Value(TestUIConfig).(*testConfig) + *kc.callTrace = append(*kc.callTrace, CodeExchange) + + c.state.visits += 1 + t.Logf("[clientAppTS] handling a callback at client app %s", r.URL.String()) + if r.URL.Query().Has("code") { + token, err := c.client.Exchange(r.Context(), r.URL.Query().Get("code")) + require.NoError(t, err) + + if token != nil && token.AccessToken != "" { + t.Log("[clientAppTS] successfully exchanged code for token") + *kc.callTrace = append(*kc.callTrace, CodeExchangeWithToken) + c.state.tokens += 1 + } else { + t.Log("[clientAppTS] did not receive a token") + } + } else { + t.Error("[clientAppTS] code query parameter is missing") + } + }) + // A new OAuth client which will also function as the callback for the code exchange + clientAppTS := testhelpers.NewHTTPTestServer(t, clientAppTSMiddleware) + + // we want to test if the registration ui is used if the flow contains an oauth2 login challenge + // so we will have Hydra redirect to the base path of the test kratos ui server which + // will then initiate the registration flow + hydraAdmin, hydraPublic := newHydra(t, kratosUITS.URL+"/registration-ts", kratosUITS.URL+"/consent") hydraAdminClient = createHydraOAuth2ApiClient(hydraAdmin) - clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, "profile email") - - defaultClient := &oauth2.Config{ - ClientID: clientID, - ClientSecret: "client-secret", - Endpoint: oauth2.Endpoint{ - AuthURL: hydraPublic + "/oauth2/auth", - TokenURL: hydraPublic + "/oauth2/token", - AuthStyle: oauth2.AuthStyleInParams, - }, - Scopes: []string{"profile", "email"}, - RedirectURL: clientAppTS.URL, - } conf.MustSet(ctx, config.ViperKeyOAuth2ProviderURL, hydraAdmin+"/") conf.MustSet(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts") conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, kratosUITS.URL+"/login-ts") - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationUI, kratosUITS.URL+"/login-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationUI, kratosUITS.URL+"/registration-ts") conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, redirTS.URL+"/return-ts") conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, redirTS.URL+"/registration-return-ts") conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) @@ -172,108 +244,643 @@ func TestOAuth2ProviderRegistration(t *testing.T) { conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), []config.SelfServiceHook{{Name: "session"}}) testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") - sharedBrowserClient := testhelpers.NewClientWithCookieJar(t, nil, true) - type state struct { cas clientAppState } - for _, tc := range []struct { - name string - configure func(c *config.Config) - cac clientAppConfig - kuc kratosUIConfig - expected state - }{ - { - name: "should prompt the user for login and consent", - configure: func(c *config.Config) { - c.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) - }, - cac: clientAppConfig{ - client: defaultClient, - expectToken: true, - }, - kuc: kratosUIConfig{ - expectLoginScreen: true, - identifier: x.NewUUID().String(), - password: x.NewUUID().String(), - browserClient: sharedBrowserClient, - kratosPublicTS: kratosPublicTS, - clientAppTS: clientAppTS, - hydraAdminClient: hydraAdminClient, - }, - expected: state{ - cas: clientAppState{ - visits: 1, - tokens: 1, - }, - }, - }, - { - name: "should prompt the user for login and consent again", - configure: func(c *config.Config) { - c.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) - }, - cac: clientAppConfig{ - client: defaultClient, - expectToken: true, - }, - kuc: kratosUIConfig{ - expectLoginScreen: true, - identifier: x.NewUUID().String(), - password: x.NewUUID().String(), - browserClient: sharedBrowserClient, - kratosPublicTS: kratosPublicTS, - clientAppTS: clientAppTS, - hydraAdminClient: hydraAdminClient, - }, - expected: state{ - cas: clientAppState{ - visits: 1, - tokens: 1, - }, + doOAuthFlow := func(t *testing.T, ctx context.Context, oauthClient *oauth2.Config, browserClient *http.Client) { + t.Helper() + + authCodeURL := makeAuthCodeURL(t, oauthClient, "", false) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, authCodeURL, nil) + require.NoError(t, err) + res, err := browserClient.Do(req) + require.NoError(t, err) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.NoError(t, res.Body.Close()) + require.Equal(t, "", string(body)) + require.Equal(t, http.StatusOK, res.StatusCode) + } + + registerNewAccount := func(t *testing.T, ctx context.Context, browserClient *http.Client, identifier, password string) { + // we need to create a new session directly with kratos + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, kratosPublicTS, false, false, false) + require.NotNil(t, f) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + values.Set("traits.foobar", identifier) + values.Set("traits.username", identifier) + values.Set("password", password) + + _, resp := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, values.Encode()) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + var cookie *http.Cookie + currentURL, err := url.Parse(kratosPublicTS.URL) + require.NoError(t, err) + + for _, c := range browserClient.Jar.Cookies(currentURL) { + if c.Name == config.DefaultSessionCookieName { + cookie = c + break + } + } + + require.NotNil(t, cookie, "expected exactly one session cookie to be set but got none") + } + + // important, we will set the persistent cookie to false for most of the tests here + // once this is true, the behavior of the consent flow changes + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) + + t.Run("case=should accept oauth login request on registration", func(t *testing.T) { + // this test initiates a new OAuth2 flow which goes directly to the registration page + // we then create a new account through the registration flow + // and expect the OAuth2 flow to succeed + scopes := []string{"profile", "email"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + + oauth2Client := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, }, - }, - { - name: "should fail because the persistent Hydra session doesn't match the new Kratos session subject", - configure: func(c *config.Config) { + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + + identifier := x.NewUUID().String() + password := x.NewUUID().String() + + ct := make([]callTrace, 0) + + ctx = context.WithValue(ctx, TestUIConfig, &testConfig{ + identifier: identifier, + password: password, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: true, + callTrace: &ct, + requestedScope: scopes, + }) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauth2Client, + expectToken: true, + state: &clientAS, + }) + + doOAuthFlow(t, ctx, + oauth2Client, + browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct, "expected the call trace to match") + }) + + t.Run("case=registration with session should redirect to login to re-authenticate and to consent", func(t *testing.T) { + // this test registers a new account which sets a session + // we then initiate a new OAuth2 flow which should redirect us to the registration page + // the registration page does a session validation and retrieves the loginRequest from Hydra + // which in this case will indicate we cannot skip the login flow (since the there is no previous OAuth flow associated) + // we then get redirected to the the login page with refresh=true + // we then sign in and expect to be redirected to the consent page + // and then back to the client app + scopes := []string{"profile", "email", "offline_access"} + + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, }, - cac: clientAppConfig{ - client: defaultClient, - expectToken: false, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + identifier := x.NewUUID().String() + password := x.NewUUID().String() + + ct := make([]callTrace, 0) + + tc := &testConfig{ + identifier: identifier, + password: password, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: true, + requestedScope: scopes, + callTrace: &ct, + } + + ctx = context.WithValue(ctx, TestUIConfig, tc) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, &clientAppConfig{ + client: oauthClient, + expectToken: true, + state: &clientAS, + }) + + registerNewAccount(t, ctx, browserClient, identifier, password) + + require.ElementsMatch(t, []callTrace{ + RegistrationUI, + RegistrationWithFlowID, + }, ct, "expected the call trace to match") + + // reset the call trace + ct = []callTrace{} + tc.callTrace = &ct + + doOAuthFlow(t, ctx, oauthClient, browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatch(t, expected, ct, "expected the call trace to match") + }) + + t.Run("case=registration should redirect to login if session exists and skip=false", func(t *testing.T) { + // we dont want to skip the consent page here + // we want the registration page to redirect to the login page + // since we have a session but do not skip the consent page + clientSkipConsent := false + consentRemember := false + + scopes := []string{"profile", "email", "offline_access"} + + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), clientSkipConsent) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, }, - kuc: kratosUIConfig{ - expectLoginScreen: true, - identifier: x.NewUUID().String(), - password: x.NewUUID().String(), - browserClient: sharedBrowserClient, - kratosPublicTS: kratosPublicTS, - clientAppTS: clientAppTS, - hydraAdminClient: hydraAdminClient, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + identifier := x.NewUUID().String() + password := x.NewUUID().String() + + ct := make([]callTrace, 0) + + tc := &testConfig{ + identifier: identifier, + password: password, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: consentRemember, + requestedScope: scopes, + callTrace: &ct, + } + + clientAppConfig := &clientAppConfig{ + client: oauthClient, + expectToken: true, + } + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + + // set the global context values + ctx = context.WithValue(ctx, TestUIConfig, tc) + ctx = context.WithValue(ctx, TestOAuthClientState, clientAppConfig) + + doSuccessfulOAuthFlow := func(t *testing.T) { + t.Helper() + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + clientAppConfig.state = &clientAS + + doOAuthFlow(t, ctx, + oauthClient, + browserClient) + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatchf(t, expected, ct, "expected the call trace to match") + } + + doSuccessfulOAuthFlow(t) + + // reset our state on the client app + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + clientAppConfig.state = &clientAS + + // we should now have a session, but not skip the consent page + doOAuthFlow(t, ctx, oauthClient, browserClient) + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected = append(expected, + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + LoginUI, + LoginWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + ) + require.ElementsMatchf(t, expected, ct, "expected the call trace to match") + }) + + t.Run("case=consent should be skipped if client is configured to skip", func(t *testing.T) { + clientSkipConsent := true + scopes := []string{"profile", "email", "offline_access"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), clientSkipConsent) + + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, }, - expected: state{ - cas: clientAppState{ - visits: 0, - tokens: 0, - }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + identifier := x.NewUUID().String() + password := x.NewUUID().String() + + ct := make([]callTrace, 0) + + tc := &testConfig{ + identifier: identifier, + password: password, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: false, + requestedScope: scopes, + callTrace: &ct, + } + + clientAppConfig := &clientAppConfig{ + client: oauthClient, + expectToken: true, + } + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + Consent, + ConsentWithChallenge, + ConsentClientSkip, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + + // set the global context values + ctx = context.WithValue(ctx, TestUIConfig, tc) + ctx = context.WithValue(ctx, TestOAuthClientState, clientAppConfig) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + clientAppConfig.state = &clientAS + + doOAuthFlow(t, ctx, + oauthClient, + browserClient) + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatchf(t, expected, ct, "expected the call trace to match") + }) + + t.Run("case=consent should be skipped if user has a session and has already consented", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) + }) + + consentRemember := true + + scopes := []string{"profile", "email", "offline_access"} + + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - *cac = tc.cac - *kuc = tc.kuc - tc.configure(conf) + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + identifier := x.NewUUID().String() + password := x.NewUUID().String() + + ct := make([]callTrace, 0) + + kratosUIConfig := &testConfig{ + identifier: identifier, + password: password, + browserClient: browserClient, + kratosPublicTS: kratosPublicTS, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: consentRemember, + requestedScope: scopes, + callTrace: &ct, + } - authCodeURL := makeAuthCodeURL(t, cac.client, "", false) - res, err := tc.kuc.browserClient.Get(authCodeURL) + clientAppConfig := &clientAppConfig{ + client: oauthClient, + expectToken: true, + } - require.NoError(t, err) - body, err := io.ReadAll(res.Body) - require.NoError(t, res.Body.Close()) - require.NoError(t, err) - require.Equal(t, "", string(body)) - require.Equal(t, http.StatusOK, res.StatusCode) - require.EqualValues(t, tc.expected.cas, cac.state) + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + + // set the global context values + ctx = context.WithValue(ctx, TestUIConfig, kratosUIConfig) + ctx = context.WithValue(ctx, TestOAuthClientState, clientAppConfig) + + doSuccessfulOAuthFlow := func(t *testing.T) { + t.Helper() + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + clientAppConfig.state = &clientAS + + doOAuthFlow(t, ctx, + oauthClient, + browserClient) + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatchf(t, expected, ct, "expected the call trace to match") + } + + doSuccessfulOAuthFlow(t) + + // reset our state on the client app + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + + clientAppConfig.state = &clientAS + + // reset the call trace + ct = []callTrace{} + kratosUIConfig.callTrace = &ct + + // we should now have a session, but not skip the consent page + doOAuthFlow(t, ctx, oauthClient, browserClient) + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + expected = []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + Consent, + ConsentWithChallenge, + ConsentSkip, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + require.ElementsMatchf(t, expected, ct, "expected the call trace to match") + }) + + t.Run("case=should fail because the persistent Hydra session doesn't match the new Kratos session subject", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, false) }) - } + + // this test re-uses the previous oauthClient + // but creates a new user account through the registration flow + // since the session with the new user does not match the hydra session it should fail + scopes := []string{"profile", "email", "offline_access"} + clientID := createOAuth2Client(t, ctx, hydraAdminClient, []string{clientAppTS.URL}, strings.Join(scopes, " "), false) + + oauthClient := &oauth2.Config{ + ClientID: clientID, + ClientSecret: "client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: hydraPublic + "/oauth2/auth", + TokenURL: hydraPublic + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: scopes, + RedirectURL: clientAppTS.URL, + } + + browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + + ct := make([]callTrace, 0) + + kratosUIConfig := &testConfig{ + identifier: x.NewUUID().String(), + password: x.NewUUID().String(), + kratosPublicTS: kratosPublicTS, + browserClient: browserClient, + clientAppTS: clientAppTS, + hydraAdminClient: hydraAdminClient, + consentRemember: true, + callTrace: &ct, + requestedScope: scopes, + } + + ctx = context.WithValue(ctx, TestUIConfig, kratosUIConfig) + + clientAppConfig := &clientAppConfig{ + client: oauthClient, + expectToken: false, + } + + ctx = context.WithValue(ctx, TestOAuthClientState, clientAppConfig) + + expected := []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + Consent, + ConsentWithChallenge, + ConsentAccept, + CodeExchange, + CodeExchangeWithToken, + } + + doSuccessfulOAuthFlow := func(t *testing.T) { + t.Helper() + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + clientAppConfig.state = &clientAS + + doOAuthFlow(t, ctx, + oauthClient, + browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 1, + tokens: 1, + }, clientAS) + + require.ElementsMatch(t, expected, ct, "expected the call trace to match") + } + + doSuccessfulOAuthFlow(t) + + clientAS := clientAppState{ + visits: 0, + tokens: 0, + } + clientAppConfig.state = &clientAS + + currentURL, err := url.Parse(kratosPublicTS.URL) + require.NoError(t, err) + cookies := browserClient.Jar.Cookies(currentURL) + + // remove the kratos session so we can register a new account + for _, c := range cookies { + if c.Name == config.DefaultSessionCookieName { + c.MaxAge = -1 + c.Value = "" + break + } + } + browserClient.Jar.SetCookies(currentURL, cookies) + + kratosUIConfig.identifier = x.NewUUID().String() + kratosUIConfig.password = x.NewUUID().String() + + // reset the call trace + ct = []callTrace{} + kratosUIConfig.callTrace = &ct + + doOAuthFlow(t, ctx, + oauthClient, + browserClient) + + assert.EqualValues(t, clientAppState{ + visits: 0, + tokens: 0, + }, clientAS) + + expected = []callTrace{ + RegistrationUI, + RegistrationWithOAuth2LoginChallenge, + RegistrationUI, + RegistrationWithFlowID, + } + require.ElementsMatch(t, expected, ct, "expected the call trace to match") + }) } diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index 2311741daad7..ddcf26a20883 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -5,6 +5,7 @@ package password_test import ( "context" + _ "embed" "fmt" "net/http" "net/http/httptest" @@ -31,8 +32,6 @@ import ( "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/x/assertx" - _ "embed" - "github.com/ory/kratos/x" ) diff --git a/session/handler_test.go b/session/handler_test.go index b565381a986c..286943796927 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -17,7 +17,7 @@ import ( "testing" "time" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/tidwall/gjson" "github.com/ory/kratos/identity" @@ -73,7 +73,8 @@ func TestSessionWhoAmI(t *testing.T) { ID: x.NewUUID(), State: identity.StateActive, Credentials: map[identity.CredentialsType]identity.Credentials{ - identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword, + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, Identifiers: []string{x.NewUUID().String()}, Config: []byte(`{"hashed_password":"$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), }, @@ -559,7 +560,7 @@ func TestHandlerAdminSessionManagement(t *testing.T) { return http.ErrUseLastResponse } - req := x.NewTestHTTPRequest(t, "GET", ts.URL+"/admin/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", ts.URL+"/admin/sessions/whoami", nil) res, err := client.Do(req) require.NoError(t, err) require.Equal(t, http.StatusTemporaryRedirect, res.StatusCode) @@ -1058,7 +1059,7 @@ func TestHandlerRefreshSessionBySessionID(t *testing.T) { }) t.Run("case=should return 404 when calling puplic server", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "PATCH", publicServer.URL+"/sessions/"+s.ID.String()+"/extend", nil) + req := testhelpers.NewTestHTTPRequest(t, "PATCH", publicServer.URL+"/sessions/"+s.ID.String()+"/extend", nil) res, err := publicServer.Client().Do(req) require.NoError(t, err) diff --git a/session/manager_http.go b/session/manager_http.go index e1c31f94f03f..fbd6574e0b2c 100644 --- a/session/manager_http.go +++ b/session/manager_http.go @@ -364,7 +364,7 @@ func (s *ManagerHTTP) SessionAddAuthenticationMethods(ctx context.Context, sid u return err } for _, m := range ams { - sess.CompletedLoginFor(m.Method, m.AAL) + sess.CompletedLoginForMethod(m) } sess.SetAuthenticatorAssuranceLevel() return s.r.SessionPersister().UpsertSession(ctx, sess) diff --git a/session/manager_http_test.go b/session/manager_http_test.go index c3b93b2d3c10..8a1e166da25c 100644 --- a/session/manager_http_test.go +++ b/session/manager_http_test.go @@ -108,8 +108,8 @@ func TestManagerHTTP(t *testing.T) { } t.Run("case=immutability", func(t *testing.T) { - cookie1 := getCookie(t, x.NewTestHTTPRequest(t, "GET", "https://baseurl.com/bar", nil)) - cookie2 := getCookie(t, x.NewTestHTTPRequest(t, "GET", "https://baseurl.com/bar", nil)) + cookie1 := getCookie(t, testhelpers.NewTestHTTPRequest(t, "GET", "https://baseurl.com/bar", nil)) + cookie2 := getCookie(t, testhelpers.NewTestHTTPRequest(t, "GET", "https://baseurl.com/bar", nil)) assert.NotEqual(t, cookie1.Value, cookie2.Value) }) @@ -151,7 +151,7 @@ func TestManagerHTTP(t *testing.T) { }) t.Run("suite=SessionAddAuthenticationMethod", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) conf, reg := internal.NewFastRegistryWithMocks(t) testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") @@ -214,7 +214,7 @@ func TestManagerHTTP(t *testing.T) { reg.RegisterPublicRoutes(context.Background(), rp) t.Run("case=valid", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) conf.MustSet(req.Context(), config.ViperKeySessionLifespan, "1m") i := identity.Identity{Traits: []byte("{}")} @@ -230,7 +230,7 @@ func TestManagerHTTP(t *testing.T) { }) t.Run("case=key rotation", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) original := conf.GetProvider(ctx).Strings(config.ViperKeySecretsCookie) t.Cleanup(func() { conf.MustSet(ctx, config.ViperKeySecretsCookie, original) @@ -256,7 +256,7 @@ func TestManagerHTTP(t *testing.T) { }) t.Run("case=no panic on invalid cookie name", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") conf.MustSet(ctx, config.ViperKeySessionName, "$%˜\"") t.Cleanup(func() { @@ -279,7 +279,7 @@ func TestManagerHTTP(t *testing.T) { }) t.Run("case=valid bearer auth as fallback", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") i := identity.Identity{Traits: []byte("{}"), State: identity.StateActive} @@ -300,7 +300,7 @@ func TestManagerHTTP(t *testing.T) { }) t.Run("case=valid x-session-token auth even if bearer is set", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") i := identity.Identity{Traits: []byte("{}"), State: identity.StateActive} @@ -321,7 +321,7 @@ func TestManagerHTTP(t *testing.T) { }) t.Run("case=expired", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) conf.MustSet(ctx, config.ViperKeySessionLifespan, "1ns") t.Cleanup(func() { conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") @@ -342,7 +342,7 @@ func TestManagerHTTP(t *testing.T) { }) t.Run("case=revoked", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) i := identity.Identity{Traits: []byte("{}")} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) s, _ = session.NewActiveSession(req, &i, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) @@ -365,7 +365,7 @@ func TestManagerHTTP(t *testing.T) { conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") t.Run("required_aal=aal2", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) run := func(t *testing.T, complete []identity.CredentialsType, requested string, i *identity.Identity, expectedError error) { s := session.NewInactiveSession() for _, m := range complete { @@ -594,7 +594,7 @@ func TestDoesSessionSatisfy(t *testing.T) { require.NoError(t, reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID)) }) - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) s := session.NewInactiveSession() for _, m := range tc.amr { s.CompletedLoginFor(m.Method, m.AAL) diff --git a/session/session.go b/session/session.go index 74204be4215c..d11a05e3bf05 100644 --- a/session/session.go +++ b/session/session.go @@ -164,15 +164,19 @@ func (s Session) TableName(ctx context.Context) string { return "sessions" } +func (s *Session) CompletedLoginForMethod(method AuthenticationMethod) { + method.CompletedAt = time.Now().UTC() + s.AMR = append(s.AMR, method) +} + func (s *Session) CompletedLoginFor(method identity.CredentialsType, aal identity.AuthenticatorAssuranceLevel) { - s.AMR = append(s.AMR, AuthenticationMethod{Method: method, AAL: aal, CompletedAt: time.Now().UTC()}) + s.CompletedLoginForMethod(AuthenticationMethod{Method: method, AAL: aal}) } func (s *Session) CompletedLoginForWithProvider(method identity.CredentialsType, aal identity.AuthenticatorAssuranceLevel, providerID string, organizationID string) { - s.AMR = append(s.AMR, AuthenticationMethod{ + s.CompletedLoginForMethod(AuthenticationMethod{ Method: method, AAL: aal, - CompletedAt: time.Now().UTC(), Provider: providerID, Organization: organizationID, }) diff --git a/session/session_test.go b/session/session_test.go index 6bb735c66c9d..4e1efe3b647a 100644 --- a/session/session_test.go +++ b/session/session_test.go @@ -9,8 +9,6 @@ import ( "testing" "time" - "github.com/ory/kratos/x" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/assert" @@ -18,6 +16,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/session" ) @@ -27,7 +26,7 @@ func TestSession(t *testing.T) { authAt := time.Now() t.Run("case=active session", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) i := new(identity.Identity) i.State = identity.StateActive @@ -60,7 +59,7 @@ func TestSession(t *testing.T) { }) t.Run("case=activate", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) s := session.NewInactiveSession() require.NoError(t, s.Activate(req, &identity.Identity{State: identity.StateActive}, conf, authAt)) @@ -94,7 +93,7 @@ func TestSession(t *testing.T) { }, } { t.Run("case=parse "+tc.input, func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) req.Header["User-Agent"] = []string{"Mozilla/5.0 (X11; Linux x86_64)", "AppleWebKit/537.36 (KHTML, like Gecko)", "Chrome/51.0.2704.103 Safari/537.36"} req.Header.Set("X-Forwarded-For", tc.input) @@ -113,7 +112,7 @@ func TestSession(t *testing.T) { }) t.Run("case=client information reverse proxy real IP set", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) req.Header["User-Agent"] = []string{"Mozilla/5.0 (X11; Linux x86_64)", "AppleWebKit/537.36 (KHTML, like Gecko)", "Chrome/51.0.2704.103 Safari/537.36"} req.Header.Set("X-Real-IP", "54.155.246.155") req.Header["X-Forwarded-For"] = []string{"54.155.246.232", "10.145.1.10"} @@ -133,7 +132,7 @@ func TestSession(t *testing.T) { }) t.Run("case=client information CF true client IP set", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) req.Header["User-Agent"] = []string{"Mozilla/5.0 (X11; Linux x86_64)", "AppleWebKit/537.36 (KHTML, like Gecko)", "Chrome/51.0.2704.103 Safari/537.36"} req.Header.Set("True-Client-IP", "54.155.246.155") req.Header.Set("X-Forwarded-For", "217.73.188.139,162.158.203.149, 172.19.2.7") @@ -153,7 +152,7 @@ func TestSession(t *testing.T) { }) t.Run("case=client information CF", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) req.Header["User-Agent"] = []string{"Mozilla/5.0 (X11; Linux x86_64)", "AppleWebKit/537.36 (KHTML, like Gecko)", "Chrome/51.0.2704.103 Safari/537.36"} req.Header.Set("True-Client-IP", "54.155.246.232") req.Header.Set("Cf-Ipcity", "Munich") @@ -341,7 +340,7 @@ func TestSession(t *testing.T) { } t.Run("case=session refresh", func(t *testing.T) { - req := x.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) conf.MustSet(ctx, config.ViperKeySessionLifespan, "24h") conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, "12h") diff --git a/session/stub/jwk.es512.broken.json b/session/stub/jwk.es512.broken.json new file mode 100644 index 000000000000..e645648bce1a --- /dev/null +++ b/session/stub/jwk.es512.broken.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "EC", + "kid": "bc7f7afc-6742-427c-bb9e-164fe0f8b6a7", + "crv": "P-521", + "alg": "ES512", + "x": "ASj36HQOpsWiaGyzK1F0GkxXRt37R01M-OCWFk8rFqH8UnFBk0qnCmVYWv3pwVPPsN0CfFiaXTrV1gUSapkkDgWY", + "y": "ALf5bqXExUq6FzQNQg01hDhR2lOKzkrC02Bc6Alld8Zji3-echbimNZltoOi4MhXbSJeWHpU8wzb3v9XAAW4eovn", + "d": "ALP0Sf7cmcELc9CQ2bWd6Qs-YxMu0N9EYZhDmR6qbYdGnvv-lcGy_ySoEJD0vPMKagA8PHDvFhC7ORwP-sBIJ4O_" + } + ] + diff --git a/session/test/persistence.go b/session/test/persistence.go index d2a37837c7e9..fb6a7c469830 100644 --- a/session/test/persistence.go +++ b/session/test/persistence.go @@ -14,7 +14,7 @@ import ( "github.com/ory/kratos/identity" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/session/tokenizer_test.go b/session/tokenizer_test.go index bb69b222b83e..e6e1621e7cd8 100644 --- a/session/tokenizer_test.go +++ b/session/tokenizer_test.go @@ -10,6 +10,8 @@ import ( "testing" "time" + "github.com/ory/herodot" + "github.com/gofrs/uuid" "github.com/golang-jwt/jwt/v5" "github.com/lestrrat-go/jwx/jwk" @@ -115,4 +117,11 @@ func TestTokenizer(t *testing.T) { snapshotx.SnapshotT(t, token.Claims, snapshotx.ExceptPaths("jti")) }) + + t.Run("case=rs512-with-broken-keyfile", func(t *testing.T) { + tid := "rs512-template" + setTokenizeConfig(conf, tid, "jwk.es512.broken.json", "file://stub/rs512-template.jsonnet") + err := tkn.TokenizeSession(ctx, tid, s) + require.ErrorIs(t, err, herodot.ErrBadRequest) + }) } diff --git a/test/e2e/.go-version b/test/e2e/.go-version index 6681c8c19ab4..2844977405c2 100644 --- a/test/e2e/.go-version +++ b/test/e2e/.go-version @@ -1 +1 @@ -1.19.8 +1.21.1 diff --git a/test/e2e/cypress/integration/profiles/oidc-provider/registration.spec.ts b/test/e2e/cypress/integration/profiles/oidc-provider/registration.spec.ts index 1452844f3c18..f29e3665e5f7 100644 --- a/test/e2e/cypress/integration/profiles/oidc-provider/registration.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc-provider/registration.spec.ts @@ -1,8 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { gen } from "../../../helpers" -import * as uuid from "uuid" +import { APP_URL, gen } from "../../../helpers" import * as oauth2 from "../../../helpers/oauth2" import * as httpbin from "../../../helpers/httpbin" @@ -66,4 +65,119 @@ context("OpenID Provider", () => { expect(idToken.amr).to.deep.equal(["password"]) }) }) + + it("registration with session, skip=false and skip=true", () => { + const email = gen.email() + const password = gen.password() + + cy.register({ + email, + password, + fields: { + "traits.website": "https://www.ory.sh", + "traits.tos": "1", + "traits.age": 22, + }, + }) + + const url = oauth2.getDefaultAuthorizeURL(client) + + cy.request(url).then((res) => { + const lastResp = res.allRequestResponses[1]["Request URL"] + const login_challenge = new URL(lastResp).searchParams.get( + "login_challenge", + ) + expect(login_challenge).to.not.be.null + cy.visit( + APP_URL + + "/self-service/registration/browser?login_challenge=" + + login_challenge, + ) + }) + + cy.url().should("contain", "/login") + cy.get("[data-testid='login-flow']").should("exist") + cy.get("[data-testid='login-flow'] [name='password']").type(password) + cy.get( + "[data-testid='login-flow'] button[name='method'][value='password']", + ).click() + + // we want to skip the consent flow here + // so we ask to remember the user + cy.get("[name='remember']").click() + cy.get("#openid").click() + cy.get("#offline").click() + cy.get("#accept").click() + + const scope = ["offline", "openid"] + httpbin.checkToken(client, scope, (token: any) => { + expect(token).to.have.property("access_token") + expect(token).to.have.property("id_token") + expect(token).to.have.property("refresh_token") + expect(token).to.have.property("token_type") + expect(token).to.have.property("expires_in") + expect(token.scope).to.equal("offline openid") + let idToken = JSON.parse( + decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))), + ) + expect(idToken).to.have.property("amr") + expect(idToken.amr).to.deep.equal(["password", "password"]) + }) + + // use the hydra origin to make a new OAuth request from it + cy.get("body") + .then((body$) => { + // Credits https://github.com/suchipi, https://github.com/cypress-io/cypress/issues/944#issuecomment-444312914 + const appWindow = body$[0].ownerDocument.defaultView + const appIframe = appWindow.parent.document.querySelector("iframe") + + return new Promise((resolve) => { + appIframe.onload = () => resolve(undefined) + appWindow.location.href = "http://localhost:4744/health/ready" + }) + }) + .then(() => { + // we don't want to redirect here since we only want the login challenge from hydra + // we reusing the challenge to navigate to the registration page + cy.request({ + url: oauth2.getDefaultAuthorizeURL(client), + followRedirect: false, + }) + .then((res) => { + expect(res.redirectedToUrl).to.include("login_challenge") + return new URL(res.redirectedToUrl).searchParams.get( + "login_challenge", + ) + }) + .then((login_challenge) => { + cy.get("body").then((body$) => { + const appWindow = body$[0].ownerDocument.defaultView + const appIframe = + appWindow.parent.document.querySelector("iframe") + + return new Promise((resolve) => { + appIframe.onload = () => resolve(undefined) + appWindow.location.href = + APP_URL + + "/self-service/registration/browser?login_challenge=" + + login_challenge + }) + }) + }) + }) + + httpbin.checkToken(client, scope, (token: any) => { + expect(token).to.have.property("access_token") + expect(token).to.have.property("id_token") + expect(token).to.have.property("refresh_token") + expect(token).to.have.property("token_type") + expect(token).to.have.property("expires_in") + expect(token.scope).to.equal("offline openid") + let idToken = JSON.parse( + decodeURIComponent(escape(window.atob(token.id_token.split(".")[1]))), + ) + expect(idToken).to.have.property("amr") + expect(idToken.amr).to.deep.equal(["password", "password"]) + }) + }) }) diff --git a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts index 51ccf8574a6f..866f4344eda6 100644 --- a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts @@ -1,6 +1,6 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { gen, website } from "../../../../helpers" +import { appPrefix, gen, website } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" import { routes as react } from "../../../../helpers/react" @@ -9,16 +9,18 @@ context("Social Sign In Successes", () => { { login: react.login, registration: react.registration, + settings: react.settings, app: "react" as "react", profile: "spa", }, { login: express.login, registration: express.registration, + settings: express.settings, app: "express" as "express", profile: "oidc", }, - ].forEach(({ login, registration, profile, app }) => { + ].forEach(({ login, registration, profile, app, settings }) => { describe(`for app ${app}`, () => { before(() => { cy.useConfigProfile(profile) @@ -37,6 +39,40 @@ context("Social Sign In Successes", () => { cy.loginOidc({ app, url: login }) }) + it.only("should be able to sign up and link existing account", () => { + const email = gen.email() + const password = gen.password() + + // Create a new account + cy.registerApi({ + email, + password, + fields: { "traits.website": website }, + }) + + // Try to log in with the same identifier through OIDC. This should fail and create a new login flow. + cy.registerOidc({ + app, + email, + website, + expectSession: false, + }) + cy.noSession() + + // Log in with the same identifier through the login flow. This should link the accounts. + cy.get(`${appPrefix(app)}input[name="identifier"]`).type(email) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + cy.location("pathname").should("not.contain", "/login") + cy.getSession() + + // Hydra OIDC should now be linked + cy.visit(settings) + cy.get('[value="hydra"]') + .should("have.attr", "name", "unlink") + .should("contain.text", "Unlink hydra") + }) + it("should be able to sign up with redirects", () => { const email = gen.email() cy.registerOidc({ diff --git a/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts index ca2e2700ef7a..fe32cbbf61a2 100644 --- a/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts @@ -244,7 +244,7 @@ context("Social Sign Up Successes", () => { route: registration, }) - cy.get('[data-testid="ui/message/4000027"]').should("be.visible") + cy.get('[data-testid="ui/message/1010016"]').should("be.visible") cy.location("href").should("contain", "/login") diff --git a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts index 69f8e0f7d737..674e24e0d668 100644 --- a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts @@ -42,9 +42,9 @@ context("Social Sign In Settings Success", () => { cy.get('input[name="traits.website"]').clear().type(website) cy.triggerOidc(app, "hydra") - cy.get('[data-testid="ui/message/4000027"]').should( + cy.get('[data-testid="ui/message/1010016"]').should( "contain.text", - "An account with the same identifier", + "Signing in will link your account", ) cy.noSession() @@ -196,6 +196,19 @@ context("Social Sign In Settings Success", () => { hydraReauthFails() }) + it("should show only linked providers during reauth", () => { + cy.shortPrivilegedSessionTime() + + cy.get('input[name="password"]').type(gen.password()) + cy.get('[value="password"]').click() + + cy.location("pathname").should("equal", "/login") + + cy.get('[value="hydra"]').should("exist") + cy.get('[value="google"]').should("not.exist") + cy.get('[value="github"]').should("not.exist") + }) + it("settings screen stays intact when the original sign up method gets removed", () => { const expectSettingsOk = () => { cy.get('[value="google"]', { timeout: 1000 }) diff --git a/test/e2e/cypress/integration/profiles/verification/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/verification/registration/success.spec.ts index eac70c02e597..5065b5f2e30d 100644 --- a/test/e2e/cypress/integration/profiles/verification/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/verification/registration/success.spec.ts @@ -70,6 +70,7 @@ context("Account Verification Registration Success", () => { email, password, query: { + return_to: "http://localhost:4455/verification_return_to_callback", after_verification_return_to: "http://localhost:4455/verification_callback", }, @@ -83,6 +84,26 @@ context("Account Verification Registration Success", () => { }, }) }) + + it("is redirected to `return_to` after verification", () => { + cy.clearAllCookies() + const { email, password } = gen.identity() + cy.register({ + email, + password, + query: { + return_to: "http://localhost:4455/verification_return_to_callback", + }, + }) + cy.login({ email, password }) + cy.verifyEmail({ + expect: { + email, + password, + redirectTo: "http://localhost:4455/verification_return_to_callback", + }, + }) + }) }) }) }) diff --git a/test/e2e/cypress/integration/profiles/verification/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/verification/settings/success.spec.ts index bba960f280ae..d17ee11b9dda 100644 --- a/test/e2e/cypress/integration/profiles/verification/settings/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/verification/settings/success.spec.ts @@ -50,12 +50,14 @@ context("Account Verification Settings Success", () => { .clear() .type(email) cy.get('[value="profile"]').click() - cy.expectSettingsSaved() - cy.get('input[name="traits.email"]').should("contain.value", email) - cy.getSession().then( - assertVerifiableAddress({ isVerified: false, email }), - ) + if (app == "express") { + cy.expectSettingsSaved() + cy.get('input[name="traits.email"]').should("contain.value", email) + cy.getSession().then( + assertVerifiableAddress({ isVerified: false, email }), + ) + } cy.verifyEmail({ expect: { email } }) }) diff --git a/test/e2e/mock/webhook/Dockerfile b/test/e2e/mock/webhook/Dockerfile index 1806cdf396e9..91faa9b4b778 100644 --- a/test/e2e/mock/webhook/Dockerfile +++ b/test/e2e/mock/webhook/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.19-alpine AS build +FROM golang:1.21-alpine AS build WORKDIR /build diff --git a/test/e2e/run.sh b/test/e2e/run.sh index a5b405fe1d2e..863a70bb910b 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash echo "Running Ory Kratos E2E Tests..." echo "" diff --git a/text/id.go b/text/id.go index c0274db092ef..fa8184f0ffaf 100644 --- a/text/id.go +++ b/text/id.go @@ -25,6 +25,9 @@ const ( InfoSelfServiceLoginContinue // 1010013 InfoSelfServiceLoginEmailWithCodeSent // 1010014 InfoSelfServiceLoginCode // 1010015 + InfoSelfServiceLoginLink // 1010016 + InfoSelfServiceLoginAndLink // 1010017 + InfoSelfServiceLoginWithAndLink // 1010018 ) const ( @@ -89,6 +92,7 @@ const ( InfoNodeLabelVerificationCode // 1070011 InfoNodeLabelRegistrationCode // 1070012 InfoNodeLabelLoginCode // 1070013 + InfoNodeLabelLoginAndLinkCredential ) const ( @@ -139,15 +143,16 @@ const ( ) const ( - ErrorValidationLogin ID = 4010000 + iota // 4010000 - ErrorValidationLoginFlowExpired // 4010001 - ErrorValidationLoginNoStrategyFound // 4010002 - ErrorValidationRegistrationNoStrategyFound // 4010003 - ErrorValidationSettingsNoStrategyFound // 4010004 - ErrorValidationRecoveryNoStrategyFound // 4010005 - ErrorValidationVerificationNoStrategyFound // 4010006 - ErrorValidationLoginRetrySuccess // 4010007 - ErrorValidationLoginCodeInvalidOrAlreadyUsed // 4010008 + ErrorValidationLogin ID = 4010000 + iota // 4010000 + ErrorValidationLoginFlowExpired // 4010001 + ErrorValidationLoginNoStrategyFound // 4010002 + ErrorValidationRegistrationNoStrategyFound // 4010003 + ErrorValidationSettingsNoStrategyFound // 4010004 + ErrorValidationRecoveryNoStrategyFound // 4010005 + ErrorValidationVerificationNoStrategyFound // 4010006 + ErrorValidationLoginRetrySuccess // 4010007 + ErrorValidationLoginCodeInvalidOrAlreadyUsed // 4010008 + ErrorValidationLoginLinkedCredentialsDoNotMatch // 4010009 ) const ( diff --git a/text/message_login.go b/text/message_login.go index 3afa27da46bf..4918cb893701 100644 --- a/text/message_login.go +++ b/text/message_login.go @@ -56,6 +56,31 @@ func NewInfoLogin() *Message { } } +func NewInfoLoginLinkMessage(dupIdentifier, provider, newLoginURL string) *Message { + return &Message{ + ID: InfoSelfServiceLoginLink, + Type: Info, + Text: fmt.Sprintf( + "Signing in will link your account to %q at provider %q. If you do not wish to link that account, please start a new login flow.", + dupIdentifier, + provider, + ), + Context: context(map[string]any{ + "duplicateIdentifier": dupIdentifier, + "provider": provider, + "newLoginUrl": newLoginURL, + }), + } +} + +func NewInfoLoginAndLink() *Message { + return &Message{ + ID: InfoSelfServiceLoginAndLink, + Text: "Sign in and link", + Type: Info, + } +} + func NewInfoLoginTOTP() *Message { return &Message{ ID: InfoLoginTOTP, @@ -91,6 +116,18 @@ func NewInfoLoginWith(provider string) *Message { } } +func NewInfoLoginWithAndLink(provider string) *Message { + + return &Message{ + ID: InfoSelfServiceLoginWithAndLink, + Text: fmt.Sprintf("Sign in with %s and link credential", provider), + Type: Info, + Context: context(map[string]any{ + "provider": provider, + }), + } +} + func NewErrorValidationLoginFlowExpired(expiredAt time.Time) *Message { return &Message{ ID: ErrorValidationLoginFlowExpired, @@ -198,3 +235,11 @@ func NewInfoSelfServiceLoginCode() *Message { Text: "Sign in with code", } } + +func NewErrorValidationLoginLinkedCredentialsDoNotMatch() *Message { + return &Message{ + ID: ErrorValidationLoginLinkedCredentialsDoNotMatch, + Text: "Linked credentials do not match.", + Type: Error, + } +} diff --git a/text/message_node.go b/text/message_node.go index 3af122ae542b..e2dfb7d6dc32 100644 --- a/text/message_node.go +++ b/text/message_node.go @@ -109,3 +109,11 @@ func NewInfoNodeResendOTP() *Message { Type: Info, } } + +func NewInfoNodeLoginAndLinkCredential() *Message { + return &Message{ + ID: InfoNodeLabelLoginAndLinkCredential, + Text: "Login and link credential", + Type: Info, + } +} diff --git a/text/message_validation.go b/text/message_validation.go index 205c27304cdb..28396180c0c5 100644 --- a/text/message_validation.go +++ b/text/message_validation.go @@ -317,8 +317,9 @@ func NewErrorValidationDuplicateCredentialsWithHints(availableCredentialTypes [] func NewErrorValidationDuplicateCredentialsOnOIDCLink() *Message { return &Message{ - ID: ErrorValidationDuplicateCredentialsOnOIDCLink, - Text: "An account with the same identifier (email, phone, username, ...) exists already. Please sign in to your existing account and link your social profile in the settings page.", + ID: ErrorValidationDuplicateCredentialsOnOIDCLink, + Text: "An account with the same identifier (email, phone, username, ...) exists already. " + + "Please sign in to your existing account to link your social profile.", Type: Error, } } diff --git a/ui/node/node_test.go b/ui/node/node_test.go index cfa425632ef2..f8867b98c2a3 100644 --- a/ui/node/node_test.go +++ b/ui/node/node_test.go @@ -17,7 +17,7 @@ import ( "github.com/ory/kratos/ui/container" - "github.com/bxcodec/faker/v3" + "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" diff --git a/x/events/events.go b/x/events/events.go index 35992663319f..52e921112d12 100644 --- a/x/events/events.go +++ b/x/events/events.go @@ -5,6 +5,7 @@ package events import ( "context" + "net/url" "time" "github.com/gofrs/uuid" @@ -32,6 +33,9 @@ const ( VerificationSucceeded semconv.Event = "VerificationSucceeded" IdentityCreated semconv.Event = "IdentityCreated" IdentityUpdated semconv.Event = "IdentityUpdated" + WebhookDelivered semconv.Event = "WebhookDelivered" + WebhookSucceeded semconv.Event = "WebhookSucceeded" + WebhookFailed semconv.Event = "WebhookFailed" ) const ( @@ -43,6 +47,12 @@ const ( attributeKeyLoginRequestedAAL semconv.AttributeKey = "LoginRequestedAAL" attributeKeyLoginRequestedPrivilegedSession semconv.AttributeKey = "LoginRequestedPrivilegedSession" attributeKeyTokenizedSessionTTL semconv.AttributeKey = "TokenizedSessionTTL" + attributeKeyWebhookURL semconv.AttributeKey = "WebhookURL" + attributeKeyWebhookRequestBody semconv.AttributeKey = "WebhookRequestBody" + attributeKeyWebhookResponseBody semconv.AttributeKey = "WebhookResponseBody" + attributeKeyWebhookResponseStatusCode semconv.AttributeKey = "WebhookResponseStatusCode" + attributeKeyWebhookAttemptNumber semconv.AttributeKey = "WebhookAttemptNumber" + attributeKeyWebhookRequestID semconv.AttributeKey = "WebhookRequestID" ) func attrSessionID(val uuid.UUID) otelattr.KeyValue { @@ -77,6 +87,30 @@ func attrSelfServiceSSOProviderUsed(val string) otelattr.KeyValue { return otelattr.String(attributeKeySelfServiceSSOProviderUsed.String(), val) } +func attrWebhookURL(URL *url.URL) otelattr.KeyValue { + return otelattr.String(attributeKeyWebhookURL.String(), URL.Redacted()) +} + +func attrWebhookReq(body []byte) otelattr.KeyValue { + return otelattr.String(attributeKeyWebhookRequestBody.String(), string(body)) +} + +func attrWebhookRes(body []byte) otelattr.KeyValue { + return otelattr.String(attributeKeyWebhookResponseBody.String(), string(body)) +} + +func attrWebhookStatus(status int) otelattr.KeyValue { + return otelattr.Int(attributeKeyWebhookResponseStatusCode.String(), status) +} + +func attrWebhookAttempt(n int) otelattr.KeyValue { + return otelattr.Int(attributeKeyWebhookAttemptNumber.String(), n) +} + +func attrWebhookRequestID(id uuid.UUID) otelattr.KeyValue { + return otelattr.String(attributeKeyWebhookRequestID.String(), id.String()) +} + func NewSessionIssued(ctx context.Context, aal string, sessionID, identityID uuid.UUID) (string, trace.EventOption) { return SessionIssued.String(), trace.WithAttributes( @@ -263,3 +297,33 @@ func NewSessionJWTIssued(ctx context.Context, sessionID, identityID uuid.UUID, t )..., ) } + +func NewWebhookDelivered(ctx context.Context, URL *url.URL, reqBody []byte, status int, resBody []byte, attempt int, requestID uuid.UUID) (string, trace.EventOption) { + return WebhookDelivered.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + attrWebhookReq(reqBody), + attrWebhookRes(resBody), + attrWebhookStatus(status), + attrWebhookURL(URL), + attrWebhookAttempt(attempt), + attrWebhookRequestID(requestID), + )..., + ) +} + +func NewWebhookSucceeded(ctx context.Context) (string, trace.EventOption) { + return WebhookSucceeded.String(), + trace.WithAttributes(semconv.AttributesFromContext(ctx)...) +} + +func NewWebhookFailed(ctx context.Context, err error) (string, trace.EventOption) { + return WebhookFailed.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + otelattr.String("Error", err.Error()), + )..., + ) +} diff --git a/x/http.go b/x/http.go index 2ee9254f9827..380ca14a1034 100644 --- a/x/http.go +++ b/x/http.go @@ -5,11 +5,8 @@ package x import ( "context" - "io" "net/http" - "net/http/cookiejar" "net/url" - "testing" "github.com/ory/x/httpx" @@ -19,49 +16,9 @@ import ( "github.com/ory/herodot" - "github.com/stretchr/testify/require" - "github.com/ory/x/stringsx" ) -func NewTestHTTPRequest(t *testing.T, method, url string, body io.Reader) *http.Request { - req, err := http.NewRequest(method, url, body) - require.NoError(t, err) - return req -} - -func EasyGet(t *testing.T, c *http.Client, url string) (*http.Response, []byte) { - res, err := c.Get(url) - require.NoError(t, err) - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - return res, body -} - -func EasyGetJSON(t *testing.T, c *http.Client, url string) (*http.Response, []byte) { - req, err := http.NewRequest("GET", url, nil) - require.NoError(t, err) - req.Header.Set("Accept", "application/json") - res, err := c.Do(req) - require.NoError(t, err) - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - return res, body -} - -func EasyGetBody(t *testing.T, c *http.Client, url string) []byte { - _, body := EasyGet(t, c, url) // nolint: bodyclose - return body -} - -func EasyCookieJar(t *testing.T, o *cookiejar.Options) *cookiejar.Jar { - cj, err := cookiejar.New(o) - require.NoError(t, err) - return cj -} - func RequestURL(r *http.Request) *url.URL { source := *r.URL source.Host = stringsx.Coalesce(source.Host, r.Header.Get("X-Forwarded-Host"), r.Host) @@ -80,42 +37,6 @@ func RequestURL(r *http.Request) *url.URL { return &source } -func NewTransportWithHeader(h http.Header) *TransportWithHeader { - return &TransportWithHeader{ - RoundTripper: http.DefaultTransport, - h: h, - } -} - -type TransportWithHeader struct { - http.RoundTripper - h http.Header -} - -func (ct *TransportWithHeader) RoundTrip(req *http.Request) (*http.Response, error) { - for k := range ct.h { - req.Header.Set(k, ct.h.Get(k)) - } - return ct.RoundTripper.RoundTrip(req) -} - -func NewTransportWithHost(host string) *TransportWithHost { - return &TransportWithHost{ - RoundTripper: http.DefaultTransport, - host: host, - } -} - -type TransportWithHost struct { - http.RoundTripper - host string -} - -func (ct *TransportWithHost) RoundTrip(req *http.Request) (*http.Response, error) { - req.Host = ct.host - return ct.RoundTripper.RoundTrip(req) -} - func AcceptToRedirectOrJSON( w http.ResponseWriter, r *http.Request, writer herodot.Writer, out interface{}, redirectTo string, ) {