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:
359
selfservice/strategy/code/strategy_registration.go
Normal file
359
selfservice/strategy/code/strategy_registration.go
Normal file
@@ -0,0 +1,359 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user