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