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
598 lines
19 KiB
Go
598 lines
19 KiB
Go
// 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:]
|
|
}
|