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:
253
selfservice/hook/verify_new_address.go
Normal file
253
selfservice/hook/verify_new_address.go
Normal file
@@ -0,0 +1,253 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user