Skip to content

Commit c445e40

Browse files
gaultierclaude
authored andcommitted
chore(kratos): rewrite recovery-address lookup as region-pinned CTE
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> GitOrigin-RevId: 4c9def2adf32a1528bddef30ac6a04d5e7143941
1 parent be5e987 commit c445e40

4 files changed

Lines changed: 66 additions & 83 deletions

File tree

identity/pool.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ type (
116116
// FindRecoveryAddressByValue returns a matching address or sql.ErrNoRows if no address could be found.
117117
FindRecoveryAddressByValue(ctx context.Context, via, address string) (*RecoveryAddress, error)
118118

119-
// FindAllRecoveryAddressesForIdentityByRecoveryAddressValue finds all recovery addresses for an identity if at least one of its recovery addresses matches the provided value.
120-
FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx context.Context, anyRecoveryAddress string) ([]RecoveryAddress, error)
119+
// FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue finds the values of all recovery addresses for an identity if at least one of its recovery addresses matches the provided value.
120+
FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx context.Context, anyRecoveryAddress string) ([]string, error)
121121
}
122122

123123
PoolProvider interface {

identity/test/pool.go

Lines changed: 34 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"encoding/json"
1010
"fmt"
1111
"net/http"
12-
"slices"
1312
"strings"
1413
"testing"
1514
"time"
@@ -2021,12 +2020,6 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
20212020
})
20222021

20232022
t.Run("suite=recovery-address", func(t *testing.T) {
2024-
sortAddresses := func(addresses []identity.RecoveryAddress) {
2025-
slices.SortFunc(addresses, func(a, b identity.RecoveryAddress) int {
2026-
return strings.Compare(a.Value, b.Value)
2027-
})
2028-
}
2029-
20302023
createIdentityWithAddresses := func(t *testing.T, email string) *identity.Identity {
20312024
var i identity.Identity
20322025
require.NoError(t, faker.FakeData(&i))
@@ -2045,7 +2038,7 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
20452038
_, err := p.FindRecoveryAddressByValue(ctx, identity.AddressTypeEmail, "does-not-exist")
20462039
require.ErrorIs(t, err, sqlcon.ErrNoRows())
20472040

2048-
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, "does-not-exist")
2041+
allAddresses, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, "does-not-exist")
20492042
require.NoError(t, err)
20502043
require.Len(t, allAddresses, 0)
20512044
})
@@ -2080,19 +2073,18 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
20802073
})
20812074
})
20822075

2083-
t.Run("method=FindAllRecoveryAddressesForIdentityByRecoveryAddressValue", func(t *testing.T) {
2076+
t.Run("method=FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue", func(t *testing.T) {
20842077
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
2085-
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, expected.Value)
2078+
allAddresses, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, expected.Value)
20862079
require.NoError(t, err)
20872080
require.Len(t, allAddresses, 2)
2088-
sortAddresses(allAddresses)
2089-
require.Equal(t, expected.Value, allAddresses[0].Value)
2090-
require.Equal(t, expected.Value+"_other", allAddresses[1].Value)
2081+
require.Equal(t, expected.Value, allAddresses[0])
2082+
require.Equal(t, expected.Value+"_other", allAddresses[1])
20912083
})
20922084

20932085
t.Run("not if on another network", func(t *testing.T) {
20942086
_, p := testhelpers.NewNetwork(t, ctx, p)
2095-
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, expected.Value)
2087+
allAddresses, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, expected.Value)
20962088
require.NoError(t, err)
20972089
require.Len(t, allAddresses, 0)
20982090
})
@@ -2108,19 +2100,18 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
21082100
_, err := p.FindRecoveryAddressByValue(ctx, identity.AddressTypeEmail, email)
21092101
require.NoError(t, err)
21102102

2111-
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, emailLower)
2103+
allAddresses, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, emailLower)
21122104
require.NoError(t, err)
21132105
require.Len(t, allAddresses, 2)
2114-
sortAddresses(allAddresses)
2115-
require.Equal(t, allAddresses[0].Value, emailLower)
2116-
require.Equal(t, allAddresses[1].Value, emailLower+"_other")
2106+
require.Equal(t, allAddresses[0], emailLower)
2107+
require.Equal(t, allAddresses[1], emailLower+"_other")
21172108

