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
254 lines
8.2 KiB
Go
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
|
|
}
|