Files
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

342 lines
11 KiB
Go

// 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(&params, 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
}