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:
248
selfservice/strategy/link/test/persistence.go
Normal file
248
selfservice/strategy/link/test/persistence.go
Normal file
@@ -0,0 +1,248 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package link
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-faker/faker/v4"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/persistence"
|
||||
"github.com/ory/kratos/pkg/testhelpers"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/selfservice/flow/recovery"
|
||||
"github.com/ory/kratos/selfservice/flow/verification"
|
||||
"github.com/ory/kratos/selfservice/strategy/link"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/assertx"
|
||||
"github.com/ory/x/contextx"
|
||||
"github.com/ory/x/sqlcon"
|
||||
)
|
||||
|
||||
func TestPersister(ctx context.Context, p interface {
|
||||
persistence.Persister
|
||||
},
|
||||
) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p)
|
||||
|
||||
ctx := contextx.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"})
|
||||
|
||||
t.Run("token=recovery", func(t *testing.T) {
|
||||
newRecoveryToken := func(t *testing.T, email string) (*link.RecoveryToken, *recovery.Flow) {
|
||||
var req recovery.Flow
|
||||
require.NoError(t, faker.FakeData(&req))
|
||||
req.State = flow.StateChooseMethod
|
||||
require.NoError(t, p.CreateRecoveryFlow(ctx, &req))
|
||||
|
||||
var i identity.Identity
|
||||
require.NoError(t, faker.FakeData(&i))
|
||||
|
||||
address := &identity.RecoveryAddress{Value: email, Via: identity.AddressTypeEmail, IdentityID: i.ID}
|
||||
i.RecoveryAddresses = append(i.RecoveryAddresses, *address)
|
||||
|
||||
require.NoError(t, p.CreateIdentity(ctx, &i))
|
||||
|
||||
return &link.RecoveryToken{
|
||||
Token: x.NewUUID().String(),
|
||||
FlowID: uuid.NullUUID{UUID: req.ID, Valid: true},
|
||||
RecoveryAddress: &i.RecoveryAddresses[0],
|
||||
ExpiresAt: time.Now(),
|
||||
IssuedAt: time.Now(),
|
||||
IdentityID: i.ID,
|
||||
TokenType: link.RecoveryTokenTypeAdmin,
|
||||
}, &req
|
||||
}
|
||||
|
||||
t.Run("case=should error when the recovery token does not exist", func(t *testing.T) {
|
||||
_, err := p.UseRecoveryToken(ctx, x.NewUUID(), "i-do-not-exist")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("case=should create a new recovery token", func(t *testing.T) {
|
||||
token, _ := newRecoveryToken(t, "foo-user@ory.sh")
|
||||
require.NoError(t, p.CreateRecoveryToken(ctx, token))
|
||||
})
|
||||
|
||||
t.Run("case=should error when token is used with different flow id", func(t *testing.T) {
|
||||
token, _ := newRecoveryToken(t, "foo-user1@ory.sh")
|
||||
require.NoError(t, p.CreateRecoveryToken(ctx, token))
|
||||
_, err := p.UseRecoveryToken(ctx, x.NewUUID(), token.Token)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("case=should create a recovery token and use it", func(t *testing.T) {
|
||||
expected, f := newRecoveryToken(t, "other-user@ory.sh")
|
||||
require.NoError(t, p.CreateRecoveryToken(ctx, expected))
|
||||
|
||||
t.Run("not work on another network", func(t *testing.T) {
|
||||
_, p := testhelpers.NewNetwork(t, ctx, p)
|
||||
_, err := p.UseRecoveryToken(ctx, f.ID, expected.Token)
|
||||
require.ErrorIs(t, err, sqlcon.ErrNoRows())
|
||||
})
|
||||
|
||||
actual, err := p.UseRecoveryToken(ctx, f.ID, expected.Token)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, nid, actual.NID)
|
||||
assert.Equal(t, expected.IdentityID, actual.IdentityID)
|
||||
assert.NotEqual(t, expected.Token, actual.Token)
|
||||
assert.EqualValues(t, expected.FlowID, actual.FlowID)
|
||||
|
||||
t.Run("double spend", func(t *testing.T) {
|
||||
_, err = p.UseRecoveryToken(ctx, f.ID, expected.Token)
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("case=concurrent use of the same token succeeds at most once", func(t *testing.T) {
|
||||
// HackerOne #3692149 / #3695625: the consume UPDATE must be
|
||||
// atomic. On READ COMMITTED backends two concurrent submissions
|
||||
// of the same valid token must not both succeed.
|
||||
expected, f := newRecoveryToken(t, "concurrent-recovery@ory.sh")
|
||||
require.NoError(t, p.CreateRecoveryToken(ctx, expected))
|
||||
|
||||
const concurrency = 8
|
||||
var success int32
|
||||
var start sync.WaitGroup
|
||||
var done sync.WaitGroup
|
||||
start.Add(1)
|
||||
for range concurrency {
|
||||
done.Add(1)
|
||||
go func() {
|
||||
defer done.Done()
|
||||
start.Wait()
|
||||
if _, err := p.UseRecoveryToken(ctx, f.ID, expected.Token); err == nil {
|
||||
atomic.AddInt32(&success, 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
start.Done()
|
||||
done.Wait()
|
||||
|
||||
require.EqualValues(t, 1, atomic.LoadInt32(&success), "exactly one concurrent consumer may use the token")
|
||||
})
|
||||
|
||||
t.Run("case=update to identity should not invalidate token", func(t *testing.T) {
|
||||
expected, f := newRecoveryToken(t, "some-user@ory.sh")
|
||||
|
||||
require.NoError(t, p.CreateRecoveryToken(ctx, expected))
|
||||
id, err := p.GetIdentity(ctx, expected.IdentityID, identity.ExpandDefault)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, p.UpdateIdentity(ctx, id))
|
||||
|
||||
actual, err := p.UseRecoveryToken(ctx, f.ID, expected.Token)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, nid, actual.NID)
|
||||
assert.Equal(t, expected.IdentityID, actual.IdentityID)
|
||||
assert.NotEqual(t, expected.Token, actual.Token)
|
||||
assert.EqualValues(t, expected.FlowID, actual.FlowID)
|
||||
|
||||
t.Run("double spend", func(t *testing.T) {
|
||||
_, err = p.UseRecoveryToken(ctx, f.ID, expected.Token)
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("token=verification", func(t *testing.T) {
|
||||
newVerificationToken := func(t *testing.T, email string) (*verification.Flow, *link.VerificationToken) {
|
||||
var f verification.Flow
|
||||
require.NoError(t, faker.FakeData(&f))
|
||||
f.State = flow.StateChooseMethod
|
||||
require.NoError(t, p.CreateVerificationFlow(ctx, &f))
|
||||
|
||||
var i identity.Identity
|
||||
require.NoError(t, faker.FakeData(&i))
|
||||
|
||||
address := &identity.VerifiableAddress{Value: email, Via: identity.AddressTypeEmail}
|
||||
i.VerifiableAddresses = append(i.VerifiableAddresses, *address)
|
||||
|
||||
require.NoError(t, p.CreateIdentity(ctx, &i))
|
||||
return &f, &link.VerificationToken{
|
||||
Token: x.NewUUID().String(),
|
||||
FlowID: f.ID,
|
||||
VerifiableAddress: &i.VerifiableAddresses[0],
|
||||
ExpiresAt: time.Now(),
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("case=should error when the verification token does not exist", func(t *testing.T) {
|
||||
_, err := p.UseVerificationToken(ctx, x.NewUUID(), "i-do-not-exist")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("case=should error when the verification token does exist but the flow does not", func(t *testing.T) {
|
||||
_, token := newVerificationToken(t, x.NewUUID().String()+"@ory.sh")
|
||||
require.NoError(t, p.CreateVerificationToken(ctx, token))
|
||||
_, err := p.UseVerificationToken(ctx, x.NewUUID(), token.Token)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("case=should create a new verification token", func(t *testing.T) {
|
||||
_, token := newVerificationToken(t, "foo-user@ory.sh")
|
||||
require.NoError(t, p.CreateVerificationToken(ctx, token))
|
||||
})
|
||||
|
||||
t.Run("case=should create a verification token and use it", func(t *testing.T) {
|
||||
f, expected := newVerificationToken(t, "other-user@ory.sh")
|
||||
require.NoError(t, p.CreateVerificationToken(ctx, expected))
|
||||
|
||||
t.Run("not work on another network", func(t *testing.T) {
|
||||
_, p := testhelpers.NewNetwork(t, ctx, p)
|
||||
_, err := p.UseVerificationToken(ctx, f.ID, expected.Token)
|
||||
require.ErrorIs(t, err, sqlcon.ErrNoRows())
|
||||
})
|
||||
|
||||
actual, err := p.UseVerificationToken(ctx, f.ID, expected.Token)
|
||||
require.NoError(t, err)
|
||||
assertx.EqualAsJSONExcept(t, expected.VerifiableAddress, actual.VerifiableAddress, []string{"created_at", "updated_at"})
|
||||
assert.Equal(t, nid, actual.NID)
|
||||
assert.Equal(t, expected.VerifiableAddress.IdentityID, actual.VerifiableAddress.IdentityID)
|
||||
assert.NotEqual(t, expected.Token, actual.Token)
|
||||
assert.EqualValues(t, expected.FlowID, actual.FlowID)
|
||||
|
||||
_, err = p.UseVerificationToken(ctx, f.ID, expected.Token)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("case=concurrent use of the same token succeeds at most once", func(t *testing.T) {
|
||||
// HackerOne #3692149 / #3695625: the consume UPDATE must be
|
||||
// atomic. On READ COMMITTED backends two concurrent submissions
|
||||
// of the same valid token must not both succeed.
|
||||
f, expected := newVerificationToken(t, "concurrent-verification@ory.sh")
|
||||
require.NoError(t, p.CreateVerificationToken(ctx, expected))
|
||||
|
||||
const concurrency = 8
|
||||
var success int32
|
||||
var start sync.WaitGroup
|
||||
var done sync.WaitGroup
|
||||
start.Add(1)
|
||||
for range concurrency {
|
||||
done.Add(1)
|
||||
go func() {
|
||||
defer done.Done()
|
||||
start.Wait()
|
||||
if _, err := p.UseVerificationToken(ctx, f.ID, expected.Token); err == nil {
|
||||
atomic.AddInt32(&success, 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
start.Done()
|
||||
done.Wait()
|
||||
|
||||
require.EqualValues(t, 1, atomic.LoadInt32(&success), "exactly one concurrent consumer may use the token")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user