mirror of https://github.com/ory/kratos
feat: recovery with any address including with a code via SMS
GitOrigin-RevId: 4fa4ea56feacf71fa7fc84fa2fc33ce94db5a21e
This commit is contained in:
parent
15f3eb3aa2
commit
71844dd75f
|
|
@ -47,6 +47,7 @@ func init() {
|
|||
"NewInfoNodeLabelVerificationCode": text.NewInfoNodeLabelVerificationCode(),
|
||||
"NewInfoNodeLabelRecoveryCode": text.NewInfoNodeLabelRecoveryCode(),
|
||||
"NewInfoNodeInputPassword": text.NewInfoNodeInputPassword(),
|
||||
"NewInfoNodeInputPhoneNumber": text.NewInfoNodeInputPhoneNumber(),
|
||||
"NewInfoNodeLabelGenerated": text.NewInfoNodeLabelGenerated("{title}"),
|
||||
"NewInfoNodeLabelSave": text.NewInfoNodeLabelSave(),
|
||||
"NewInfoNodeLabelSubmit": text.NewInfoNodeLabelSubmit(),
|
||||
|
|
@ -144,6 +145,11 @@ func init() {
|
|||
"NewRecoverySuccessful": text.NewRecoverySuccessful(inAMinute),
|
||||
"NewRecoveryEmailSent": text.NewRecoveryEmailSent(),
|
||||
"NewRecoveryEmailWithCodeSent": text.NewRecoveryEmailWithCodeSent(),
|
||||
"NewRecoveryCodeRecoverySelectAddressSent": text.NewRecoveryCodeRecoverySelectAddressSent("{masked_address}"),
|
||||
"NewRecoveryAskAnyRecoveryAddress": text.NewRecoveryAskAnyRecoveryAddress(),
|
||||
"NewRecoveryAskForFullAddress": text.NewRecoveryAskForFullAddress(),
|
||||
"NewRecoveryAskToChooseAddress": text.NewRecoveryAskToChooseAddress(),
|
||||
"NewRecoveryBack": text.NewRecoveryBack(),
|
||||
"NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed": text.NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed(),
|
||||
"NewErrorValidationRecoveryCodeInvalidOrAlreadyUsed": text.NewErrorValidationRecoveryCodeInvalidOrAlreadyUsed(),
|
||||
"NewErrorValidationRecoveryRetrySuccess": text.NewErrorValidationRecoveryRetrySuccess(),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ func NewSMSTemplateFromMessage(d template.Dependencies, m Message) (SMSTemplate,
|
|||
return nil, err
|
||||
}
|
||||
return sms.NewVerificationCodeValid(d, &t), nil
|
||||
case template.TypeRecoveryCodeValid:
|
||||
var t sms.RecoveryCodeValidModel
|
||||
if err := json.Unmarshal(m.TemplateData, &t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sms.NewRecoveryCodeValid(d, &t), nil
|
||||
case template.TypeTestStub:
|
||||
var t sms.TestStubModel
|
||||
if err := json.Unmarshal(m.TemplateData, &t); err != nil {
|
||||
|
|
|
|||
|
|
@ -3012,7 +3012,7 @@
|
|||
"choose_recovery_address": {
|
||||
"type": "boolean",
|
||||
"title": "Enable new recovery screens to pick which address to send a recovery code/link to",
|
||||
"description": "If enabled, enable new recovery screens to pick which address to send a recovery code/link to, and can send a code via SMS",
|
||||
"description": "If enabled, enable new recovery screens to pick which address to send a recovery code to, and can send a code via SMS. It is safe to toggle it back and forth, existing recovery flows will be handled with their respective logic. That is because it is decided at creation time whether a recovery flow is V1 or V2 and this cannot be changed afterwards. Thus, if a recovery flow is created with this flag enabled, it will be created as a recovery v2 flow. If this flag is disabled while this flow is still active, this flow will still be handled with the correct logic (v2).",
|
||||
"default": false
|
||||
},
|
||||
"legacy_continue_with_verification_ui": {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ type (
|
|||
|
||||
// FindRecoveryAddressByValue returns a matching address or sql.ErrNoRows if no address could be found.
|
||||
FindRecoveryAddressByValue(ctx context.Context, via RecoveryAddressType, address string) (*RecoveryAddress, error)
|
||||
|
||||
// FindAllRecoveryAddressesForIdentityByRecoveryAddressValue finds all recovery addresses for an identity if at least one of its recovery addresses matches the provided value.
|
||||
FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx context.Context, anyRecoveryAddress string) ([]RecoveryAddress, error)
|
||||
}
|
||||
|
||||
PoolProvider interface {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -1319,12 +1320,22 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
|
|||
})
|
||||
|
||||
t.Run("suite=recovery-address", func(t *testing.T) {
|
||||
sortAddresses := func(addresses []identity.RecoveryAddress) {
|
||||
slices.SortFunc(addresses, func(a, b identity.RecoveryAddress) int {
|
||||
return strings.Compare(a.Value, b.Value)
|
||||
})
|
||||
}
|
||||
|
||||
createIdentityWithAddresses := func(t *testing.T, email string) *identity.Identity {
|
||||
var i identity.Identity
|
||||
require.NoError(t, faker.FakeData(&i))
|
||||
i.Traits = []byte(`{"email":"` + email + `"}`)
|
||||
address := identity.NewRecoveryEmailAddress(email, i.ID)
|
||||
i.RecoveryAddresses = append(i.RecoveryAddresses, *address)
|
||||
|
||||
addressOther := identity.NewRecoveryEmailAddress(email+"_other", i.ID)
|
||||
i.RecoveryAddresses = append(i.RecoveryAddresses, *addressOther)
|
||||
|
||||
require.NoError(t, p.CreateIdentity(ctx, &i))
|
||||
return &i
|
||||
}
|
||||
|
|
@ -1332,6 +1343,10 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
|
|||
t.Run("case=not found", func(t *testing.T) {
|
||||
_, err := p.FindRecoveryAddressByValue(ctx, identity.RecoveryAddressTypeEmail, "does-not-exist")
|
||||
require.Equal(t, sqlcon.ErrNoRows, errorsx.Cause(err))
|
||||
|
||||
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, "does-not-exist")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allAddresses, 0)
|
||||
})
|
||||
|
||||
t.Run("case=create and find", func(t *testing.T) {
|
||||
|
|
@ -1363,7 +1378,26 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("method=FindAllRecoveryAddressesForIdentityByRecoveryAddressValue", func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
|
||||
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, expected.Value)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allAddresses, 2)
|
||||
sortAddresses(allAddresses)
|
||||
require.Equal(t, expected.Value, allAddresses[0].Value)
|
||||
require.Equal(t, expected.Value+"_other", allAddresses[1].Value)
|
||||
})
|
||||
|
||||
t.Run("not if on another network", func(t *testing.T) {
|
||||
_, p := testhelpers.NewNetwork(t, ctx, p)
|
||||
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, expected.Value)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allAddresses, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("case=create and update and find", func(t *testing.T) {
|
||||
|
|
@ -1372,22 +1406,41 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
|
|||
_, err := p.FindRecoveryAddressByValue(ctx, identity.RecoveryAddressTypeEmail, "recovery.TestPersister.Update@ory.sh")
|
||||
require.NoError(t, err)
|
||||
|
||||
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, "recovery.testpersister.update@ory.sh")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allAddresses, 2)
|
||||
sortAddresses(allAddresses)
|
||||
require.Equal(t, allAddresses[0].Value, "recovery.testpersister.update@ory.sh")
|
||||
require.Equal(t, allAddresses[1].Value, "recovery.testpersister.update@ory.sh_other")
|
||||
|
||||
t.Run("can not find if on another network", func(t *testing.T) {
|
||||
_, p := testhelpers.NewNetwork(t, ctx, p)
|
||||
_, err := p.FindRecoveryAddressByValue(ctx, identity.RecoveryAddressTypeEmail, "Recovery.TestPersister.Update@ory.sh")
|
||||
require.ErrorIs(t, err, sqlcon.ErrNoRows)
|
||||
|
||||
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, "recovery.testpersister.update@ory.sh")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allAddresses, 0)
|
||||
})
|
||||
|
||||
id.RecoveryAddresses = []identity.RecoveryAddress{{Via: identity.RecoveryAddressTypeEmail, Value: "recovery.TestPersister.Update-next@ory.sh"}}
|
||||
id.RecoveryAddresses = []identity.RecoveryAddress{{Via: identity.RecoveryAddressTypeEmail, Value: "recovery.TestPersister.Update-next@ory.sh"}, {Via: identity.RecoveryAddressTypeEmail, Value: "recovery.TestPersister.Update-next@ory.sh_other"}}
|
||||
require.NoError(t, p.UpdateIdentity(ctx, id))
|
||||
|
||||
_, err = p.FindRecoveryAddressByValue(ctx, identity.RecoveryAddressTypeEmail, "recovery.TestPersister.Update@ory.sh")
|
||||
require.EqualError(t, err, sqlcon.ErrNoRows.Error())
|
||||
|
||||
allAddresses, err = p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, "recovery.testpersister.update@ory.sh")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allAddresses, 0)
|
||||
|
||||
t.Run("can not find if on another network", func(t *testing.T) {
|
||||
_, p := testhelpers.NewNetwork(t, ctx, p)
|
||||
_, err := p.FindRecoveryAddressByValue(ctx, identity.RecoveryAddressTypeEmail, "recovery.TestPersister.Update@ory.sh")
|
||||
require.ErrorIs(t, err, sqlcon.ErrNoRows)
|
||||
|
||||
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, "recovery.testpersister.update@ory.sh")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allAddresses, 0)
|
||||
})
|
||||
|
||||
actual, err := p.FindRecoveryAddressByValue(ctx, identity.RecoveryAddressTypeEmail, "recovery.TestPersister.Update-next@ory.sh")
|
||||
|
|
@ -1395,10 +1448,23 @@ func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager,
|
|||
assert.Equal(t, identity.RecoveryAddressTypeEmail, actual.Via)
|
||||
assert.Equal(t, "recovery.testpersister.update-next@ory.sh", actual.Value)
|
||||
|
||||
allAddresses, err = p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, "recovery.testpersister.update-next@ory.sh")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allAddresses, 2)
|
||||
sortAddresses(allAddresses)
|
||||
assert.Equal(t, identity.RecoveryAddressTypeEmail, allAddresses[0].Via)
|
||||
assert.Equal(t, "recovery.testpersister.update-next@ory.sh", allAddresses[0].Value)
|
||||
assert.Equal(t, identity.RecoveryAddressTypeEmail, allAddresses[1].Via)
|
||||
assert.Equal(t, "recovery.testpersister.update-next@ory.sh_other", allAddresses[1].Value)
|
||||
|
||||
t.Run("can not find if on another network", func(t *testing.T) {
|
||||
_, p := testhelpers.NewNetwork(t, ctx, p)
|
||||
_, err := p.FindRecoveryAddressByValue(ctx, identity.RecoveryAddressTypeEmail, "recovery.TestPersister.Update-next@ory.sh")
|
||||
require.ErrorIs(t, err, sqlcon.ErrNoRows)
|
||||
|
||||
allAddresses, err := p.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx, "recovery.testpersister.update-next@ory.sh")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allAddresses, 0)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ type UpdateRecoveryFlowWithCodeMethod struct {
|
|||
RecoveryConfirmAddress *string `json:"recovery_confirm_address,omitempty"`
|
||||
// If there are multiple addresses registered for the user, a choice is presented and this field stores the result of this choice. Addresses are 'masked' (never sent in full to the client and shown partially in the UI) since at this point in the recovery flow, the user has not yet proven that it knows the full address and we want to avoid information exfiltration. So for all intents and purposes, the value of this field should be treated as an opaque identifier. Used in RecoveryV2.
|
||||
RecoverySelectAddress *string `json:"recovery_select_address,omitempty"`
|
||||
// Set to \"previous\" to return to the previous screen. Used in RecoveryV2.
|
||||
// Set to \"previous\" to go back in the flow, meaningfully. Used in RecoveryV2.
|
||||
Screen *string `json:"screen,omitempty"`
|
||||
// Transient data to pass along to any webhooks
|
||||
TransientPayload map[string]interface{} `json:"transient_payload,omitempty"`
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ type UpdateRecoveryFlowWithCodeMethod struct {
|
|||
RecoveryConfirmAddress *string `json:"recovery_confirm_address,omitempty"`
|
||||
// If there are multiple addresses registered for the user, a choice is presented and this field stores the result of this choice. Addresses are 'masked' (never sent in full to the client and shown partially in the UI) since at this point in the recovery flow, the user has not yet proven that it knows the full address and we want to avoid information exfiltration. So for all intents and purposes, the value of this field should be treated as an opaque identifier. Used in RecoveryV2.
|
||||
RecoverySelectAddress *string `json:"recovery_select_address,omitempty"`
|
||||
// Set to \"previous\" to return to the previous screen. Used in RecoveryV2.
|
||||
// Set to \"previous\" to go back in the flow, meaningfully. Used in RecoveryV2.
|
||||
Screen *string `json:"screen,omitempty"`
|
||||
// Transient data to pass along to any webhooks
|
||||
TransientPayload map[string]interface{} `json:"transient_payload,omitempty"`
|
||||
|
|
|
|||
|
|
@ -12,3 +12,7 @@ import (
|
|||
func RandomEmail() string {
|
||||
return strings.ToLower(randx.MustString(16, randx.Alpha) + "@ory.sh")
|
||||
}
|
||||
|
||||
func RandomPhone() string {
|
||||
return "+49151" + randx.MustString(8, randx.Numeric)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,47 @@ func (p *IdentityPersister) FindRecoveryAddressByValue(ctx context.Context, via
|
|||
return &address, nil
|
||||
}
|
||||
|
||||
// Find all recovery addresses for an identity if at least one of its recovery addresses matches the provided value.
|
||||
func (p *IdentityPersister) FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(ctx context.Context, anyRecoveryAddress string) (_ []identity.RecoveryAddress, err error) {
|
||||
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindAllRecoveryAddressesForIdentityByRecoveryAddressValue",
|
||||
trace.WithAttributes(
|
||||
attribute.Stringer("network.id", p.NetworkID(ctx))))
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
var recoveryAddresses []identity.RecoveryAddress
|
||||
|
||||
// SQL explanation:
|
||||
// 1. Find a row (`B`) with the value matching `anyRecoveryAddress`.
|
||||
// This row has an identity id (`B.identity_id`).
|
||||
// 2. Find all rows (`A`) with this identity id.
|
||||
// Meaning: find all recovery addresses for this identity.
|
||||
// The result set includes the user provided address (`anyRecoveryAddress`).
|
||||
// NOTE: Should we exclude from the result set the login address for more security?
|
||||
//
|
||||
// This is all done in one query with a self-join.
|
||||
// We also bound the results for safety.
|
||||
err = p.GetConnection(ctx).RawQuery(
|
||||
`
|
||||
SELECT A.id, A.via, A.value, A.identity_id, A.created_at, A.updated_at, A.nid
|
||||
FROM identity_recovery_addresses A
|
||||
JOIN identity_recovery_addresses B
|
||||
ON A.identity_id = B.identity_id
|
||||
AND A.nid = B.nid
|
||||
WHERE B.value = ?
|
||||
AND A.nid = ?
|
||||
LIMIT 10
|
||||
`,
|
||||
stringToLowerTrim(anyRecoveryAddress),
|
||||
p.NetworkID(ctx),
|
||||
).
|
||||
All(&recoveryAddresses)
|
||||
|
||||
if err != nil {
|
||||
return nil, sqlcon.HandleError(err)
|
||||
}
|
||||
return recoveryAddresses, nil
|
||||
}
|
||||
|
||||
func (p *IdentityPersister) VerifyAddress(ctx context.Context, code string) (err error) {
|
||||
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.VerifyAddress",
|
||||
trace.WithAttributes(
|
||||
|
|
|
|||
|
|
@ -130,6 +130,11 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques
|
|||
return nil, err
|
||||
}
|
||||
|
||||
state := flow.StateChooseMethod
|
||||
if conf.ChooseRecoveryAddress(r.Context()) {
|
||||
state = flow.StateRecoveryAwaitingAddress
|
||||
}
|
||||
|
||||
flow := &Flow{
|
||||
ID: id,
|
||||
ExpiresAt: now.Add(exp),
|
||||
|
|
@ -139,7 +144,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques
|
|||
Method: "POST",
|
||||
Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(),
|
||||
},
|
||||
State: flow.StateChooseMethod,
|
||||
State: state,
|
||||
CSRFToken: csrf,
|
||||
Type: ft,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
|
@ -25,6 +26,8 @@ import (
|
|||
type State string
|
||||
|
||||
// #nosec G101 -- only a key constant
|
||||
// Define the various states for all flows.
|
||||
// Recovery flows for V2 have different states (see below).
|
||||
const (
|
||||
StateChooseMethod State = "choose_method"
|
||||
// Note: this state should actually be called `StateMessageSent`,
|
||||
|
|
@ -33,6 +36,21 @@ const (
|
|||
StatePassedChallenge State = "passed_challenge"
|
||||
StateShowForm State = "show_form"
|
||||
StateSuccess State = "success"
|
||||
|
||||
// Recovery V2.
|
||||
// The initial state is different from `StateChooseMethod` to distinguish recovery v1 vs v2.
|
||||
// This avoids the issue of the feature flag being toggled while some recovery flows are on-going.
|
||||
// This would lead to an inconsistent state machine/logic for these flows.
|
||||
StateRecoveryAwaitingAddress State = "recovery_awaiting_address"
|
||||
StateRecoveryAwaitingAddressChoice State = "recovery_awaiting_address_choice"
|
||||
StateRecoveryAwaitingAddressConfirm State = "recovery_confirming_address"
|
||||
StateRecoveryAwaitingCode State = "recovery_awaiting_code"
|
||||
// The final success state is the same as in Recovery V1 (`passed_challenge`).
|
||||
// Since this is the terminal state, it is not affected by toggling the feature flag.
|
||||
|
||||
// State machine diagrams:
|
||||
// - ./state_recovery_v1.mermaid
|
||||
// - ./state_recovery_v2.mermaid
|
||||
)
|
||||
|
||||
var states = []State{
|
||||
|
|
@ -54,6 +72,10 @@ func HasReachedState(expected, actual State) bool {
|
|||
return indexOf(actual) >= indexOf(expected)
|
||||
}
|
||||
|
||||
func IsStateRecoveryV2(state State) bool {
|
||||
return strings.HasPrefix(state.String(), "recovery_")
|
||||
}
|
||||
|
||||
func NextState(current State) State {
|
||||
if current == StatePassedChallenge {
|
||||
return StatePassedChallenge
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
stateDiagram-v2
|
||||
[*] --> choose_method
|
||||
choose_method --> sent_email: provided an existing email address
|
||||
sent_email --> sent_email: clicked 'resend code'
|
||||
choose_method --> sent_email: provided a non existing email address - pretend we sent a code
|
||||
sent_email --> passed_challenge: provided valid code
|
||||
passed_challenge --> [*]
|
||||
|
||||
note right of sent_email
|
||||
If the email exists, a recovery code is sent to it.
|
||||
Otherwise, an email mentioning that this is an unknown address may be sent depending on the configuration.
|
||||
end note
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
stateDiagram-v2
|
||||
[*] --> recovery_awaiting_address
|
||||
|
||||
recovery_awaiting_address --> recovery_awaiting_address_choice: provided any address which exists
|
||||
recovery_awaiting_address --> recovery_awaiting_code: provided any address which does not exist - pretend we sent a code
|
||||
recovery_awaiting_address --> recovery_awaiting_code: provided any address & auto-picked the only existing address
|
||||
recovery_awaiting_address_choice --> recovery_confirming_address: chose a masked address
|
||||
recovery_confirming_address --> recovery_awaiting_address_choice : choose different address
|
||||
recovery_awaiting_address_choice --> recovery_awaiting_code: chose a masked address & it is the one provided initially (do not ask again for the full address)
|
||||
recovery_confirming_address --> recovery_awaiting_code: provided the full address corresponding to the masked address
|
||||
recovery_awaiting_code --> recovery_awaiting_code: clicked 'resend code'
|
||||
recovery_awaiting_code --> passed_challenge: provided valid code
|
||||
recovery_awaiting_code --> recovery_awaiting_address_choice : choose different address
|
||||
|
||||
passed_challenge --> [*]
|
||||
|
||||
|
||||
note right of recovery_awaiting_code
|
||||
If the address exists, a recovery code is sent to it.
|
||||
Otherwise, an email mentioning that this is an unknown address may be sent depending on the configuration
|
||||
end note
|
||||
|
||||
|
|
@ -13,6 +13,18 @@
|
|||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"recovery_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"recovery_select_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"recovery_confirm_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"screen": {
|
||||
"type": "string"
|
||||
},
|
||||
"flow": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address (Email, phone number, etc)",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address (Email, phone number, etc)",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address (Email, phone number, etc)",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_confirm_address",
|
||||
"type": "email",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070007,
|
||||
"text": "Email",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_confirm_address",
|
||||
"type": "email",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070007,
|
||||
"text": "Email",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_confirm_address",
|
||||
"type": "email",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070007,
|
||||
"text": "Email",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"pattern": "[0-9]+",
|
||||
"disabled": false,
|
||||
"maxlength": 6,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070010,
|
||||
"text": "Recovery code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "code",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_confirm_address",
|
||||
"type": "submit",
|
||||
"value": "test-api@ory.sh",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070008,
|
||||
"text": "Resend code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_address",
|
||||
"type": "hidden",
|
||||
"value": "test-api@ory.sh",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "screen",
|
||||
"type": "submit",
|
||||
"value": "previous",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1060007,
|
||||
"text": "Back",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"pattern": "[0-9]+",
|
||||
"disabled": false,
|
||||
"maxlength": 6,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070010,
|
||||
"text": "Recovery code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "code",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_confirm_address",
|
||||
"type": "submit",
|
||||
"value": "test-browser@ory.sh",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070008,
|
||||
"text": "Resend code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_address",
|
||||
"type": "hidden",
|
||||
"value": "test-browser@ory.sh",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "screen",
|
||||
"type": "submit",
|
||||
"value": "previous",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1060007,
|
||||
"text": "Back",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"pattern": "[0-9]+",
|
||||
"disabled": false,
|
||||
"maxlength": 6,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070010,
|
||||
"text": "Recovery code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "code",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_confirm_address",
|
||||
"type": "submit",
|
||||
"value": "test-spa@ory.sh",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070008,
|
||||
"text": "Resend code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_address",
|
||||
"type": "hidden",
|
||||
"value": "test-spa@ory.sh",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "screen",
|
||||
"type": "submit",
|
||||
"value": "previous",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1060007,
|
||||
"text": "Back",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"pattern": "[0-9]+",
|
||||
"disabled": false,
|
||||
"maxlength": 6,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070010,
|
||||
"text": "Recovery code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "code",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_confirm_address",
|
||||
"type": "submit",
|
||||
"value": "+491705550177",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070008,
|
||||
"text": "Resend code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_address",
|
||||
"type": "hidden",
|
||||
"value": "+491705550177",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "screen",
|
||||
"type": "submit",
|
||||
"value": "previous",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1060007,
|
||||
"text": "Back",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"pattern": "[0-9]+",
|
||||
"disabled": false,
|
||||
"maxlength": 6,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070010,
|
||||
"text": "Recovery code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "code",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_confirm_address",
|
||||
"type": "submit",
|
||||
"value": "+491705550176",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070008,
|
||||
"text": "Resend code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_address",
|
||||
"type": "hidden",
|
||||
"value": "+491705550176",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "screen",
|
||||
"type": "submit",
|
||||
"value": "previous",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1060007,
|
||||
"text": "Back",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"pattern": "[0-9]+",
|
||||
"disabled": false,
|
||||
"maxlength": 6,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070010,
|
||||
"text": "Recovery code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "code",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_confirm_address",
|
||||
"type": "submit",
|
||||
"value": "+491705550178",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070008,
|
||||
"text": "Resend code",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_address",
|
||||
"type": "hidden",
|
||||
"value": "+491705550178",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "screen",
|
||||
"type": "submit",
|
||||
"value": "previous",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1060007,
|
||||
"text": "Back",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "hidden"
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "recovery_address",
|
||||
"node_type": "input",
|
||||
"required": true,
|
||||
"type": "text"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070016,
|
||||
"text": "Recovery address",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"node_type": "input",
|
||||
"type": "submit",
|
||||
"value": "code"
|
||||
},
|
||||
"group": "code",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070009,
|
||||
"text": "Continue",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_select_address",
|
||||
"type": "submit",
|
||||
"value": "BgkSG7BOfSWi+9/IRUclTboO0eGGkgnBFQiBv/v2Jw4=",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070000,
|
||||
"text": "te****@ory.sh",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_select_address",
|
||||
"type": "submit",
|
||||
"value": "MEIbSbZvtBRV9TH7wdFTF5CGyhxaaM2zuLDSIELCIAI=",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070000,
|
||||
"text": "+49****67",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "hidden",
|
||||
"value": "code",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_address",
|
||||
"type": "hidden",
|
||||
"value": "+491705550167",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_select_address",
|
||||
"type": "submit",
|
||||
"value": "9lRwDY12jB69oE1+tGQXV2XFJbgBtjjRwBjH+iHmHjA=",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070000,
|
||||
"text": "te****@ory.sh",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_select_address",
|
||||
"type": "submit",
|
||||
"value": "52McLavwKA6+jlSuOBVOPvkoLn8r1fUhZDMj2eytcac=",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070000,
|
||||
"text": "+49****66",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "hidden",
|
||||
"value": "code",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_address",
|
||||
"type": "hidden",
|
||||
"value": "+491705550166",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_select_address",
|
||||
"type": "submit",
|
||||
"value": "pti/PkfHLV3dkx+jIA+yDc8KCLmQ/K872SvKfFQ7C/c=",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070000,
|
||||
"text": "te****@ory.sh",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_select_address",
|
||||
"type": "submit",
|
||||
"value": "zx+NolDxaddHeLJ05eafzEcdjhJ0F8h5MAPtmbBSZkE=",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070000,
|
||||
"text": "+49****68",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "hidden",
|
||||
"value": "code",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "code",
|
||||
"attributes": {
|
||||
"name": "recovery_address",
|
||||
"type": "hidden",
|
||||
"value": "+491705550168",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
|
|
@ -284,7 +284,7 @@ func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, c
|
|||
var t courier.Template
|
||||
|
||||
switch code.RecoveryAddress.Via {
|
||||
case identity.ChannelTypeEmail:
|
||||
case identity.RecoveryAddressTypeEmail:
|
||||
t = email.NewRecoveryCodeValid(s.deps, &email.RecoveryCodeValidModel{
|
||||
To: code.RecoveryAddress.Value,
|
||||
RecoveryCode: codeString,
|
||||
|
|
@ -293,7 +293,7 @@ func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, c
|
|||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()),
|
||||
})
|
||||
case identity.ChannelTypeSMS:
|
||||
case identity.RecoveryAddressTypeSMS:
|
||||
u, err := url.Parse(f.GetRequestURL())
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -4,14 +4,19 @@
|
|||
package code
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ory/kratos/x/redir"
|
||||
|
||||
"github.com/ory/x/pointerx"
|
||||
"github.com/ory/x/sqlcon"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -38,12 +43,31 @@ func (s *Strategy) RecoveryStrategyID() string {
|
|||
return string(recovery.RecoveryStrategyCode)
|
||||
}
|
||||
|
||||
// This builds the initial UI (first recovery screen).
|
||||
func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) error {
|
||||
f.UI.SetCSRF(s.deps.GenerateCSRFToken(r))
|
||||
f.UI.GetNodes().Upsert(
|
||||
node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).
|
||||
WithMetaLabel(text.NewInfoNodeInputEmail()),
|
||||
)
|
||||
switch f.State {
|
||||
case flow.StateChooseMethod:
|
||||
f.UI.SetCSRF(s.deps.GenerateCSRFToken(r))
|
||||
f.UI.GetNodes().Upsert(
|
||||
node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).
|
||||
WithMetaLabel(text.NewInfoNodeInputEmail()),
|
||||
)
|
||||
case flow.StateRecoveryAwaitingAddress:
|
||||
// re-initialize the UI with a "clean" new state
|
||||
f.UI = &container.Container{
|
||||
Method: "POST",
|
||||
Action: flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), recovery.RouteSubmitFlow), f.ID).String(),
|
||||
}
|
||||
f.UI.SetCSRF(s.deps.GenerateCSRFToken(r))
|
||||
f.UI.GetNodes().Append(
|
||||
node.NewInputField("recovery_address", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).
|
||||
WithMetaLabel(text.NewRecoveryAskAnyRecoveryAddress()),
|
||||
)
|
||||
default:
|
||||
// Unreachable.
|
||||
return errors.Errorf("unreachable state: %s", f.State)
|
||||
}
|
||||
|
||||
f.UI.
|
||||
GetNodes().
|
||||
Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
|
|
@ -114,7 +138,7 @@ type updateRecoveryFlowWithCodeMethod struct {
|
|||
// Used in RecoveryV2.
|
||||
RecoveryConfirmAddress string `json:"recovery_confirm_address" form:"recovery_confirm_address"`
|
||||
|
||||
// Set to "previous" to return to the previous screen.
|
||||
// Set to "previous" to go back in the flow, meaningfully.
|
||||
// Used in RecoveryV2.
|
||||
Screen string `json:"screen" form:"screen"`
|
||||
}
|
||||
|
|
@ -158,12 +182,17 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F
|
|||
|
||||
f.UI.ResetMessages()
|
||||
|
||||
// If the email is present in the submission body, the user needs a new code via resend
|
||||
if f.State != flow.StateChooseMethod && len(body.Email) == 0 {
|
||||
if err := flow.MethodEnabledAndAllowed(ctx, flow.RecoveryFlow, sID, sID, s.deps); err != nil {
|
||||
return s.HandleRecoveryError(w, r, nil, body, err)
|
||||
// NOTE: This is implicitly looking at the state machine (for Recovery v1), by inspecting which fields are present,
|
||||
// instead of inspecting the state explicitly.
|
||||
// For Recovery v2 we inspect the state explicitly, a few lines below.
|
||||
if !flow.IsStateRecoveryV2(f.State) {
|
||||
// If the email is not present in the submission body, the user needs a new code via resend
|
||||
if f.State != flow.StateChooseMethod && len(body.Email) == 0 {
|
||||
if err := flow.MethodEnabledAndAllowed(ctx, flow.RecoveryFlow, sID, sID, s.deps); err != nil {
|
||||
return s.HandleRecoveryError(w, r, nil, body, err)
|
||||
}
|
||||
return s.recoveryUseCode(w, r, body, f)
|
||||
}
|
||||
return s.recoveryUseCode(w, r, body, f)
|
||||
}
|
||||
|
||||
if _, err := s.deps.SessionManager().FetchFromRequest(ctx, r); err == nil {
|
||||
|
|
@ -176,8 +205,12 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F
|
|||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
}
|
||||
|
||||
if err := flow.MethodEnabledAndAllowed(ctx, flow.RecoveryFlow, sID, body.Method, s.deps); err != nil {
|
||||
return s.HandleRecoveryError(w, r, nil, body, err)
|
||||
// Recovery V1 sets some magic fields in the UI and inspects them in the body, e.g. `method`.
|
||||
// This is brittle and rendered unnecessary in Recovery V2 by properly inspecting the `state` (and the CSRF token).
|
||||
if !flow.IsStateRecoveryV2(f.State) {
|
||||
if err := flow.MethodEnabledAndAllowed(ctx, flow.RecoveryFlow, sID, body.Method, s.deps); err != nil {
|
||||
return s.HandleRecoveryError(w, r, nil, body, err)
|
||||
}
|
||||
}
|
||||
|
||||
recoveryFlow, err := s.deps.RecoveryFlowPersister().GetRecoveryFlow(ctx, x.ParseUUID(body.Flow))
|
||||
|
|
@ -189,6 +222,10 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F
|
|||
return s.HandleRecoveryError(w, r, recoveryFlow, body, err)
|
||||
}
|
||||
|
||||
if body.Screen == "previous" {
|
||||
return s.recoveryV2HandleGoBack(r, f, body)
|
||||
}
|
||||
|
||||
switch recoveryFlow.State {
|
||||
case flow.StateChooseMethod,
|
||||
flow.StateEmailSent:
|
||||
|
|
@ -196,6 +233,17 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F
|
|||
case flow.StatePassedChallenge:
|
||||
// was already handled, do not allow retry
|
||||
return s.retryRecoveryFlow(w, r, recoveryFlow.Type, RetryWithMessage(text.NewErrorValidationRecoveryRetrySuccess()))
|
||||
|
||||
// Recovery V2.
|
||||
case flow.StateRecoveryAwaitingAddress:
|
||||
return s.recoveryV2HandleStateAwaitingAddress(r, recoveryFlow, body)
|
||||
case flow.StateRecoveryAwaitingAddressChoice:
|
||||
return s.recoveryV2HandleStateAwaitingAddressChoice(r, recoveryFlow, body)
|
||||
case flow.StateRecoveryAwaitingAddressConfirm:
|
||||
return s.recoveryV2HandleStateConfirmingAddress(r, recoveryFlow, body)
|
||||
case flow.StateRecoveryAwaitingCode:
|
||||
return s.recoveryV2HandleStateAwaitingCode(w, r, recoveryFlow, body)
|
||||
|
||||
default:
|
||||
return s.retryRecoveryFlow(w, r, recoveryFlow.Type, RetryWithMessage(text.NewErrorValidationRecoveryStateFailure()))
|
||||
}
|
||||
|
|
@ -283,6 +331,10 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request,
|
|||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
}
|
||||
|
||||
// NOTE: This function handles two cases:
|
||||
// - A code was submitted: try to use it
|
||||
// - No code was submitted: delete all existing codes, re-generate a new one, send it.
|
||||
// This corresponds to the user clicking on the 're-send code' button.
|
||||
func (s *Strategy) recoveryUseCode(w http.ResponseWriter, r *http.Request, body *recoverySubmitPayload, f *recovery.Flow) error {
|
||||
ctx := r.Context()
|
||||
code, err := s.deps.RecoveryCodePersister().UseRecoveryCode(ctx, f.ID, body.Code)
|
||||
|
|
@ -397,7 +449,283 @@ func (s *Strategy) retryRecoveryFlow(w http.ResponseWriter, r *http.Request, ft
|
|||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
}
|
||||
|
||||
// recoveryHandleFormSubmission handles the submission of an Email for recovery
|
||||
func AddressToHashBase64(address string) string {
|
||||
hash := sha256.Sum256([]byte(address))
|
||||
return base64.StdEncoding.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func (s *Strategy) recoveryV2HandleStateAwaitingAddress(r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error {
|
||||
if f.State != flow.StateRecoveryAwaitingAddress {
|
||||
return errors.Errorf("unreachable state: %s", f.State)
|
||||
}
|
||||
|
||||
if len(body.RecoveryAddress) == 0 {
|
||||
return schema.NewRequiredError("#/recovery_address", "recovery_address")
|
||||
}
|
||||
|
||||
// Need to retrieve all possible recovery addresses and present a choice.
|
||||
recoveryAddresses, err := s.deps.IdentityPool().FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress)
|
||||
// Real error.
|
||||
if err != nil && !errors.Is(err, sqlcon.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
|
||||
// No rows returned.
|
||||
if len(recoveryAddresses) == 0 {
|
||||
// To avoid an attacker from using this case to probe for existing addresses, we pretend it exists.
|
||||
// This is the same behavior as in Recovery V1.
|
||||
recoveryAddresses = append(recoveryAddresses, identity.RecoveryAddress{Value: body.RecoveryAddress})
|
||||
}
|
||||
|
||||
f.State = flow.StateRecoveryAwaitingAddressChoice
|
||||
|
||||
if len(recoveryAddresses) == 1 && recoveryAddresses[0].Value == body.RecoveryAddress {
|
||||
// Skip two states for convenience:
|
||||
// - No need to present a choice with only one option
|
||||
// - No need to ask for the full address if there is only one and it was just provided in full
|
||||
|
||||
body.RecoveryConfirmAddress = body.RecoveryAddress
|
||||
f.State = flow.StateRecoveryAwaitingAddressConfirm
|
||||
if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.recoveryV2HandleStateConfirmingAddress(r, f, body)
|
||||
}
|
||||
|
||||
// re-initialize the UI with a "clean" new state
|
||||
f.UI = &container.Container{
|
||||
Method: "POST",
|
||||
Action: flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), recovery.RouteSubmitFlow), f.ID).String(),
|
||||
}
|
||||
|
||||
f.UI.SetCSRF(f.CSRFToken)
|
||||
|
||||
f.State = flow.StateRecoveryAwaitingAddressChoice
|
||||
f.UI.Messages.Set(text.NewRecoveryAskToChooseAddress())
|
||||
|
||||
for _, a := range recoveryAddresses {
|
||||
// NOTE: Only send the masked value and the hash, to avoid information exfiltration.
|
||||
// Why the hash? So that we can recognize later, when the user chooses the masked address in the list,
|
||||
// that the chosen masked address is the `recovery_address` provided in the beginning,
|
||||
// and then we do not ask again the user to provide it in full.
|
||||
hashBase64 := AddressToHashBase64(a.Value)
|
||||
f.UI.GetNodes().Append(node.NewInputField("recovery_select_address", hashBase64, node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(&text.Message{
|
||||
ID: text.InfoNodeLabel,
|
||||
Text: MaskAddress(a.Value),
|
||||
Type: text.Info,
|
||||
}))
|
||||
}
|
||||
|
||||
f.UI.Nodes.Append(node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden))
|
||||
f.UI.Nodes.Append(node.NewInputField("recovery_address", body.RecoveryAddress, node.CodeGroup, node.InputAttributeTypeHidden))
|
||||
// No back button here because there is no point for the user.
|
||||
|
||||
if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) recoveryV2HandleStateAwaitingAddressChoice(r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error {
|
||||
if f.State != flow.StateRecoveryAwaitingAddressChoice {
|
||||
return errors.Errorf("unreachable state: %s", f.State)
|
||||
}
|
||||
|
||||
if len(body.RecoverySelectAddress) == 0 {
|
||||
return schema.NewRequiredError("#/recovery_select_address", "recovery_select_address")
|
||||
}
|
||||
|
||||
if len(body.RecoveryAddress) == 0 {
|
||||
return schema.NewRequiredError("#/recovery_address", "recovery_address")
|
||||
}
|
||||
|
||||
// Is the chosen masked address the same as the address provided in full at the beginning?
|
||||
// If yes, then do not ask it again in full.
|
||||
// Technically we check `hash(recovery_address) == recovery_select_address` and
|
||||
// `recovery_select_address` is `hash(recovery_address)`.
|
||||
hashBase64 := AddressToHashBase64(body.RecoveryAddress)
|
||||
|
||||
// Better safe than sorry, use constant time comparison.
|
||||
if subtle.ConstantTimeCompare([]byte(hashBase64), []byte(body.RecoverySelectAddress)) == 1 {
|
||||
// Skip a state: do not ask the user again to provide the full address.
|
||||
body.RecoveryConfirmAddress = body.RecoveryAddress
|
||||
f.State = flow.StateRecoveryAwaitingAddressConfirm
|
||||
return s.recoveryV2HandleStateConfirmingAddress(r, f, body)
|
||||
}
|
||||
|
||||
// re-initialize the UI with a "clean" new state
|
||||
f.UI = &container.Container{
|
||||
Method: "POST",
|
||||
Action: flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), recovery.RouteSubmitFlow), f.ID).String(),
|
||||
}
|
||||
f.UI.SetCSRF(f.CSRFToken)
|
||||
|
||||
f.State = flow.StateRecoveryAwaitingAddressConfirm
|
||||
f.UI.Messages.Set(text.NewRecoveryAskForFullAddress())
|
||||
|
||||
var inputType node.UiNodeInputAttributeType
|
||||
var label *text.Message
|
||||
if strings.ContainsRune(body.RecoverySelectAddress, '@') {
|
||||
inputType = node.InputAttributeTypeEmail
|
||||
label = text.NewInfoNodeInputEmail()
|
||||
} else {
|
||||
inputType = node.InputAttributeTypeTel
|
||||
label = text.NewInfoNodeInputPhoneNumber()
|
||||
}
|
||||
|
||||
f.UI.Nodes.Append(node.NewInputField("recovery_confirm_address", body.RecoveryConfirmAddress, node.CodeGroup, inputType, node.WithRequiredInputAttribute).
|
||||
WithMetaLabel(label),
|
||||
)
|
||||
f.UI.Nodes.Append(node.NewInputField("recovery_address", body.RecoveryAddress, node.CodeGroup, node.InputAttributeTypeHidden))
|
||||
|
||||
f.UI.
|
||||
GetNodes().
|
||||
Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewInfoNodeLabelContinue()))
|
||||
buttonScreen := node.NewInputField("screen", "previous", node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewRecoveryBack())
|
||||
f.UI.GetNodes().Append(buttonScreen)
|
||||
|
||||
if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) recoveryV2HandleStateConfirmingAddress(r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error {
|
||||
if f.State != flow.StateRecoveryAwaitingAddressConfirm {
|
||||
return errors.Errorf("unreachable state: %s", f.State)
|
||||
}
|
||||
|
||||
if len(body.RecoveryConfirmAddress) == 0 {
|
||||
return schema.NewRequiredError("#/recovery_confirm_address", "recovery_confirm_address")
|
||||
}
|
||||
|
||||
if err := s.deps.RecoveryCodePersister().DeleteRecoveryCodesOfFlow(r.Context(), f.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.TransientPayload = body.TransientPayload
|
||||
|
||||
var addressType identity.RecoveryAddressType
|
||||
// Inferring the address type like this is a bit hacky, and actually not really necessary.
|
||||
// That's because `SendRecoveryCode` expects it, but not because it fundamentally is required.
|
||||
if strings.ContainsRune(body.RecoveryConfirmAddress, '@') {
|
||||
addressType = identity.RecoveryAddressTypeEmail
|
||||
} else {
|
||||
addressType = identity.RecoveryAddressTypeSMS
|
||||
}
|
||||
|
||||
// NOTE: We do not fetch the db address here. We only (try to) send the code to the user provided address.
|
||||
// That way we avoid information exfiltration.
|
||||
// `SendRecoveryCode` will anyway check by itself if the provided address is a known address or not.
|
||||
if err := s.deps.CodeSender().SendRecoveryCode(r.Context(), f, addressType, body.RecoveryConfirmAddress); err != nil {
|
||||
if !errors.Is(err, ErrUnknownAddress) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Continue execution
|
||||
}
|
||||
|
||||
// re-initialize the UI with a "clean" new state
|
||||
f.UI = &container.Container{
|
||||
Method: "POST",
|
||||
Action: flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(r.Context()), recovery.RouteSubmitFlow), f.ID).String(),
|
||||
}
|
||||
f.UI.SetCSRF(f.CSRFToken)
|
||||
|
||||
f.State = flow.StateRecoveryAwaitingCode
|
||||
|
||||
uiText := text.NewRecoveryCodeRecoverySelectAddressSent(MaskAddress(body.RecoveryConfirmAddress))
|
||||
|
||||
f.UI.Messages.Set(uiText)
|
||||
f.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithInputAttributes(func(a *node.InputAttributes) {
|
||||
a.Required = true
|
||||
a.Pattern = "[0-9]+"
|
||||
a.MaxLength = CodeLength
|
||||
})).
|
||||
WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()),
|
||||
)
|
||||
|
||||
f.UI.Nodes.Append(node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewInfoNodeLabelContinue()),
|
||||
)
|
||||
|
||||
// Required to make 'resend' work.
|
||||
f.UI.Nodes.Append(node.NewInputField("recovery_confirm_address", body.RecoveryConfirmAddress, node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewInfoNodeResendOTP()),
|
||||
)
|
||||
f.UI.Nodes.Append(node.NewInputField("recovery_address", body.RecoveryAddress, node.CodeGroup, node.InputAttributeTypeHidden))
|
||||
|
||||
buttonScreen := node.NewInputField("screen", "previous", node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewRecoveryBack())
|
||||
f.UI.GetNodes().Append(buttonScreen)
|
||||
|
||||
if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) recoveryV2HandleStateAwaitingCode(w http.ResponseWriter, r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error {
|
||||
if f.State != flow.StateRecoveryAwaitingCode {
|
||||
return errors.Errorf("unreachable state: %s", f.State)
|
||||
}
|
||||
|
||||
if len(body.Code) == 0 {
|
||||
// The 're-send' button was clicked. We handle it as if the user first arrived at the state `RecoveryV2StateAwaitingAddressConfirm`.
|
||||
// That will invalidate all existing codes and send a new code.
|
||||
f.State = flow.StateRecoveryAwaitingAddressConfirm
|
||||
return s.recoveryV2HandleStateConfirmingAddress(r, f, body)
|
||||
} else {
|
||||
return s.recoveryUseCode(w, r, body, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Strategy) recoveryV2HandleGoBack(r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error {
|
||||
// If no address choice needs to take place, just go to the first screen.
|
||||
recoveryAddresses, _ := s.deps.IdentityPool().FindAllRecoveryAddressesForIdentityByRecoveryAddressValue(r.Context(), body.RecoveryAddress)
|
||||
if len(recoveryAddresses) <= 1 {
|
||||
f.State = flow.StateRecoveryAwaitingAddress
|
||||
err := s.PopulateRecoveryMethod(r, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.deps.RecoveryFlowPersister().UpdateRecoveryFlow(r.Context(), f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch f.State {
|
||||
// Go back to the second screen (choose an address) by essentially going to the first screen
|
||||
// and re-submitting the form (to arrive at the second screen).
|
||||
// This contraption is necessary since the UI nodes are stored in the database and not generated on the fly.
|
||||
// So simply redirecting to a previous screen (as in: 'web page') would do nothing, it would just show the same UI.
|
||||
// This way we force the UI generation code to re-run and the new UI nodes to be stored to the database.
|
||||
case flow.StateRecoveryAwaitingCode:
|
||||
fallthrough
|
||||
case flow.StateRecoveryAwaitingAddressConfirm:
|
||||
// Reset some body fields since we are going to (almost) the beginning of the flow.
|
||||
body.RecoveryConfirmAddress = ""
|
||||
body.RecoverySelectAddress = ""
|
||||
body.Screen = ""
|
||||
|
||||
f.State = flow.StateRecoveryAwaitingAddress
|
||||
|
||||
return s.recoveryV2HandleStateAwaitingAddress(r, f, body)
|
||||
default:
|
||||
// Should not trigger, but do something sensible: start from scratch.
|
||||
return s.PopulateRecoveryMethod(r, f)
|
||||
}
|
||||
}
|
||||
|
||||
// recoveryHandleFormSubmission handles the submission of an address for recovery
|
||||
func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.Request, f *recovery.Flow, body *recoverySubmitPayload) error {
|
||||
if len(body.Email) == 0 {
|
||||
return s.HandleRecoveryError(w, r, f, body, schema.NewRequiredError("#/email", "email"))
|
||||
|
|
@ -472,15 +800,19 @@ func (s *Strategy) markRecoveryAddressVerified(w http.ResponseWriter, r *http.Re
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) HandleRecoveryError(w http.ResponseWriter, r *http.Request, flow *recovery.Flow, body *recoverySubmitPayload, err error) error {
|
||||
if flow != nil {
|
||||
func (s *Strategy) HandleRecoveryError(w http.ResponseWriter, r *http.Request, fl *recovery.Flow, body *recoverySubmitPayload, err error) error {
|
||||
if fl != nil {
|
||||
if flow.IsStateRecoveryV2(fl.State) {
|
||||
// Unreachable: RecoveryV2 never uses this function.
|
||||
return err
|
||||
}
|
||||
email := ""
|
||||
if body != nil {
|
||||
email = body.Email
|
||||
}
|
||||
|
||||
flow.UI.SetCSRF(s.deps.GenerateCSRFToken(r))
|
||||
flow.UI.GetNodes().Upsert(
|
||||
fl.UI.SetCSRF(s.deps.GenerateCSRFToken(r))
|
||||
fl.UI.GetNodes().Upsert(
|
||||
node.NewInputField("email", email, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).
|
||||
WithMetaLabel(text.NewInfoNodeInputEmail()),
|
||||
)
|
||||
|
|
@ -496,6 +828,12 @@ type recoverySubmitPayload struct {
|
|||
Flow string `json:"flow" form:"flow"`
|
||||
Email string `json:"email" form:"email"`
|
||||
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
|
||||
|
||||
// Used in RecoveryV2.
|
||||
RecoveryAddress string `json:"recovery_address" form:"recovery_address"`
|
||||
RecoverySelectAddress string `json:"recovery_select_address" form:"recovery_select_address"`
|
||||
RecoveryConfirmAddress string `json:"recovery_confirm_address" form:"recovery_confirm_address"`
|
||||
Screen string `json:"screen" form:"screen"`
|
||||
}
|
||||
|
||||
func (s *Strategy) decodeRecovery(r *http.Request) (*recoverySubmitPayload, error) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3224,7 +3224,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
"screen": {
|
||||
"description": "Set to \"previous\" to return to the previous screen.\nUsed in RecoveryV2.",
|
||||
"description": "Set to \"previous\" to go back in the flow, meaningfully.\nUsed in RecoveryV2.",
|
||||
"type": "string"
|
||||
},
|
||||
"transient_payload": {
|
||||
|
|
|
|||
|
|
@ -6464,7 +6464,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
"screen": {
|
||||
"description": "Set to \"previous\" to return to the previous screen.\nUsed in RecoveryV2.",
|
||||
"description": "Set to \"previous\" to go back in the flow, meaningfully.\nUsed in RecoveryV2.",
|
||||
"type": "string"
|
||||
},
|
||||
"transient_payload": {
|
||||
|
|
|
|||
14
text/id.go
14
text/id.go
|
|
@ -81,10 +81,14 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
InfoSelfServiceRecovery ID = 1060000 + iota // 1060000
|
||||
InfoSelfServiceRecoverySuccessful // 1060001
|
||||
InfoSelfServiceRecoveryEmailSent // 1060002
|
||||
InfoSelfServiceRecoveryEmailWithCodeSent // 1060003
|
||||
InfoSelfServiceRecovery ID = 1060000 + iota // 1060000
|
||||
InfoSelfServiceRecoverySuccessful // 1060001
|
||||
InfoSelfServiceRecoveryEmailSent // 1060002
|
||||
InfoSelfServiceRecoveryEmailWithCodeSent // 1060003
|
||||
InfoSelfServiceRecoveryMessageMaskedWithCodeSent // 1060004
|
||||
InfoSelfServiceRecoveryAskForFullAddress // 1060005
|
||||
InfoSelfServiceRecoveryAskToChooseAddress // 1060006
|
||||
InfoSelfServiceRecoveryBack // 1060007
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -104,6 +108,8 @@ const (
|
|||
InfoNodeLabelLoginCode // 1070013
|
||||
InfoNodeLabelLoginAndLinkCredential // 1070014
|
||||
InfoNodeLabelCaptcha // 1070015
|
||||
InfoNodeLabelRecoveryAddress // 1070016
|
||||
InfoNodeLabelPhoneNumber // 1070017
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -102,6 +102,14 @@ func NewInfoNodeInputEmail() *Message {
|
|||
}
|
||||
}
|
||||
|
||||
func NewInfoNodeInputPhoneNumber() *Message {
|
||||
return &Message{
|
||||
ID: InfoNodeLabelPhoneNumber,
|
||||
Text: "Phone number",
|
||||
Type: Info,
|
||||
}
|
||||
}
|
||||
|
||||
func NewInfoNodeResendOTP() *Message {
|
||||
return &Message{
|
||||
ID: InfoNodeLabelResendOTP,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,49 @@ func NewRecoveryEmailWithCodeSent() *Message {
|
|||
}
|
||||
}
|
||||
|
||||
func NewRecoveryAskAnyRecoveryAddress() *Message {
|
||||
return &Message{
|
||||
ID: InfoNodeLabelRecoveryAddress,
|
||||
Text: "Recovery address",
|
||||
Type: Info,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRecoveryCodeRecoverySelectAddressSent(maskedAddress string) *Message {
|
||||
return &Message{
|
||||
ID: InfoSelfServiceRecoveryMessageMaskedWithCodeSent,
|
||||
Type: Info,
|
||||
Text: fmt.Sprintf("A recovery code has been sent to %s. If you have not received it, check the spelling of the address and make sure to use the address you registered with.", maskedAddress),
|
||||
Context: context(map[string]any{
|
||||
"masked_address": maskedAddress,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func NewRecoveryAskForFullAddress() *Message {
|
||||
return &Message{
|
||||
ID: InfoSelfServiceRecoveryAskForFullAddress,
|
||||
Type: Info,
|
||||
Text: "Recover access to your account by providing your recovery address in full.",
|
||||
}
|
||||
}
|
||||
|
||||
func NewRecoveryAskToChooseAddress() *Message {
|
||||
return &Message{
|
||||
ID: InfoSelfServiceRecoveryAskToChooseAddress,
|
||||
Type: Info,
|
||||
Text: "How do you want to recover your account?",
|
||||
}
|
||||
}
|
||||
|
||||
func NewRecoveryBack() *Message {
|
||||
return &Message{
|
||||
ID: InfoSelfServiceRecoveryBack,
|
||||
Type: Info,
|
||||
Text: "Back",
|
||||
}
|
||||
}
|
||||
|
||||
func NewErrorValidationRecoveryTokenInvalidOrAlreadyUsed() *Message {
|
||||
return &Message{
|
||||
ID: ErrorValidationRecoveryTokenInvalidOrAlreadyUsed,
|
||||
|
|
|
|||
Loading…
Reference in New Issue