Files
upstream-ory-kratos/selfservice/strategy/code/strategy_verification.go
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

493 lines
18 KiB
Go

// 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)
}