21182109
t.Run("can not find if on another network", func(t *testing.T) {
21192110
_, p := testhelpers.NewNetwork(t, ctx, p)
21202111
_, err := p.FindRecoveryAddressByValue(ctx, identity.AddressTypeEmail, email)
21212112
require.ErrorIs(t, err, sqlcon.ErrNoRows())
21222113

2123-
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, emailLower)
2114+
allAddresses, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, emailLower)
21242115
require.NoError(t, err)
21252116
require.Len(t, allAddresses, 0)
21262117
})
@@ -2132,7 +2123,7 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
21322123
_, err = p.FindRecoveryAddressByValue(ctx, identity.AddressTypeEmail, email)
21332124
require.EqualError(t, err, sqlcon.ErrNoRows().Error())
21342125

2135-
allAddresses, err = p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, emailLower)
2126+
allAddresses, err = p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, emailLower)
21362127
require.NoError(t, err)
21372128
require.Len(t, allAddresses, 0)
21382129

@@ -2141,7 +2132,7 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
21412132
_, err := p.FindRecoveryAddressByValue(ctx, identity.AddressTypeEmail, email)
21422133
require.ErrorIs(t, err, sqlcon.ErrNoRows())
21432134

2144-
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, emailLower)
2135+
allAddresses, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, emailLower)
21452136
require.NoError(t, err)
21462137
require.Len(t, allAddresses, 0)
21472138
})
@@ -2152,21 +2143,18 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
21522143
assert.Equal(t, identity.AddressTypeEmail, actual.Via)
21532144
assert.Equal(t, emailNextLower, actual.Value)
21542145

2155-
allAddresses, err = p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, emailNextLower)
2146+
allAddresses, err = p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, emailNextLower)
21562147
require.NoError(t, err)
21572148
require.Len(t, allAddresses, 2)
2158-
sortAddresses(allAddresses)
2159-
assert.Equal(t, identity.AddressTypeEmail, allAddresses[0].Via)
2160-
assert.Equal(t, emailNextLower, allAddresses[0].Value)
2161-
assert.Equal(t, identity.AddressTypeEmail, allAddresses[1].Via)
2162-
assert.Equal(t, emailNextLower+"_other", allAddresses[1].Value)
2149+
assert.Equal(t, emailNextLower, allAddresses[0])
2150+
assert.Equal(t, emailNextLower+"_other", allAddresses[1])
21632151

21642152
t.Run("can not find if on another network", func(t *testing.T) {
21652153
_, p := testhelpers.NewNetwork(t, ctx, p)
21662154
_, err := p.FindRecoveryAddressByValue(ctx, identity.AddressTypeEmail, emailNext)
21672155
require.ErrorIs(t, err, sqlcon.ErrNoRows())
21682156

2169-
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, emailNextLower)
2157+
allAddresses, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, emailNextLower)
21702158
require.NoError(t, err)
21712159
require.Len(t, allAddresses, 0)
21722160
})
@@ -2367,30 +2355,25 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
23672355
createdIDs = append(createdIDs, i.ID)
23682356

23692357
// Test 1: Find all addresses using normalized phone1
2370-
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, normalizedPhone1)
2358+
allAddresses, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, normalizedPhone1)
23712359
require.NoError(t, err, "should find all recovery addresses with normalized phone")
23722360
require.Len(t, allAddresses, 2, "should return all recovery addresses for the identity")
2373-
sortAddresses(allAddresses)
2374-
assert.Equal(t, normalizedPhone1, allAddresses[0].Value)
2375-
assert.Equal(t, normalizedPhone2, allAddresses[1].Value)
2376-
assert.Equal(t, identity.AddressTypeSMS, allAddresses[0].Via)
2377-
assert.Equal(t, identity.AddressTypeSMS, allAddresses[1].Via)
2361+
assert.Equal(t, normalizedPhone1, allAddresses[0])
2362+
assert.Equal(t, normalizedPhone2, allAddresses[1])
23782363

