Files
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

280 lines
8.7 KiB
Go

// 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)
}
}