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
397 lines
14 KiB
Go
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
|
|
}
|