23792364
// Test 2: Find all addresses using non-normalized phone1 (gets normalized, then matches)
2380-
allAddresses2, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, nonNormalizedPhone1)
2365+
allAddresses2, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, nonNormalizedPhone1)
23812366
require.NoError(t, err, "should find all recovery addresses with non-normalized phone")
23822367
require.Len(t, allAddresses2, 2, "should return all recovery addresses when querying with non-normalized phone")
2383-
sortAddresses(allAddresses2)
2384-
assert.Equal(t, normalizedPhone1, allAddresses2[0].Value)
2385-
assert.Equal(t, normalizedPhone2, allAddresses2[1].Value)
2368+
assert.Equal(t, normalizedPhone1, allAddresses2[0])
2369+
assert.Equal(t, normalizedPhone2, allAddresses2[1])
23862370

23872371
// Test 3: Find all addresses using normalized phone2
2388-
allAddresses3, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, normalizedPhone2)
2372+
allAddresses3, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, normalizedPhone2)
23892373
require.NoError(t, err)
23902374
require.Len(t, allAddresses3, 2, "should return all recovery addresses when querying with second phone")
2391-
sortAddresses(allAddresses3)
2392-
assert.Equal(t, normalizedPhone1, allAddresses3[0].Value)
2393-
assert.Equal(t, normalizedPhone2, allAddresses3[1].Value)
2375+
assert.Equal(t, normalizedPhone1, allAddresses3[0])
2376+
assert.Equal(t, normalizedPhone2, allAddresses3[1])
23942377
})
23952378

23962379
t.Run("create with multiple SMS addresses and find all with non-normalized phone", func(t *testing.T) {
@@ -2413,22 +2396,18 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
24132396
createdIDs = append(createdIDs, i.ID)
24142397

24152398
// Test 1: Find all addresses using non-normalized phone1 (gets normalized for search)
2416-
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, nonNormPhone1)
2399+
allAddresses, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, nonNormPhone1)
24172400
require.NoError(t, err, "should find all recovery addresses with non-normalized phone")
24182401
require.Len(t, allAddresses, 2, "should return all recovery addresses for the identity")
2419-
sortAddresses(allAddresses)
2420-
assert.Equal(t, normPhone1, allAddresses[0].Value, "stored values should be normalized")
2421-
assert.Equal(t, normPhone2, allAddresses[1].Value)
2422-
assert.Equal(t, identity.AddressTypeSMS, allAddresses[0].Via)
2423-
assert.Equal(t, identity.AddressTypeSMS, allAddresses[1].Via)
2402+
assert.Equal(t, normPhone1, allAddresses[0], "stored values should be normalized")
2403+
assert.Equal(t, normPhone2, allAddresses[1])
24242404

24252405
// Test 2: Find all addresses using normalized phone1 (direct match)
2426-
allAddresses2, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, normPhone1)
2406+
allAddresses2, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, normPhone1)
24272407
require.NoError(t, err)
24282408
require.Len(t, allAddresses2, 2, "should return all recovery addresses when querying with normalized phone")
2429-
sortAddresses(allAddresses2)
2430-
assert.Equal(t, normPhone1, allAddresses2[0].Value)
2431-
assert.Equal(t, normPhone2, allAddresses2[1].Value)
2409+
assert.Equal(t, normPhone1, allAddresses2[0])
2410+
assert.Equal(t, normPhone2, allAddresses2[1])
24322411
})
24332412

