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:
308
selfservice/flow/registration/flow.go
Normal file
308
selfservice/flow/registration/flow.go
Normal file
@@ -0,0 +1,308 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package registration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// swagger:model registrationFlow
|
||||
type Flow struct {
|
||||
// ID represents the flow's unique ID. When performing the registration flow, this
|
||||
// represents the id in the registration ui's query parameter: http://<selfservice.flows.registration.ui_url>/?flow=<id>
|
||||
//
|
||||
// required: true
|
||||
ID uuid.UUID `json:"id" faker:"-" db:"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 occurred.
|
||||
//
|
||||
// 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" faker:"url" db:"request_url"`
|
||||
|
||||
// ReturnTo contains the requested return_to URL.
|
||||
ReturnTo string `json:"return_to,omitempty" db:"-"`
|
||||
|
||||
// ReturnToVerification contains the redirect URL for the verification flow.
|
||||
ReturnToVerification string `json:"-" db:"-"`
|
||||
|
||||
// Active, if set, contains the registration method that is being used. It is initially
|
||||
// not set.
|
||||
Active identity.CredentialsType `json:"active,omitempty" faker:"identity_credentials_type" 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:"-" faker:"-" db:"created_at"`
|
||||
|
||||
// UpdatedAt is a helper struct field for gobuffalo.pop.
|
||||
UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"`
|
||||
|
||||
// CSRFToken contains the anti-csrf token associated with this flow. Only set for browser flows.
|
||||
CSRFToken string `json:"-" db:"csrf_token"`
|
||||
NID uuid.UUID `json:"-" faker:"-" db:"nid"`
|
||||
OrganizationID uuid.NullUUID `json:"organization_id,omitempty" faker:"-" db:"organization_id"`
|
||||
|
||||
// TransientPayload is used to pass data from the registration to a webhook
|
||||
//
|
||||
// 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:"-" `
|
||||
|
||||
// SessionTokenExchangeCode holds the secret code that the client can use to retrieve a session token after the 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 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 (e.g. registration with email)
|
||||
// - sent_email: the email has been sent to the user
|
||||
// - passed_challenge: the request was successful and the registration challenge was passed.
|
||||
// required: true
|
||||
State State `json:"state" faker:"-" db:"state"`
|
||||
|
||||
// only used internally
|
||||
IDToken string `json:"-" faker:"-" db:"-"`
|
||||
// Only used internally
|
||||
RawIDTokenNonce string `json:"-" 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"`
|
||||
}
|
||||
|
||||
var _ flow.Flow = new(Flow)
|
||||
|
||||
// flowDependencies are the dependencies NewFlow needs to construct a
|
||||
// registration 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, ft flow.Type) (*Flow, error) {
|
||||
conf := reg.Config()
|
||||
now := reg.Clock().Now().UTC()
|
||||
exp := conf.SelfServiceFlowRegistrationRequestLifespan(r.Context())
|
||||
id := x.NewUUID()
|
||||
|
||||
// Pre-validate the return to URL which is contained in the HTTP request.
|
||||
requestURL := x.RequestURL(r).String()
|
||||
_, 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
|
||||
}
|
||||
|
||||
hlc, err := hydra.GetLoginChallengeID(conf, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identitySchema := ""
|
||||
if requestedSchema := r.URL.Query().Get("identity_schema"); requestedSchema != "" {
|
||||
identitySchema, err = conf.SelfServiceFlowIdentitySchema(r.Context(), requestedSchema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
f := &Flow{
|
||||
ID: id,
|
||||
OAuth2LoginChallenge: hlc,
|
||||
ExpiresAt: now.Add(exp),
|
||||
IssuedAt: now,
|
||||
RequestURL: requestURL,
|
||||
UI: &container.Container{
|
||||
Method: "POST",
|
||||
Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r.Context()), RouteSubmitFlow), id).String(),
|
||||
},
|
||||
CSRFToken: reg.GenerateCSRFToken(r),
|
||||
Type: ft,
|
||||
InternalContext: []byte("{}"),
|
||||
State: flow.StateChooseMethod,
|
||||
IdentitySchema: flow.IdentitySchema(identitySchema),
|
||||
}
|
||||
if err := flow.SetRequestBaseURL(f, x.BaseURLStringFromContext(r.Context())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (Flow) TableName() string { return "selfservice_registration_flows" }
|
||||
func (f Flow) GetID() uuid.UUID { return f.ID }
|
||||
func (f *Flow) AppendTo(src *url.URL) *url.URL { return flow.AppendFlowTo(src, f.ID) }
|
||||
func (f *Flow) GetType() flow.Type { return f.Type }
|
||||
func (f *Flow) GetRequestURL() string { return f.RequestURL }
|
||||
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() State { return f.State }
|
||||
func (Flow) GetFlowName() flow.FlowName { return flow.RegistrationFlow }
|
||||
func (f *Flow) SetState(state State) { f.State = state }
|
||||
func (f *Flow) GetTransientPayload() json.RawMessage { return f.TransientPayload }
|
||||
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) 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("{}")
|
||||
}
|
||||
}
|
||||
|
||||
const internalContextAccountLinkingPath = "converted_for_account_linking"
|
||||
|
||||
// MarkAsAccountLinking records in the flow's internal context that this
|
||||
// registration flow was created internally by converting a login flow to
|
||||
// perform account linking. Such flows may exist while registration is
|
||||
// disabled, but must never create a new identity in that case.
|
||||
func (f *Flow) MarkAsAccountLinking() {
|
||||
f.EnsureInternalContext()
|
||||
// The path is a constant and the internal context is guaranteed to be a
|
||||
// JSON object at this point, so this cannot fail.
|
||||
if bytes, err := sjson.SetBytes(f.InternalContext, internalContextAccountLinkingPath, true); err == nil {
|
||||
f.InternalContext = bytes
|
||||
}
|
||||
}
|
||||
|
||||
// IsAccountLinking returns true if this registration flow was created
|
||||
// internally by converting a login flow to perform account linking.
|
||||
func (f *Flow) IsAccountLinking() bool {
|
||||
return gjson.GetBytes(f.InternalContext, internalContextAccountLinkingPath).Bool()
|
||||
}
|
||||
|
||||
func (f Flow) MarshalJSON() ([]byte, error) {
|
||||
type local Flow
|
||||
f.SetReturnTo()
|
||||
return json.Marshal(local(f))
|
||||
}
|
||||
|
||||
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) AddContinueWith(c flow.ContinueWith) {
|
||||
f.ContinueWithItems = append(f.ContinueWithItems, c)
|
||||
}
|
||||
|
||||
func (f *Flow) ContinueWith() []flow.ContinueWith { return f.ContinueWithItems }
|
||||
|
||||
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().SelfServiceFlowRegistrationReturnTo(ctx, f.Active.String())),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user