Files
upstream-ory-kratos/selfservice/strategy/code/strategy_registration.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

360 lines
11 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package code
import (
"context"
"encoding/json"
"net/http"
"strings"
"github.com/tidwall/gjson"
"github.com/ory/herodot"
"github.com/ory/x/otelx"
"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/ui/container"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/x/urlx"
)
var (
_ registration.Strategy = new(Strategy)
_ registration.FormHydrator = new(Strategy)
)
// Update Registration Flow with Code Method
//
// swagger:model updateRegistrationFlowWithCodeMethod
type updateRegistrationFlowWithCodeMethod struct {
// The identity's traits
//
// required: true
Traits json.RawMessage `json:"traits" form:"traits"`
// The OTP Code sent to the user
//
// required: false
Code string `json:"code" form:"code"`
// The CSRF Token
CSRFToken string `json:"csrf_token" form:"csrf_token"`
// Method to use
//
// This field must be set to `code` when using the code method.
//
// required: true
Method string `json:"method" form:"method"`
// Resend restarts the flow with a new code
//
// required: false
Resend string `json:"resend" form:"resend"`
// Transient data to pass along to any webhooks
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
}
func (p *updateRegistrationFlowWithCodeMethod) GetResend() string {
return p.Resend
}
func (s *Strategy) HandleRegistrationError(ctx context.Context, r *http.Request, f *registration.Flow, body *updateRegistrationFlowWithCodeMethod, err error) error {
if errors.Is(err, flow.ErrCompletedByStrategy) {
return err
}
if f != nil {
if body != nil {
action := f.AppendTo(urlx.AppendPaths(s.deps.Config().SelfPublicURL(ctx), registration.RouteSubmitFlow)).String()
for _, n := range container.NewFromJSON(action, node.DefaultGroup, body.Traits, "traits").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())
}
}
f.UI.SetCSRF(s.deps.GenerateCSRFToken(r))
}
return err
}
func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error {
if !s.deps.Config().SelfServiceCodeStrategy(r.Context()).PasswordlessEnabled {
return nil
}
f.GetUI().Nodes.Append(nodeSubmitRegistration())
f.UI.SetCSRF(s.deps.GenerateCSRFToken(r))
return nil
}
func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error {
if !s.deps.Config().SelfServiceCodeStrategy(r.Context()).PasswordlessEnabled {
return nil
}
f.GetUI().Nodes.RemoveMatching(nodeRegistrationResendNode())
f.GetUI().Nodes.RemoveMatching(nodeRegistrationSelectCredentialsNode())
f.GetUI().Nodes.RemoveMatching(nodeContinueButton())
f.GetUI().Nodes.RemoveMatching(nodeCodeInputFieldHidden())
f.GetUI().Nodes.RemoveMatching(nodeCodeInputField())
f.GetUI().Nodes.Append(nodeSubmitRegistration())
f.UI.SetCSRF(s.deps.GenerateCSRFToken(r))
return nil
}
func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error {
if !s.deps.Config().SelfServiceCodeStrategy(r.Context()).PasswordlessEnabled {
return nil
}
f.GetUI().Nodes.RemoveMatching(nodeSubmitRegistration())
f.GetUI().Nodes.RemoveMatching(nodeRegistrationResendNode())
f.GetUI().Nodes.RemoveMatching(nodeRegistrationSelectCredentialsNode())
f.GetUI().Nodes.RemoveMatching(nodeContinueButton())
f.GetUI().Nodes.RemoveMatching(nodeCodeInputFieldHidden())
f.GetUI().Nodes.RemoveMatching(nodeCodeInputField())
f.UI.SetCSRF(s.deps.GenerateCSRFToken(r))
return nil
}
func (s *Strategy) validateTraits(ctx context.Context, traits json.RawMessage, i *identity.Identity) error {
i.Traits = []byte("{}")
if gjson.ValidBytes(traits) {
i.Traits = identity.Traits(traits)
}
// Validate the identity
if err := s.deps.IdentityValidator().Validate(ctx, i); err != nil {
return err
}
return nil
}
func (s *Strategy) validateAndGetCredentialsFromTraits(ctx context.Context, i *identity.Identity, traits json.RawMessage) (*identity.Credentials, *identity.CredentialsCode, error) {
if err := s.validateTraits(ctx, traits, i); err != nil {
return nil, nil, errors.WithStack(err)
}
cred, ok := i.GetCredentials(identity.CredentialsTypeCodeAuth)
if !ok {
return nil, nil, errors.WithStack(schema.NewMissingIdentifierError())
} else if len(strings.Join(cred.Identifiers, "")) == 0 {
return nil, nil, errors.WithStack(schema.NewMissingIdentifierError())
}
var conf identity.CredentialsCode
if len(cred.Config) > 0 {
if err := json.Unmarshal(cred.Config, &conf); err != nil {
return nil, nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to unmarshal credentials config: %s", err))
}
}
return cred, &conf, nil
}
func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) {
ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.Strategy.Register")
defer otelx.End(span, &err)
if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil {
return err
}
ds, err := f.IdentitySchema.URL(ctx, s.deps.Config())
if err != nil {
return err
}
var p updateRegistrationFlowWithCodeMethod
if err := registration.DecodeBody(&p, r, registrationSchema, ds); err != nil {
return s.HandleRegistrationError(ctx, r, f, &p, err)
}
f.TransientPayload = p.TransientPayload
if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil {
return s.HandleRegistrationError(ctx, r, f, &p, err)
}
// By Default the flow should be in the 'choose method' state.
SetDefaultFlowState(f, p.Resend)
switch f.GetState() {
case flow.StateChooseMethod:
return s.HandleRegistrationError(ctx, r, f, &p, s.registrationSendEmail(ctx, w, r, f, &p, i))
case flow.StateEmailSent:
return s.HandleRegistrationError(ctx, r, f, &p, s.registrationVerifyCode(ctx, f, &p, i))
case flow.StatePassedChallenge:
return s.HandleRegistrationError(ctx, r, f, &p, errors.WithStack(schema.NewNoRegistrationStrategyResponsible()))
}
return s.HandleRegistrationError(ctx, r, f, &p, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unexpected flow state: %s", f.GetState())))
}
func (s *Strategy) registrationSendEmail(ctx context.Context, w http.ResponseWriter, r *http.Request, f *registration.Flow, p *updateRegistrationFlowWithCodeMethod, i *identity.Identity) (err error) {
ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.Strategy.registrationSendEmail")
defer otelx.End(span, &err)
if len(p.Traits) == 0 {
return errors.WithStack(schema.NewRequiredError("#/traits", "traits"))
}
// Create the Registration code
// Step 1: validate the identity's traits
_, conf, err := s.validateAndGetCredentialsFromTraits(ctx, i, p.Traits)
if err != nil {
return err
}
// Step 2: Delete any previous registration codes for this flow ID
if err := s.deps.RegistrationCodePersister().DeleteRegistrationCodesOfFlow(ctx, f.ID); err != nil {
return errors.WithStack(err)
}
// Step 3: Get the identity email and send the code
var addresses []Address
for _, address := range conf.Addresses {
addresses = append(addresses, Address{To: address.Address, Via: address.Channel})
}
// Fetch the OAuth2 login request before sending out the code so that
// courier templates can access the OAuth2 client that initiated the flow.
if f.OAuth2LoginChallenge != "" {
hlr, err := s.deps.Hydra().GetLoginRequest(ctx, string(f.OAuth2LoginChallenge))
if err != nil {
return errors.WithStack(err)
}
f.HydraLoginRequest = hlr
}
// kratos only supports `email` identifiers at the moment with the code method
// this is validated in the identity validation step above
if err := s.deps.CodeSender().SendCode(ctx, f, i, r.Header, addresses...); err != nil {
return errors.WithStack(err)
}
// sets the flow state to code sent
f.SetState(flow.NextState(f.GetState()))
// Step 4: Generate the UI for the `code` input form
// re-initialize the UI with a "clean" new state
// this should also provide a "resend" button and an option to change the email address
if err := s.NewCodeUINodes(r, f, p.Traits); err != nil {
return errors.WithStack(err)
}
f.Active = identity.CredentialsTypeCodeAuth
if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil {
return errors.WithStack(err)
}
if x.IsJSONRequest(r) {
s.deps.Writer().WriteCode(w, r, http.StatusBadRequest, f)
} else {
http.Redirect(w, r, f.AppendTo(s.deps.Config().SelfServiceFlowRegistrationUI(ctx)).String(), http.StatusSeeOther)
}
// we return an error to the flow handler so that it does not continue execution of the hooks.
// we are not done with the registration flow yet. The user needs to verify the code and then we need to persist the identity.
return errors.WithStack(flow.ErrCompletedByStrategy)
}
func (s *Strategy) registrationVerifyCode(ctx context.Context, f *registration.Flow, p *updateRegistrationFlowWithCodeMethod, i *identity.Identity) (err error) {
ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.Strategy.registrationVerifyCode")
defer otelx.End(span, &err)
if len(p.Code) == 0 {
return errors.WithStack(schema.NewRequiredError("#/code", "code"))
}
if len(p.Traits) == 0 {
return errors.WithStack(schema.NewRequiredError("#/traits", "traits"))
}
// Step 1: Re-validate the identity's traits
// this is important since the client could have switched out the identity's traits
// this method also returns the credentials for a temporary identity
cred, _, err := s.validateAndGetCredentialsFromTraits(ctx, i, p.Traits)
if err != nil {
return err
}
// Step 2: Check if the flow traits match the identity traits
for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes {
ui := f.GetUI()
if ui == nil {
continue
}
nodes := ui.GetNodes()
if nodes == nil {
continue
}
node := nodes.Find(n.ID())
if node == nil {
continue
}
nodeAttrs := node.Attributes
if nodeAttrs == nil {
continue
}
nAttrs := n.Attributes
if nAttrs == nil {
continue
}
if nodeAttrs.GetValue() != nAttrs.GetValue() {
return errors.WithStack(schema.NewTraitsMismatch())
}
}
// Step 3: Attempt to use the code
registrationCode, err := s.deps.RegistrationCodePersister().UseRegistrationCode(ctx, f.ID, p.Code, cred.Identifiers...)
if err != nil {
if errors.Is(err, ErrCodeNotFound()) {
return errors.WithStack(schema.NewRegistrationCodeInvalid())
}
return errors.WithStack(err)
}
// Step 4: Verify the address
if err := s.verifyAddress(ctx, i, Address{
To: registrationCode.Address,
Via: registrationCode.AddressType,
}, false); err != nil {
return err
}
// since nothing has errored yet, we can assume that the code is correct
// and we can update the registration flow
f.SetState(flow.NextState(f.GetState()))
if err := s.deps.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, f); err != nil {
return errors.WithStack(err)
}
return nil
}