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
167 lines
5.0 KiB
Go
167 lines
5.0 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package hook
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gofrs/uuid"
|
|
|
|
"github.com/ory/kratos/driver/config"
|
|
"github.com/ory/kratos/selfservice/flow"
|
|
"github.com/ory/kratos/selfservice/flow/verification"
|
|
"github.com/ory/kratos/text"
|
|
"github.com/ory/kratos/x"
|
|
"github.com/ory/kratos/x/nosurfx"
|
|
"github.com/ory/x/clock"
|
|
"github.com/ory/x/httpx"
|
|
"github.com/ory/x/otelx"
|
|
"github.com/ory/x/otelx/semconv"
|
|
"github.com/ory/x/sqlxx"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/ory/kratos/ui/node"
|
|
|
|
"github.com/ory/herodot"
|
|
|
|
"github.com/ory/kratos/identity"
|
|
"github.com/ory/kratos/selfservice/flow/login"
|
|
"github.com/ory/kratos/session"
|
|
)
|
|
|
|
var _ login.PostHookExecutor = new(AddressVerifier)
|
|
|
|
type (
|
|
addressVerifierDependencies interface {
|
|
clock.Provider
|
|
config.Provider
|
|
nosurfx.CSRFTokenGeneratorProvider
|
|
nosurfx.CSRFProvider
|
|
verification.StrategyProvider
|
|
verification.FlowPersistenceProvider
|
|
identity.PrivilegedPoolProvider
|
|
httpx.WriterProvider
|
|
otelx.Provider
|
|
}
|
|
AddressVerifier struct {
|
|
r addressVerifierDependencies
|
|
}
|
|
)
|
|
|
|
func NewAddressVerifier(r addressVerifierDependencies) *AddressVerifier {
|
|
return &AddressVerifier{
|
|
r: r,
|
|
}
|
|
}
|
|
|
|
func (e *AddressVerifier) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, f *login.Flow, s *session.Session) (err error) {
|
|
ctx, span := e.r.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.hook.Verifier.do")
|
|
r = r.WithContext(ctx)
|
|
defer otelx.End(span, &err)
|
|
|
|
// TODO remove once flag is removed.
|
|
if e.r.Config().UseLegacyRequireVerifiedLoginError(ctx) {
|
|
if f.Active != identity.CredentialsTypePassword {
|
|
span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "legacy_require_verified_login_error"))
|
|
return nil
|
|
}
|
|
}
|
|
// END TODO
|
|
|
|
if len(s.Identity.VerifiableAddresses) == 0 {
|
|
return errors.WithStack(herodot.ErrMisconfiguration().WithReason("A misconfiguration prevents login. Expected to find a verification address but this identity does not have one assigned."))
|
|
}
|
|
|
|
for _, va := range s.Identity.VerifiableAddresses {
|
|
if va.Verified {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
strategies, primaryStrategy, err := e.r.GetActiveVerificationStrategies(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO remove once flag is removed.
|
|
if e.r.Config().UseLegacyRequireVerifiedLoginError(ctx) {
|
|
span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "legacy_require_verified_login_error"))
|
|
return login.ErrAddressNotVerified()
|
|
}
|
|
// END TODO
|
|
|
|
i := s.Identity
|
|
for k := range i.VerifiableAddresses {
|
|
address := &i.VerifiableAddresses[k]
|
|
if address.Value == "" {
|
|
continue
|
|
}
|
|
|
|
verificationFlow, err := verification.NewPostHookFlow(e.r,
|
|
e.r.Config().SelfServiceFlowVerificationRequestLifespan(ctx),
|
|
e.r.GenerateCSRFToken(r), r, strategies, f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
verificationFlow.State = flow.StateEmailSent
|
|
for _, strategy := range strategies {
|
|
if ps, isPrimary := strategy.(verification.PrimaryStrategy); isPrimary {
|
|
verificationFlow.Active = sqlxx.NullString(ps.NodeGroup())
|
|
}
|
|
if err := strategy.PopulateVerificationMethod(r, verificationFlow); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// The code strategy's PopulateVerificationMethod sets the email "code sent"
|
|
// message unconditionally; override it with the message that matches the
|
|
// channel the code was sent over. The legacy link strategy sends a link,
|
|
// not a code, and sets no message — leave it untouched.
|
|
if primaryStrategy.NodeGroup() == node.CodeGroup {
|
|
verificationFlow.UI.Messages.Set(text.VerificationCodeSentMessage(address.Via))
|
|
}
|
|
|
|
verificationFlow.SessionID = uuid.NullUUID{UUID: s.ID, Valid: true}
|
|
verificationFlow.IdentityID = uuid.NullUUID{UUID: s.Identity.ID, Valid: true}
|
|
|
|
verificationFlow.UI.Nodes.Append(
|
|
node.NewInputField(address.Via, address.Value, node.CodeGroup, node.InputAttributeTypeSubmit).
|
|
WithMetaLabel(text.NewInfoNodeResendOTP()),
|
|
)
|
|
|
|
if err := e.r.VerificationFlowPersister().CreateVerificationFlow(ctx, verificationFlow); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := primaryStrategy.SendVerificationCode(ctx, verificationFlow, i, address); err != nil {
|
|
return err
|
|
}
|
|
|
|
address.Status = identity.VerifiableAddressStatusSent
|
|
if err := e.r.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "status"); err != nil {
|
|
return err
|
|
}
|
|
|
|
flowURL := verificationFlow.AppendTo(e.r.Config().SelfServiceFlowVerificationUI(ctx)).String()
|
|
continueWith := flow.NewContinueWithVerificationUI(verificationFlow.ID, address.Value, flowURL)
|
|
f.AddContinueWith(continueWith)
|
|
|
|
if x.IsJSONRequest(r) {
|
|
e.r.Writer().WriteErrorCode(w, r, http.StatusForbidden, flow.ErrorWithContinueWith(login.ErrAddressNotVerified(), continueWith))
|
|
return errors.WithStack(login.ErrHookAbortFlow)
|
|
}
|
|
|
|
if x.IsBrowserRequest(r) {
|
|
http.Redirect(w, r, flowURL, http.StatusSeeOther)
|
|
return errors.WithStack(login.ErrHookAbortFlow)
|
|
}
|
|
|
|
return errors.WithStack(login.ErrHookAbortFlow)
|
|
}
|
|
|
|
return login.ErrAddressNotVerified()
|
|
}
|