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:
539
selfservice/strategy/code/code_sender.go
Normal file
539
selfservice/strategy/code/code_sender.go
Normal file
@@ -0,0 +1,539 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package code
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
"github.com/ory/kratos/courier"
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/sms"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/hydra"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/selfservice/flow/recovery"
|
||||
"github.com/ory/kratos/selfservice/flow/verification"
|
||||
"github.com/ory/kratos/selfservice/hook"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/httpx"
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/sqlcon"
|
||||
"github.com/ory/x/stringsx"
|
||||
"github.com/ory/x/urlx"
|
||||
)
|
||||
|
||||
type (
|
||||
senderDependencies interface {
|
||||
courier.Provider
|
||||
courier.ConfigProvider
|
||||
|
||||
identity.PoolProvider
|
||||
identity.ManagementProvider
|
||||
identity.PrivilegedPoolProvider
|
||||
logrusx.Provider
|
||||
config.Provider
|
||||
|
||||
RecoveryCodePersistenceProvider
|
||||
VerificationCodePersistenceProvider
|
||||
RegistrationCodePersistenceProvider
|
||||
LoginCodePersistenceProvider
|
||||
|
||||
hydra.Provider
|
||||
httpx.ClientProvider
|
||||
}
|
||||
SenderProvider interface {
|
||||
CodeSender() *Sender
|
||||
}
|
||||
|
||||
Sender struct {
|
||||
deps senderDependencies
|
||||
}
|
||||
Address struct {
|
||||
To string
|
||||
Via identity.CodeChannel
|
||||
}
|
||||
)
|
||||
|
||||
func ErrUnknownAddress() *herodot.DefaultError {
|
||||
return herodot.ErrNotFound().WithReason("recovery requested for unknown address")
|
||||
}
|
||||
|
||||
func NewSender(deps senderDependencies) *Sender {
|
||||
return &Sender{deps: deps}
|
||||
}
|
||||
|
||||
func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identity, header http.Header, addresses ...Address) error {
|
||||
s.deps.Logger().
|
||||
WithSensitiveField("address", addresses).
|
||||
Debugf("Preparing %s code", f.GetFlowName())
|
||||
|
||||
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.
|
||||
var oauth2LoginRequest *template.OAuth2LoginRequest
|
||||
if p, ok := f.(flow.HydraLoginRequestProvider); ok {
|
||||
oauth2LoginRequest = template.NewOAuth2LoginRequest(p.GetHydraLoginRequest())
|
||||
}
|
||||
|
||||
// send to all addresses
|
||||
for _, address := range addresses {
|
||||
// We have to generate a unique code per address, or otherwise it is not possible to link which
|
||||
// address was used to verify the code.
|
||||
//
|
||||
// See also [this discussion](https://github.com/ory/kratos/pull/3456#discussion_r1307560988).
|
||||
rawCode := GenerateCode()
|
||||
|
||||
switch f.GetFlowName() {
|
||||
case flow.RegistrationFlow:
|
||||
code, err := s.deps.
|
||||
RegistrationCodePersister().
|
||||
CreateRegistrationCode(ctx, &CreateRegistrationCodeParams{
|
||||
AddressType: address.Via,
|
||||
RawCode: rawCode,
|
||||
ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx),
|
||||
FlowID: f.GetID(),
|
||||
Address: address.To,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
model, err := x.StructToMap(id.Traits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.deps.Logger().
|
||||
WithField("registration_flow_id", code.FlowID).
|
||||
WithField("registration_code_id", code.ID).
|
||||
WithSensitiveField("registration_code", rawCode).
|
||||
Info("Sending out registration email with code.")
|
||||
|
||||
var t courier.Template
|
||||
switch address.Via {
|
||||
case identity.ChannelTypeEmail:
|
||||
t = email.NewRegistrationCodeValid(s.deps, &email.RegistrationCodeValidModel{
|
||||
To: address.To,
|
||||
RegistrationCode: rawCode,
|
||||
Traits: model,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()),
|
||||
OAuth2LoginRequest: oauth2LoginRequest,
|
||||
UserRequestHeaders: hook.RemoveDisallowedHeaders(header, s.deps.Config().WebhookHeaderAllowlist(ctx)),
|
||||
})
|
||||
case identity.ChannelTypeSMS:
|
||||
t = sms.NewRegistrationCodeValid(s.deps, &sms.RegistrationCodeValidModel{
|
||||
To: address.To,
|
||||
RegistrationCode: rawCode,
|
||||
Identity: model,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
RequestURLDomain: requestURLDomain(f.GetRequestURL()),
|
||||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()),
|
||||
OAuth2LoginRequest: oauth2LoginRequest,
|
||||
UserRequestHeaders: hook.RemoveDisallowedHeaders(header, s.deps.Config().WebhookHeaderAllowlist(ctx)),
|
||||
})
|
||||
}
|
||||
|
||||
if err := s.send(ctx, string(address.Via), t); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
case flow.LoginFlow:
|
||||
code, err := s.deps.
|
||||
LoginCodePersister().
|
||||
CreateLoginCode(ctx, &CreateLoginCodeParams{
|
||||
AddressType: address.Via,
|
||||
Address: address.To,
|
||||
RawCode: rawCode,
|
||||
ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx),
|
||||
FlowID: f.GetID(),
|
||||
IdentityID: id.ID,
|
||||
IdentityRegion: id.Region,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
model, err := x.StructToMap(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.deps.Logger().
|
||||
WithField("login_flow_id", code.FlowID).
|
||||
WithField("login_code_id", code.ID).
|
||||
WithSensitiveField("login_code", rawCode).
|
||||
Info("Sending out login email with code.")
|
||||
|
||||
var t courier.Template
|
||||
switch address.Via {
|
||||
case identity.ChannelTypeEmail:
|
||||
t = email.NewLoginCodeValid(s.deps, &email.LoginCodeValidModel{
|
||||
To: address.To,
|
||||
LoginCode: rawCode,
|
||||
Identity: model,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()),
|
||||
OAuth2LoginRequest: oauth2LoginRequest,
|
||||
UserRequestHeaders: hook.RemoveDisallowedHeaders(header, s.deps.Config().WebhookHeaderAllowlist(ctx)),
|
||||
})
|
||||
case identity.ChannelTypeSMS:
|
||||
t = sms.NewLoginCodeValid(s.deps, &sms.LoginCodeValidModel{
|
||||
To: address.To,
|
||||
LoginCode: rawCode,
|
||||
Identity: model,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
RequestURLDomain: requestURLDomain(f.GetRequestURL()),
|
||||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()),
|
||||
OAuth2LoginRequest: oauth2LoginRequest,
|
||||
UserRequestHeaders: hook.RemoveDisallowedHeaders(header, s.deps.Config().WebhookHeaderAllowlist(ctx)),
|
||||
})
|
||||
}
|
||||
|
||||
if err := s.send(ctx, string(address.Via), t); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
default:
|
||||
return errors.WithStack(errors.New("received unknown flow type"))
|
||||
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendRecoveryCode sends a recovery code 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) SendRecoveryCode(ctx context.Context, f *recovery.Flow, via, to string, requestHeader http.Header) error {
|
||||
s.deps.Logger().
|
||||
WithField("via", via).
|
||||
WithSensitiveField("address", to).
|
||||
Debug("Preparing recovery code.")
|
||||
|
||||
address, err := s.deps.IdentityPool().FindRecoveryAddressByValue(ctx, via, to)
|
||||
if errors.Is(err, sqlcon.ErrNoRows()) {
|
||||
notifyUnknownRecipients := s.deps.Config().SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx)
|
||||
s.deps.Logger().
|
||||
WithField("via", via).
|
||||
WithSensitiveField("address", to).
|
||||
WithField("strategy", "code").
|
||||
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)
|
||||
}
|
||||
|
||||
// We only send a notification if the configuration allows it *and* the channel is email.
|
||||
// That's because we pay per SMS sent (typically) so we want to avoid that, contrary to email.
|
||||
shouldNotifyOfUnkownRecipient := notifyUnknownRecipients && via == identity.AddressTypeEmail
|
||||
|
||||
if !shouldNotifyOfUnkownRecipient {
|
||||
// do nothing
|
||||
} else if err := s.send(ctx, via, email.NewRecoveryCodeInvalid(s.deps, &email.RecoveryCodeInvalidModel{
|
||||
To: to,
|
||||
RequestURL: f.RequestURL,
|
||||
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.deps.IdentityPool().GetIdentity(ctx, address.IdentityID, identity.ExpandDefault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawCode := GenerateCode()
|
||||
|
||||
var code *RecoveryCode
|
||||
if code, err = s.deps.
|
||||
RecoveryCodePersister().
|
||||
CreateRecoveryCode(ctx, &CreateRecoveryCodeParams{
|
||||
RawCode: rawCode,
|
||||
CodeType: RecoveryCodeTypeSelfService,
|
||||
ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx),
|
||||
RecoveryAddress: address,
|
||||
FlowID: f.ID,
|
||||
IdentityID: i.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.SendRecoveryCodeTo(ctx, i, rawCode, code, f, requestHeader)
|
||||
}
|
||||
|
||||
func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, codeString string, code *RecoveryCode, f *recovery.Flow, requestHeader http.Header) error {
|
||||
s.deps.Logger().
|
||||
WithField("via", code.RecoveryAddress.Via).
|
||||
WithField("identity_id", code.RecoveryAddress.IdentityID).
|
||||
WithField("recovery_code_id", code.ID).
|
||||
WithSensitiveField("address", code.RecoveryAddress.Value).
|
||||
WithSensitiveField("recovery_code", codeString).
|
||||
Info("Sending out recovery code.")
|
||||
|
||||
model, err := x.StructToMap(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
var t courier.Template
|
||||
|
||||
switch code.RecoveryAddress.Via {
|
||||
case identity.AddressTypeEmail:
|
||||
t = email.NewRecoveryCodeValid(s.deps, &email.RecoveryCodeValidModel{
|
||||
To: code.RecoveryAddress.Value,
|
||||
RecoveryCode: codeString,
|
||||
Identity: model,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()),
|
||||
UserRequestHeaders: hook.RemoveDisallowedHeaders(requestHeader, s.deps.Config().WebhookHeaderAllowlist(ctx)),
|
||||
})
|
||||
case identity.AddressTypeSMS:
|
||||
u, err := url.Parse(f.GetRequestURL())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t = sms.NewRecoveryCodeValid(s.deps, &sms.RecoveryCodeValidModel{
|
||||
To: code.RecoveryAddress.Value,
|
||||
RecoveryCode: codeString,
|
||||
Identity: model,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
RequestURLDomain: u.Hostname(),
|
||||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()),
|
||||
UserRequestHeaders: hook.RemoveDisallowedHeaders(requestHeader, s.deps.Config().WebhookHeaderAllowlist(ctx)),
|
||||
})
|
||||
default:
|
||||
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected email or sms but got %s", code.RecoveryAddress.Via))
|
||||
}
|
||||
|
||||
return s.send(ctx, code.RecoveryAddress.Via, t)
|
||||
}
|
||||
|
||||
// SendVerificationCode sends a verification code & 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) SendVerificationCode(ctx context.Context, f *verification.Flow, via string, to string) error {
|
||||
s.deps.Logger().
|
||||
WithField("via", via).
|
||||
WithSensitiveField("address", to).
|
||||
Debug("Preparing verification code.")
|
||||
|
||||
address, err := s.deps.IdentityPool().FindVerifiableAddressByValue(ctx, via, to)
|
||||
if errors.Is(err, sqlcon.ErrNoRows()) {
|
||||
notifyUnknownRecipients := s.deps.Config().SelfServiceFlowVerificationNotifyUnknownRecipients(ctx)
|
||||
s.deps.Logger().
|
||||
WithField("via", via).
|
||||
WithField("strategy", "code").
|
||||
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 || via != identity.AddressTypeEmail {
|
||||
// Only send unknown-address notifications via email. SMS costs money per
|
||||
// message, so we skip notification for unknown phone numbers.
|
||||
} else if err := s.send(ctx, via, email.NewVerificationCodeInvalid(s.deps, &email.VerificationCodeInvalidModel{
|
||||
To: to,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
TransientPayload: transientPayload,
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.WithStack(ErrUnknownAddress())
|
||||
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawCode := GenerateCode()
|
||||
if _, err = s.deps.VerificationCodePersister().CreateVerificationCode(ctx, &CreateVerificationCodeParams{
|
||||
RawCode: rawCode,
|
||||
ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx),
|
||||
VerifiableAddress: address,
|
||||
FlowID: f.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the identity associated with the recovery address
|
||||
i, err := s.deps.IdentityPool().GetIdentity(ctx, address.IdentityID, identity.ExpandDefault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.SendVerificationCodeTo(ctx, f, i, rawCode, address); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
address.Status = identity.VerifiableAddressStatusSent
|
||||
return s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, address, "status")
|
||||
}
|
||||
|
||||
func (s *Sender) constructVerificationLink(ctx context.Context, f *verification.Flow, codeStr string) string {
|
||||
return urlx.CopyWithQuery(
|
||||
urlx.AppendPaths(x.CourierBaseURL(f.GetCourierBaseURL(), s.deps.Config().SelfPublicURL(ctx)), verification.RouteSubmitFlow),
|
||||
url.Values{
|
||||
"flow": {f.ID.String()},
|
||||
"code": {codeStr},
|
||||
}).String()
|
||||
}
|
||||
|
||||
func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flow, i *identity.Identity, codeString string, address identity.VerifiableAddressLike) error {
|
||||
to, via := address.Address(), address.DeliveryVia()
|
||||
s.deps.Logger().
|
||||
WithField("via", via).
|
||||
WithField("identity_id", i.ID).
|
||||
WithSensitiveField("email_address", to).
|
||||
WithSensitiveField("verification_link_token", codeString).
|
||||
Info("Sending out verification email with verification code.")
|
||||
|
||||
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.deps.Hydra().GetLoginRequest(ctx, string(f.OAuth2LoginChallenge))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
f.HydraLoginRequest = hlr
|
||||
}
|
||||
oauth2LoginRequest := template.NewOAuth2LoginRequest(f.HydraLoginRequest)
|
||||
|
||||
var t courier.Template
|
||||
|
||||
// TODO: this can likely be abstracted by making templates not specific to the channel they're using
|
||||
switch via {
|
||||
case identity.ChannelTypeEmail:
|
||||
t = email.NewVerificationCodeValid(s.deps, &email.VerificationCodeValidModel{
|
||||
To: to,
|
||||
VerificationURL: s.constructVerificationLink(ctx, f, codeString),
|
||||
Identity: model,
|
||||
VerificationCode: codeString,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()),
|
||||
OAuth2LoginRequest: oauth2LoginRequest,
|
||||
})
|
||||
case identity.ChannelTypeSMS:
|
||||
t = sms.NewVerificationCodeValid(s.deps, &sms.VerificationCodeValidModel{
|
||||
To: to,
|
||||
VerificationCode: codeString,
|
||||
Identity: model,
|
||||
RequestURL: f.GetRequestURL(),
|
||||
RequestURLDomain: requestURLDomain(f.GetRequestURL()),
|
||||
TransientPayload: transientPayload,
|
||||
ExpiresInMinutes: int(s.deps.Config().SelfServiceCodeMethodLifespan(ctx).Minutes()),
|
||||
OAuth2LoginRequest: oauth2LoginRequest,
|
||||
})
|
||||
default:
|
||||
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected email or sms but got %s", via))
|
||||
}
|
||||
|
||||
return s.send(ctx, via, t)
|
||||
}
|
||||
|
||||
func (s *Sender) send(ctx context.Context, via string, t courier.Template) error {
|
||||
switch f := stringsx.SwitchExact(via); {
|
||||
case f.AddCase(identity.ChannelTypeEmail):
|
||||
c, err := s.deps.Courier(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, ok := t.(courier.EmailTemplate)
|
||||
if !ok {
|
||||
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected email template but got %T", t))
|
||||
}
|
||||
|
||||
_, err = c.QueueEmail(ctx, t)
|
||||
return err
|
||||
case f.AddCase(identity.ChannelTypeSMS):
|
||||
c, err := s.deps.Courier(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, ok := t.(courier.SMSTemplate)
|
||||
if !ok {
|
||||
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected sms template but got %T", t))
|
||||
}
|
||||
|
||||
_, err = c.QueueSMS(ctx, t)
|
||||
return err
|
||||
default:
|
||||
return f.ToUnknownCaseErr()
|
||||
}
|
||||
}
|
||||
|
||||
// requestURLDomain returns the bare hostname of the flow's request URL. It is
|
||||
// used to bind the SMS one-time code to its origin for the Web OTP API. The
|
||||
// result is a best-effort hint: a malformed URL yields an empty string and must
|
||||
// not block delivery of the code.
|
||||
func requestURLDomain(rawURL string) string {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.Hostname()
|
||||
}
|
||||
|
||||
// hackyInferChannel infers the channel (email or sms) based on the address format.
|
||||
// Once we support arbitrary custom channels, we need to refactor the recovery/verification
|
||||
// flow to first look up the address, and let the user select the channel if multiple are available.
|
||||
func hackyInferChannel(addr string) string {
|
||||
// Inferring the address type like this is a bit hacky, and actually not really necessary.
|
||||
// That's because `SendRecoveryCode` expects it, but not because it fundamentally is required.
|
||||
if strings.ContainsRune(addr, '@') {
|
||||
return identity.AddressTypeEmail
|
||||
}
|
||||
return identity.AddressTypeSMS
|
||||
}
|
||||
Reference in New Issue
Block a user