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:
341
selfservice/strategy/profile/registration.go
Normal file
341
selfservice/strategy/profile/registration.go
Normal file
@@ -0,0 +1,341 @@
|
||||
// Copyright © 2024 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/schema"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/selfservice/flow/registration"
|
||||
"github.com/ory/kratos/text"
|
||||
"github.com/ory/kratos/ui/container"
|
||||
"github.com/ory/kratos/ui/node"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/decoderx"
|
||||
"github.com/ory/x/otelx"
|
||||
"github.com/ory/x/otelx/semconv"
|
||||
)
|
||||
|
||||
//go:embed .schema/registration.schema.json
|
||||
var registrationSchema []byte
|
||||
|
||||
// The RegistrationScreen
|
||||
// swagger:enum RegistrationScreen
|
||||
type RegistrationScreen string
|
||||
|
||||
const (
|
||||
//nolint:gosec // not a credential
|
||||
RegistrationScreenCredentialSelection RegistrationScreen = "credential-selection"
|
||||
RegistrationScreenPrevious RegistrationScreen = "previous"
|
||||
)
|
||||
|
||||
var _ registration.Strategy = new(Strategy)
|
||||
var _ registration.FormHydrator = new(Strategy)
|
||||
|
||||
// Update Registration Flow with Profile Method
|
||||
//
|
||||
// swagger:model updateRegistrationFlowWithProfileMethod
|
||||
//
|
||||
//nolint:deadcode,unused
|
||||
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
||||
type updateRegistrationFlowWithProfileMethod struct {
|
||||
// Traits
|
||||
//
|
||||
// The identity's traits.
|
||||
//
|
||||
// required: true
|
||||
Traits json.RawMessage `json:"traits"`
|
||||
|
||||
// Method
|
||||
//
|
||||
// Should be set to profile when trying to update a profile.
|
||||
//
|
||||
// required: true
|
||||
Method string `json:"method"`
|
||||
|
||||
// Screen requests navigation to a previous screen.
|
||||
//
|
||||
// This must be set to credential-selection to go back to the credential
|
||||
// selection screen.
|
||||
//
|
||||
// required: false
|
||||
Screen RegistrationScreen `json:"screen" form:"screen"`
|
||||
|
||||
// FlowIDRequestID is the flow ID.
|
||||
//
|
||||
// swagger:ignore
|
||||
FlowID string `json:"flow"`
|
||||
|
||||
// The Anti-CSRF Token
|
||||
//
|
||||
// This token is only required when performing browser flows.
|
||||
CSRFToken string `json:"csrf_token"`
|
||||
|
||||
// Transient data to pass along to any webhooks
|
||||
//
|
||||
// required: false
|
||||
TransientPayload json.RawMessage `json:"transient_payload,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Strategy) ID() identity.CredentialsType {
|
||||
return identity.CredentialsTypeProfile
|
||||
}
|
||||
|
||||
func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error {
|
||||
f.UI.Nodes.Append(nodePreviousScreen())
|
||||
f.UI.Nodes.RemoveMatching(nodeSubmitProfile())
|
||||
|
||||
for _, n := range f.UI.Nodes {
|
||||
if n.Group != node.DefaultGroup || n.Type != node.Input {
|
||||
continue
|
||||
}
|
||||
if attr, ok := n.Attributes.(*node.InputAttributes); ok {
|
||||
attr.Type = node.InputAttributeTypeHidden
|
||||
}
|
||||
}
|
||||
|
||||
f.UI.Messages.Add(text.NewInfoSelfServiceChooseCredentials())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error {
|
||||
ds, err := f.IdentitySchema.URL(r.Context(), s.d.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conf := new(registration.FormHydratorOptions)
|
||||
for _, o := range options {
|
||||
o(conf)
|
||||
}
|
||||
|
||||
f.UI.Nodes.RemoveMatching(nodePreviousScreen())
|
||||
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
||||
|
||||
nodes, err := container.NodesFromJSONSchema(r.Context(), node.DefaultGroup, ds.String(), "", nil, s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range nodes {
|
||||
f.UI.Nodes.Upsert(n)
|
||||
}
|
||||
|
||||
if len(conf.WithTraits) > 0 {
|
||||
f.UI.UpdateNodeValuesFromJSON(conf.WithTraits, "traits", node.DefaultGroup)
|
||||
}
|
||||
|
||||
f.UI.Nodes.Append(nodeSubmitProfile())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error {
|
||||
ds, err := f.IdentitySchema.URL(r.Context(), s.d.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
||||
nodes, err := container.NodesFromJSONSchema(r.Context(), node.DefaultGroup, ds.String(), "", nil, s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, n := range nodes {
|
||||
f.UI.SetNode(n)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) decode(p *updateRegistrationFlowWithProfileMethod, r *http.Request, ds *url.URL) error {
|
||||
compiler, err := decoderx.HTTPRawJSONSchemaCompiler(registrationSchema)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := decoderx.Decode(r, p, compiler,
|
||||
decoderx.HTTPKeepRequestBody(true),
|
||||
decoderx.HTTPDecoderSetValidatePayloads(false),
|
||||
decoderx.HTTPDecoderJSONFollowsFormFormat(),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Method != "profile" && p.Method != "profile:back" && len(p.Screen) == 0 {
|
||||
return errors.WithStack(flow.ErrStrategyNotResponsible)
|
||||
}
|
||||
|
||||
return registration.DecodeBody(p, r, registrationSchema, ds)
|
||||
}
|
||||
|
||||
func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity) (err error) {
|
||||
ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.profile.Strategy.Register")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
if !s.d.Config().SelfServiceFlowRegistrationTwoSteps(ctx) {
|
||||
return flow.ErrStrategyNotResponsible
|
||||
}
|
||||
|
||||
var params updateRegistrationFlowWithProfileMethod
|
||||
|
||||
ds, err := regFlow.IdentitySchema.URL(ctx, s.d.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.decode(¶ms, r, ds); err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
}
|
||||
if params.Method == "profile" || len(params.Screen) > 0 {
|
||||
switch params.Screen {
|
||||
case RegistrationScreenCredentialSelection:
|
||||
return s.showCredentialsSelection(ctx, w, r, regFlow, i, params)
|
||||
case RegistrationScreenPrevious:
|
||||
return s.returnToProfileForm(ctx, w, r, regFlow, params)
|
||||
default:
|
||||
// FIXME In this scenario we are on the first step of the registration flow and the user clicked on "continue".
|
||||
// FIXME The appropriate solution would be to also have `screen=credential-selection` available, but that
|
||||
// FIXME is not the case right now. So instead, we fall back.
|
||||
span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "profile:missing_screen_parameter"))
|
||||
return s.showCredentialsSelection(ctx, w, r, regFlow, i, params)
|
||||
}
|
||||
} else if params.Method == "profile:back" {
|
||||
// "profile:back" is kept for backwards compatibility.
|
||||
// FIXME remove this at some point.
|
||||
span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "profile:back"))
|
||||
return s.returnToProfileForm(ctx, w, r, regFlow, params)
|
||||
}
|
||||
|
||||
return flow.ErrStrategyNotResponsible
|
||||
}
|
||||
|
||||
func (s *Strategy) returnToProfileForm(ctx context.Context, w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, params updateRegistrationFlowWithProfileMethod) error {
|
||||
regFlow.UI.ResetMessages()
|
||||
regFlow.OrganizationID = uuid.NullUUID{}
|
||||
regFlow.UI.UpdateNodeValuesFromJSON(params.Traits, "traits", node.DefaultGroup)
|
||||
|
||||
for _, ls := range SortForHydration(s.d.RegistrationStrategies(ctx)) {
|
||||
populator, ok := ls.(registration.FormHydrator)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := populator.PopulateRegistrationMethodProfile(r, regFlow, registration.WithTraits(params.Traits)); err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
}
|
||||
}
|
||||
|
||||
ds, err := regFlow.IdentitySchema.URL(r.Context(), s.d.Config())
|
||||
if err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
}
|
||||
|
||||
if err := registration.SortNodes(r.Context(), regFlow.UI.Nodes, ds.String()); err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
}
|
||||
|
||||
if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
}
|
||||
|
||||
redirectTo := regFlow.AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(ctx)).String()
|
||||
if x.IsJSONRequest(r) {
|
||||
s.d.Writer().WriteCode(w, r, http.StatusBadRequest, regFlow)
|
||||
} else {
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
return flow.ErrCompletedByStrategy
|
||||
}
|
||||
|
||||
func (s *Strategy) showCredentialsSelection(ctx context.Context, w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity, params updateRegistrationFlowWithProfileMethod) error {
|
||||
// Reset state-esque flow fields
|
||||
regFlow.Active = ""
|
||||
regFlow.State = "choose_method"
|
||||
|
||||
regFlow.UI.ResetMessages()
|
||||
regFlow.TransientPayload = params.TransientPayload
|
||||
|
||||
if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, params.CSRFToken); err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
}
|
||||
|
||||
if len(params.Traits) == 0 {
|
||||
params.Traits = json.RawMessage("{}")
|
||||
}
|
||||
|
||||
i.Traits = identity.Traits(params.Traits)
|
||||
if err := s.d.IdentityValidator().Validate(ctx, i); err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
}
|
||||
|
||||
var didPopulate bool
|
||||
for _, ls := range SortForHydration(s.d.RegistrationStrategies(ctx)) {
|
||||
populator, ok := ls.(registration.FormHydrator)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := populator.PopulateRegistrationMethodCredentials(r, regFlow, registration.WithTraits([]byte(i.Traits))); errors.Is(err, registration.ErrBreakRegistrationPopulate) {
|
||||
didPopulate = true
|
||||
break
|
||||
} else if err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
} else {
|
||||
didPopulate = true
|
||||
}
|
||||
}
|
||||
|
||||
// If no strategy populated, it means that the account (very likely) does not exist. We show a user not found error,
|
||||
// but only if account enumeration mitigation is disabled. Otherwise, we proceed to render the rest of the form.
|
||||
if !didPopulate && !s.d.Config().SecurityAccountEnumerationMitigate(ctx) {
|
||||
return s.handleRegistrationError(r, regFlow, params, errors.WithStack(schema.NewNoRegistrationStrategyResponsible()))
|
||||
}
|
||||
|
||||
regFlow.UI.UpdateNodeValuesFromJSON(json.RawMessage(i.Traits), "traits", node.DefaultGroup)
|
||||
|
||||
ds, err := regFlow.IdentitySchema.URL(r.Context(), s.d.Config())
|
||||
if err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
}
|
||||
|
||||
if err := registration.SortNodes(r.Context(), regFlow.UI.Nodes, ds.String()); err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
}
|
||||
|
||||
if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil {
|
||||
return s.handleRegistrationError(r, regFlow, params, err)
|
||||
}
|
||||
|
||||
redirectTo := regFlow.AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(ctx)).String()
|
||||
if x.IsJSONRequest(r) {
|
||||
s.d.Writer().WriteCode(w, r, http.StatusBadRequest, regFlow)
|
||||
} else {
|
||||
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
return flow.ErrCompletedByStrategy
|
||||
}
|
||||
|
||||
func (s *Strategy) handleRegistrationError(r *http.Request, regFlow *registration.Flow, params updateRegistrationFlowWithProfileMethod, err error) error {
|
||||
if regFlow != nil {
|
||||
for _, n := range container.NewFromJSON("", node.DefaultGroup, params.Traits, "traits").Nodes {
|
||||
// we only set the value and not the whole field because we want to keep types from the initial form generation
|
||||
regFlow.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue())
|
||||
}
|
||||
|
||||
regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user