24342413
t.Run("backward compatibility with legacy non-normalized phone data", func(t *testing.T) {
@@ -2470,14 +2449,11 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
24702449

24712450
// Test: Find all addresses with exact non-normalized phone (what user would enter)
24722451
// Should find all recovery addresses for the identity
2473-
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, legacyPhone1)
2452+
allAddresses, err := p.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx, legacyPhone1)
24742453
require.NoError(t, err, "should find all recovery addresses with non-normalized search")
24752454
require.Len(t, allAddresses, 2, "should return all recovery addresses for the identity")
2476-
sortAddresses(allAddresses)
2477-
assert.Equal(t, legacyPhone1, allAddresses[0].Value)
2478-
assert.Equal(t, legacyPhone2, allAddresses[1].Value)
2479-
assert.Equal(t, identity.AddressTypeSMS, allAddresses[0].Via)
2480-
assert.Equal(t, identity.AddressTypeSMS, allAddresses[1].Via)
2455+
assert.Equal(t, legacyPhone1, allAddresses[0])
2456+
assert.Equal(t, legacyPhone2, allAddresses[1])
24812457
})
24822458
})
24832459
})

persistence/sql/identity/persister_identity.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,11 +1541,11 @@ func (p *IdentityPersister) FindRecoveryAddressByValue(ctx context.Context, via,
15411541
return &addr, nil
15421542
}
15431543

