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
251 lines
8.1 KiB
Go
251 lines
8.1 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package password
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/ory/x/otelx/semconv"
|
|
|
|
"github.com/ory/x/otelx"
|
|
|
|
"github.com/ory/kratos/text"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/ory/herodot"
|
|
"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"
|
|
)
|
|
|
|
func nodePasswordInput() *node.Node {
|
|
return NewPasswordNode("password", node.InputAttributeAutocompleteNewPassword)
|
|
}
|
|
|
|
func nodeSubmit() *node.Node {
|
|
return node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoRegistration())
|
|
}
|
|
|
|
var _ registration.FormHydrator = new(Strategy)
|
|
|
|
// Update Registration Flow with Password Method
|
|
//
|
|
// swagger:model updateRegistrationFlowWithPasswordMethod
|
|
type UpdateRegistrationFlowWithPasswordMethod struct {
|
|
// Password to sign the user up with
|
|
//
|
|
// required: true
|
|
Password string `json:"password"`
|
|
|
|
// The identity's traits
|
|
//
|
|
// required: true
|
|
Traits json.RawMessage `json:"traits"`
|
|
|
|
// The CSRF Token
|
|
CSRFToken string `json:"csrf_token"`
|
|
|
|
// Method to use
|
|
//
|
|
// This field must be set to `password` when using the password method.
|
|
//
|
|
// required: true
|
|
Method string `json:"method"`
|
|
|
|
// Transient data to pass along to any webhooks
|
|
//
|
|
// required: false
|
|
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
|
|
}
|
|
|
|
func (s *Strategy) handleRegistrationError(r *http.Request, f *registration.Flow, p UpdateRegistrationFlowWithPasswordMethod, err error) error {
|
|
if f != nil {
|
|
for _, n := range container.NewFromJSON("", node.DefaultGroup, p.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())
|
|
}
|
|
|
|
if f.Type == flow.TypeBrowser {
|
|
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *Strategy) decode(p *UpdateRegistrationFlowWithPasswordMethod, r *http.Request, ds *url.URL) (err error) {
|
|
return registration.DecodeBody(p, r, registrationSchema, ds)
|
|
}
|
|
|
|
func (s *Strategy) Register(_ http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) {
|
|
ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.password.Strategy.Register")
|
|
defer otelx.End(span, &err)
|
|
|
|
if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil {
|
|
return err
|
|
}
|
|
|
|
ds, err := f.IdentitySchema.URL(ctx, s.d.Config())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var p UpdateRegistrationFlowWithPasswordMethod
|
|
if err := s.decode(&p, r, ds); err != nil {
|
|
return s.handleRegistrationError(r, f, p, err)
|
|
}
|
|
|
|
f.TransientPayload = p.TransientPayload
|
|
|
|
if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
|
|
return s.handleRegistrationError(r, f, p, err)
|
|
}
|
|
|
|
if len(p.Password) == 0 {
|
|
return s.handleRegistrationError(r, f, p, schema.NewRequiredError("#/password", "password"))
|
|
}
|
|
|
|
if len(p.Traits) == 0 {
|
|
p.Traits = json.RawMessage("{}")
|
|
}
|
|
|
|
hpw := make(chan []byte)
|
|
errC := make(chan error)
|
|
go func() {
|
|
defer close(hpw)
|
|
defer close(errC)
|
|
|
|
h, err := s.d.Hasher(ctx).Generate(ctx, []byte(p.Password))
|
|
if err != nil {
|
|
errC <- err
|
|
return
|
|
}
|
|
hpw <- h
|
|
}()
|
|
|
|
if err != nil {
|
|
return s.handleRegistrationError(r, f, p, err)
|
|
}
|
|
|
|
i.Traits = identity.Traits(p.Traits)
|
|
// We have to set the credential here, so the identity validator can populate the identifiers.
|
|
// The password hash is computed in parallel and set later.
|
|
if err := i.SetCredentialsWithConfig(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}}, json.RawMessage("{}")); err != nil {
|
|
return s.handleRegistrationError(r, f, p, err)
|
|
}
|
|
|
|
if err := s.validateCredentials(ctx, i, p.Password); err != nil {
|
|
return s.handleRegistrationError(r, f, p, err)
|
|
}
|
|
|
|
select {
|
|
case err := <-errC:
|
|
return s.handleRegistrationError(r, f, p, err)
|
|
case h := <-hpw:
|
|
co, err := json.Marshal(&identity.CredentialsPassword{HashedPassword: string(h)})
|
|
if err != nil {
|
|
return s.handleRegistrationError(r, f, p, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to encode password options to JSON: %s", err)))
|
|
}
|
|
i.UpsertCredentialsConfig(s.ID(), co, 0)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Strategy) validateCredentials(ctx context.Context, i *identity.Identity, pw string) (err error) {
|
|
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.password.Strategy.validateCredentials")
|
|
defer otelx.End(span, &err)
|
|
|
|
if err := s.d.IdentityValidator().Validate(ctx, i); err != nil {
|
|
return err
|
|
}
|
|
|
|
c, ok := i.GetCredentials(identity.CredentialsTypePassword)
|
|
if !ok {
|
|
// This should never happen
|
|
return errors.WithStack(x.PseudoPanic.WithReasonf("identity object did not provide the %s CredentialType unexpectedly", identity.CredentialsTypePassword))
|
|
} else if len(c.Identifiers) == 0 {
|
|
return schema.NewMissingIdentifierError()
|
|
}
|
|
|
|
for _, id := range c.Identifiers {
|
|
if err := s.d.PasswordValidator().Validate(ctx, id, pw); err != nil {
|
|
if herodotErr := new(herodot.DefaultError); errors.As(err, &herodotErr) {
|
|
return err
|
|
}
|
|
if message := new(text.Message); errors.As(err, &message) {
|
|
return schema.NewPasswordPolicyViolationError("#/password", message)
|
|
}
|
|
return schema.NewPasswordPolicyViolationError("#/password", text.NewErrorValidationPasswordPolicyViolationGeneric(err.Error()))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) (err error) {
|
|
ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.password.Strategy.PopulateRegistrationMethod")
|
|
defer otelx.End(span, &err)
|
|
|
|
ds, err := f.IdentitySchema.URL(ctx, s.d.Config())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The group used to be `password`, but to make it consistent with other methods and two-step registration,
|
|
// it is now `default`. To make this switch backwards compatible, this feature flag is used.
|
|
//
|
|
// Previously, the behavior would be that the group of NodesFromJSONSchema would be `password`, but as soon
|
|
// as any other method (code, passkeys, two-step) would be enabled, the group would be `default`.
|
|
//
|
|
// Going forward, the default is that the group is `default` and the feature flag is not set.
|
|
//
|
|
// TODO remove me when everyone has migrated.
|
|
if !s.d.Config().SelfServiceFlowRegistrationTwoSteps(r.Context()) && node.UiNodeGroup(s.d.Config().SelfServiceFlowRegistrationPasswordMethodProfileGroup(r.Context())) == node.PasswordGroup {
|
|
span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "password_profile_registration_node_group=password"))
|
|
|
|
// This is the legacy code path. In the new code path, the profile method is responsible for hydrating the form
|
|
// nodes. In the old code path, the password method is responsible for hydrating the form nodes if it is
|
|
// the only method enabled.
|
|
nodes, err := container.NodesFromJSONSchema(r.Context(), node.PasswordGroup, ds.String(), "", nil, s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, n := range nodes {
|
|
f.UI.SetNode(n)
|
|
}
|
|
}
|
|
// TODO end
|
|
|
|
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
|
f.UI.Nodes.Upsert(nodePasswordInput())
|
|
f.UI.Nodes.Upsert(nodeSubmit())
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Strategy) PopulateRegistrationMethodCredentials(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error {
|
|
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
|
f.UI.Nodes.Upsert(nodePasswordInput())
|
|
f.UI.Nodes.Upsert(nodeSubmit())
|
|
return nil
|
|
}
|
|
|
|
func (s *Strategy) PopulateRegistrationMethodProfile(r *http.Request, f *registration.Flow, options ...registration.FormHydratorModifier) error {
|
|
// The profile method is responsible for rendering the profile form fields.
|
|
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
|
f.UI.Nodes.RemoveMatching(nodePasswordInput())
|
|
f.UI.Nodes.RemoveMatching(nodeSubmit())
|
|
return nil
|
|
}
|