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

397 lines
14 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package registration
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/kratos/x/redir"
"github.com/pkg/errors"
"go.opentelemetry.io/otel/attribute"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/hydra"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/sessiontokenexchange"
"github.com/ory/kratos/session"
"github.com/ory/kratos/ui/container"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/events"
"github.com/ory/x/httpx"
"github.com/ory/x/logrusx"
"github.com/ory/x/otelx"
"github.com/ory/x/sqlcon"
)
type (
PreHookExecutor interface {
ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error
}
PreHookExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow) error
PostHookPostPersistExecutor interface {
ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error
}
PostHookPostPersistExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error
PostHookPrePersistExecutor interface {
ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, a *Flow, i *identity.Identity) error
}
PostHookPrePersistExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow, i *identity.Identity) error
HooksProvider interface {
PreRegistrationHooks(ctx context.Context) ([]PreHookExecutor, error)
PostRegistrationPrePersistHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]PostHookPrePersistExecutor, error)
PostRegistrationPostPersistHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]PostHookPostPersistExecutor, error)
}
)
func ExecutorNames[T any](e []T) []string {
names := make([]string, len(e))
for k, ee := range e {
names[k] = fmt.Sprintf("%T", ee)
}
return names
}
func (f PreHookExecutorFunc) ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error {
return f(w, r, a)
}
func (f PostHookPostPersistExecutorFunc) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error {
return f(w, r, a, s)
}
func (f PostHookPrePersistExecutorFunc) ExecutePostRegistrationPrePersistHook(w http.ResponseWriter, r *http.Request, a *Flow, i *identity.Identity) error {
return f(w, r, a, i)
}
type (
executorDependencies interface {
config.Provider
identity.ManagementProvider
identity.PrivilegedPoolProvider
identity.ValidationProvider
login.FlowPersistenceProvider
login.StrategyProvider
session.PersistenceProvider
session.ManagementProvider
HooksProvider
FlowPersistenceProvider
hydra.Provider
nosurfx.CSRFTokenGeneratorProvider
httpx.ClientProvider
logrusx.Provider
httpx.WriterProvider
otelx.Provider
sessiontokenexchange.PersistenceProvider
}
HookExecutor struct {
d executorDependencies
}
HookExecutorProvider interface {
RegistrationExecutor() *HookExecutor
}
)
func NewHookExecutor(d executorDependencies) *HookExecutor {
return &HookExecutor{d: d}
}
// PostRegistrationHook finalizes a successful registration. The
// authMethod argument describes how the user authenticated during
// registration and is recorded on the resulting session verbatim. For
// OIDC registrations it carries the upstream acr/amr claims and the
// provider-computed AAL; for other strategies it is typically
// `{Method: strategy.ID(), AAL: AuthenticatorAssuranceLevel1}`.
func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Request, registrationFlow *Flow, i *identity.Identity, authMethod session.AuthenticationMethod) (err error) {
ct := authMethod.Method
ctx := r.Context()
ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "HookExecutor.PostRegistrationHook")
r = r.WithContext(ctx)
defer otelx.End(span, &err)
e.d.Logger().
WithRequest(r).
WithField("identity_id", i.ID).
WithField("flow_method", ct).
Debug("Running PostRegistrationPrePersistHooks.")
preHooks, err := e.d.PostRegistrationPrePersistHooks(ctx, ct)
if err != nil {
return err
}
for k, executor := range preHooks {
if err := executor.ExecutePostRegistrationPrePersistHook(w, r, registrationFlow, i); err != nil {
if errors.Is(err, ErrHookAbortFlow) {
e.d.Logger().
WithRequest(r).
WithField("executor", fmt.Sprintf("%T", executor)).
WithField("executor_position", k).
WithField("executors", ExecutorNames(preHooks)).
WithField("identity_id", i.ID).
WithField("flow_method", ct).
Debug("A ExecutePostRegistrationPrePersistHook hook aborted early.")
return nil
}
e.d.Logger().
WithRequest(r).
WithField("executor", fmt.Sprintf("%T", executor)).
WithField("executor_position", k).
WithField("executors", ExecutorNames(preHooks)).
WithField("identity_id", i.ID).
WithField("flow_method", ct).
WithError(err).
Error("ExecutePostRegistrationPostPersistHook hook failed with an error.")
traits := i.Traits
return flow.HandleHookError(w, r, registrationFlow, traits, ct.ToUiNodeGroup(), err, e.d, e.d)
}
e.d.Logger().WithRequest(r).
WithField("executor", fmt.Sprintf("%T", executor)).
WithField("executor_position", k).
WithField("executors", ExecutorNames(preHooks)).
WithField("identity_id", i.ID).
WithField("flow_method", ct).
Debug("ExecutePostRegistrationPrePersistHook completed successfully.")
}
// We need to make sure that the identity has a valid schema before passing it down to the identity pool.
if err := e.d.IdentityValidator().Validate(ctx, i); err != nil {
return err
}
if registrationFlow.IsAccountLinking() && !e.d.Config().SelfServiceFlowRegistrationEnabled(ctx) {
// This flow was created internally from a login flow to link a new
// credential to an existing identity while registration is disabled.
// It must never create a new identity, so require that a conflicting
// identity exists before attempting to persist. The create below then
// fails with a unique violation, which triggers the account linking
// conversion back to a login flow.
if _, _, _, err := e.d.IdentityManager().ConflictingIdentity(ctx, i); err != nil {
if errors.Is(err, sqlcon.ErrNoRows()) {
return errors.WithStack(ErrRegistrationDisabled())
}
return err
}
}
// We're now creating the identity because any of the hooks could trigger a "redirect" or a "session" which
// would imply that the identity has to exist already.
if err := e.d.IdentityManager().Create(ctx, i); err != nil {
if errors.Is(err, sqlcon.ErrUniqueViolation()) {
strategy, err := e.d.AllLoginStrategies().Strategy(ct)
if err != nil {
return err
}
if strategy, ok := strategy.(login.LinkableStrategy); ok {
duplicateIdentifier, err := e.getDuplicateIdentifier(ctx, i)
if err != nil {
return err
}
if err := strategy.SetDuplicateCredentials(
registrationFlow,
duplicateIdentifier,
i.Credentials[ct],
authMethod.Provider,
); err != nil {
return err
}
}
}
// Restore submitted trait values to the flow UI so they are preserved in the error response.
for _, n := range container.NewFromJSON("", node.DefaultGroup, json.RawMessage(i.Traits), "traits").Nodes {
registrationFlow.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue())
}
return err
}
// At this point the identity is already created and will not be rolled back, so
// we want all PostPersist hooks to be able to continue even when the client cancels the request.
ctx = context.WithoutCancel(ctx)
r = r.WithContext(ctx)
// Verify the redirect URL before we do any other processing.
c := e.d.Config()
returnTo, err := redir.SecureRedirectTo(r, c.SelfServiceBrowserDefaultReturnTo(ctx),
redir.SecureRedirectReturnTo(registrationFlow.ReturnTo),
redir.SecureRedirectUseSourceURL(registrationFlow.RequestURL),
redir.SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(ctx)),
redir.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(ctx)),
redir.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowRegistrationReturnTo(ctx, ct.String())),
)
if err != nil {
return err
}
span.SetAttributes(otelx.StringAttrs(map[string]string{
"return_to": returnTo.String(),
"flow_type": string(registrationFlow.Type),
"redirect_reason": "registration successful",
})...)
if registrationFlow.Type == flow.TypeBrowser && x.IsJSONRequest(r) {
registrationFlow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String()))
}
e.d.Logger().
WithRequest(r).
WithField("identity_id", i.ID).
Info("A new identity has registered using self-service registration.")
span.AddEvent(events.NewRegistrationSucceeded(ctx, registrationFlow.ID, i.ID, string(registrationFlow.Type), ct.String(), authMethod.Provider))
s := session.NewInactiveSession()
s.CompletedLoginForMethod(authMethod)
if err := e.d.SessionManager().ActivateSession(r, s, i, time.Now().UTC()); err != nil {
return err
}
// We persist the session here so that subsequent hooks (like verification) can use it.
if err := e.d.SessionPersister().UpsertSession(ctx, s); err != nil {
return err
}
e.d.Logger().
WithRequest(r).
WithField("identity_id", i.ID).
WithField("flow_method", ct).
Debug("Running PostRegistrationPostPersistHooks.")
postHooks, err := e.d.PostRegistrationPostPersistHooks(ctx, ct)
if err != nil {
return err
}
for k, executor := range postHooks {
if err := executor.ExecutePostRegistrationPostPersistHook(w, r, registrationFlow, s); err != nil {
if errors.Is(err, ErrHookAbortFlow) {
e.d.Logger().
WithRequest(r).
WithField("executor", fmt.Sprintf("%T", executor)).
WithField("executor_position", k).
WithField("executors", ExecutorNames(postHooks)).
WithField("identity_id", i.ID).
WithField("flow_method", ct).
Debug("A ExecutePostRegistrationPostPersistHook hook aborted early.")
span.SetAttributes(attribute.String("redirect_reason", "aborted by hook"), attribute.String("executor", fmt.Sprintf("%T", executor)))
return nil
}
e.d.Logger().
WithRequest(r).
WithField("executor", fmt.Sprintf("%T", executor)).
WithField("executor_position", k).
WithField("executors", ExecutorNames(postHooks)).
WithField("identity_id", i.ID).
WithField("flow_method", ct).
WithError(err).
Error("ExecutePostRegistrationPostPersistHook hook failed with an error.")
span.SetAttributes(attribute.String("redirect_reason", "hook error"), attribute.String("executor", fmt.Sprintf("%T", executor)))
traits := i.Traits
return flow.HandleHookError(w, r, registrationFlow, traits, ct.ToUiNodeGroup(), err, e.d, e.d)
}
e.d.Logger().WithRequest(r).
WithField("executor", fmt.Sprintf("%T", executor)).
WithField("executor_position", k).
WithField("executors", ExecutorNames(postHooks)).
WithField("identity_id", i.ID).
WithField("flow_method", ct).
Debug("ExecutePostRegistrationPostPersistHook completed successfully.")
}
e.d.Logger().
WithRequest(r).
WithField("flow_method", ct).
WithField("identity_id", i.ID).
Debug("Post registration execution hooks completed successfully.")
if registrationFlow.Type == flow.TypeAPI || x.IsJSONRequest(r) {
span.SetAttributes(attribute.String("flow_type", string(flow.TypeAPI)))
if registrationFlow.IDToken != "" {
// We don't want to redirect with the code, if the flow was submitted with an ID token.
// This is the case for Sign in with native Apple SDK or Google SDK.
} else if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, registrationFlow, s.ID, ct.ToUiNodeGroup()); err != nil {
return errors.WithStack(err)
} else if handled {
return nil
}
e.d.Writer().Write(w, r, &APIFlowResponse{
Identity: i,
ContinueWith: registrationFlow.ContinueWith(),
})
return nil
}
finalReturnTo := returnTo.String()
if registrationFlow.OAuth2LoginChallenge != "" {
if registrationFlow.ReturnToVerification != "" {
// Special case: If Kratos is used as a login UI *and* we want to show the verification UI,
// redirect to the verification URL first and then return to Hydra.
finalReturnTo = registrationFlow.ReturnToVerification
} else {
callbackURL, err := e.d.Hydra().AcceptLoginRequest(ctx,
hydra.AcceptLoginRequestParams{
LoginChallenge: string(registrationFlow.OAuth2LoginChallenge),
IdentityID: i.ID.String(),
SessionID: s.ID.String(),
AuthenticationMethods: s.AMR,
})
if err != nil {
return err
}
finalReturnTo = callbackURL
}
span.SetAttributes(attribute.String("redirect_reason", "oauth2 login challenge"))
} else if registrationFlow.ReturnToVerification != "" {
finalReturnTo = registrationFlow.ReturnToVerification
span.SetAttributes(attribute.String("redirect_reason", "verification requested"))
}
span.SetAttributes(attribute.String("return_to", finalReturnTo))
redir.ContentNegotiationRedirection(w, r, s.Declassified(), e.d.Writer(), finalReturnTo)
return nil
}
func (e *HookExecutor) getDuplicateIdentifier(ctx context.Context, i *identity.Identity) (string, error) {
_, id, _, err := e.d.IdentityManager().ConflictingIdentity(ctx, i)
if err != nil {
return "", err
}
return id, nil
}
func (e *HookExecutor) PreRegistrationHook(w http.ResponseWriter, r *http.Request, a *Flow) error {
hooks, err := e.d.PreRegistrationHooks(r.Context())
if err != nil {
return err
}
for _, executor := range hooks {
if err := executor.ExecuteRegistrationPreHook(w, r, a); err != nil {
return err
}
}
return nil
}