snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
932
selfservice/strategy/link/strategy_verification_test.go
Normal file
932
selfservice/strategy/link/strategy_verification_test.go
Normal file
@@ -0,0 +1,932 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package link_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ory/kratos/x/nosurfx"
|
||||
|
||||
"github.com/ory/x/urlx"
|
||||
|
||||
"github.com/ory/kratos/selfservice/strategy/link"
|
||||
"github.com/ory/kratos/ui/node"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/pkg"
|
||||
"github.com/ory/kratos/pkg/testhelpers"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/selfservice/flow/settings"
|
||||
"github.com/ory/kratos/selfservice/flow/verification"
|
||||
"github.com/ory/kratos/text"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/assertx"
|
||||
"github.com/ory/x/ioutilx"
|
||||
"github.com/ory/x/sqlxx"
|
||||
)
|
||||
|
||||
func TestVerification(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
initViper(t, conf)
|
||||
|
||||
identityToVerify := &identity.Identity{
|
||||
ID: x.NewUUID(),
|
||||
Traits: identity.Traits(`{"email":"verifyme@ory.sh"}`),
|
||||
SchemaID: config.DefaultIdentityTraitsSchemaID,
|
||||
Credentials: map[identity.CredentialsType]identity.Credentials{
|
||||
"password": {Type: "password", Identifiers: []string{"recoverme@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)},
|
||||
},
|
||||
}
|
||||
|
||||
verificationEmail := gjson.GetBytes(identityToVerify.Traits, "email").String()
|
||||
|
||||
_ = testhelpers.NewVerificationUIFlowEchoServer(t, reg)
|
||||
_ = testhelpers.NewLoginUIFlowEchoServer(t, reg)
|
||||
_ = testhelpers.NewSettingsUIFlowEchoServer(t, reg)
|
||||
_ = testhelpers.NewErrorTestServer(t, reg)
|
||||
_ = testhelpers.NewRedirTS(t, "returned", conf)
|
||||
|
||||
public, _ := testhelpers.NewKratosServerWithCSRF(t, reg)
|
||||
|
||||
require.NoError(t, reg.IdentityManager().Create(context.Background(), identityToVerify,
|
||||
identity.ManagerAllowWriteProtectedTraits))
|
||||
|
||||
expect := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string {
|
||||
if hc == nil {
|
||||
hc = testhelpers.NewDebugClient(t)
|
||||
if !isAPI {
|
||||
hc = testhelpers.NewClientWithCookies(t)
|
||||
hc.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewTestTransport(t), t).RoundTripper
|
||||
}
|
||||
}
|
||||
|
||||
return testhelpers.SubmitVerificationForm(t, isAPI, isSPA, hc, public, values, c,
|
||||
testhelpers.ExpectURL(isAPI || isSPA, public.URL+verification.RouteSubmitFlow, conf.SelfServiceFlowVerificationUI(ctx).String()))
|
||||
}
|
||||
|
||||
expectValidationError := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string {
|
||||
return expect(t, hc, isAPI, isSPA, values, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK))
|
||||
}
|
||||
|
||||
expectSuccess := func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values)) string {
|
||||
return expect(t, hc, isAPI, isSPA, values, http.StatusOK)
|
||||
}
|
||||
|
||||
t.Run("description=should set all the correct verification payloads after submission", func(t *testing.T) {
|
||||
body := expectSuccess(t, nil, false, false, func(v url.Values) {
|
||||
v.Set("email", "test@ory.sh")
|
||||
})
|
||||
testhelpers.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.nodes").String()), []string{"0.attributes.value"})
|
||||
})
|
||||
|
||||
t.Run("description=should set all the correct verification payloads", func(t *testing.T) {
|
||||
c := testhelpers.NewClientWithCookies(t)
|
||||
rs := testhelpers.GetVerificationFlow(t, c, public)
|
||||
|
||||
testhelpers.SnapshotTExcept(t, rs.Ui.Nodes, []string{"0.attributes.value"})
|
||||
assert.EqualValues(t, public.URL+verification.RouteSubmitFlow+"?flow="+rs.Id, rs.Ui.Action)
|
||||
assert.Empty(t, rs.Ui.Messages)
|
||||
})
|
||||
|
||||
t.Run("description=should not execute submit without correct method set", func(t *testing.T) {
|
||||
c := testhelpers.NewClientWithCookies(t)
|
||||
rs := testhelpers.GetVerificationFlow(t, c, public)
|
||||
|
||||
res, err := c.PostForm(rs.Ui.Action, url.Values{"method": {"not-link"}, "email": {verificationEmail}})
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, http.StatusOK, res.StatusCode)
|
||||
assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String())
|
||||
|
||||
body := ioutilx.MustReadAll(res.Body)
|
||||
require.NoError(t, res.Body.Close())
|
||||
assert.Equal(t, "Could not find a strategy to verify your account with. Did you fill out the form correctly?", gjson.GetBytes(body, "ui.messages.0.text").String(), "%s", body)
|
||||
})
|
||||
|
||||
t.Run("description=should require an email to be sent", func(t *testing.T) {
|
||||
check := func(t *testing.T, actual string) {
|
||||
assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
|
||||
assert.EqualValues(t, "Property email is missing.",
|
||||
gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(),
|
||||
"%s", actual)
|
||||
}
|
||||
|
||||
values := func(v url.Values) {
|
||||
v.Del("email")
|
||||
}
|
||||
|
||||
t.Run("type=browser", func(t *testing.T) {
|
||||
check(t, expectValidationError(t, nil, false, false, values))
|
||||
})
|
||||
|
||||
t.Run("type=spa", func(t *testing.T) {
|
||||
check(t, expectValidationError(t, nil, false, true, values))
|
||||
})
|
||||
|
||||
t.Run("type=api", func(t *testing.T) {
|
||||
check(t, expectValidationError(t, nil, true, false, values))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("description=should require a valid email to be sent", func(t *testing.T) {
|
||||
check := func(t *testing.T, actual string, value string) {
|
||||
assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
|
||||
assert.EqualValues(t, "Enter a valid email address",
|
||||
gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(),
|
||||
"%s", actual)
|
||||
}
|
||||
|
||||
for _, email := range []string{"\\", "asdf", "...", "aiacobelli.sec@gmail.com,alejandro.iacobelli@mercadolibre.com"} {
|
||||
values := func(v url.Values) {
|
||||
v.Set("email", email)
|
||||
}
|
||||
|
||||
t.Run("type=browser", func(t *testing.T) {
|
||||
check(t, expectValidationError(t, nil, false, false, values), email)
|
||||
})
|
||||
|
||||
t.Run("type=spa", func(t *testing.T) {
|
||||
check(t, expectValidationError(t, nil, false, true, values), email)
|
||||
})
|
||||
|
||||
t.Run("type=api", func(t *testing.T) {
|
||||
check(t, expectValidationError(t, nil, true, false, values), email)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("description=should try to verify an email that does not exist", func(t *testing.T) {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, true)
|
||||
|
||||
t.Cleanup(func() {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationNotifyUnknownRecipients, false)
|
||||
})
|
||||
var email string
|
||||
check := func(t *testing.T, actual string) {
|
||||
assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
|
||||
assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual)
|
||||
assertx.EqualAsJSON(t, text.NewVerificationEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw))
|
||||
|
||||
message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Someone tried to verify this email address")
|
||||
assert.Contains(t, message.Body, "If this was you, check if you signed up using a different address.")
|
||||
}
|
||||
|
||||
values := func(v url.Values) {
|
||||
v.Set("email", email)
|
||||
}
|
||||
|
||||
t.Run("type=browser", func(t *testing.T) {
|
||||
email = x.NewUUID().String() + "@ory.sh"
|
||||
check(t, expectSuccess(t, nil, false, false, values))
|
||||
})
|
||||
|
||||
t.Run("type=spa", func(t *testing.T) {
|
||||
email = x.NewUUID().String() + "@ory.sh"
|
||||
check(t, expectSuccess(t, nil, false, true, values))
|
||||
})
|
||||
|
||||
t.Run("type=api", func(t *testing.T) {
|
||||
email = x.NewUUID().String() + "@ory.sh"
|
||||
check(t, expectSuccess(t, nil, true, false, values))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("description=should not be able to use an invalid link", func(t *testing.T) {
|
||||
c := testhelpers.NewClientWithCookies(t)
|
||||
f := testhelpers.InitializeVerificationFlowViaBrowser(t, c, false, public)
|
||||
res, err := c.Get(public.URL + verification.RouteSubmitFlow + "?flow=" + f.Id + "&token=i-do-not-exist")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String()+"?flow=")
|
||||
|
||||
sr, _, err := testhelpers.NewSDKCustomClient(public, c).FrontendAPI.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, sr.Ui.Messages, 1)
|
||||
assert.Equal(t, "The verification token is invalid or has already been used. Please retry the flow.", sr.Ui.Messages[0].Text)
|
||||
})
|
||||
|
||||
t.Run("description=should not be able to request link with an outdated flow", func(t *testing.T) {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationRequestLifespan, time.Millisecond*200)
|
||||
t.Cleanup(func() {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationRequestLifespan, time.Minute)
|
||||
})
|
||||
|
||||
c := testhelpers.NewClientWithCookies(t)
|
||||
rs := testhelpers.GetVerificationFlow(t, c, public)
|
||||
|
||||
time.Sleep(time.Millisecond * 201)
|
||||
|
||||
res, err := c.PostForm(rs.Ui.Action, url.Values{"method": {"link"}, "email": {verificationEmail}})
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, http.StatusOK, res.StatusCode)
|
||||
assert.NotContains(t, res.Request.URL.String(), "flow="+rs.Id)
|
||||
assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String())
|
||||
})
|
||||
|
||||
t.Run("description=should not be able to use link with an outdated flow", func(t *testing.T) {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationRequestLifespan, time.Millisecond*200)
|
||||
t.Cleanup(func() {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationRequestLifespan, time.Minute)
|
||||
})
|
||||
|
||||
c := testhelpers.NewClientWithCookies(t)
|
||||
body := expectSuccess(t, c, false, false, func(v url.Values) {
|
||||
v.Set("email", verificationEmail)
|
||||
})
|
||||
|
||||
message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address")
|
||||
assert.Contains(t, message.Body, "Verify your account by opening the following link")
|
||||
|
||||
verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1)
|
||||
|
||||
time.Sleep(time.Millisecond * 201)
|
||||
|
||||
// Clear cookies as link might be opened in another browser
|
||||
c = testhelpers.NewClientWithCookies(t)
|
||||
res, err := c.Get(verificationLink)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, http.StatusOK, res.StatusCode)
|
||||
assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String())
|
||||
assert.NotContains(t, res.Request.URL.String(), gjson.Get(body, "id").String())
|
||||
|
||||
sr, _, err := testhelpers.NewSDKCustomClient(public, c).FrontendAPI.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, sr.Ui.Messages, 1)
|
||||
assert.Contains(t, sr.Ui.Messages[0].Text, "The verification flow expired")
|
||||
})
|
||||
|
||||
t.Run("description=should verify an email address", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
testhelpers.NewVerifyAfterHookWebHookTarget(ctx, t, conf, func(t *testing.T, msg []byte) {
|
||||
defer wg.Done()
|
||||
assert.EqualValues(t, true, gjson.GetBytes(msg, "identity.verifiable_addresses.0.verified").Bool(), string(msg))
|
||||
assert.EqualValues(t, "completed", gjson.GetBytes(msg, "identity.verifiable_addresses.0.status").String(), string(msg))
|
||||
})
|
||||
check := func(t *testing.T, actual string) {
|
||||
assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
|
||||
assert.EqualValues(t, verificationEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual)
|
||||
assertx.EqualAsJSON(t, text.NewVerificationEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw))
|
||||
|
||||
message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address")
|
||||
assert.Contains(t, message.Body, "Verify your account by opening the following link")
|
||||
|
||||
verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1)
|
||||
|
||||
assert.Contains(t, verificationLink, public.URL+verification.RouteSubmitFlow)
|
||||
assert.Contains(t, verificationLink, "token=")
|
||||
|
||||
cl := testhelpers.NewClientWithCookies(t)
|
||||
res, err := cl.Get(verificationLink)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String())
|
||||
body := string(ioutilx.MustReadAll(res.Body))
|
||||
assert.EqualValues(t, "passed_challenge", gjson.Get(body, "state").String())
|
||||
assert.EqualValues(t, text.NewInfoSelfServiceVerificationSuccessful().Text, gjson.Get(body, "ui.messages.0.text").String())
|
||||
|
||||
id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), identityToVerify.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, id.VerifiableAddresses, 1)
|
||||
|
||||
address := id.VerifiableAddresses[0]
|
||||
assert.EqualValues(t, verificationEmail, address.Value)
|
||||
assert.True(t, address.Verified)
|
||||
assert.EqualValues(t, identity.VerifiableAddressStatusCompleted, address.Status)
|
||||
assert.True(t, time.Time(*address.VerifiedAt).Add(time.Second*5).After(time.Now()))
|
||||
}
|
||||
|
||||
values := func(v url.Values) {
|
||||
v.Set("email", verificationEmail)
|
||||
}
|
||||
|
||||
t.Run("type=browser", func(t *testing.T) {
|
||||
wg.Add(1)
|
||||
check(t, expectSuccess(t, nil, false, false, values))
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("type=spa", func(t *testing.T) {
|
||||
wg.Add(1)
|
||||
check(t, expectSuccess(t, nil, false, true, values))
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("type=api", func(t *testing.T) {
|
||||
wg.Add(1)
|
||||
check(t, expectSuccess(t, nil, true, false, values))
|
||||
wg.Wait()
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("description=should verify an email address when the link is opened in another browser", func(t *testing.T) {
|
||||
check := func(t *testing.T, actual string) {
|
||||
message := testhelpers.CourierExpectMessage(ctx, t, reg, verificationEmail, "Please verify your email address")
|
||||
verificationLink := testhelpers.CourierExpectLinkInMessage(t, message, 1)
|
||||
|
||||
cl := testhelpers.NewClientWithCookies(t)
|
||||
res, err := cl.Get(verificationLink)
|
||||
require.NoError(t, err)
|
||||
body := string(ioutilx.MustReadAll(res.Body))
|
||||
require.NoError(t, res.Body.Close())
|
||||
require.Len(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL)), 1)
|
||||
assert.Contains(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL))[0].Name, nosurfx.CSRFTokenName)
|
||||
|
||||
actualRes, err := cl.Get(public.URL + verification.RouteGetFlow + "?id=" + gjson.Get(body, "id").String())
|
||||
require.NoError(t, err)
|
||||
actualBody := string(ioutilx.MustReadAll(actualRes.Body))
|
||||
require.NoError(t, actualRes.Body.Close())
|
||||
assert.Equal(t, http.StatusOK, actualRes.StatusCode)
|
||||
|
||||
assertx.EqualAsJSON(t, body, actualBody)
|
||||
assert.EqualValues(t, "passed_challenge", gjson.Get(actualBody, "state").String())
|
||||
}
|
||||
|
||||
values := func(v url.Values) {
|
||||
v.Set("email", verificationEmail)
|
||||
}
|
||||
|
||||
check(t, expectSuccess(t, nil, false, false, values))
|
||||
})
|
||||
|
||||
newValidFlow := func(t *testing.T, fType flow.Type, requestURL string) (*verification.Flow, *link.VerificationToken) {
|
||||
f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, httptest.NewRequest("GET", requestURL, nil), nil, fType)
|
||||
require.NoError(t, err)
|
||||
f.State = flow.StateEmailSent
|
||||
require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(context.Background(), f))
|
||||
email := identity.NewVerifiableEmailAddress(verificationEmail, identityToVerify.ID)
|
||||
identityToVerify.VerifiableAddresses = append(identityToVerify.VerifiableAddresses, *email)
|
||||
require.NoError(t, reg.IdentityManager().Update(context.Background(), identityToVerify, identity.ManagerAllowWriteProtectedTraits))
|
||||
|
||||
token := link.NewSelfServiceVerificationToken(&identityToVerify.VerifiableAddresses[0], f, time.Hour)
|
||||
require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(context.Background(), token))
|
||||
return f, token
|
||||
}
|
||||
|
||||
newValidBrowserFlow := func(t *testing.T, requestURL string) (*verification.Flow, *link.VerificationToken) {
|
||||
return newValidFlow(t, flow.TypeBrowser, requestURL)
|
||||
}
|
||||
|
||||
t.Run("case=respects return_to URI parameter", func(t *testing.T) {
|
||||
returnToURL := public.URL + "/after-verification"
|
||||
conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnToURL})
|
||||
|
||||
for _, fType := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} {
|
||||
t.Run(fmt.Sprintf("type=%s", fType), func(t *testing.T) {
|
||||
client := testhelpers.NewClientWithCookies(t)
|
||||
flow, token := newValidFlow(t, fType, public.URL+verification.RouteInitBrowserFlow+"?"+url.Values{"return_to": {returnToURL}}.Encode())
|
||||
|
||||
res, err := client.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {flow.ID.String()}, "token": {token.Token}}.Encode())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
responseBody := gjson.ParseBytes(ioutilx.MustReadAll(res.Body))
|
||||
|
||||
assert.Equal(t, responseBody.Get("state").String(), "passed_challenge", "%v", responseBody)
|
||||
assert.True(t, responseBody.Get("ui.nodes.#(attributes.id==continue)").Exists(), "%v", responseBody)
|
||||
assert.Equal(t, returnToURL, responseBody.Get("ui.nodes.#(attributes.id==continue).attributes.href").String(), "%v", responseBody)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("case=contains default return to url", func(t *testing.T) {
|
||||
globalReturnTo := public.URL + "/global"
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, globalReturnTo)
|
||||
|
||||
for _, fType := range []flow.Type{flow.TypeBrowser, flow.TypeAPI} {
|
||||
t.Run(fmt.Sprintf("type=%s", fType), func(t *testing.T) {
|
||||
client := testhelpers.NewClientWithCookies(t)
|
||||
flow, token := newValidFlow(t, fType, public.URL+verification.RouteInitBrowserFlow)
|
||||
|
||||
res, err := client.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {flow.ID.String()}, "token": {token.Token}}.Encode())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
responseBody := gjson.ParseBytes(ioutilx.MustReadAll(res.Body))
|
||||
|
||||
assert.Equal(t, responseBody.Get("state").String(), "passed_challenge", "%v", responseBody)
|
||||
assert.True(t, responseBody.Get("ui.nodes.#(attributes.id==continue)").Exists(), "%v", responseBody)
|
||||
assert.Equal(t, globalReturnTo, responseBody.Get("ui.nodes.#(attributes.id==continue).attributes.href").String(), "%v", responseBody)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("case=should not be able to use code from different flow", func(t *testing.T) {
|
||||
f1, _ := newValidBrowserFlow(t, public.URL+verification.RouteInitBrowserFlow)
|
||||
|
||||
_, t2 := newValidBrowserFlow(t, public.URL+verification.RouteInitBrowserFlow)
|
||||
|
||||
formValues := url.Values{
|
||||
"flow": {f1.ID.String()},
|
||||
"token": {t2.Token},
|
||||
}
|
||||
submitUrl := public.URL + verification.RouteSubmitFlow + "?" + formValues.Encode()
|
||||
|
||||
res, err := public.Client().Get(submitUrl)
|
||||
require.NoError(t, err)
|
||||
body := ioutilx.MustReadAll(res.Body)
|
||||
|
||||
assert.Equal(t, "The verification token is invalid or has already been used. Please retry the flow.", gjson.GetBytes(body, "ui.messages.0.text").String())
|
||||
})
|
||||
|
||||
t.Run("description=should apply pending traits change when token is redeemed", func(t *testing.T) {
|
||||
// Sibling subtests "fires settings post-persist webhook..." and
|
||||
// "post-persist webhook error..." mutate the shared
|
||||
// selfservice.flows.settings.after.profile.hooks config via
|
||||
// conf.MustSet and clean up on subtest exit. Running this subtest
|
||||
// in parallel races against that cleanup and against the shared
|
||||
// SQLite database (SQLITE_BUSY), so it stays sequential.
|
||||
// Create an identity with original traits.
|
||||
pendingID := &identity.Identity{
|
||||
ID: x.NewUUID(),
|
||||
State: identity.StateActive,
|
||||
Traits: identity.Traits(`{"email":"original@ory.sh"}`),
|
||||
SchemaID: config.DefaultIdentityTraitsSchemaID,
|
||||
Credentials: map[identity.CredentialsType]identity.Credentials{
|
||||
"password": {Type: "password", Identifiers: []string{"original@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)},
|
||||
},
|
||||
}
|
||||
require.NoError(t, reg.IdentityManager().Create(ctx, pendingID, identity.ManagerAllowWriteProtectedTraits))
|
||||
|
||||
// The apply step now requires a live session that created the change.
|
||||
sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, pendingID,
|
||||
time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
|
||||
|
||||
// Create a verification flow in StateEmailSent.
|
||||
f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser)
|
||||
require.NoError(t, err)
|
||||
f.State = flow.StateEmailSent
|
||||
require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f))
|
||||
|
||||
// Create a settings flow that acts as the origin for the PTC.
|
||||
sf := &settings.Flow{
|
||||
ID: x.NewUUID(),
|
||||
ExpiresAt: time.Now().UTC().Add(time.Hour),
|
||||
IssuedAt: time.Now().UTC(),
|
||||
RequestURL: public.URL + "/settings",
|
||||
IdentityID: pendingID.ID,
|
||||
Identity: pendingID,
|
||||
Type: flow.TypeBrowser,
|
||||
State: flow.StateShowForm,
|
||||
InternalContext: []byte("{}"),
|
||||
}
|
||||
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sf))
|
||||
|
||||
// Create a PendingTraitsChange record linked to this flow.
|
||||
sessID := sess.ID
|
||||
originFlowID := sf.ID
|
||||
ptc := &identity.PendingTraitsChange{
|
||||
ID: x.NewUUID(),
|
||||
IdentityID: pendingID.ID,
|
||||
SessionID: uuid.NullUUID{UUID: sessID, Valid: true},
|
||||
OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowID, Valid: true},
|
||||
NewAddressValue: "changed@ory.sh",
|
||||
NewAddressVia: string(identity.AddressTypeEmail),
|
||||
OriginalTraitsHash: identity.HashTraits(json.RawMessage(pendingID.Traits)),
|
||||
ProposedTraits: json.RawMessage(`{"email":"changed@ory.sh"}`),
|
||||
VerificationFlowID: f.ID,
|
||||
Status: identity.PendingTraitsChangeStatusPending,
|
||||
}
|
||||
require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc))
|
||||
|
||||
// Create a verification token with a nil address ID (pending-change signal).
|
||||
// The address is only used to construct the token; nil it before persisting
|
||||
// so pop does not try to create a non-existent association row.
|
||||
token := link.NewSelfServiceVerificationToken(ptc, f, time.Hour)
|
||||
token.VerifiableAddress = nil
|
||||
require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(ctx, token))
|
||||
|
||||
// Redeem the token via HTTP GET.
|
||||
cl := testhelpers.NewClientWithCookies(t)
|
||||
res, err := cl.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {f.ID.String()}, "token": {token.Token}}.Encode())
|
||||
require.NoError(t, err)
|
||||
body := string(ioutilx.MustReadAll(res.Body))
|
||||
require.NoError(t, res.Body.Close())
|
||||
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
require.EqualValues(t, "passed_challenge", gjson.Get(body, "state").String(), "%s", body)
|
||||
assert.EqualValues(t, text.NewInfoSelfServiceVerificationSuccessful().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
|
||||
|
||||
// Verify the identity traits were updated.
|
||||
updated, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, pendingID.ID)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, "changed@ory.sh", gjson.GetBytes([]byte(updated.Traits), "email").String())
|
||||
|
||||
// Verify the new address is marked as verified.
|
||||
var foundAddress *identity.VerifiableAddress
|
||||
for idx := range updated.VerifiableAddresses {
|
||||
if updated.VerifiableAddresses[idx].Value == "changed@ory.sh" {
|
||||
foundAddress = &updated.VerifiableAddresses[idx]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, foundAddress, "new address should exist on identity")
|
||||
assert.True(t, foundAddress.Verified)
|
||||
assert.EqualValues(t, identity.VerifiableAddressStatusCompleted, foundAddress.Status)
|
||||
})
|
||||
|
||||
t.Run("description=fires settings post-persist webhook after pending traits change is applied", func(t *testing.T) {
|
||||
// Set up a recording webhook target.
|
||||
var receivedBody []byte
|
||||
receivedCh := make(chan struct{}, 1)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
receivedBody, _ = io.ReadAll(r.Body)
|
||||
select {
|
||||
case receivedCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
// Register webhook as a settings-profile post-persist hook.
|
||||
conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{
|
||||
{
|
||||
"hook": "web_hook",
|
||||
"config": map[string]any{
|
||||
"url": ts.URL,
|
||||
"method": "POST",
|
||||
"body": "base64://" + base64.StdEncoding.EncodeToString([]byte(`function(ctx) ctx`)),
|
||||
},
|
||||
},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{})
|
||||
})
|
||||
|
||||
// Create an identity + active session.
|
||||
ident := &identity.Identity{
|
||||
ID: x.NewUUID(),
|
||||
Traits: identity.Traits(`{"email":"posthook-link-original@ory.sh"}`),
|
||||
SchemaID: config.DefaultIdentityTraitsSchemaID,
|
||||
State: identity.StateActive,
|
||||
}
|
||||
require.NoError(t, reg.IdentityManager().Create(ctx, ident, identity.ManagerAllowWriteProtectedTraits))
|
||||
sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, ident, time.Now().UTC(),
|
||||
identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
|
||||
|
||||
// Create verification flow + PTC linked to the session.
|
||||
f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken,
|
||||
httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser)
|
||||
|
||||
require.NoError(t, err)
|
||||
f.State = flow.StateEmailSent
|
||||
require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f))
|
||||
|
||||
// Create a settings flow that acts as the origin for the PTC.
|
||||
sfPosthook := &settings.Flow{
|
||||
ID: x.NewUUID(),
|
||||
ExpiresAt: time.Now().UTC().Add(time.Hour),
|
||||
IssuedAt: time.Now().UTC(),
|
||||
RequestURL: public.URL + "/settings",
|
||||
IdentityID: ident.ID,
|
||||
Identity: ident,
|
||||
Type: flow.TypeBrowser,
|
||||
State: flow.StateShowForm,
|
||||
InternalContext: []byte("{}"),
|
||||
}
|
||||
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfPosthook))
|
||||
|
||||
sessID := sess.ID
|
||||
originFlowIDPosthook := sfPosthook.ID
|
||||
ptc := &identity.PendingTraitsChange{
|
||||
ID: x.NewUUID(),
|
||||
IdentityID: ident.ID,
|
||||
SessionID: uuid.NullUUID{UUID: sessID, Valid: true},
|
||||
OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDPosthook, Valid: true},
|
||||
NewAddressValue: "posthook-link-new@ory.sh",
|
||||
NewAddressVia: string(identity.AddressTypeEmail),
|
||||
OriginalTraitsHash: identity.HashTraits(json.RawMessage(ident.Traits)),
|
||||
ProposedTraits: json.RawMessage(`{"email":"posthook-link-new@ory.sh"}`),
|
||||
VerificationFlowID: f.ID,
|
||||
Status: identity.PendingTraitsChangeStatusPending,
|
||||
}
|
||||
require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc))
|
||||
|
||||
// Create a verification token with a nil address ID (pending-change signal).
|
||||
token := link.NewSelfServiceVerificationToken(ptc, f, time.Hour)
|
||||
token.VerifiableAddress = nil
|
||||
require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(ctx, token))
|
||||
|
||||
// Redeem the token via HTTP GET (the link-based verification path).
|
||||
cl := testhelpers.NewClientWithCookies(t)
|
||||
res, err := cl.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {f.ID.String()}, "token": {token.Token}}.Encode())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, res.Body.Close())
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
select {
|
||||
case <-receivedCh:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("settings post-persist webhook was not invoked after pending traits change applied")
|
||||
}
|
||||
assert.Contains(t, string(receivedBody), "posthook-link-new@ory.sh")
|
||||
})
|
||||
|
||||
t.Run("description=post-persist webhook error after pending traits change is surfaced", func(t *testing.T) {
|
||||
// Webhook target that always returns 500.
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(`{"error":"boom"}`))
|
||||
}))
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{
|
||||
{
|
||||
"hook": "web_hook",
|
||||
"config": map[string]any{
|
||||
"url": ts.URL,
|
||||
"method": "POST",
|
||||
"body": "base64://" + base64.StdEncoding.EncodeToString([]byte(`function(ctx) ctx`)),
|
||||
},
|
||||
},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
conf.MustSet(ctx, "selfservice.flows.settings.after.profile.hooks", []map[string]any{})
|
||||
})
|
||||
|
||||
ident := &identity.Identity{
|
||||
ID: x.NewUUID(),
|
||||
Traits: identity.Traits(`{"email":"posthook-link-err-original@ory.sh"}`),
|
||||
SchemaID: config.DefaultIdentityTraitsSchemaID,
|
||||
State: identity.StateActive,
|
||||
}
|
||||
require.NoError(t, reg.IdentityManager().Create(ctx, ident, identity.ManagerAllowWriteProtectedTraits))
|
||||
sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, ident, time.Now().UTC(),
|
||||
identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
|
||||
|
||||
f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken,
|
||||
httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser)
|
||||
|
||||
require.NoError(t, err)
|
||||
f.State = flow.StateEmailSent
|
||||
require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f))
|
||||
|
||||
// Create a settings flow that acts as the origin for the PTC.
|
||||
sfErr := &settings.Flow{
|
||||
ID: x.NewUUID(),
|
||||
ExpiresAt: time.Now().UTC().Add(time.Hour),
|
||||
IssuedAt: time.Now().UTC(),
|
||||
RequestURL: public.URL + "/settings",
|
||||
IdentityID: ident.ID,
|
||||
Identity: ident,
|
||||
Type: flow.TypeBrowser,
|
||||
State: flow.StateShowForm,
|
||||
InternalContext: []byte("{}"),
|
||||
}
|
||||
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfErr))
|
||||
|
||||
sessID := sess.ID
|
||||
originFlowIDErr := sfErr.ID
|
||||
ptc := &identity.PendingTraitsChange{
|
||||
ID: x.NewUUID(),
|
||||
IdentityID: ident.ID,
|
||||
SessionID: uuid.NullUUID{UUID: sessID, Valid: true},
|
||||
OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDErr, Valid: true},
|
||||
NewAddressValue: "posthook-link-err-new@ory.sh",
|
||||
NewAddressVia: string(identity.AddressTypeEmail),
|
||||
OriginalTraitsHash: identity.HashTraits(json.RawMessage(ident.Traits)),
|
||||
ProposedTraits: json.RawMessage(`{"email":"posthook-link-err-new@ory.sh"}`),
|
||||
VerificationFlowID: f.ID,
|
||||
Status: identity.PendingTraitsChangeStatusPending,
|
||||
}
|
||||
require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc))
|
||||
|
||||
// Create a verification token with a nil address ID (pending-change signal).
|
||||
token := link.NewSelfServiceVerificationToken(ptc, f, time.Hour)
|
||||
token.VerifiableAddress = nil
|
||||
require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(ctx, token))
|
||||
|
||||
// Redeem the token via HTTP GET (the link-based verification path).
|
||||
cl := testhelpers.NewClientWithCookies(t)
|
||||
res, err := cl.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {f.ID.String()}, "token": {token.Token}}.Encode())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, res.Body.Close())
|
||||
|
||||
// Apply already committed before the webhook fired: the new traits and
|
||||
// the verified address persist even though the hook failed.
|
||||
updated, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ident.ID)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, "posthook-link-err-new@ory.sh", gjson.GetBytes([]byte(updated.Traits), "email").String(),
|
||||
"traits are committed before the post-persist hook runs")
|
||||
|
||||
var foundAddress *identity.VerifiableAddress
|
||||
for idx := range updated.VerifiableAddresses {
|
||||
if updated.VerifiableAddresses[idx].Value == "posthook-link-err-new@ory.sh" {
|
||||
foundAddress = &updated.VerifiableAddresses[idx]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, foundAddress, "new address should exist on identity")
|
||||
assert.True(t, foundAddress.Verified, "new address should be verified even though the webhook failed")
|
||||
assert.EqualValues(t, identity.VerifiableAddressStatusCompleted, foundAddress.Status)
|
||||
})
|
||||
|
||||
t.Run("description=should reject pending traits change on concurrent modification", func(t *testing.T) {
|
||||
// See the note on "should apply pending traits change when token
|
||||
// is redeemed" — this subtest shares config and SQLite state with
|
||||
// the webhook-mutating siblings above and must stay sequential.
|
||||
// Create an identity with original traits.
|
||||
concurrentID := &identity.Identity{
|
||||
ID: x.NewUUID(),
|
||||
State: identity.StateActive,
|
||||
Traits: identity.Traits(`{"email":"original2@ory.sh"}`),
|
||||
SchemaID: config.DefaultIdentityTraitsSchemaID,
|
||||
Credentials: map[identity.CredentialsType]identity.Credentials{
|
||||
"password": {Type: "password", Identifiers: []string{"original2@ory.sh"}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)},
|
||||
},
|
||||
}
|
||||
require.NoError(t, reg.IdentityManager().Create(ctx, concurrentID, identity.ManagerAllowWriteProtectedTraits))
|
||||
|
||||
// A live session is required so we reach the traits-hash check rather
|
||||
// than bailing at the SessionID-nil guard first.
|
||||
sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, concurrentID,
|
||||
time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
|
||||
|
||||
// Compute traits hash from the original traits.
|
||||
originalHash := identity.HashTraits(json.RawMessage(concurrentID.Traits))
|
||||
|
||||
// Simulate a concurrent modification: update the identity's traits directly.
|
||||
concurrentID.Traits = identity.Traits(`{"email":"concurrent@ory.sh"}`)
|
||||
require.NoError(t, reg.IdentityManager().Update(ctx, concurrentID, identity.ManagerAllowWriteProtectedTraits))
|
||||
|
||||
// Create a verification flow in StateEmailSent.
|
||||
f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken, httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser)
|
||||
require.NoError(t, err)
|
||||
f.State = flow.StateEmailSent
|
||||
require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f))
|
||||
|
||||
// Create a settings flow that acts as the origin for the PTC.
|
||||
sfConcurrent := &settings.Flow{
|
||||
ID: x.NewUUID(),
|
||||
ExpiresAt: time.Now().UTC().Add(time.Hour),
|
||||
IssuedAt: time.Now().UTC(),
|
||||
RequestURL: public.URL + "/settings",
|
||||
IdentityID: concurrentID.ID,
|
||||
Identity: concurrentID,
|
||||
Type: flow.TypeBrowser,
|
||||
State: flow.StateShowForm,
|
||||
InternalContext: []byte("{}"),
|
||||
}
|
||||
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfConcurrent))
|
||||
|
||||
// Create a PendingTraitsChange with the OLD traits hash (before concurrent modification).
|
||||
sessID := sess.ID
|
||||
originFlowIDConcurrent := sfConcurrent.ID
|
||||
ptc := &identity.PendingTraitsChange{
|
||||
ID: x.NewUUID(),
|
||||
IdentityID: concurrentID.ID,
|
||||
SessionID: uuid.NullUUID{UUID: sessID, Valid: true},
|
||||
OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDConcurrent, Valid: true},
|
||||
NewAddressValue: "changed2@ory.sh",
|
||||
NewAddressVia: string(identity.AddressTypeEmail),
|
||||
OriginalTraitsHash: originalHash,
|
||||
ProposedTraits: json.RawMessage(`{"email":"changed2@ory.sh"}`),
|
||||
VerificationFlowID: f.ID,
|
||||
Status: identity.PendingTraitsChangeStatusPending,
|
||||
}
|
||||
require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc))
|
||||
|
||||
// Create a verification token with a nil address ID.
|
||||
token := link.NewSelfServiceVerificationToken(ptc, f, time.Hour)
|
||||
token.VerifiableAddress = nil
|
||||
require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(ctx, token))
|
||||
|
||||
// Redeem via GET (the link-based path).
|
||||
cl := testhelpers.NewClientWithCookies(t)
|
||||
res, err := cl.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {f.ID.String()}, "token": {token.Token}}.Encode())
|
||||
require.NoError(t, err)
|
||||
body := string(ioutilx.MustReadAll(res.Body))
|
||||
require.NoError(t, res.Body.Close())
|
||||
|
||||
// Should show the "token invalid" error (concurrent modification detected).
|
||||
assert.Contains(t, body, "The verification token is invalid or has already been used")
|
||||
|
||||
// Identity traits should NOT have been updated.
|
||||
unchanged, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, concurrentID.ID)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, "concurrent@ory.sh", gjson.GetBytes([]byte(unchanged.Traits), "email").String(),
|
||||
"traits should remain at the concurrent value, not the proposed value")
|
||||
})
|
||||
|
||||
t.Run("description=should reject pending traits change when session is revoked", func(t *testing.T) {
|
||||
ident := &identity.Identity{
|
||||
ID: x.NewUUID(),
|
||||
State: identity.StateActive,
|
||||
Traits: identity.Traits(`{"email":"revoke-link-original@ory.sh"}`),
|
||||
SchemaID: config.DefaultIdentityTraitsSchemaID,
|
||||
}
|
||||
require.NoError(t, reg.IdentityManager().Create(ctx, ident, identity.ManagerAllowWriteProtectedTraits))
|
||||
|
||||
sess, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/", nil), reg, ident,
|
||||
time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
|
||||
|
||||
f, err := verification.NewFlow(reg, time.Hour, nosurfx.FakeCSRFToken,
|
||||
httptest.NewRequest("GET", public.URL+verification.RouteInitBrowserFlow, nil), nil, flow.TypeBrowser)
|
||||
|
||||
require.NoError(t, err)
|
||||
f.State = flow.StateEmailSent
|
||||
require.NoError(t, reg.VerificationFlowPersister().CreateVerificationFlow(ctx, f))
|
||||
|
||||
// Create a settings flow that acts as the origin for the PTC.
|
||||
sfRevoke := &settings.Flow{
|
||||
ID: x.NewUUID(),
|
||||
ExpiresAt: time.Now().UTC().Add(time.Hour),
|
||||
IssuedAt: time.Now().UTC(),
|
||||
RequestURL: public.URL + "/settings",
|
||||
IdentityID: ident.ID,
|
||||
Identity: ident,
|
||||
Type: flow.TypeBrowser,
|
||||
State: flow.StateShowForm,
|
||||
InternalContext: []byte("{}"),
|
||||
}
|
||||
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, sfRevoke))
|
||||
|
||||
sessID := sess.ID
|
||||
originFlowIDRevoke := sfRevoke.ID
|
||||
ptc := &identity.PendingTraitsChange{
|
||||
ID: x.NewUUID(),
|
||||
IdentityID: ident.ID,
|
||||
SessionID: uuid.NullUUID{UUID: sessID, Valid: true},
|
||||
OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowIDRevoke, Valid: true},
|
||||
NewAddressValue: "revoke-link-new@ory.sh",
|
||||
NewAddressVia: string(identity.AddressTypeEmail),
|
||||
OriginalTraitsHash: identity.HashTraits(json.RawMessage(ident.Traits)),
|
||||
ProposedTraits: json.RawMessage(`{"email":"revoke-link-new@ory.sh"}`),
|
||||
VerificationFlowID: f.ID,
|
||||
Status: identity.PendingTraitsChangeStatusPending,
|
||||
}
|
||||
require.NoError(t, reg.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc))
|
||||
|
||||
token := link.NewSelfServiceVerificationToken(ptc, f, time.Hour)
|
||||
token.VerifiableAddress = nil
|
||||
require.NoError(t, reg.VerificationTokenPersister().CreateVerificationToken(ctx, token))
|
||||
|
||||
// Revoke the session AFTER the PTC is created — simulates logout between flow start and verification.
|
||||
require.NoError(t, reg.SessionPersister().RevokeSessionById(ctx, sess.ID))
|
||||
|
||||
cl := testhelpers.NewClientWithCookies(t)
|
||||
res, err := cl.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{"flow": {f.ID.String()}, "token": {token.Token}}.Encode())
|
||||
require.NoError(t, err)
|
||||
body := string(ioutilx.MustReadAll(res.Body))
|
||||
require.NoError(t, res.Body.Close())
|
||||
|
||||
assert.Contains(t, body, "The verification token is invalid or has already been used")
|
||||
|
||||
// Traits should NOT be updated.
|
||||
unchanged, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ident.ID)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, "revoke-link-original@ory.sh", gjson.GetBytes([]byte(unchanged.Traits), "email").String())
|
||||
})
|
||||
|
||||
t.Run("case=doesn't continue with OAuth2 flow if code is invalid", func(t *testing.T) {
|
||||
globalReturnTo := public.URL + "/global"
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, globalReturnTo)
|
||||
|
||||
client := testhelpers.NewClientWithCookies(t)
|
||||
flow, _ := newValidFlow(t, flow.TypeBrowser, public.URL+verification.RouteInitBrowserFlow)
|
||||
|
||||
res, err := client.Get(public.URL + verification.RouteSubmitFlow + "?" + url.Values{
|
||||
"flow": {flow.ID.String()},
|
||||
"token": {"invalid token"},
|
||||
}.Encode())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
responseBody := gjson.ParseBytes(ioutilx.MustReadAll(res.Body))
|
||||
|
||||
assert.Equal(t, "choose_method", responseBody.Get("state").String(), "%v", responseBody)
|
||||
assert.Len(t, responseBody.Get("ui.messages").Array(), 1, "%v", responseBody)
|
||||
assert.Equal(t, "The verification token is invalid or has already been used. Please retry the flow.", responseBody.Get("ui.messages.0.text").String(), "%v", responseBody)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user