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
360 lines
11 KiB
Go
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
|
|
}
|