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
933 lines
41 KiB
Go
933 lines
41 KiB
Go
// 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)
|
|
})
|
|
}
|