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

350 lines
13 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package login
import (
"cmp"
"context"
"database/sql"
"encoding/json"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/tidwall/gjson"
hydraclientgo "github.com/ory/hydra-client-go/v2"
"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/ui/container"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/kratos/x/redir"
"github.com/ory/pop/v6"
"github.com/ory/x/clock"
"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
)
// Login Flow
//
// This object represents a login flow. A login flow is initiated at the "Initiate Login API / Browser Flow"
// endpoint by a client.
//
// Once a login flow is completed successfully, a session cookie or session token will be issued.
//
// swagger:model loginFlow
type Flow struct {
// ID represents the flow's unique ID. When performing the login flow, this
// represents the id in the login UI's query parameter: http://<selfservice.flows.login.ui_url>/?flow=<flow_id>
//
// required: true
ID uuid.UUID `json:"id" faker:"-" db:"id" rw:"r"`
NID uuid.UUID `json:"-" faker:"-" db:"nid"`
OrganizationID uuid.NullUUID `json:"organization_id,omitempty" faker:"-" db:"organization_id"`
// Ory OAuth 2.0 Login Challenge.
//
// This value is set using the `login_challenge` query parameter of the registration and login endpoints.
// If set will cooperate with Ory OAuth2 and OpenID to act as an OAuth2 server / OpenID Provider.
OAuth2LoginChallenge sqlxx.NullString `json:"oauth2_login_challenge,omitempty" faker:"-" db:"oauth2_login_challenge_data"`
// HydraLoginRequest is an optional field whose presence indicates that Kratos
// is being used as an identity provider in a Hydra OAuth2 flow. Kratos
// populates this field by retrieving its value from Hydra and it is used by
// the login and consent UIs.
HydraLoginRequest *hydraclientgo.OAuth2LoginRequest `json:"oauth2_login_request,omitempty" faker:"-" db:"-"`
// Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction.
//
// required: true
Type flow.Type `json:"type" db:"type" faker:"flow_type"`
// ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in,
// a new flow has to be initiated.
//
// required: true
ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"`
// IssuedAt is the time (UTC) when the flow started.
//
// required: true
IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"`
// InternalContext stores internal context used by internals - for example MFA keys.
InternalContext sqlxx.JSONRawMessage `db:"internal_context" json:"-" faker:"-"`
// RequestURL is the initial URL that was requested from Ory Kratos. It can be used
// to forward information contained in the URL's path or query for example.
//
// required: true
RequestURL string `json:"request_url" db:"request_url"`
// ReturnTo contains the requested return_to URL.
ReturnTo string `json:"return_to,omitempty" db:"-"`
// The active login method
//
// If set contains the login method used. If the flow is new, it is unset.
Active identity.CredentialsType `json:"active,omitempty" db:"active_method"`
// UI contains data which must be shown in the user interface.
//
// required: true
UI *container.Container `json:"ui" db:"ui"`
// CreatedAt is a helper struct field for gobuffalo.pop.
CreatedAt time.Time `json:"created_at" db:"created_at"`
// UpdatedAt is a helper struct field for gobuffalo.pop.
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
// CSRFToken contains the anti-csrf token associated with this flow. Only set for browser flows.
CSRFToken string `json:"-" db:"csrf_token"`
// Refresh stores whether this login flow should enforce re-authentication.
Refresh bool `json:"refresh" db:"forced"`
// RequestedAAL stores if the flow was requested to update the authenticator assurance level.
//
// This value can be one of "aal1", "aal2", "aal3".
RequestedAAL identity.AuthenticatorAssuranceLevel `json:"requested_aal" faker:"len=4" db:"requested_aal"`
// SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the login flow has been completed.
// This is only set if the client has requested a session token exchange code, and if the flow is of type "api",
// and only on creating the login flow.
SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"`
// State represents the state of this request:
//
// - choose_method: ask the user to choose a method to sign in with
// - sent_email: the email has been sent to the user
// - passed_challenge: the request was successful and the login challenge was passed.
//
// required: true
State State `json:"state" faker:"-" db:"state"`
// Only used internally
IDToken string `json:"-" db:"-"`
// Only used internally
RawIDTokenNonce string `json:"-" db:"-"`
// TransientPayload is used to pass data from the login to hooks and email templates
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"`
// Contains a list of actions, that could follow this flow
//
// It can, for example, contain a reference to the verification flow, created as part of the user's
// registration.
ContinueWithItems []flow.ContinueWith `json:"-" db:"-" faker:"-" `
// ReturnToVerification contains the redirect URL for the verification flow.
ReturnToVerification string `json:"-" db:"-"`
isAccountLinkingFlow bool `db:"-"`
// IdentitySchema optionally holds the ID of the identity schema that is used
// for this flow. This value can be set by the user when creating the flow and
// should be retained when the flow is saved or converted to another flow.
IdentitySchema flow.IdentitySchema `json:"identity_schema,omitempty" faker:"-" db:"identity_schema_id"`
// TestFlow marks this flow as an admin-created dry-run OIDC test. Test
// flows short-circuit the OIDC callback: no identity is persisted and no
// session is issued. The captured debug data is returned via the derived
// TestContext field below.
//
// Nullable so that existing rows created before the column was added read
// as a SQL NULL (effectively false); see IsTest.
TestFlow sql.NullBool `json:"-" faker:"-" db:"test_flow"`
// TestContext is the derived API view of test-mode data. It is populated
// from InternalContext["test"] during marshal and is nil for regular
// login flows. See test_context.go.
//
// required: false
// readOnly: true
TestContext *TestContext `json:"test_context,omitempty" faker:"-" db:"-"`
}
var (
_ flow.Flow = (*Flow)(nil)
_ flow.FlowWithContinueWith = (*Flow)(nil)
)
// flowDependencies are the dependencies NewFlow needs to construct a login
// flow: the configuration (for the lifespan and return-to validation), the
// clock, and the CSRF token generator.
type flowDependencies interface {
config.Provider
clock.Provider
nosurfx.CSRFTokenGeneratorProvider
}
func NewFlow(reg flowDependencies, r *http.Request, flowType flow.Type) (*Flow, error) {
conf := reg.Config()
now := reg.Clock().Now().UTC()
exp := conf.SelfServiceFlowLoginRequestLifespan(r.Context())
id := x.NewUUID()
requestURL := x.RequestURL(r).String()
// Pre-validate the return to URL which is contained in the HTTP request.
_, err := redir.SecureRedirectTo(r,
conf.SelfServiceBrowserDefaultReturnTo(r.Context()),
redir.SecureRedirectUseSourceURL(requestURL),
redir.SecureRedirectAllowURLs(conf.SelfServiceBrowserAllowedReturnToDomains(r.Context())),
redir.SecureRedirectAllowSelfServiceURLs(conf.SelfPublicURL(r.Context())),
)
if err != nil {
return nil, err
}
hydraLoginChallenge, err := hydra.GetLoginChallengeID(conf, r)
if err != nil {
return nil, err
}
refresh, _ := strconv.ParseBool(r.URL.Query().Get("refresh"))
f := &Flow{
ID: id,
OAuth2LoginChallenge: hydraLoginChallenge,
ExpiresAt: now.Add(exp),
IssuedAt: now,
UI: &container.Container{
Method: "POST",
Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(),
},
RequestURL: requestURL,
CSRFToken: reg.GenerateCSRFToken(r),
Type: flowType,
Refresh: refresh,
RequestedAAL: identity.AuthenticatorAssuranceLevel(strings.ToLower(cmp.Or(
r.URL.Query().Get("aal"),
string(identity.AuthenticatorAssuranceLevel1)))),
InternalContext: []byte("{}"),
State: flow.StateChooseMethod,
}
if err := flow.SetRequestBaseURL(f, x.BaseURLStringFromContext(r.Context())); err != nil {
return nil, err
}
return f, nil
}
func (f *Flow) GetType() flow.Type { return f.Type }
func (f *Flow) GetRequestURL() string { return f.RequestURL }
func (f *Flow) GetID() uuid.UUID { return f.ID }
func (f *Flow) GetInternalContext() sqlxx.JSONRawMessage { return f.InternalContext }
func (f *Flow) SetInternalContext(bytes sqlxx.JSONRawMessage) { f.InternalContext = bytes }
func (f *Flow) GetUI() *container.Container { return f.UI }
func (f *Flow) GetState() flow.State { return f.State }
func (Flow) GetFlowName() flow.FlowName { return flow.LoginFlow }
func (Flow) TableName() string { return "selfservice_login_flows" }
func (f *Flow) ContinueWith() []flow.ContinueWith { return f.ContinueWithItems }
func (f *Flow) SetReturnToVerification(to string) { f.ReturnToVerification = to }
func (f *Flow) GetOAuth2LoginChallenge() sqlxx.NullString { return f.OAuth2LoginChallenge }
func (f *Flow) GetHydraLoginRequest() *hydraclientgo.OAuth2LoginRequest { return f.HydraLoginRequest }
func (f *Flow) AppendTo(src *url.URL) *url.URL { return flow.AppendFlowTo(src, f.ID) }
func (f *Flow) SetState(state flow.State) { f.State = state }
func (f *Flow) GetTransientPayload() json.RawMessage { return f.TransientPayload }
// IsRefresh returns true if the login flow was triggered to re-authenticate the user.
// This is the case if the refresh query parameter is set to true.
func (f *Flow) IsRefresh() bool { return f.Refresh }
func (f *Flow) Valid(c clock.Clock) error {
if f.ExpiresAt.Before(c.Now()) {
return errors.WithStack(flow.NewFlowExpiredError(c, f.ExpiresAt))
}
return nil
}
func (f *Flow) EnsureInternalContext() {
if !gjson.ParseBytes(f.InternalContext).IsObject() {
f.InternalContext = []byte("{}")
}
}
func (f Flow) MarshalJSON() ([]byte, error) {
type local Flow
f.SetReturnTo()
// Populate the derived TestContext field for test-mode flows so the
// admin UI sees provider_id and (once captured) debug_payload.
if err := (&f).LoadTestContext(); err != nil {
return nil, err
}
return json.Marshal(local(f))
}
// UnmarshalJSON drops any incoming test_context. The field is derived from
// InternalContext and must only be set via LoadTestContext, otherwise a caller
// submitting a Flow blob could forge a debug payload.
func (f *Flow) UnmarshalJSON(data []byte) error {
type local Flow
if err := json.Unmarshal(data, (*local)(f)); err != nil {
return err
}
f.TestContext = nil
return nil
}
func (f *Flow) SetReturnTo() {
// Return to is already set, do not overwrite it.
if len(f.ReturnTo) > 0 {
return
}
if u, err := url.Parse(f.RequestURL); err == nil {
f.ReturnTo = u.Query().Get("return_to")
}
}
func (f *Flow) AfterFind(*pop.Connection) error {
f.SetReturnTo()
return nil
}
func (f *Flow) AfterSave(*pop.Connection) error {
f.SetReturnTo()
return nil
}
func (f *Flow) SecureRedirectToOpts(ctx context.Context, cfg config.Provider) (opts []redir.SecureRedirectOption) {
return []redir.SecureRedirectOption{
redir.SecureRedirectReturnTo(f.ReturnTo),
redir.SecureRedirectUseSourceURL(f.RequestURL),
redir.SecureRedirectAllowURLs(cfg.Config().SelfServiceBrowserAllowedReturnToDomains(ctx)),
redir.SecureRedirectAllowSelfServiceURLs(cfg.Config().SelfPublicURL(ctx)),
redir.SecureRedirectOverrideDefaultReturnTo(cfg.Config().SelfServiceFlowLoginReturnTo(ctx, f.Active.String())),
}
}
func (f *Flow) AddContinueWith(c flow.ContinueWith) {
f.ContinueWithItems = append(f.ContinueWithItems, c)
}
func (f *Flow) ToLoggerField() map[string]any {
if f == nil {
return map[string]any{}
}
return map[string]any{
"id": f.ID.String(),
"return_to": f.ReturnTo,
"request_url": f.RequestURL,
"active": f.Active,
"type": f.Type,
"nid": f.NID,
"state": f.State,
"refresh": f.Refresh,
"requested_aal": f.RequestedAAL,
}
}