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:
242
selfservice/strategy/password/settings.go
Normal file
242
selfservice/strategy/password/settings.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package password
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/ory/x/otelx"
|
||||
|
||||
"github.com/ory/kratos/hash"
|
||||
"github.com/ory/kratos/text"
|
||||
|
||||
"github.com/ory/kratos/ui/node"
|
||||
|
||||
"github.com/ory/kratos/session"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
"github.com/ory/kratos/continuity"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/schema"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/selfservice/flow/settings"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/decoderx"
|
||||
)
|
||||
|
||||
func (s *Strategy) SettingsStrategyID() string {
|
||||
return identity.CredentialsTypePassword.String()
|
||||
}
|
||||
|
||||
// Update Settings Flow with Password Method
|
||||
//
|
||||
// swagger:model updateSettingsFlowWithPasswordMethod
|
||||
type updateSettingsFlowWithPasswordMethod struct {
|
||||
// Password is the updated password
|
||||
//
|
||||
// required: true
|
||||
Password string `json:"password"`
|
||||
|
||||
// CSRFToken is the anti-CSRF token
|
||||
CSRFToken string `json:"csrf_token"`
|
||||
|
||||
// Method
|
||||
//
|
||||
// Should be set to password when trying to update a password.
|
||||
//
|
||||
// required: true
|
||||
Method string `json:"method"`
|
||||
|
||||
// Flow is flow ID.
|
||||
//
|
||||
// swagger:ignore
|
||||
Flow string `json:"flow"`
|
||||
|
||||
// Transient data to pass along to any webhooks
|
||||
//
|
||||
// required: false
|
||||
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
|
||||
}
|
||||
|
||||
func (p *updateSettingsFlowWithPasswordMethod) GetFlowID() uuid.UUID {
|
||||
return x.ParseUUID(p.Flow)
|
||||
}
|
||||
|
||||
func (p *updateSettingsFlowWithPasswordMethod) SetFlowID(rid uuid.UUID) {
|
||||
p.Flow = rid.String()
|
||||
}
|
||||
|
||||
func (s *Strategy) Settings(ctx context.Context, w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) {
|
||||
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.password.Strategy.Settings")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
var p updateSettingsFlowWithPasswordMethod
|
||||
ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p)
|
||||
if errors.Is(err, settings.ErrContinuePreviousAction) {
|
||||
return ctxUpdate, s.continueSettingsFlow(ctx, r, ctxUpdate, p)
|
||||
} else if err != nil {
|
||||
return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
|
||||
}
|
||||
|
||||
if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil {
|
||||
return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
|
||||
}
|
||||
|
||||
if err := s.decodeSettingsFlow(r, &p); err != nil {
|
||||
return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
|
||||
}
|
||||
|
||||
// This does not come from the payload!
|
||||
p.Flow = ctxUpdate.Flow.ID.String()
|
||||
if err := s.continueSettingsFlow(ctx, r, ctxUpdate, p); err != nil {
|
||||
return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
|
||||
}
|
||||
|
||||
return ctxUpdate, nil
|
||||
}
|
||||
|
||||
func (s *Strategy) decodeSettingsFlow(r *http.Request, dest interface{}) error {
|
||||
compiler, err := decoderx.HTTPRawJSONSchemaCompiler(settingsSchema)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return decoderx.Decode(r, dest, compiler,
|
||||
decoderx.HTTPKeepRequestBody(true),
|
||||
decoderx.HTTPDecoderAllowedMethods("POST", "GET"),
|
||||
decoderx.HTTPDecoderSetValidatePayloads(true),
|
||||
decoderx.HTTPDecoderJSONFollowsFormFormat(),
|
||||
)
|
||||
}
|
||||
|
||||
// Try to find a password hash in the credentials. Returns it if found, otherwise return an empty string.
|
||||
func getPasswordHashFromCredential(creds map[identity.CredentialsType]identity.Credentials) string {
|
||||
if creds == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
cred, ok := creds[identity.CredentialsTypePassword]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
var hashedPassword identity.CredentialsPassword
|
||||
if err := json.Unmarshal(cred.Config, &hashedPassword); err != nil {
|
||||
return ""
|
||||
}
|
||||
return hashedPassword.HashedPassword
|
||||
}
|
||||
|
||||
// Detect whether the new password is the same as the old password.
|
||||
// This is helpful to a user, e.g. in the case of a password leak: they want to change their password,
|
||||
// and unknowingly set the new password to be the same as the old one (that leaked). We force them to
|
||||
// set a different password in that case.
|
||||
func isNewPasswordSameAsOld(ctx context.Context, oldHashedPassword string, newPassword string) bool {
|
||||
if oldHashedPassword == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// `hash.Compare` returns `nil` on 'success' i.e. old and new are the same.
|
||||
return hash.Compare(ctx, []byte(newPassword), []byte(oldHashedPassword)) == nil
|
||||
}
|
||||
|
||||
func (s *Strategy) continueSettingsFlow(ctx context.Context, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithPasswordMethod) error {
|
||||
if err := flow.MethodEnabledAndAllowed(ctx, flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)).Before(time.Now()) {
|
||||
return errors.WithStack(settings.NewFlowNeedsReAuth())
|
||||
}
|
||||
|
||||
if len(p.Password) == 0 {
|
||||
return schema.NewRequiredError("#/password", "password")
|
||||
}
|
||||
|
||||
i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
var newPasswordHash []byte
|
||||
// Extract an immutable value to avoid data races between goroutines.
|
||||
oldHashedPassword := getPasswordHashFromCredential(i.Credentials)
|
||||
|
||||
// Do in parallel due to limitations of the `bcrypt` library and for performance:
|
||||
// - `hash(newPassword)` (expensive).
|
||||
// - Check that the new password is not the same as the old password,
|
||||
// which internally computes `hash(newPassword)` (expensive).
|
||||
// - `validateCredentials` which may call the HaveIBeenPawned external API.
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
newPasswordHash, err = s.d.Hasher(ctx).Generate(ctx, []byte(p.Password))
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
if isNewPasswordSameAsOld(ctx, oldHashedPassword, p.Password) {
|
||||
return schema.NewPasswordPolicyViolationError("#/password", text.NewErrorValidationPasswordNewSameAsOld())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
g.Go(func() error {
|
||||
// Note: this goroutine mutates `i` so careful not to share it with other goroutines!
|
||||
|
||||
// The credentials could have been modified in many ways possible. To keep it simple, we reset, and the validators
|
||||
// will populate it correctly.
|
||||
i.UpsertCredentialsConfig(s.ID(), []byte("{}"), 0)
|
||||
return s.validateCredentials(ctx, i, p.Password)
|
||||
})
|
||||
if err := g.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
co, err := json.Marshal(&identity.CredentialsPassword{HashedPassword: string(newPasswordHash)})
|
||||
if err != nil {
|
||||
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to encode password options to JSON: %s", err))
|
||||
}
|
||||
i.UpsertCredentialsConfig(s.ID(), co, 0)
|
||||
ctxUpdate.UpdateIdentity(i)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) PopulateSettingsMethod(ctx context.Context, r *http.Request, _ *identity.Identity, f *settings.Flow) (err error) {
|
||||
_, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.password.Strategy.PopulateSettingsMethod")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
||||
f.UI.Nodes.Upsert(NewPasswordNode("password", node.InputAttributeAutocompleteNewPassword).WithMetaLabel(text.NewInfoNodeInputPassword()))
|
||||
f.UI.Nodes.Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSave()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) handleSettingsError(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithPasswordMethod, err error) error {
|
||||
// Do not pause flow if the flow type is an API flow as we can't save cookies in those flows.
|
||||
if e := new(settings.FlowNeedsReAuth); errors.As(err, &e) && ctxUpdate.Flow != nil && ctxUpdate.Flow.Type == flow.TypeBrowser {
|
||||
if _, err := s.d.ContinuityManager().Pause(ctx, w, r, settings.ContinuityKey(s.SettingsStrategyID()), continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)), settings.ContinuityOptions(p, ctxUpdate.GetSessionIdentity())...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ctxUpdate.Flow != nil {
|
||||
ctxUpdate.Flow.UI.ResetMessages()
|
||||
ctxUpdate.Flow.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user