1544-
// FindAllRecoveryAddressesForIdentityByRecoveryAddressValue returns all
1545-
// recovery addresses for an identity if at least one of those addresses matches
1546-
// the provided value.
1547-
func (p *IdentityPersister) FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx context.Context, anyRecoveryAddress string) (recoveryAddresses []identity.RecoveryAddress, err error) {
1548-
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue",
1544+
// FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue returns the
1545+
// values of all recovery addresses for an identity if at least one of those
1546+
// addresses matches the provided value.
1547+
func (p *IdentityPersister) FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(ctx context.Context, anyRecoveryAddress string) (recoveryAddresses []string, err error) {
1548+
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue",
15491549
trace.WithAttributes(
15501550
attribute.Stringer("network.id", p.NetworkID(ctx))))
15511551
defer otelx.End(span, &err)
@@ -1560,19 +1560,28 @@ func (p *IdentityPersister) FindAllRecoveryAddressesForIdentityByRecoveryAddress
15601560
//
15611561
// This is all done in one query with a self-join.
15621562
// We also bound the results for safety.
1563+
via := identity.AddressTypeSMS
1564+
if strings.ContainsRune(anyRecoveryAddress, '@') {
1565+
via = identity.AddressTypeEmail
1566+
}
1567+
1568+
nid := p.NetworkID(ctx)
15631569
err = p.GetConnection(ctx).RawQuery(`
1564-
SELECT A.id, A.via, A.value, A.identity_id, A.created_at, A.updated_at, A.nid, A.break_glass_for_organization
1570+
SELECT A.value
15651571
FROM identity_recovery_addresses A
15661572
JOIN identity_recovery_addresses B
1567-
ON A.identity_id = B.identity_id
1568-
AND A.nid = B.nid
1569-
WHERE B.value IN (?,?)
1570-
AND A.nid = ?
1573+
ON A.identity_id = B.identity_id
1574+
AND A.nid = B.nid
1575+
WHERE A.nid = ?
1576+
AND B.via = ?
1577+
AND B.value IN (?, ?)
1578+
ORDER BY A.value
15711579
LIMIT 10
15721580
`,
1581+
nid,
1582+
via,
15731583
x.GracefulNormalization(anyRecoveryAddress),
15741584
anyRecoveryAddress,
1575-
p.NetworkID(ctx),
15761585
).
15771586
All(&recoveryAddresses)
15781587
if err != nil {

selfservice/strategy/code/strategy_recovery.go

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ func (s *Strategy) recoveryV2HandleStateAwaitingAddress(r *http.Request, f *reco
481481
}
482482

483483
// Need to retrieve all possible recovery addresses and present a choice.
484-
recoveryAddresses, err := s.deps.IdentityPool().FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress)
484+
recoveryAddresses, err := s.deps.IdentityPool().FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress)
485485
// Real error.
486486
if err != nil && !errors.Is(err, sqlcon.ErrNoRows()) {
487487
return err
@@ -491,12 +491,12 @@ func (s *Strategy) recoveryV2HandleStateAwaitingAddress(r *http.Request, f *reco
491491
if len(recoveryAddresses) == 0 {
492492
// To avoid an attacker from using this case to probe for existing addresses, we pretend it exists.
493493
// This is the same behavior as in Recovery V1.
494-
recoveryAddresses = append(recoveryAddresses, identity.RecoveryAddress{Value: body.RecoveryAddress})
494+
recoveryAddresses = append(recoveryAddresses, body.RecoveryAddress)
495495
}
496496

497497
f.State = flow.StateRecoveryAwaitingAddressChoice
498498

499-
if len(recoveryAddresses) == 1 && recoveryAddresses[0].Value == body.RecoveryAddress {
499+
if len(recoveryAddresses) == 1 && recoveryAddresses[0] == body.RecoveryAddress {
500500
// Skip two states for convenience:
501501
// - No need to present a choice with only one option
502502
// - No need to ask for the full address if there is only one and it was just provided in full
@@ -522,20 +522,18 @@ func (s *Strategy) recoveryV2HandleStateAwaitingAddress(r *http.Request, f *reco
522522
f.State = flow.StateRecoveryAwaitingAddressChoice
523523
f.UI.Messages.Set(text.NewRecoveryAskToChooseAddress())
524524

525-
slices.SortFunc(recoveryAddresses, func(a, b identity.RecoveryAddress) int {
526-
return strings.Compare(a.Value, b.Value)
527-
})
525+
slices.Sort(recoveryAddresses)
528526

529527
for _, a := range recoveryAddresses {
530528
// NOTE: Only send the masked value and the hash, to avoid information exfiltration.
531529
// Why the hash? So that we can recognize later, when the user chooses the masked address in the list,
532530
// that the chosen masked address is the `recovery_address` provided in the beginning,
533531
// and then we do not ask again the user to provide it in full.
534-
hashBase64 := AddressToHashBase64(a.Value)
532+
hashBase64 := AddressToHashBase64(a)
535533
f.UI.GetNodes().Append(node.NewInputField("recovery_select_address", hashBase64, node.CodeGroup, node.InputAttributeTypeSubmit).
536534
WithMetaLabel(&text.Message{
537535
ID: text.InfoNodeLabel,
538-
Text: MaskAddress(a.Value),
536+
Text: MaskAddress(a),
539537
Type: text.Info,
540538
}))
541539
}
@@ -592,11 +590,11 @@ func (s *Strategy) recoveryV2HandleStateAwaitingAddressChoice(r *http.Request, f
592590

593591
// Retrieve the selected recovery address in plaintext to determine the input label and type.
594592
var plaintextRecoveryAddress string
595-
recoveryAddresses, err := s.deps.IdentityPool().FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress)
593+
recoveryAddresses, err := s.deps.IdentityPool().FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress)
596594
if err == nil {
597595
for _, a := range recoveryAddresses {
598-
if subtle.ConstantTimeCompare([]byte(AddressToHashBase64(a.Value)), []byte(body.RecoverySelectAddress)) == 1 {
599-
plaintextRecoveryAddress = a.Value
596+
if subtle.ConstantTimeCompare([]byte(AddressToHashBase64(a)), []byte(body.RecoverySelectAddress)) == 1 {
597+
plaintextRecoveryAddress = a
600598
break
601599
}
602600
}
@@ -728,7 +726,7 @@ func (s *Strategy) recoveryV2HandleStateAwaitingCode(w http.ResponseWriter, r *h
728726

729727
func (s *Strategy) recoveryV2HandleGoBack(r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error {
730728
// If no address choice needs to take place, just go to the first screen.
731-
recoveryAddresses, _ := s.deps.IdentityPool().FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress)
729+
recoveryAddresses, _ := s.deps.IdentityPool().FindAllRecoveryAddressValuesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress)
732730
if len(recoveryAddresses) <= 1 {
733731
f.State = flow.StateRecoveryAwaitingAddress
734732
err := s.PopulateRecoveryMethod(r, f)

0 commit comments

Comments
 (0)