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