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:
279
selfservice/strategy/link/sender.go
Normal file
279
selfservice/strategy/link/sender.go
Normal file
@@ -0,0 +1,279 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package link
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/kratos/courier"
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/hydra"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/selfservice/flow/recovery"
|
||||
"github.com/ory/kratos/selfservice/flow/verification"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/httpx"
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/sqlcon"
|
||||
"github.com/ory/x/urlx"
|
||||
)
|
||||
|
||||
type (
|
||||
senderDependencies interface {
|
||||
courier.Provider
|
||||
courier.ConfigProvider
|
||||
|
||||
identity.PoolProvider
|
||||
identity.ManagementProvider
|
||||
identity.PrivilegedPoolProvider
|
||||
logrusx.Provider
|
||||
config.Provider
|
||||
|
||||
VerificationTokenPersistenceProvider
|
||||
RecoveryTokenPersistenceProvider
|
||||
|
||||
hydra.Provider
|
||||
httpx.ClientProvider
|
||||
}
|
||||
SenderProvider interface {
|
||||
LinkSender() *Sender
|
||||
}
|
||||
|
||||
Sender struct {
|
||||
r senderDependencies
|
||||
}
|
||||
)
|
||||
|
||||
var ErrUnknownAddress = errors.New("verification requested for unknown address")
|
||||
|
||||
func NewSender(r senderDependencies) *Sender {
|
||||
return &Sender{r: r}
|
||||
}
|
||||
|
||||
// SendRecoveryLink sends a recovery link to the specified address
|
||||
//
|
||||
// If the address does not exist in the store and dispatching invalid emails is enabled (CourierEnableInvalidDispatch is
|
||||
// true), an email is still being sent to prevent account enumeration attacks. In that case, this function returns the
|
||||
// ErrUnknownAddress error.
|
||||
func (s *Sender) SendRecoveryLink(ctx context.Context, f *recovery.Flow, via, to string) error {
|
||||
s.r.Logger().
|
||||
WithField("via", via).
|
||||
WithSensitiveField("address", to).
|
||||
Debug("Preparing recovery link.")
|
||||
|
||||
address, err := s.r.IdentityPool().FindRecoveryAddressByValue(ctx, via, to)
|
||||
if errors.Is(err, sqlcon.ErrNoRows()) {
|
||||
notifyUnknownRecipients := s.r.Config().SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx)
|
||||
s.r.Logger().
|
||||
WithField("via", via).
|
||||
WithField("strategy", "link").
|
||||
WithSensitiveField("email_address", address).
|
||||
WithField("was_notified", notifyUnknownRecipients).
|
||||
Info("Account recovery was requested for an unknown address.")
|
||||
|
||||
transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if !notifyUnknownRecipients {
|
||||
// do nothing
|
||||
} else if err := s.send(ctx, string(via), email.NewRecoveryInvalid(s.r, &email.RecoveryInvalidModel{
|
||||
To: to,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
TransientPayload: transientPayload,
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.WithStack(ErrUnknownAddress)
|
||||
} else if err != nil {
|
||||
// DB error
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the identity associated with the recovery address
|
||||
i, err := s.r.IdentityPool().GetIdentity(ctx, address.IdentityID, identity.ExpandDefault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token := NewSelfServiceRecoveryToken(address, f, s.r.Config().SelfServiceLinkMethodLifespan(ctx))
|
||||
if err := s.r.RecoveryTokenPersister().CreateRecoveryToken(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.SendRecoveryTokenTo(ctx, f, i, address, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendVerificationLink sends a verification link to the specified address
|
||||
//
|
||||
// If the address does not exist in the store and dispatching invalid emails is enabled (CourierEnableInvalidDispatch is
|
||||
// true), an email is still being sent to prevent account enumeration attacks. In that case, this function returns the
|
||||
// ErrUnknownAddress error.
|
||||
func (s *Sender) SendVerificationLink(ctx context.Context, f *verification.Flow, via, to string) error {
|
||||
s.r.Logger().
|
||||
WithField("via", via).
|
||||
WithSensitiveField("address", to).
|
||||
Debug("Preparing verification link.")
|
||||
|
||||
address, err := s.r.IdentityPool().FindVerifiableAddressByValue(ctx, via, to)
|
||||
if errors.Is(err, sqlcon.ErrNoRows()) {
|
||||
notifyUnknownRecipients := s.r.Config().SelfServiceFlowVerificationNotifyUnknownRecipients(ctx)
|
||||
s.r.Logger().
|
||||
WithField("via", via).
|
||||
WithField("strategy", "link").
|
||||
WithSensitiveField("email_address", to).
|
||||
WithField("was_notified", notifyUnknownRecipients).
|
||||
Info("Address verification was requested for an unknown address.")
|
||||
|
||||
transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if !notifyUnknownRecipients {
|
||||
// do nothing
|
||||
} else if err := s.send(ctx, string(via), email.NewVerificationInvalid(s.r, &email.VerificationInvalidModel{
|
||||
To: to,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
TransientPayload: transientPayload,
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.WithStack(ErrUnknownAddress)
|
||||
} else if err != nil {
|
||||
// DB error
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the identity associated with the verification address
|
||||
i, err := s.r.IdentityPool().GetIdentity(ctx, address.IdentityID, identity.ExpandDefault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token := NewSelfServiceVerificationToken(address, f, s.r.Config().SelfServiceLinkMethodLifespan(ctx))
|
||||
if err := s.r.VerificationTokenPersister().CreateVerificationToken(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.SendVerificationTokenTo(ctx, f, i, address, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
address.Status = identity.VerifiableAddressStatusSent
|
||||
return s.r.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "status")
|
||||
}
|
||||
|
||||
func (s *Sender) SendRecoveryTokenTo(ctx context.Context, f *recovery.Flow, i *identity.Identity, address *identity.RecoveryAddress, token *RecoveryToken) error {
|
||||
s.r.Logger().
|
||||
WithField("via", address.Via).
|
||||
WithField("identity_id", address.IdentityID).
|
||||
WithField("recovery_link_id", token.ID).
|
||||
WithSensitiveField("email_address", address.Value).
|
||||
WithSensitiveField("recovery_link_token", token.Token).
|
||||
Info("Sending out recovery email with recovery link.")
|
||||
|
||||
model, err := x.StructToMap(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
recoveryUrl := urlx.CopyWithQuery(
|
||||
urlx.AppendPaths(x.CourierBaseURL(f.GetCourierBaseURL(), s.r.Config().SelfPublicURL(ctx)), recovery.RouteSubmitFlow),
|
||||
url.Values{
|
||||
"token": {token.Token},
|
||||
"flow": {f.ID.String()},
|
||||
}).
|
||||
String()
|
||||
|
||||
return s.send(ctx, string(address.Via), email.NewRecoveryValid(s.r,
|
||||
&email.RecoveryValidModel{
|
||||
To: address.Value,
|
||||
RecoveryURL: recoveryUrl,
|
||||
Identity: model,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.r.Config().SelfServiceLinkMethodLifespan(ctx).Minutes()),
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *Sender) SendVerificationTokenTo(ctx context.Context, f *verification.Flow, i *identity.Identity, sendable identity.VerifiableAddressLike, token *VerificationToken) error {
|
||||
address, via := sendable.Address(), sendable.DeliveryVia()
|
||||
s.r.Logger().
|
||||
WithField("via", via).
|
||||
WithField("identity_id", i.ID).
|
||||
WithField("verification_link_id", token.ID).
|
||||
WithSensitiveField("email_address", address).
|
||||
WithSensitiveField("verification_link_token", token.Token).
|
||||
Info("Sending out verification email with verification link.")
|
||||
|
||||
model, err := x.StructToMap(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// If the flow was initiated by an OAuth2 login challenge, expose a scoped
|
||||
// view of the OAuth2 client to the courier templates so that messages can
|
||||
// be branded per client.
|
||||
if f.OAuth2LoginChallenge != "" && f.HydraLoginRequest == nil {
|
||||
hlr, err := s.r.Hydra().GetLoginRequest(ctx, string(f.OAuth2LoginChallenge))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
f.HydraLoginRequest = hlr
|
||||
}
|
||||
|
||||
verificationUrl := urlx.CopyWithQuery(
|
||||
urlx.AppendPaths(x.CourierBaseURL(f.GetCourierBaseURL(), s.r.Config().SelfPublicURL(ctx)), verification.RouteSubmitFlow),
|
||||
url.Values{
|
||||
"flow": {f.ID.String()},
|
||||
"token": {token.Token},
|
||||
}).String()
|
||||
|
||||
if err := s.send(ctx, via, email.NewVerificationValid(s.r,
|
||||
&email.VerificationValidModel{
|
||||
To: address,
|
||||
VerificationURL: verificationUrl,
|
||||
Identity: model,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.r.Config().SelfServiceLinkMethodLifespan(ctx).Minutes()),
|
||||
OAuth2LoginRequest: template.NewOAuth2LoginRequest(f.HydraLoginRequest),
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Sender) send(ctx context.Context, via string, t courier.EmailTemplate) error {
|
||||
switch via {
|
||||
case identity.AddressTypeEmail:
|
||||
c, err := s.r.Courier(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.QueueEmail(ctx, t)
|
||||
return err
|
||||
default:
|
||||
return errors.Errorf("received unexpected via type: %s", via)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user