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
280 lines
8.7 KiB
Go
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)
|
|
}
|
|
}
|