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
245 lines
9.0 KiB
Go
245 lines
9.0 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package password
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
|
|
"github.com/ory/x/otelx"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/ory/herodot"
|
|
"github.com/ory/kratos/hash"
|
|
"github.com/ory/kratos/identity"
|
|
"github.com/ory/kratos/schema"
|
|
"github.com/ory/kratos/selfservice/flow"
|
|
"github.com/ory/kratos/selfservice/flow/login"
|
|
"github.com/ory/kratos/selfservice/flowhelpers"
|
|
"github.com/ory/kratos/selfservice/hook"
|
|
"github.com/ory/kratos/selfservice/strategy/idfirst"
|
|
"github.com/ory/kratos/session"
|
|
"github.com/ory/kratos/text"
|
|
"github.com/ory/kratos/ui/node"
|
|
"github.com/ory/kratos/x"
|
|
"github.com/ory/x/decoderx"
|
|
)
|
|
|
|
var _ login.AAL1FormHydrator = new(Strategy)
|
|
|
|
func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, payload updateLoginFlowWithPasswordMethod, err error) error {
|
|
if f != nil {
|
|
f.UI.Nodes.ResetNodes("password")
|
|
f.UI.Nodes.SetValueAttribute("identifier", cmp.Or(payload.Identifier, payload.LegacyIdentifier))
|
|
if f.Type == flow.TypeBrowser {
|
|
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (i *identity.Identity, err error) {
|
|
ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.password.Strategy.Login")
|
|
defer otelx.End(span, &err)
|
|
|
|
if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil {
|
|
span.SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not AAL1"))
|
|
return nil, err
|
|
}
|
|
|
|
if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var p updateLoginFlowWithPasswordMethod
|
|
if err := decoderx.Decode(r, &p,
|
|
decoderx.HTTPDecoderSetValidatePayloads(true),
|
|
decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema),
|
|
decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil {
|
|
return nil, s.handleLoginError(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 nil, s.handleLoginError(r, f, p, err)
|
|
}
|
|
|
|
identifier := cmp.Or(p.Identifier, p.LegacyIdentifier)
|
|
i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), identifier)
|
|
if err != nil {
|
|
time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(ctx).ExpectedDuration, s.d.Config().HasherArgon2(ctx).ExpectedDeviation))
|
|
return nil, s.handleLoginError(r, f, p, errors.WithStack(schema.NewInvalidCredentialsError()))
|
|
}
|
|
|
|
var o identity.CredentialsPassword
|
|
d := json.NewDecoder(bytes.NewBuffer(c.Config))
|
|
if err := d.Decode(&o); err != nil {
|
|
return nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrInternalServerError().WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err)), i.ID)
|
|
}
|
|
|
|
if o.ShouldUsePasswordMigrationHook() {
|
|
pwHook := s.d.Config().PasswordMigrationHook(ctx)
|
|
if !pwHook.Enabled {
|
|
return nil, x.WrapWithIdentityIDError(errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("Password migration hook is not enabled but password migration is requested.")), i.ID)
|
|
}
|
|
|
|
migrationHook := hook.NewPasswordMigrationHook(s.d, &pwHook.Config)
|
|
err = migrationHook.Execute(ctx, r, f, &hook.PasswordMigrationRequest{
|
|
Identifier: identifier,
|
|
Password: p.Password,
|
|
Identity: i,
|
|
})
|
|
if err != nil {
|
|
return nil, s.handleLoginError(r, f, p, x.WrapWithIdentityIDError(err, i.ID))
|
|
}
|
|
|
|
if err := s.migratePasswordHash(ctx, i.ID, []byte(p.Password)); err != nil {
|
|
return nil, s.handleLoginError(r, f, p, x.WrapWithIdentityIDError(err, i.ID))
|
|
}
|
|
} else {
|
|
if err := hash.Compare(ctx, []byte(p.Password), []byte(o.HashedPassword)); err != nil {
|
|
return nil, s.handleLoginError(r, f, p, errors.WithStack(x.WrapWithIdentityIDError(schema.NewInvalidCredentialsError(), i.ID)))
|
|
}
|
|
|
|
if !s.d.Hasher(ctx).Understands([]byte(o.HashedPassword)) {
|
|
if err := s.migratePasswordHash(ctx, i.ID, []byte(p.Password)); err != nil {
|
|
s.d.Logger().Warnf("Unable to migrate password hash for identity %s: %s Keeping existing password hash and continuing.", i.ID, x.WrapWithIdentityIDError(err, i.ID))
|
|
}
|
|
}
|
|
}
|
|
|
|
f.Active = s.ID()
|
|
if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil {
|
|
return nil, s.handleLoginError(r, f, p, errors.WithStack(x.WrapWithIdentityIDError(herodot.ErrInternalServerError().WithReason("Could not update flow").WithDebug(err.Error()), i.ID)))
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
func (s *Strategy) migratePasswordHash(ctx context.Context, identifier uuid.UUID, password []byte) (err error) {
|
|
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.password.Strategy.migratePasswordHash")
|
|
defer otelx.End(span, &err)
|
|
|
|
hpw, err := s.d.Hasher(ctx).Generate(ctx, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
co, err := json.Marshal(&identity.CredentialsPassword{HashedPassword: string(hpw)})
|
|
if err != nil {
|
|
return errors.Wrap(err, "unable to encode password configuration to JSON")
|
|
}
|
|
|
|
i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, identifier)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c, ok := i.GetCredentials(s.ID())
|
|
if !ok {
|
|
return errors.New("expected to find password credential but could not")
|
|
}
|
|
|
|
c.Config = co
|
|
i.SetCredentials(s.ID(), *c)
|
|
|
|
return s.d.IdentityManager().Update(ctx, i, identity.ManagerAllowWriteProtectedTraits)
|
|
}
|
|
|
|
func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow, _ *session.Session) (err error) {
|
|
ctx := r.Context()
|
|
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.password.Strategy.PopulateLoginMethodFirstFactorRefresh")
|
|
defer otelx.End(span, &err)
|
|
|
|
identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID())
|
|
if identifier == "" {
|
|
return nil
|
|
}
|
|
|
|
// If we don't have a password set, do not show the password field.
|
|
count, err := s.CountActiveFirstFactorCredentials(ctx, id.Credentials)
|
|
if err != nil {
|
|
return err
|
|
} else if count == 0 {
|
|
return nil
|
|
}
|
|
|
|
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
|
sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden))
|
|
sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
|
|
sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin()))
|
|
return nil
|
|
}
|
|
|
|
func (s *Strategy) addIdentifierNode(r *http.Request, sr *login.Flow) error {
|
|
ds, err := sr.IdentitySchema.URL(r.Context(), s.d.Config())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String(), s.d.Config().SecurityDisallowRefInIdentitySchemas(r.Context()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel))
|
|
// node.WithInputAttributes(func(a *node.InputAttributes) { // TODO: https://github.com/ory-corp/cloud/issues/12416
|
|
// // Hint password managers to autofill the saved username for this site.
|
|
// a.Autocomplete = node.InputAttributeAutocompleteUsername
|
|
// })).WithMetaLabel(identifierLabel))
|
|
return nil
|
|
}
|
|
|
|
func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error {
|
|
if err := s.addIdentifierNode(r, sr); err != nil {
|
|
return err
|
|
}
|
|
|
|
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
|
sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
|
|
sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword()))
|
|
return nil
|
|
}
|
|
|
|
func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) (err error) {
|
|
ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.password.Strategy.PopulateLoginMethodIdentifierFirstCredentials")
|
|
defer otelx.End(span, &err)
|
|
|
|
o := login.NewFormHydratorOptions(opts)
|
|
|
|
var count int
|
|
if o.IdentityHint != nil {
|
|
var err error
|
|
// If we have an identity hint we can perform identity credentials discovery and
|
|
// hide this credential if it should not be included.
|
|
if count, err = s.CountActiveFirstFactorCredentials(ctx, o.IdentityHint.Credentials); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(ctx) {
|
|
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
|
|
sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
|
|
sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword()))
|
|
}
|
|
|
|
if count == 0 {
|
|
return errors.WithStack(idfirst.ErrNoCredentialsFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error {
|
|
return nil
|
|
}
|