Files
upstream-ory-kratos/selfservice/flow/login/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

465 lines
14 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package login
import (
"context"
"fmt"
"net/http"
"net/url"
"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/schema"
"github.com/ory/kratos/selfservice/flow"
"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"
)
type (
PreHookExecutor interface {
ExecuteLoginPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error
}
PostHookExecutor interface {
ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, a *Flow, s *session.Session) error
}
HooksProvider interface {
PreLoginHooks(ctx context.Context) ([]PreHookExecutor, error)
PostLoginHooks(ctx context.Context, credentialsType identity.CredentialsType) ([]PostHookExecutor, error)
}
)
type (
executorDependencies interface {
config.Provider
hydra.Provider
identity.PrivilegedPoolProvider
identity.ManagementProvider
session.ManagementProvider
session.PersistenceProvider
nosurfx.CSRFTokenGeneratorProvider
httpx.WriterProvider
logrusx.Provider
otelx.Provider
sessiontokenexchange.PersistenceProvider
HandlerProvider
FlowPersistenceProvider
HooksProvider
StrategyProvider
}
HookExecutor struct {
d executorDependencies
}
HookExecutorProvider interface {
LoginHookExecutor() *HookExecutor
}
)
func PostHookExecutorNames(e []PostHookExecutor) []string {
names := make([]string, len(e))
for k, ee := range e {
names[k] = fmt.Sprintf("%T", ee)
}
return names
}
func NewHookExecutor(d executorDependencies) *HookExecutor {
return &HookExecutor{d: d}
}
func (e *HookExecutor) checkAAL(ctx context.Context, s *session.Session, a *Flow) error {
err := e.d.SessionManager().DoesSessionSatisfy(ctx, s, e.d.Config().SessionWhoAmIAAL(ctx))
if err == nil {
return nil
}
if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) {
if a != nil && aalErr.PassReturnToAndLoginChallengeParameters(a.RequestURL) != nil {
_ = aalErr.WithDetail("pass_request_params_error", "failed to pass request parameters to aalErr.RedirectTo")
}
return aalErr
}
return err
}
func (e *HookExecutor) handleLoginError(_ http.ResponseWriter, r *http.Request, g node.UiNodeGroup, f *Flow, i *identity.Identity, flowError error) error {
if f != nil {
if i != nil {
cont, err := container.NewFromStruct("", g, i.Traits, "traits")
if err != nil {
e.d.Logger().WithError(err).Warn("could not update flow UI")
return err
}
for _, n := range cont.Nodes {
// we only set the value and not the whole field because we want to keep types from the initial form generation
f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue())
}
}
if f.Type == flow.TypeBrowser {
f.UI.SetCSRF(e.d.GenerateCSRFToken(r))
}
}
return flowError
}
func (e *HookExecutor) PostLoginHook(
w http.ResponseWriter,
r *http.Request,
g node.UiNodeGroup,
f *Flow,
i *identity.Identity,
s *session.Session,
provider string,
) (err error) {
ctx := r.Context()
ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "HookExecutor.PostLoginHook")
r = r.WithContext(ctx)
defer otelx.End(span, &err)
// We need to set the identity here because we check the available AAL in maybeLinkCredentials.
s.IdentityID = i.ID
s.Identity = i
if err := e.maybeLinkCredentials(ctx, s, i, f); err != nil {
return err
}
if err := e.d.SessionManager().ActivateSession(r, s, i, time.Now().UTC()); err != nil {
return err
}
c := e.d.Config()
// Verify the redirect URL before we do any other processing.
returnTo, err := redir.SecureRedirectTo(r,
c.SelfServiceBrowserDefaultReturnTo(ctx),
redir.SecureRedirectReturnTo(f.ReturnTo),
redir.SecureRedirectUseSourceURL(f.RequestURL),
redir.SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(ctx)),
redir.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(ctx)),
redir.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowLoginReturnTo(ctx, f.Active.String())),
)
if err != nil {
return err
}
span.SetAttributes(otelx.StringAttrs(map[string]string{
"return_to": returnTo.String(),
"flow_type": string(flow.TypeBrowser),
"redirect_reason": "login successful",
})...)
if f.Type == flow.TypeBrowser && x.IsJSONRequest(r) {
f.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String()))
}
classified := s
s = s.Declassified()
e.d.Logger().
WithRequest(r).
WithField("identity_id", i.ID).
WithField("flow_method", f.Active).
Debug("Running ExecuteLoginPostHook.")
hooks, err := e.d.PostLoginHooks(ctx, f.Active)
if err != nil {
return err
}
for k, executor := range hooks {
if err := executor.ExecuteLoginPostHook(w, r, g, f, 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", PostHookExecutorNames(hooks)).
WithField("identity_id", i.ID).
WithField("flow_method", f.Active).
Debug("A ExecuteLoginPostHook hook aborted early.")
span.SetAttributes(attribute.String("redirect_reason", "aborted by hook"), attribute.String("executor", fmt.Sprintf("%T", executor)))
return nil
}
return e.handleLoginError(w, r, g, f, i, err)
}
e.d.Logger().
WithRequest(r).
WithField("executor", fmt.Sprintf("%T", executor)).
WithField("executor_position", k).
WithField("executors", PostHookExecutorNames(hooks)).
WithField("identity_id", i.ID).
WithField("flow_method", f.Active).
Debug("ExecuteLoginPostHook completed successfully.")
}
if f.Type == flow.TypeAPI {
span.SetAttributes(attribute.String("flow_type", string(flow.TypeAPI)))
if err := e.d.SessionPersister().UpsertSession(ctx, s); err != nil {
return errors.WithStack(err)
}
e.d.Logger().
WithRequest(r).
WithField("session_id", s.ID).
WithField("identity_id", i.ID).
Info("Identity authenticated successfully and was issued an Ory Kratos Session Token.")
span.AddEvent(events.NewLoginSucceeded(ctx, &events.LoginSucceededOpts{
SessionID: s.ID,
IdentityID: i.ID,
FlowID: f.ID,
FlowType: string(f.Type),
RequestedAAL: string(f.RequestedAAL),
IsRefresh: f.Refresh,
Method: f.Active.String(),
SSOProvider: provider,
}))
if f.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, f, s.ID, g); err != nil {
return errors.WithStack(err)
} else if handled {
return nil
}
response := &APIFlowResponse{
Session: s,
Token: s.Token,
ContinueWith: f.ContinueWith(),
}
if e.checkAAL(ctx, classified, f) != nil {
// If AAL is not satisfied, we omit the identity to preserve the user's privacy in case of a phishing attack.
response.Session.Identity = nil
}
e.d.Writer().Write(w, r, response)
return nil
}
if err := e.d.SessionManager().UpsertAndIssueCookie(ctx, w, r, s); err != nil {
return errors.WithStack(err)
}
e.d.Logger().
WithRequest(r).
WithField("identity_id", i.ID).
WithField("session_id", s.ID).
Info("Identity authenticated successfully and was issued an Ory Kratos Session Cookie.")
span.AddEvent(events.NewLoginSucceeded(ctx, &events.LoginSucceededOpts{
SessionID: s.ID,
FlowID: f.ID,
IdentityID: i.ID, FlowType: string(f.Type), RequestedAAL: string(f.RequestedAAL), IsRefresh: f.Refresh, Method: f.Active.String(),
SSOProvider: provider,
}))
if x.IsJSONRequest(r) {
span.SetAttributes(attribute.String("flow_type", "spa"))
// Browser flows rely on cookies. Adding tokens in the mix will confuse consumers.
s.Token = ""
// If we detect that whoami would require a higher AAL, we redirect!
if err := e.checkAAL(ctx, classified, f); err != nil {
if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) {
if data, _ := flow.DuplicateCredentials(f); data == nil {
span.SetAttributes(attribute.String("return_to", aalErr.RedirectTo), attribute.String("redirect_reason", "requires aal2"))
e.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(aalErr.RedirectTo))
return nil
}
// Special case: If we are in a flow that wants to link credentials, we create a
// new login flow here that asks for the require AAL, but also copies over the
// internal context and the organization ID.
r.URL, err = url.Parse(aalErr.RedirectTo)
if err != nil {
return errors.WithStack(err)
}
newFlow, _, err := e.d.LoginHandler().NewLoginFlow(w, r, flow.TypeBrowser,
WithInternalContext(f.InternalContext),
WithOrganizationID(f.OrganizationID),
)
if err != nil {
return errors.WithStack(err)
}
x.SendFlowCompletedAsRedirectOrJSON(w, r, e.d.Writer(), newFlow, newFlow.AppendTo(e.d.Config().SelfServiceFlowLoginUI(ctx)).String())
return nil
}
return err
}
// If Kratos is used as a Hydra login provider, we need to redirect back to Hydra by returning a 422 status
// with the post login challenge URL as the body.
if f.OAuth2LoginChallenge != "" {
postChallengeURL, err := e.d.Hydra().AcceptLoginRequest(ctx,
hydra.AcceptLoginRequestParams{
LoginChallenge: string(f.OAuth2LoginChallenge),
IdentityID: i.ID.String(),
SessionID: s.ID.String(),
AuthenticationMethods: s.AMR,
})
if err != nil {
return err
}
span.SetAttributes(attribute.String("return_to", postChallengeURL), attribute.String("redirect_reason", "oauth2 login challenge"))
e.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(postChallengeURL))
return nil
}
response := &APIFlowResponse{
Session: s,
ContinueWith: f.ContinueWith(),
}
e.d.Writer().Write(w, r, response)
return nil
}
// If we detect that whoami would require a higher AAL, we redirect!
if err := e.checkAAL(ctx, classified, f); err != nil {
if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) {
if data, _ := flow.DuplicateCredentials(f); data == nil {
http.Redirect(w, r, aalErr.RedirectTo, http.StatusSeeOther)
return nil
}
// Special case: If we are in a flow that wants to link credentials, we create a
// new login flow here that asks for the require AAL, but also copies over the
// internal context and the organization ID.
r.URL, err = url.Parse(aalErr.RedirectTo)
if err != nil {
return errors.WithStack(err)
}
newFlow, _, err := e.d.LoginHandler().NewLoginFlow(w, r, flow.TypeBrowser,
WithInternalContext(f.InternalContext),
WithOrganizationID(f.OrganizationID),
)
if err != nil {
return errors.WithStack(err)
}
x.SendFlowCompletedAsRedirectOrJSON(w, r, e.d.Writer(), newFlow, newFlow.AppendTo(e.d.Config().SelfServiceFlowLoginUI(ctx)).String())
return nil
}
return errors.WithStack(err)
}
finalReturnTo := returnTo.String()
if f.OAuth2LoginChallenge != "" {
rt, err := e.d.Hydra().AcceptLoginRequest(ctx,
hydra.AcceptLoginRequestParams{
LoginChallenge: string(f.OAuth2LoginChallenge),
IdentityID: i.ID.String(),
SessionID: s.ID.String(),
AuthenticationMethods: s.AMR,
})
if err != nil {
return err
}
finalReturnTo = rt
span.SetAttributes(attribute.String("return_to", rt), attribute.String("redirect_reason", "oauth2 login challenge"))
} else if f.ReturnToVerification != "" {
finalReturnTo = f.ReturnToVerification
span.SetAttributes(attribute.String("redirect_reason", "verification requested"))
}
redir.ContentNegotiationRedirection(w, r, s, e.d.Writer(), finalReturnTo)
return nil
}
func (e *HookExecutor) PreLoginHook(w http.ResponseWriter, r *http.Request, a *Flow) error {
hooks, err := e.d.PreLoginHooks(r.Context())
if err != nil {
return err
}
for _, h := range hooks {
if err := h.ExecuteLoginPreHook(w, r, a); err != nil {
return err
}
}
return nil
}
// maybeLinkCredentials links the identity with the credentials of the inner context of the login flow.
func (e *HookExecutor) maybeLinkCredentials(ctx context.Context, sess *session.Session, ident *identity.Identity, loginFlow *Flow) (err error) {
ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "HookExecutor.PostLoginHook.maybeLinkCredentials")
defer otelx.End(span, &err)
if e.checkAAL(ctx, sess, loginFlow) != nil {
// we don't yet want to link credentials because the required AAL is not satisfied
return nil
}
lc, err := flow.DuplicateCredentials(loginFlow)
if err != nil {
return err
} else if lc == nil {
return nil
}
if err = e.checkDuplicateCredentialsIdentifierMatch(ctx, ident, lc.DuplicateIdentifier); err != nil {
return err
}
strategy, err := e.d.AllLoginStrategies().Strategy(lc.CredentialsType)
if err != nil {
return err
}
linkableStrategy, ok := strategy.(LinkableStrategy)
if !ok {
// This should never happen because we check for this in the registration flow.
return errors.Errorf("strategy is not linkable: %T", linkableStrategy)
}
if err := linkableStrategy.Link(ctx, ident, lc.CredentialsConfig); err != nil {
return err
}
if err = linkableStrategy.CompletedLogin(sess, lc); err != nil {
return err
}
return nil
}
func (e *HookExecutor) checkDuplicateCredentialsIdentifierMatch(ctx context.Context, i *identity.Identity, match string) error {
if len(i.Credentials) == 0 {
if err := e.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, identity.ExpandCredentials); err != nil {
return err
}
}
for _, credentials := range i.Credentials {
for _, identifier := range credentials.Identifiers {
if identifier == match {
return nil
}
}
}
return schema.NewLinkedCredentialsDoNotMatch()
}