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:
492
selfservice/strategy/code/strategy_verification.go
Normal file
492
selfservice/strategy/code/strategy_verification.go
Normal file
@@ -0,0 +1,492 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package code
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/schema"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/selfservice/flow/settings"
|
||||
"github.com/ory/kratos/selfservice/flow/verification"
|
||||
"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/x/decoderx"
|
||||
"github.com/ory/x/otelx"
|
||||
"github.com/ory/x/sqlcon"
|
||||
"github.com/ory/x/sqlxx"
|
||||
)
|
||||
|
||||
func (s *Strategy) VerificationStrategyID() string {
|
||||
return string(verification.VerificationStrategyCode)
|
||||
}
|
||||
|
||||
// PopulateVerificationMethod set's the appropriate UI nodes on this flow
|
||||
//
|
||||
// If the flow's state is `sent_email`, the `code` input and the success notification is set
|
||||
// Otherwise, the default email input is added.
|
||||
// If the flow is a browser flow, the CSRF token is added to the UI.
|
||||
func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.Flow) error {
|
||||
return s.PopulateMethod(r, f)
|
||||
}
|
||||
|
||||
func (s *Strategy) decodeVerification(r *http.Request) (*updateVerificationFlowWithCodeMethod, error) {
|
||||
var body updateVerificationFlowWithCodeMethod
|
||||
|
||||
compiler, err := decoderx.HTTPRawJSONSchemaCompiler(verificationMethodSchema)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := decoderx.Decode(r, &body, compiler,
|
||||
decoderx.HTTPDecoderUseQueryAndBody(),
|
||||
decoderx.HTTPKeepRequestBody(true),
|
||||
decoderx.HTTPDecoderAllowedMethods("POST", "GET"),
|
||||
decoderx.HTTPDecoderSetValidatePayloads(true),
|
||||
decoderx.HTTPDecoderJSONFollowsFormFormat(),
|
||||
); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &body, nil
|
||||
}
|
||||
|
||||
// handleVerificationError is a convenience function for handling all types of errors that may occur (e.g. validation error).
|
||||
func (s *Strategy) handleVerificationError(r *http.Request, f *verification.Flow, body *updateVerificationFlowWithCodeMethod, err error) error {
|
||||
if f != nil {
|
||||
f.UI.SetCSRF(s.deps.GenerateCSRFToken(r))
|
||||
email := ""
|
||||
if body != nil {
|
||||
email = body.Email
|
||||
}
|
||||
label := text.NewInfoNodeInputEmail()
|
||||
inputType := node.InputAttributeTypeEmail
|
||||
if channels, chErr := s.deps.Config().CourierChannels(r.Context()); chErr == nil {
|
||||
for _, ch := range channels {
|
||||
if ch.ID == "sms" {
|
||||
label = text.NewInfoNodeInputEmailOrPhone()
|
||||
inputType = node.InputAttributeTypeText
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
f.UI.GetNodes().Upsert(
|
||||
node.NewInputField("email", email, node.CodeGroup, inputType, node.WithRequiredInputAttribute).WithMetaLabel(label),
|
||||
)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// swagger:model updateVerificationFlowWithCodeMethod
|
||||
type updateVerificationFlowWithCodeMethod struct {
|
||||
// The email address or phone number to verify
|
||||
//
|
||||
// If the provided address belongs to a valid account, a verification email or SMS will be sent.
|
||||
//
|
||||
// If you want to notify the email address if the account does not exist, see
|
||||
// the [notify_unknown_recipients flag](https://www.ory.com/docs/kratos/self-service/flows/verify-email-account-activation#attempted-verification-notifications)
|
||||
//
|
||||
// If a code was already sent, including this field in the payload will invalidate the sent code and re-send a new code.
|
||||
//
|
||||
// required: false
|
||||
Email string `form:"email" json:"email"`
|
||||
|
||||
// Sending the anti-csrf token is only required for browser login flows.
|
||||
CSRFToken string `form:"csrf_token" json:"csrf_token"`
|
||||
|
||||
// Method is the method that should be used for this verification flow
|
||||
//
|
||||
// Allowed values are `link` and `code`.
|
||||
//
|
||||
// required: true
|
||||
Method verification.VerificationStrategy `json:"method"`
|
||||
|
||||
// Code from the recovery email
|
||||
//
|
||||
// If you want to submit a code, use this field, but make sure to _not_ include the email field, as well.
|
||||
//
|
||||
// required: false
|
||||
Code string `json:"code" form:"code"`
|
||||
|
||||
// The id of the flow
|
||||
Flow string `json:"-" form:"-"`
|
||||
|
||||
// Transient data to pass along to any webhooks
|
||||
//
|
||||
// required: false
|
||||
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
|
||||
}
|
||||
|
||||
// getMethod returns the method of this submission or "" if no method could be found
|
||||
func (body *updateVerificationFlowWithCodeMethod) getMethod() verification.VerificationStrategy {
|
||||
if body.Method != "" {
|
||||
return body.Method
|
||||
}
|
||||
if body.Code != "" {
|
||||
return verification.VerificationStrategyCode
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verification.Flow) (err error) {
|
||||
ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.Strategy.Verify")
|
||||
span.SetAttributes(attribute.String("selfservice_flows_verification_use", s.deps.Config().SelfServiceFlowVerificationUse(ctx)))
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
body, err := s.decodeVerification(r)
|
||||
if err != nil {
|
||||
return s.handleVerificationError(r, nil, body, err)
|
||||
}
|
||||
|
||||
f.TransientPayload = body.TransientPayload
|
||||
|
||||
if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.VerificationStrategyID(), string(body.getMethod()), s.deps); err != nil {
|
||||
return s.handleVerificationError(r, f, body, err)
|
||||
}
|
||||
|
||||
if err := f.Valid(s.deps.Clock()); err != nil {
|
||||
return s.handleVerificationError(r, f, body, err)
|
||||
}
|
||||
|
||||
switch f.State {
|
||||
case flow.StateChooseMethod:
|
||||
fallthrough
|
||||
case flow.StateEmailSent:
|
||||
return s.verificationHandleFormSubmission(ctx, w, r, f, body)
|
||||
case flow.StatePassedChallenge:
|
||||
return s.retryVerificationFlowWithMessage(ctx, w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess())
|
||||
default:
|
||||
return s.retryVerificationFlowWithMessage(ctx, w, r, f.Type, text.NewErrorValidationVerificationStateFailure())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Strategy) handleLinkClick(ctx context.Context, w http.ResponseWriter, r *http.Request, f *verification.Flow, code string) error {
|
||||
// Pre-fill the code
|
||||
if codeField := f.UI.Nodes.Find("code"); codeField != nil {
|
||||
codeField.Attributes.SetValue(code)
|
||||
}
|
||||
|
||||
// In the verification flow, we can't enforce CSRF if the flow is opened from an email, so we initialize the CSRF
|
||||
// token here, so all subsequent interactions are protected
|
||||
csrfToken := s.deps.CSRFHandler().RegenerateToken(w, r)
|
||||
f.UI.SetCSRF(csrfToken)
|
||||
f.CSRFToken = csrfToken
|
||||
|
||||
if err := s.deps.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// we always redirect to the browser UI here to allow API flows to complete aswell
|
||||
// TODO: In the future, we might want to redirect to a custom URI scheme here, to allow to open an app on the device of
|
||||
// the user to handle the flow directly.
|
||||
http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther)
|
||||
|
||||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
}
|
||||
|
||||
func (s *Strategy) verificationHandleFormSubmission(ctx context.Context, w http.ResponseWriter, r *http.Request, f *verification.Flow, body *updateVerificationFlowWithCodeMethod) error {
|
||||
if len(body.Code) > 0 {
|
||||
if r.Method == http.MethodGet {
|
||||
// Special case: in the code strategy we send out links as well, that contain the code
|
||||
return s.handleLinkClick(ctx, w, r, f, body.Code)
|
||||
}
|
||||
|
||||
// If not GET: try to use the submitted code
|
||||
return s.verificationUseCode(ctx, w, r, body.Code, f)
|
||||
} else if len(body.Email) == 0 {
|
||||
// If no code and no email was provided, fail with a validation error
|
||||
return s.handleVerificationError(r, f, body, schema.NewRequiredError("#/email", "email"))
|
||||
}
|
||||
|
||||
if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, body.CSRFToken); err != nil {
|
||||
return s.handleVerificationError(r, f, body, err)
|
||||
}
|
||||
|
||||
if err := s.deps.VerificationCodePersister().DeleteVerificationCodesOfFlow(ctx, f.ID); err != nil {
|
||||
return s.handleVerificationError(r, f, body, err)
|
||||
}
|
||||
|
||||
via := hackyInferChannel(body.Email)
|
||||
|
||||
ptc, ptcErr := s.deps.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, f.ID)
|
||||
if ptcErr != nil {
|
||||
if !errors.Is(ptcErr, sqlcon.ErrNoRows()) {
|
||||
return s.handleVerificationError(r, f, body, ptcErr)
|
||||
}
|
||||
if err := s.deps.CodeSender().SendVerificationCode(ctx, f, via, body.Email); err != nil {
|
||||
if !errors.Is(err, ErrUnknownAddress()) {
|
||||
return s.handleVerificationError(r, f, body, err)
|
||||
}
|
||||
// Continue execution
|
||||
}
|
||||
} else {
|
||||
// Re-check whether the proposed address is still unclaimed before resending.
|
||||
// Another identity may have claimed it between the failed code submission and this resend.
|
||||
existingAddr, addrErr := s.deps.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, ptc.NewAddressVia, ptc.NewAddressValue)
|
||||
if addrErr != nil && !errors.Is(addrErr, sqlcon.ErrNoRows()) {
|
||||
return s.handleVerificationError(r, f, body, addrErr)
|
||||
}
|
||||
if addrErr == nil && existingAddr.IdentityID != ptc.IdentityID {
|
||||
if err := s.updateVerificationFlowWithMessage(ctx, w, r, f, text.NewErrorValidationDuplicateCredentials(), http.StatusBadRequest); err != nil {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err)
|
||||
}
|
||||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
}
|
||||
|
||||
proposedIdentity, err := s.deps.IdentityPool().GetIdentity(ctx, ptc.IdentityID, identity.ExpandDefault)
|
||||
if err != nil {
|
||||
return s.handleVerificationError(r, f, body, err)
|
||||
}
|
||||
if err := s.SendVerificationCode(ctx, f, proposedIdentity, ptc); err != nil {
|
||||
return s.handleVerificationError(r, f, body, err)
|
||||
}
|
||||
}
|
||||
|
||||
f.State = flow.StateEmailSent
|
||||
|
||||
if err := s.PopulateVerificationMethod(r, f); err != nil {
|
||||
return s.handleVerificationError(r, f, body, err)
|
||||
}
|
||||
|
||||
// PopulateVerificationMethod sets the email "code sent" message unconditionally.
|
||||
// Override it with the message that matches the channel the code was sent over.
|
||||
f.UI.Messages.Set(text.VerificationCodeSentMessage(via))
|
||||
|
||||
if body.Email != "" {
|
||||
f.UI.Nodes.Append(
|
||||
node.NewInputField("email", body.Email, node.CodeGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(text.NewInfoNodeResendOTP()),
|
||||
)
|
||||
}
|
||||
|
||||
if err := s.deps.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil {
|
||||
return s.handleVerificationError(r, f, body, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) updateVerificationFlowWithMessage(ctx context.Context, w http.ResponseWriter, r *http.Request, f *verification.Flow, message *text.Message, statusCode int) error {
|
||||
f.UI.Messages.Clear()
|
||||
f.UI.Messages.Add(message)
|
||||
|
||||
if err := s.deps.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if x.IsBrowserRequest(r) {
|
||||
http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther)
|
||||
} else {
|
||||
s.deps.Writer().WriteCode(w, r, statusCode, f)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) verificationUseCode(ctx context.Context, w http.ResponseWriter, r *http.Request, codeString string, f *verification.Flow) error {
|
||||
code, err := s.deps.VerificationCodePersister().UseVerificationCode(ctx, f.ID, codeString)
|
||||
if errors.Is(err, ErrCodeNotFound()) {
|
||||
if err := s.updateVerificationFlowWithMessage(ctx, w, r, f, text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(), http.StatusOK); err != nil {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err)
|
||||
}
|
||||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
} else if err != nil {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err)
|
||||
}
|
||||
|
||||
var identityID uuid.UUID
|
||||
var addressVia string
|
||||
var ptcIdentity *identity.Identity
|
||||
|
||||
if code.VerifiableAddress == nil {
|
||||
// Pending-change path: no persisted address — look up by verification flow ID.
|
||||
ptc, ptcErr := s.deps.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, f.ID)
|
||||
if ptcErr != nil {
|
||||
if !errors.Is(ptcErr, sqlcon.ErrNoRows()) {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, ptcErr)
|
||||
}
|
||||
// PTC vanished between code issuance and use — show the standard
|
||||
// "invalid or already used" message.
|
||||
if err := s.updateVerificationFlowWithMessage(ctx, w, r, f, text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(), http.StatusOK); err != nil {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err)
|
||||
}
|
||||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
}
|
||||
|
||||
// Security note: the verification flow is unauthenticated by design. The
|
||||
// user clicks a link from email/SMS. The pending change is bound to the
|
||||
// identity via IdentityID, but we cannot validate session ownership at
|
||||
// completion time. The security model relies on:
|
||||
// 1. The verification code is sent to the *new* address (attacker needs inbox access).
|
||||
// 2. The verification flow has a TTL (enforced by the flow handler).
|
||||
// 3. The one-time code is consumed on use (cannot be replayed).
|
||||
// Creation of pending changes is gated by a privileged session check;
|
||||
// ApplyPendingTraitsChange revalidates the session atomically with the apply.
|
||||
updatedIdentity, applyErr := s.deps.SettingsHookExecutor().ApplyPendingTraitsChange(ctx, w, r, ptc)
|
||||
if applyErr != nil {
|
||||
if errors.Is(applyErr, identity.ErrConcurrentModification) ||
|
||||
errors.Is(applyErr, settings.ErrPendingTraitsChangeSessionInvalid) {
|
||||
if err := s.updateVerificationFlowWithMessage(ctx, w, r, f, text.NewErrorValidationVerificationCodeInvalidOrAlreadyUsed(), http.StatusOK); err != nil {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err)
|
||||
}
|
||||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
}
|
||||
if errors.Is(applyErr, sqlcon.ErrUniqueViolation()) {
|
||||
if err := s.updateVerificationFlowWithMessage(ctx, w, r, f, text.NewErrorValidationDuplicateCredentials(), http.StatusBadRequest); err != nil {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err)
|
||||
}
|
||||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
}
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, applyErr)
|
||||
}
|
||||
|
||||
ptcIdentity = updatedIdentity
|
||||
addressVia = ptc.NewAddressVia
|
||||
identityID = ptc.IdentityID
|
||||
} else {
|
||||
// Normal path: update the existing verifiable address directly.
|
||||
address := code.VerifiableAddress
|
||||
address.Verified = true
|
||||
verifiedAt := sqlxx.NullTime(time.Now().UTC())
|
||||
address.VerifiedAt = &verifiedAt
|
||||
address.Status = identity.VerifiableAddressStatusCompleted
|
||||
if err := s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "verified", "verified_at", "status"); err != nil {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err)
|
||||
}
|
||||
|
||||
addressVia = address.Via
|
||||
identityID = code.VerifiableAddress.IdentityID
|
||||
}
|
||||
|
||||
var i *identity.Identity
|
||||
if ptcIdentity != nil {
|
||||
i = ptcIdentity
|
||||
} else {
|
||||
i, err = s.deps.IdentityPool().GetIdentity(ctx, identityID, identity.ExpandDefault)
|
||||
if err != nil {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Remainder is shared between both paths.
|
||||
returnTo := f.ContinueURL(ctx, s.deps.Config())
|
||||
|
||||
f.UI = &container.Container{
|
||||
Method: "GET",
|
||||
Action: returnTo.String(),
|
||||
}
|
||||
|
||||
f.State = flow.StatePassedChallenge
|
||||
f.SetCSRFToken(flow.GetCSRFToken(s.deps, w, r, f.Type))
|
||||
if addressVia == identity.AddressTypeSMS {
|
||||
f.UI.Messages.Set(text.NewInfoSelfServiceVerificationPhoneSuccessful())
|
||||
} else {
|
||||
f.UI.Messages.Set(text.NewInfoSelfServiceVerificationSuccessful())
|
||||
}
|
||||
f.UI.Nodes.Append(node.NewAnchorField("continue", returnTo.String(), node.CodeGroup, text.NewInfoNodeLabelContinue()).
|
||||
WithMetaLabel(text.NewInfoNodeLabelContinue()))
|
||||
|
||||
if err := s.deps.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err)
|
||||
}
|
||||
|
||||
if err := s.deps.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil {
|
||||
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) retryVerificationFlowWithMessage(ctx context.Context, w http.ResponseWriter, r *http.Request, ft flow.Type, message *text.Message) error {
|
||||
s.deps.
|
||||
Logger().
|
||||
WithRequest(r).
|
||||
WithField("message", message).
|
||||
Debug("A verification flow is being retried because a validation error occurred.")
|
||||
|
||||
f, err := verification.NewFlow(s.deps,
|
||||
s.deps.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.deps.CSRFHandler().RegenerateToken(w, r), r, verification.Strategies{s}, ft)
|
||||
if err != nil {
|
||||
return s.handleVerificationError(r, f, nil, err)
|
||||
}
|
||||
|
||||
f.UI.Messages.Add(message)
|
||||
|
||||
if err := s.deps.VerificationFlowPersister().CreateVerificationFlow(ctx, f); err != nil {
|
||||
return s.handleVerificationError(r, f, nil, err)
|
||||
}
|
||||
|
||||
if x.IsJSONRequest(r) {
|
||||
s.deps.Writer().WriteError(w, r, flow.NewFlowReplacedError(text.NewErrorSystemGeneric("An error occured, please use the new flow.")).WithFlow(f))
|
||||
} else {
|
||||
http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
}
|
||||
|
||||
func (s *Strategy) retryVerificationFlowWithError(ctx context.Context, w http.ResponseWriter, r *http.Request, ft flow.Type, verErr error) error {
|
||||
s.deps.
|
||||
Logger().
|
||||
WithRequest(r).
|
||||
WithError(verErr).
|
||||
Debug("A verification flow is being retried because an error occurred.")
|
||||
|
||||
f, err := verification.NewFlow(s.deps,
|
||||
s.deps.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.deps.CSRFHandler().RegenerateToken(w, r), r, verification.Strategies{s}, ft)
|
||||
if err != nil {
|
||||
return s.handleVerificationError(r, f, nil, err)
|
||||
}
|
||||
|
||||
var toReturn error
|
||||
|
||||
if expired := new(flow.ExpiredError); errors.As(verErr, &expired) {
|
||||
f.UI.Messages.Add(text.NewErrorValidationVerificationFlowExpired(s.deps.Clock(), expired.ExpiredAt))
|
||||
toReturn = expired.WithFlow(f)
|
||||
} else if err := f.UI.ParseError(node.LinkGroup, verErr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.deps.VerificationFlowPersister().CreateVerificationFlow(ctx, f); err != nil {
|
||||
return s.handleVerificationError(r, f, nil, err)
|
||||
}
|
||||
|
||||
if x.IsJSONRequest(r) {
|
||||
if toReturn == nil {
|
||||
toReturn = flow.NewFlowReplacedError(text.NewErrorSystemGeneric("An error occured, please retry the flow.")).
|
||||
WithFlow(f)
|
||||
}
|
||||
s.deps.Writer().WriteError(w, r, toReturn)
|
||||
} else {
|
||||
http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
return errors.WithStack(flow.ErrCompletedByStrategy)
|
||||
}
|
||||
|
||||
func (s *Strategy) SendVerificationCode(ctx context.Context, f *verification.Flow, i *identity.Identity, a identity.VerifiableAddressLike) (err error) {
|
||||
rawCode := GenerateCode()
|
||||
|
||||
if _, err = s.deps.VerificationCodePersister().CreateVerificationCode(ctx, &CreateVerificationCodeParams{
|
||||
RawCode: rawCode,
|
||||
ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx),
|
||||
VerifiableAddress: a,
|
||||
FlowID: f.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.deps.CodeSender().SendVerificationCodeTo(ctx, f, i, rawCode, a)
|
||||
}
|
||||
Reference in New Issue
Block a user