Files
upstream-ory-kratos/selfservice/strategy/link/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

369 lines
14 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package link
import (
"context"
"encoding/json"
"net/http"
"net/url"
"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"
"github.com/ory/x/urlx"
)
func (s *Strategy) VerificationStrategyID() string {
return string(verification.VerificationStrategyLink)
}
func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.Flow) error {
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
f.UI.GetNodes().Upsert(
// v0.5: form.Field{Name: "email", Type: "email", Required: true}
node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
)
f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
return nil
}
type verificationSubmitPayload struct {
Method string `json:"method" form:"method"`
Token string `json:"token" form:"token"`
CSRFToken string `json:"csrf_token" form:"csrf_token"`
Flow string `json:"flow" form:"flow"`
Email string `json:"email" form:"email"`
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
}
func (s *Strategy) decodeVerification(r *http.Request) (*verificationSubmitPayload, error) {
var body verificationSubmitPayload
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 *verificationSubmitPayload, err error) error {
if f != nil {
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
email := ""
if body != nil {
email = body.Email
}
f.UI.GetNodes().Upsert(
// v0.5: form.Field{Name: "email", Type: "email", Required: true, Value: body.Body.Email}
node.NewInputField("email", email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
)
}
return err
}
// Update Verification Flow with Link Method
//
// swagger:model updateVerificationFlowWithLinkMethod
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type updateVerificationFlowWithLinkMethod struct {
// Email to Verify
//
// Needs to be set when initiating the flow. If the email is a registered
// verification email, a verification link will be sent. If the email is not known,
// a email with details on what happened will be sent instead.
//
// format: email
// required: true
Email string `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"`
// Transient data to pass along to any webhooks
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
}
func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verification.Flow) (err error) {
ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.link.Strategy.Verify")
span.SetAttributes(attribute.String("selfservice_flows_verification_use", s.d.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 len(body.Token) > 0 {
if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.VerificationStrategyID(), s.VerificationStrategyID(), s.d); err != nil {
return s.handleVerificationError(r, nil, body, err)
}
return s.verificationUseToken(ctx, w, r, body, f)
}
if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.VerificationStrategyID(), body.Method, s.d); err != nil {
return s.handleVerificationError(r, f, body, err)
}
if err := f.Valid(s.d.Clock()); err != nil {
return s.handleVerificationError(r, f, body, err)
}
switch f.State {
case flow.StateChooseMethod, flow.StateEmailSent:
return s.verificationHandleFormSubmission(ctx, r, f)
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) verificationHandleFormSubmission(ctx context.Context, r *http.Request, f *verification.Flow) error {
body, err := s.decodeVerification(r)
if err != nil {
return s.handleVerificationError(r, f, body, err)
}
if len(body.Email) == 0 {
return s.handleVerificationError(r, f, body, schema.NewRequiredError("#/email", "email"))
}
if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, body.CSRFToken); err != nil {
return s.handleVerificationError(r, f, body, err)
}
if err := s.d.LinkSender().SendVerificationLink(ctx, f, identity.AddressTypeEmail, body.Email); err != nil {
if !errors.Is(err, ErrUnknownAddress) {
return s.handleVerificationError(r, f, body, err)
}
// Continue execution
}
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
f.UI.GetNodes().Upsert(
// v0.5: form.Field{Name: "email", Type: "email", Required: true, Value: body.Body.Email}
node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
)
f.Active = sqlxx.NullString(s.NodeGroup())
f.State = flow.StateEmailSent
f.UI.Messages.Set(text.NewVerificationEmailSent())
if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil {
return s.handleVerificationError(r, f, body, err)
}
return nil
}
func (s *Strategy) verificationUseToken(ctx context.Context, w http.ResponseWriter, r *http.Request, body *verificationSubmitPayload, f *verification.Flow) error {
token, err := s.d.VerificationTokenPersister().UseVerificationToken(ctx, f.ID, body.Token)
if err != nil {
if errors.Is(err, sqlcon.ErrNoRows()) {
return s.retryVerificationFlowWithMessage(ctx, w, r, flow.TypeBrowser, text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed())
}
return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err)
}
if err := token.Valid(s.d.Clock()); err != nil {
return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err)
}
var identityID uuid.UUID
var addressVia string
var ptcIdentity *identity.Identity
if token.VerifiableAddress == nil {
ptc, ptcErr := s.d.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, f.ID)
if ptcErr != nil {
if errors.Is(ptcErr, sqlcon.ErrNoRows()) {
return s.retryVerificationFlowWithMessage(ctx, w, r, flow.TypeBrowser, text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed())
}
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, ptcErr)
}
updatedIdentity, applyErr := s.d.SettingsHookExecutor().ApplyPendingTraitsChange(ctx, w, r, ptc)
if applyErr != nil {
if errors.Is(applyErr, identity.ErrConcurrentModification) ||
errors.Is(applyErr, settings.ErrPendingTraitsChangeSessionInvalid) {
f.UI.Messages.Clear()
f.UI.Messages.Add(text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed())
if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil {
return s.retryVerificationFlowWithError(ctx, w, r, f.Type, err)
}
if x.IsBrowserRequest(r) {
http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther)
} else {
s.d.Writer().Write(w, r, f)
}
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 := token.VerifiableAddress
address.Verified = true
verifiedAt := sqlxx.NullTime(time.Now().UTC())
address.VerifiedAt = &verifiedAt
address.Status = identity.VerifiableAddressStatusCompleted
if err := s.d.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "verified", "verified_at", "status"); err != nil {
return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err)
}
addressVia = address.Via
identityID = address.IdentityID
}
var i *identity.Identity
if ptcIdentity != nil {
i = ptcIdentity
} else {
i, err = s.d.IdentityPool().GetIdentity(ctx, identityID, identity.ExpandDefault)
if err != nil {
return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err)
}
}
// Remainder is shared between both paths.
returnTo := f.ContinueURL(ctx, s.d.Config())
f.UI = &container.Container{
Method: "GET",
Action: returnTo.String(),
}
f.State = flow.StatePassedChallenge
// See https://github.com/ory/kratos/issues/1547
f.SetCSRFToken(flow.GetCSRFToken(s.d, 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.LinkGroup, text.NewInfoNodeLabelContinue()).
WithMetaLabel(text.NewInfoNodeLabelContinue()))
if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(ctx, f); err != nil {
return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err)
}
if err := s.d.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.d.Logger().WithRequest(r).WithField("message", message).Debug("A verification flow is being retried because a validation error occurred.")
f, err := verification.NewFlow(s.d,
s.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.d.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.d.VerificationFlowPersister().CreateVerificationFlow(ctx, f); err != nil {
return s.handleVerificationError(r, f, nil, err)
}
if ft == flow.TypeBrowser {
http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther)
} else {
http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(ctx),
verification.RouteGetFlow), url.Values{"id": {f.ID.String()}}).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.d.Logger().WithRequest(r).WithError(verErr).Debug("A verification flow is being retried because an error occurred.")
f, err := verification.NewFlow(s.d,
s.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.d.CSRFHandler().RegenerateToken(w, r), r, verification.Strategies{s}, ft)
if err != nil {
return s.handleVerificationError(r, f, nil, err)
}
if expired := new(flow.ExpiredError); errors.As(verErr, &expired) {
return s.retryVerificationFlowWithMessage(ctx, w, r, ft, text.NewErrorValidationVerificationFlowExpired(s.d.Clock(), expired.ExpiredAt))
} else {
if err := f.UI.ParseError(node.LinkGroup, verErr); err != nil {
return err
}
}
if err := s.d.VerificationFlowPersister().CreateVerificationFlow(ctx, f); err != nil {
return s.handleVerificationError(r, f, nil, err)
}
if ft == flow.TypeBrowser {
http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther)
} else {
http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(ctx),
verification.RouteGetFlow), url.Values{"id": {f.ID.String()}}).String(), http.StatusSeeOther)
}
return errors.WithStack(flow.ErrCompletedByStrategy)
}
func (s *Strategy) SendVerificationCode(ctx context.Context, f *verification.Flow, i *identity.Identity, addressLike identity.VerifiableAddressLike) error {
token := NewSelfServiceVerificationToken(addressLike, f, s.d.Config().SelfServiceLinkMethodLifespan(ctx))
if err := s.d.VerificationTokenPersister().CreateVerificationToken(ctx, token); err != nil {
return err
}
return s.d.LinkSender().SendVerificationTokenTo(ctx, f, i, addressLike, token)
}