snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
597
selfservice/strategy/code/strategy.go
Normal file
597
selfservice/strategy/code/strategy.go
Normal file
@@ -0,0 +1,597 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package code
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/hydra"
|
||||
"github.com/ory/kratos/x/nosurfx"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
"github.com/ory/kratos/continuity"
|
||||
"github.com/ory/kratos/courier"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/schema"
|
||||
"github.com/ory/kratos/selfservice/errorx"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/selfservice/flow/login"
|
||||
"github.com/ory/kratos/selfservice/flow/recovery"
|
||||
"github.com/ory/kratos/selfservice/flow/registration"
|
||||
"github.com/ory/kratos/selfservice/flow/settings"
|
||||
"github.com/ory/kratos/selfservice/flow/verification"
|
||||
"github.com/ory/kratos/selfservice/sessiontokenexchange"
|
||||
"github.com/ory/kratos/session"
|
||||
"github.com/ory/kratos/text"
|
||||
"github.com/ory/kratos/ui/container"
|
||||
"github.com/ory/kratos/ui/node"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/kratos/x/transaction"
|
||||
"github.com/ory/x/clock"
|
||||
"github.com/ory/x/httpx"
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/otelx"
|
||||
"github.com/ory/x/otelx/semconv"
|
||||
"github.com/ory/x/randx"
|
||||
"github.com/ory/x/urlx"
|
||||
)
|
||||
|
||||
var (
|
||||
_ recovery.Strategy = (*Strategy)(nil)
|
||||
_ verification.Strategy = (*Strategy)(nil)
|
||||
_ login.Strategy = (*Strategy)(nil)
|
||||
_ registration.Strategy = (*Strategy)(nil)
|
||||
_ identity.ActiveCredentialsCounter = (*Strategy)(nil)
|
||||
_ x.Handler = (*Strategy)(nil)
|
||||
)
|
||||
|
||||
type (
|
||||
// FlowMethod contains the configuration for this selfservice strategy.
|
||||
FlowMethod struct {
|
||||
*container.Container
|
||||
}
|
||||
|
||||
dependencies interface {
|
||||
clock.Provider
|
||||
nosurfx.CSRFProvider
|
||||
nosurfx.CSRFTokenGeneratorProvider
|
||||
httpx.WriterProvider
|
||||
logrusx.Provider
|
||||
otelx.Provider
|
||||
transaction.PersistenceProvider
|
||||
|
||||
config.Provider
|
||||
|
||||
session.HandlerProvider
|
||||
session.ManagementProvider
|
||||
settings.HandlerProvider
|
||||
settings.FlowPersistenceProvider
|
||||
settings.HookExecutorProvider
|
||||
|
||||
identity.ValidationProvider
|
||||
identity.ManagementProvider
|
||||
identity.PoolProvider
|
||||
identity.PrivilegedPoolProvider
|
||||
identity.PendingTraitsChangePersistenceProvider
|
||||
|
||||
courier.Provider
|
||||
|
||||
errorx.ManagementProvider
|
||||
|
||||
recovery.ErrorHandlerProvider
|
||||
recovery.FlowPersistenceProvider
|
||||
recovery.StrategyProvider
|
||||
recovery.HookExecutorProvider
|
||||
|
||||
verification.FlowPersistenceProvider
|
||||
verification.StrategyProvider
|
||||
verification.HookExecutorProvider
|
||||
|
||||
login.StrategyProvider
|
||||
login.FlowPersistenceProvider
|
||||
|
||||
registration.StrategyProvider
|
||||
registration.FlowPersistenceProvider
|
||||
|
||||
RecoveryCodePersistenceProvider
|
||||
VerificationCodePersistenceProvider
|
||||
SenderProvider
|
||||
|
||||
RegistrationCodePersistenceProvider
|
||||
LoginCodePersistenceProvider
|
||||
|
||||
schema.IdentitySchemaProvider
|
||||
session.PersistenceProvider
|
||||
|
||||
sessiontokenexchange.PersistenceProvider
|
||||
|
||||
continuity.ManagementProvider
|
||||
|
||||
hydra.Provider
|
||||
}
|
||||
|
||||
Strategy struct{ deps dependencies }
|
||||
|
||||
codeIdentifier struct {
|
||||
Identifier string `json:"identifier"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *Strategy) CountActiveFirstFactorCredentials(ctx context.Context, cc map[identity.CredentialsType]identity.Credentials) (int, error) {
|
||||
codeConfig := s.deps.Config().SelfServiceCodeStrategy(ctx)
|
||||
if codeConfig.PasswordlessEnabled {
|
||||
// Login with code for passwordless is enabled
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *Strategy) CountActiveMultiFactorCredentials(ctx context.Context, cc map[identity.CredentialsType]identity.Credentials) (int, error) {
|
||||
codeConfig := s.deps.Config().SelfServiceCodeStrategy(ctx)
|
||||
if !codeConfig.MFAEnabled {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Get code credentials if they exist
|
||||
creds, ok := cc[identity.CredentialsTypeCodeAuth]
|
||||
if !ok {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Check if the credentials config is valid JSON
|
||||
if !gjson.Valid(string(creds.Config)) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Check for v0 format with address_type field
|
||||
if gjson.GetBytes(creds.Config, "address_type").Exists() {
|
||||
addressType := gjson.GetBytes(creds.Config, "address_type").String()
|
||||
if addressType != "" {
|
||||
return 1, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var conf identity.CredentialsCode
|
||||
if err := json.Unmarshal(creds.Config, &conf); err != nil {
|
||||
return 0, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to unmarshal credentials config: %s", err))
|
||||
}
|
||||
|
||||
// If no addresses configured, return 0
|
||||
if len(conf.Addresses) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Count valid addresses configured for MFA
|
||||
validAddresses := 0
|
||||
for _, addr := range conf.Addresses {
|
||||
if addr.Address != "" {
|
||||
validAddresses++
|
||||
}
|
||||
}
|
||||
|
||||
return validAddresses, nil
|
||||
}
|
||||
|
||||
func NewStrategy(deps dependencies) *Strategy { return &Strategy{deps: deps} }
|
||||
|
||||
func (s *Strategy) ID() identity.CredentialsType {
|
||||
return identity.CredentialsTypeCodeAuth
|
||||
}
|
||||
|
||||
func (s *Strategy) NodeGroup() node.UiNodeGroup {
|
||||
return node.CodeGroup
|
||||
}
|
||||
|
||||
func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error {
|
||||
codeConfig := s.deps.Config().SelfServiceCodeStrategy(r.Context())
|
||||
switch f := f.(type) {
|
||||
case *login.Flow:
|
||||
if f.RequestedAAL == identity.AuthenticatorAssuranceLevel2 {
|
||||
if !codeConfig.MFAEnabled {
|
||||
// if the flow is requesting AAL2 but MFA is not enabled for the code strategy, we return nil so that
|
||||
// other strategies can fulfil the request
|
||||
return nil
|
||||
}
|
||||
} else if !codeConfig.PasswordlessEnabled {
|
||||
// if the flow is a normal login flow but passwordless is not enabled for the code strategy,
|
||||
// we return nil so that other strategies can fulfil the request
|
||||
return nil
|
||||
}
|
||||
case *registration.Flow:
|
||||
// for registration flows, we don't have AAL requirements, so we just check that passwordless is enabled.
|
||||
if !codeConfig.PasswordlessEnabled {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if string(f.GetState()) == "" {
|
||||
f.SetState(flow.StateChooseMethod)
|
||||
}
|
||||
|
||||
f.GetUI().ResetMessages()
|
||||
|
||||
switch f.GetState() {
|
||||
case flow.StateChooseMethod:
|
||||
if err := s.populateChooseMethodFlow(r, f); err != nil {
|
||||
return err
|
||||
}
|
||||
case flow.StateEmailSent:
|
||||
if err := s.populateEmailSentFlow(r.Context(), f); err != nil {
|
||||
return err
|
||||
}
|
||||
case flow.StatePassedChallenge:
|
||||
fallthrough
|
||||
default:
|
||||
return errors.WithStack(herodot.ErrBadRequest().WithReason("received an unexpected flow state"))
|
||||
}
|
||||
|
||||
// no matter the flow type or state we need to set the CSRF token
|
||||
if f.GetType() == flow.TypeBrowser {
|
||||
f.GetUI().SetCSRF(s.deps.GenerateCSRFToken(r))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) populateChooseMethodFlow(r *http.Request, f flow.Flow) error {
|
||||
ctx := r.Context()
|
||||
switch f := f.(type) {
|
||||
case *verification.Flow:
|
||||
label := text.NewInfoNodeInputEmail()
|
||||
inputType := node.InputAttributeTypeEmail
|
||||
if channels, err := s.deps.Config().CourierChannels(ctx); err == nil {
|
||||
for _, ch := range channels {
|
||||
if ch.ID == "sms" {
|
||||
label = text.NewInfoNodeInputEmailOrPhone()
|
||||
inputType = node.InputAttributeTypeText
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
f.GetUI().Nodes.Append(
|
||||
node.NewInputField("email", nil, node.CodeGroup, inputType, node.WithRequiredInputAttribute).
|
||||
WithMetaLabel(label),
|
||||
)
|
||||
f.GetUI().Nodes.Append(
|
||||
node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewInfoNodeLabelContinue()),
|
||||
)
|
||||
case *recovery.Flow:
|
||||
f.GetUI().Nodes.Append(
|
||||
node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).
|
||||
WithMetaLabel(text.NewInfoNodeInputEmail()),
|
||||
)
|
||||
f.GetUI().Nodes.Append(
|
||||
node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewInfoNodeLabelContinue()),
|
||||
)
|
||||
case *login.Flow:
|
||||
ds, err := f.IdentitySchema.URL(ctx, s.deps.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.RequestedAAL == identity.AuthenticatorAssuranceLevel2 {
|
||||
via := r.URL.Query().Get("via")
|
||||
|
||||
// The full expansion is required: finding the code addresses reads the identity's
|
||||
// credentials.
|
||||
sess, err := s.deps.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We need to load the identity's credentials.
|
||||
if len(sess.Identity.Credentials) == 0 {
|
||||
if err := s.deps.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, sess.Identity, identity.ExpandCredentials); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
addresses, err := s.FindCodeAddresses(ctx, sess, via)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, address := range addresses {
|
||||
f.GetUI().Nodes.Append(node.NewInputField("address", address.To, node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewInfoSelfServiceLoginAAL2CodeAddress(string(address.Via), address.To)))
|
||||
}
|
||||
} else if f.Refresh && s.deps.Config().RefreshLoginChooseAddress(ctx) {
|
||||
// On a refresh login the identity is already known from the active
|
||||
// session. Asking for the identifier again makes the privileged
|
||||
// login look like a fresh unified login (ory/kratos#4194). Instead,
|
||||
// render one "Send code to <address>" button per available code address,
|
||||
// exactly like the second-factor flow above.
|
||||
// The full expansion is required: finding the code addresses reads the identity's
|
||||
// credentials.
|
||||
sess, err := s.deps.SessionManager().FetchFromRequest(ctx, r, session.ExpandEverything, identity.ExpandEverything)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(sess.Identity.Credentials) == 0 {
|
||||
if err := s.deps.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, sess.Identity, identity.ExpandCredentials); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
addresses, err := s.FindCodeAddresses(ctx, sess, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the identity has no code address, append nothing so another
|
||||
// first-factor strategy (e.g. password) can fulfil the refresh.
|
||||
for _, address := range addresses {
|
||||
f.GetUI().Nodes.Append(node.NewInputField("address", address.To, node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewInfoSelfServiceLoginAAL2CodeAddress(string(address.Via), address.To)))
|
||||
}
|
||||
} else {
|
||||
if f.Refresh {
|
||||
// The refresh address picker is disabled, so we fall back to
|
||||
// re-asking for the identifier. This is deprecated: the identity
|
||||
// is already fixed by the active session (ory/kratos#4194).
|
||||
trace.SpanFromContext(ctx).AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "refresh_login_identifier_input"))
|
||||
}
|
||||
|
||||
identifierLabel, err := login.GetIdentifierLabelFromSchema(ctx, ds.String(), s.deps.Config().SecurityDisallowRefInIdentitySchemas(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.GetUI().Nodes.Upsert(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel))
|
||||
f.GetUI().Nodes.Append(
|
||||
node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginCode()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error {
|
||||
// fresh ui node group
|
||||
freshNodes := f.GetUI().Nodes
|
||||
freshNodes.ClearTransientNodes()
|
||||
var route string
|
||||
var codeMetaLabel *text.Message
|
||||
var message *text.Message
|
||||
|
||||
var resendNode *node.Node
|
||||
var backNode *node.Node
|
||||
|
||||
switch f.GetFlowName() {
|
||||
case flow.RecoveryFlow:
|
||||
route = recovery.RouteSubmitFlow
|
||||
codeMetaLabel = text.NewInfoNodeLabelRecoveryCode()
|
||||
message = text.NewRecoveryEmailWithCodeSent()
|
||||
|
||||
resendNode = node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).
|
||||
WithMetaLabel(text.NewInfoNodeResendOTP())
|
||||
|
||||
case flow.VerificationFlow:
|
||||
route = verification.RouteSubmitFlow
|
||||
codeMetaLabel = text.NewInfoNodeLabelVerificationCode()
|
||||
message = text.NewVerificationEmailWithCodeSent()
|
||||
|
||||
case flow.LoginFlow:
|
||||
route = login.RouteSubmitFlow
|
||||
codeMetaLabel = text.NewInfoNodeLabelLoginCode()
|
||||
// On refresh and second factor flows the recipient address is bound to
|
||||
// the authenticated identity, not typed by the user. Use a message that
|
||||
// reflects that — the standard "address you provided" / "check the
|
||||
// spelling" wording is inaccurate in those cases.
|
||||
if lf, ok := f.(*login.Flow); ok && (lf.Refresh || lf.RequestedAAL == identity.AuthenticatorAssuranceLevel2) {
|
||||
message = text.NewLoginCodeSentForAuthenticatedUser()
|
||||
} else {
|
||||
message = text.NewLoginCodeSent()
|
||||
}
|
||||
|
||||
// preserve the login identifier that was submitted
|
||||
// so we can retry the code flow with the same data
|
||||
for _, n := range f.GetUI().Nodes {
|
||||
if n.ID() == "identifier" || n.ID() == "address" {
|
||||
if input, ok := n.Attributes.(*node.InputAttributes); ok {
|
||||
input.Type = "hidden"
|
||||
n.Attributes = input
|
||||
input.Name = "identifier"
|
||||
}
|
||||
freshNodes = append(freshNodes, n)
|
||||
}
|
||||
}
|
||||
|
||||
resendNode = node.NewInputField("resend", "code", node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewInfoNodeResendOTP())
|
||||
|
||||
case flow.RegistrationFlow:
|
||||
route = registration.RouteSubmitFlow
|
||||
codeMetaLabel = text.NewInfoNodeLabelRegistrationCode()
|
||||
message = text.NewRegistrationEmailWithCodeSent()
|
||||
|
||||
// in the registration flow we need to preserve the trait fields that were submitted
|
||||
// so we can retry the code flow with the same data
|
||||
for _, n := range f.GetUI().Nodes {
|
||||
if t, ok := n.Attributes.(*node.InputAttributes); ok && t.Type == node.InputAttributeTypeSubmit {
|
||||
continue
|
||||
}
|
||||
|
||||
if n.Group == node.DefaultGroup {
|
||||
// we don't need the user to change the values here
|
||||
// for better UX let's make them disabled
|
||||
// when there are errors we won't hide the fields
|
||||
if len(n.Messages) == 0 {
|
||||
if input, ok := n.Attributes.(*node.InputAttributes); ok {
|
||||
input.Type = "hidden"
|
||||
n.Attributes = input
|
||||
}
|
||||
}
|
||||
freshNodes = append(freshNodes, n)
|
||||
}
|
||||
}
|
||||
|
||||
resendNode = nodeRegistrationResendNode()
|
||||
|
||||
// Insert a back button if we have a two-step registration screen, so that the
|
||||
// user can navigate back to the credential selection screen.
|
||||
if s.deps.Config().SelfServiceFlowRegistrationTwoSteps(ctx) {
|
||||
backNode = nodeRegistrationSelectCredentialsNode()
|
||||
}
|
||||
default:
|
||||
return errors.WithStack(herodot.ErrBadRequest().WithReason("received an unexpected flow type"))
|
||||
}
|
||||
|
||||
// Hidden field Required for the re-send code button
|
||||
// !!important!!: this field must be appended before the code submit button since upsert will replace the first node with the same name
|
||||
freshNodes.Upsert(nodeCodeInputFieldHidden())
|
||||
|
||||
// code input field
|
||||
freshNodes.Upsert(nodeCodeInputField().WithMetaLabel(codeMetaLabel))
|
||||
|
||||
// code submit button
|
||||
freshNodes.Append(nodeContinueButton())
|
||||
|
||||
if resendNode != nil {
|
||||
freshNodes.Append(resendNode)
|
||||
}
|
||||
|
||||
if backNode != nil {
|
||||
freshNodes.Append(backNode)
|
||||
}
|
||||
|
||||
f.GetUI().Nodes = freshNodes
|
||||
|
||||
f.GetUI().Method = "POST"
|
||||
f.GetUI().Action = flow.AppendFlowTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(ctx), route), f.GetID()).String()
|
||||
|
||||
f.GetUI().Messages.Set(message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCodeUINodes creates a fresh UI for the code flow.
|
||||
// this is used with the `recovery`, `verification`, `registration` and `login` flows.
|
||||
func (s *Strategy) NewCodeUINodes(r *http.Request, f flow.Flow, data any) error {
|
||||
if err := s.PopulateMethod(r, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix := "" // The login flow does not process traits
|
||||
if f.GetFlowName() == flow.RegistrationFlow {
|
||||
// The registration form does however
|
||||
prefix = "traits"
|
||||
}
|
||||
|
||||
cont, err := container.NewFromStruct("", node.CodeGroup, data, prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, n := range cont.Nodes {
|
||||
// we only set the value and not the whole field because we want to keep types from the initial form generation
|
||||
f.GetUI().GetNodes().SetValueAttribute(n.ID(), n.Attributes.GetValue())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) FindCodeAddresses(ctx context.Context, sess *session.Session, via string) (result []Address, _ error) {
|
||||
// The via parameter lets us hint at the OTP address to use for 2fa.
|
||||
if via == "" {
|
||||
addresses, found, err := FindCodeAddressCandidates(sess.Identity, s.deps.Config().SelfServiceCodeMethodMissingCredentialFallbackEnabled(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !found {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sort.SliceStable(addresses, func(i, j int) bool {
|
||||
return addresses[i].To < addresses[j].To && addresses[i].Via < addresses[j].Via
|
||||
})
|
||||
|
||||
return addresses, nil
|
||||
} else {
|
||||
value := gjson.GetBytes(sess.Identity.Traits, via).String()
|
||||
if value == "" {
|
||||
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("No value found for trait %s in the current identity.", via))
|
||||
}
|
||||
|
||||
// TODO Remove this normalization once the via parameter is deprecated.
|
||||
//
|
||||
// Here we need to normalize the via parameter to the actual address. This is necessary because otherwise
|
||||
// we won't find the address in the list of addresses.
|
||||
//
|
||||
// Since we don't know if the via parameter is an email address or a phone number, we need to normalize for both.
|
||||
value = x.GracefulNormalization(value)
|
||||
|
||||
addresses, found, err := FindCodeAddressCandidates(sess.Identity, s.deps.Config().SelfServiceCodeMethodMissingCredentialFallbackEnabled(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !found {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
address, found := lo.Find(addresses, func(item Address) bool {
|
||||
return item.To == value
|
||||
})
|
||||
if !found {
|
||||
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("You can only reference a trait that matches a verification email address in the via parameter, or a registered credential."))
|
||||
}
|
||||
|
||||
return []Address{address}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetDefaultFlowState(f flow.Flow, resend string) {
|
||||
// By Default the flow should be in the 'choose method' state.
|
||||
if f.GetState() == "" {
|
||||
f.SetState(flow.StateChooseMethod)
|
||||
}
|
||||
|
||||
if strings.EqualFold(resend, "code") {
|
||||
f.SetState(flow.StateChooseMethod)
|
||||
}
|
||||
}
|
||||
|
||||
const CodeLength = 6
|
||||
|
||||
func GenerateCode() string {
|
||||
return randx.MustString(CodeLength, randx.Numeric)
|
||||
}
|
||||
|
||||
// MaskAddress masks an address by replacing the middle part with asterisks.
|
||||
//
|
||||
// If the address contains an @, the part before the @ is masked by taking the first 2 characters and adding 4 *
|
||||
// (if the part before the @ is less than 2 characters the full value is used).
|
||||
// Otherwise, the first 3 characters and last two characters are taken and 4 * are added in between.
|
||||
//
|
||||
// Examples:
|
||||
// - foo@bar -> fo****@bar
|
||||
// - foobar -> fo****ar
|
||||
// - f@bar -> f@bar
|
||||
// - fo@bar -> fo****@bar
|
||||
// - +12345678910 -> +12****10
|
||||
func MaskAddress(input string) string {
|
||||
if strings.Contains(input, "@") {
|
||||
pre, post, found := strings.Cut(input, "@")
|
||||
if !found || len(pre) < 2 {
|
||||
return input
|
||||
}
|
||||
return pre[:2] + strings.Repeat("*", 4) + "@" + post
|
||||
}
|
||||
if len(input) < 6 {
|
||||
return input
|
||||
}
|
||||
return input[:3] + strings.Repeat("*", 4) + input[len(input)-2:]
|
||||
}
|
||||
Reference in New Issue
Block a user