Files
upstream-ory-kratos/selfservice/hook/verify_new_address.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

254 lines
8.2 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hook
import (
"context"
"encoding/json"
"net/http"
"slices"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/tidwall/sjson"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/selfservice/flow/verification"
"github.com/ory/kratos/session"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/x/clock"
"github.com/ory/x/httpx"
"github.com/ory/x/otelx"
"github.com/ory/x/sqlcon"
)
var _ settings.PostHookPrePersistExecutor = new(VerifyNewAddress)
type (
verifyNewAddressDependencies interface {
clock.Provider
config.Provider
nosurfx.CSRFTokenGeneratorProvider
nosurfx.CSRFProvider
verification.StrategyProvider
verification.FlowPersistenceProvider
identity.PrivilegedPoolProvider
identity.PendingTraitsChangePersistenceProvider
settings.FlowPersistenceProvider
httpx.WriterProvider
otelx.Provider
session.ManagementProvider
}
VerifyNewAddressProvider interface {
HookVerifyNewAddress() *VerifyNewAddress
}
VerifyNewAddress struct {
r verifyNewAddressDependencies
}
)
func NewVerifyNewAddress(r verifyNewAddressDependencies) *VerifyNewAddress {
return &VerifyNewAddress{r: r}
}
func (e *VerifyNewAddress) ExecuteSettingsPrePersistHook(
w http.ResponseWriter, r *http.Request,
params settings.PostHookPrePersistExecutorParams,
) error {
return otelx.WithSpan(r.Context(), "selfservice.hook.VerifyNewAddress.ExecuteSettingsPrePersistHook", func(ctx context.Context) error {
return e.execute(ctx, w, r, params.Flow, params.Identity, params.Session)
})
}
func (e *VerifyNewAddress) execute(
ctx context.Context, w http.ResponseWriter, r *http.Request,
f *settings.Flow, proposed *identity.Identity, s *session.Session,
) error {
original := s.Identity
if original == nil {
return nil
}
// Find verifiable addresses that changed between original and proposed.
changed := findChangedAddresses(original.VerifiableAddresses, proposed.VerifiableAddresses)
if len(changed) == 0 {
return nil // No address changes — let the flow proceed normally.
}
// At this point, we know the user is trying to change an address. We need to check that the session is currently privileged.
if !e.r.SessionManager().IsPrivileged(ctx, s) {
return errors.WithStack(settings.NewFlowNeedsReAuth())
}
// Only one address change can be verified at a time. Reject if multiple
// addresses changed to prevent unverified address changes from being applied.
if len(changed) > 1 {
f.UI.Messages.Clear()
f.UI.Messages.Add(text.NewErrorValidationSettingsTooManyAddressChanges())
if err := e.r.SettingsFlowPersister().UpdateSettingsFlow(ctx, f); err != nil {
return err
}
if x.IsJSONRequest(r) {
e.r.Writer().Write(w, r, f)
return errors.WithStack(settings.ErrHookAbortFlow)
}
http.Redirect(w, r, f.AppendTo(e.r.Config().SelfServiceFlowSettingsUI(ctx)).String(), http.StatusSeeOther)
return errors.WithStack(settings.ErrHookAbortFlow)
}
// Process the first changed address (we can only redirect to one verification flow).
addr := changed[0]
// Reject if the new address already belongs to a different identity
existingAddr, err := e.r.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, addr.Via, addr.Value)
if err != nil && !errors.Is(err, sqlcon.ErrNoRows()) {
return err
}
if err == nil && existingAddr.IdentityID != original.ID {
f.UI.Messages.Clear()
f.UI.Messages.Add(text.NewErrorValidationDuplicateCredentials())
if err := e.r.SettingsFlowPersister().UpdateSettingsFlow(ctx, f); err != nil {
return err
}
if x.IsJSONRequest(r) {
e.r.Writer().WriteCode(w, r, http.StatusBadRequest, f)
return errors.WithStack(settings.ErrHookAbortFlow)
}
http.Redirect(w, r, f.AppendTo(e.r.Config().SelfServiceFlowSettingsUI(ctx)).String(), http.StatusSeeOther)
return errors.WithStack(settings.ErrHookAbortFlow)
}
// Delete any existing pending changes for this identity.
if err := e.r.PendingTraitsChangePersister().DeletePendingTraitsChangesByIdentity(ctx, original.ID); err != nil {
return err
}
// Create the verification flow.
strategies, primaryStrategy, err := e.r.GetActiveVerificationStrategies(ctx)
if err != nil {
return err
}
var csrf string
if f.Type == flow.TypeBrowser {
csrf = e.r.GenerateCSRFToken(r)
}
verificationFlow, err := verification.NewPostHookFlow(e.r,
e.r.Config().SelfServiceFlowVerificationRequestLifespan(ctx),
csrf, r, strategies, f)
if err != nil {
return err
}
verificationFlow.State = flow.StateEmailSent
if err := primaryStrategy.PopulateVerificationMethod(r, verificationFlow); err != nil {
return err
}
// The code strategy's PopulateVerificationMethod sets the email "code sent"
// message unconditionally; override it with the message that matches the
// channel the code was sent over. The legacy link strategy sends a link,
// not a code, and sets no message — leave it untouched.
if primaryStrategy.NodeGroup() == node.CodeGroup {
verificationFlow.UI.Messages.Set(text.VerificationCodeSentMessage(addr.Via))
}
verificationFlow.UI.Nodes.Append(
node.NewInputField(addr.Via, addr.Value, node.CodeGroup, node.InputAttributeTypeSubmit).
WithMetaLabel(text.NewInfoNodeResendOTP()),
)
if err := e.r.VerificationFlowPersister().CreateVerificationFlow(ctx, verificationFlow); err != nil {
return err
}
// Create PendingTraitsChange record.
sessionID := s.ID
originFlowID := f.ID
ptc := &identity.PendingTraitsChange{
IdentityID: original.ID,
IdentityRegion: original.Region,
SessionID: uuid.NullUUID{UUID: sessionID, Valid: true},
OriginSettingsFlowID: uuid.NullUUID{UUID: originFlowID, Valid: true},
NewAddressValue: addr.Value,
NewAddressVia: addr.Via,
OriginalTraitsHash: identity.HashTraits(json.RawMessage(original.Traits)),
ProposedTraits: json.RawMessage(proposed.Traits),
VerificationFlowID: verificationFlow.ID,
Status: identity.PendingTraitsChangeStatusPending,
}
if err := e.r.PendingTraitsChangePersister().CreatePendingTraitsChange(ctx, ptc); err != nil {
return err
}
// Send verification code to the new address.
if err := primaryStrategy.SendVerificationCode(ctx, verificationFlow, proposed, ptc); err != nil {
return err
}
// Store verification flow reference in InternalContext (for show_verification_ui).
flowURL := ""
if verificationFlow.Type == flow.TypeBrowser {
flowURL = verificationFlow.AppendTo(e.r.Config().SelfServiceFlowVerificationUI(ctx)).String()
}
continueWith := flow.NewContinueWithVerificationUI(verificationFlow.ID, addr.Value, flowURL)
internalContext, err := sjson.SetBytes(f.GetInternalContext(), InternalContextRegistrationVerificationFlow, continueWith.Flow)
if err != nil {
return err
}
f.SetInternalContext(internalContext)
f.AddContinueWith(continueWith)
// Persist the updated settings flow (with InternalContext/ContinueWith).
if err := e.r.SettingsFlowPersister().UpdateSettingsFlow(ctx, f); err != nil {
return err
}
// Write the HTTP response and abort the flow.
if x.IsJSONRequest(r) || verificationFlow.Type == flow.TypeAPI {
e.r.Writer().Write(w, r, f)
return errors.WithStack(settings.ErrHookAbortFlow)
}
if flowURL != "" {
http.Redirect(w, r, flowURL, http.StatusSeeOther)
return errors.WithStack(settings.ErrHookAbortFlow)
}
// This should not happen: we only end up here if, the flow is an API flow, but
return errors.WithStack(settings.ErrHookAbortFlow)
}
type changedAddress struct {
Value string
Via string
}
// findChangedAddresses compares original and proposed verifiable addresses and
// returns addresses that are new or have a different value.
func findChangedAddresses(original, proposed []identity.VerifiableAddress) []changedAddress {
origByVia := make(map[string][]string, len(original))
for _, a := range original {
origByVia[a.Via] = append(origByVia[a.Via], a.Value)
}
var changed []changedAddress
for _, p := range proposed {
if !slices.Contains(origByVia[p.Via], p.Value) {
changed = append(changed, changedAddress{Value: p.Value, Via: p.Via})
}
}
return changed
}