Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] disable email delivery #1419

Merged
merged 7 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 33 additions & 20 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,24 @@ import (

// Config is the central configuration type
type Config struct {
Server Server `yaml:"server" json:"server,omitempty" koanf:"server"`
Webauthn WebauthnSettings `yaml:"webauthn" json:"webauthn,omitempty" koanf:"webauthn"`
Smtp SMTP `yaml:"smtp" json:"smtp,omitempty" koanf:"smtp"`
Passcode Passcode `yaml:"passcode" json:"passcode" koanf:"passcode"`
Password Password `yaml:"password" json:"password,omitempty" koanf:"password"`
Database Database `yaml:"database" json:"database" koanf:"database"`
Secrets Secrets `yaml:"secrets" json:"secrets" koanf:"secrets"`
Service Service `yaml:"service" json:"service" koanf:"service"`
Session Session `yaml:"session" json:"session,omitempty" koanf:"session"`
AuditLog AuditLog `yaml:"audit_log" json:"audit_log,omitempty" koanf:"audit_log" split_words:"true"`
Emails Emails `yaml:"emails" json:"emails,omitempty" koanf:"emails"`
RateLimiter RateLimiter `yaml:"rate_limiter" json:"rate_limiter,omitempty" koanf:"rate_limiter" split_words:"true"`
ThirdParty ThirdParty `yaml:"third_party" json:"third_party,omitempty" koanf:"third_party" split_words:"true"`
Log LoggerConfig `yaml:"log" json:"log,omitempty" koanf:"log"`
Account Account `yaml:"account" json:"account,omitempty" koanf:"account"`
Saml config.Saml `yaml:"saml" json:"saml,omitempty" koanf:"saml"`
Webhooks WebhookSettings `yaml:"webhooks" json:"webhooks,omitempty" koanf:"webhooks"`
Server Server `yaml:"server" json:"server,omitempty" koanf:"server"`
Webauthn WebauthnSettings `yaml:"webauthn" json:"webauthn,omitempty" koanf:"webauthn"`
Smtp SMTP `yaml:"smtp" json:"smtp,omitempty" koanf:"smtp"`
EmailDelivery EmailDelivery `yaml:"email_delivery" json:"email_delivery,omitempty" koanf:"email_delivery" split_words:"true"`
Passcode Passcode `yaml:"passcode" json:"passcode" koanf:"passcode"`
Password Password `yaml:"password" json:"password,omitempty" koanf:"password"`
Database Database `yaml:"database" json:"database" koanf:"database"`
Secrets Secrets `yaml:"secrets" json:"secrets" koanf:"secrets"`
Service Service `yaml:"service" json:"service" koanf:"service"`
Session Session `yaml:"session" json:"session,omitempty" koanf:"session"`
AuditLog AuditLog `yaml:"audit_log" json:"audit_log,omitempty" koanf:"audit_log" split_words:"true"`
Emails Emails `yaml:"emails" json:"emails,omitempty" koanf:"emails"`
RateLimiter RateLimiter `yaml:"rate_limiter" json:"rate_limiter,omitempty" koanf:"rate_limiter" split_words:"true"`
ThirdParty ThirdParty `yaml:"third_party" json:"third_party,omitempty" koanf:"third_party" split_words:"true"`
Log LoggerConfig `yaml:"log" json:"log,omitempty" koanf:"log"`
Account Account `yaml:"account" json:"account,omitempty" koanf:"account"`
Saml config.Saml `yaml:"saml" json:"saml,omitempty" koanf:"saml"`
Webhooks WebhookSettings `yaml:"webhooks" json:"webhooks,omitempty" koanf:"webhooks"`
}

var (
Expand Down Expand Up @@ -118,6 +119,9 @@ func DefaultConfig() *Config {
Smtp: SMTP{
Port: "465",
},
EmailDelivery: EmailDelivery{
Enabled: true,
},
Passcode: Passcode{
TTL: 300,
Email: Email{
Expand Down Expand Up @@ -200,9 +204,11 @@ func (c *Config) Validate() error {
if err != nil {
return fmt.Errorf("failed to validate webauthn settings: %w", err)
}
err = c.Smtp.Validate()
if err != nil {
return fmt.Errorf("failed to validate smtp settings: %w", err)
if c.EmailDelivery.Enabled {
err = c.Smtp.Validate()
if err != nil {
return fmt.Errorf("failed to validate smtp settings: %w", err)
}
}
err = c.Passcode.Validate()
if err != nil {
Expand Down Expand Up @@ -379,6 +385,10 @@ func (s *SMTP) Validate() error {
return nil
}

type EmailDelivery struct {
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled" jsonschema:"default=true"`
}

type Email struct {
FromAddress string `yaml:"from_address" json:"from_address,omitempty" koanf:"from_address" split_words:"true" jsonschema:"[email protected]"`
FromName string `yaml:"from_name" json:"from_name,omitempty" koanf:"from_name" split_words:"true" jsonschema:"default=Hanko"`
Expand Down Expand Up @@ -684,6 +694,9 @@ func (c *Config) PostProcess() error {
}

func (c *Config) arrangeSmtpSettings() {
if !c.EmailDelivery.Enabled {
return
}
if c.Passcode.Smtp.Validate() == nil {
if c.Smtp.Validate() == nil {
zeroLogger.Warn().Msg("Both root smtp and passcode.smtp are set. Using smtp settings from root configuration")
Expand Down
18 changes: 18 additions & 0 deletions backend/docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,18 @@ webauthn:
origins:
- "android:apk-key-hash:nLSu7wVTbnMOxLgC52f2faTnv..."
- "https://login.example.com"
## email_delivery ##
#
# Settings needed for email delivery.
#
email_delivery:
## enabled ##
#
# Enable or disable email delivery by hanko. Disable if you want to send the emails yourself. To send emails yourself you must subscribe to the `email.create` webhook event.
#
# Default: true
#
enabled: true
## audit_log ##
#
# Configures audit logging
Expand Down Expand Up @@ -972,4 +984,10 @@ webhooks:
# Email - Triggers on: change of primary email
#
- user.update.email.primary
##
#
# Triggers on: an email was sent or should be sent
#
- email.create
FreddyDevelop marked this conversation as resolved.
Show resolved Hide resolved

```
26 changes: 26 additions & 0 deletions backend/dto/webhook/email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package webhook

type EmailSend struct {
Subject string `json:"subject"` // subject
BodyPlain string `json:"body_plain"` // used for string templates
Body string `json:"body,omitempty"` // used for html templates
ToEmailAddress string `json:"to_email_address"`
DeliveredByHanko bool `json:"delivered_by_hanko"`
AcceptLanguage string `json:"accept_language"` // accept_language header from http request
Type EmailType `json:"type"` // type of the email, currently only "passcode", but other could be added later

Data interface{} `json:"data"`
}

type PasscodeData struct {
ServiceName string `json:"service_name"`
OtpCode string `json:"otp_code"`
TTL int `json:"ttl"`
ValidUntil int64 `json:"valid_until"` // UnixTimestamp
}

type EmailType string

var (
EmailTypePasscode EmailType = "passcode"
)
6 changes: 3 additions & 3 deletions backend/handler/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func (h *EmailHandler) Create(c echo.Context) error {
var evt events.Event

if len(user.Emails) >= 1 {
evt = events.EmailCreate
evt = events.UserEmailCreate
} else {
evt = events.UserCreate
}
Expand Down Expand Up @@ -212,7 +212,7 @@ func (h *EmailHandler) SetPrimaryEmail(c echo.Context) error {
return fmt.Errorf("failed to create audit log: %w", err)
}

utils.NotifyUserChange(c, tx, h.persister, events.EmailPrimary, userId)
utils.NotifyUserChange(c, tx, h.persister, events.UserEmailPrimary, userId)

return c.NoContent(http.StatusNoContent)
})
Expand Down Expand Up @@ -256,7 +256,7 @@ func (h *EmailHandler) Delete(c echo.Context) error {
return fmt.Errorf("failed to create audit log: %w", err)
}

utils.NotifyUserChange(c, tx, h.persister, events.EmailDelete, userId)
utils.NotifyUserChange(c, tx, h.persister, events.UserEmailDelete, userId)

return c.NoContent(http.StatusNoContent)
})
Expand Down
6 changes: 3 additions & 3 deletions backend/handler/email_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (h *emailAdminHandler) Create(ctx echo.Context) error {
err = h.persister.GetPrimaryEmailPersisterWithConnection(tx).Create(*primaryEmail)
}

utils.NotifyUserChange(ctx, tx, h.persister, events.EmailCreate, userId)
utils.NotifyUserChange(ctx, tx, h.persister, events.UserEmailCreate, userId)

return ctx.JSON(http.StatusCreated, admin.FromEmailModel(email))
})
Expand Down Expand Up @@ -229,7 +229,7 @@ func (h *emailAdminHandler) Delete(ctx echo.Context) error {
return fmt.Errorf("failed to delete email from db: %w", err)
}

utils.NotifyUserChange(ctx, tx, h.persister, events.EmailDelete, userId)
utils.NotifyUserChange(ctx, tx, h.persister, events.UserEmailDelete, userId)

return ctx.NoContent(http.StatusNoContent)
})
Expand Down Expand Up @@ -275,7 +275,7 @@ func (h *emailAdminHandler) SetPrimaryEmail(ctx echo.Context) error {
return err
}

utils.NotifyUserChange(ctx, tx, h.persister, events.EmailPrimary, userId)
utils.NotifyUserChange(ctx, tx, h.persister, events.UserEmailPrimary, userId)

return ctx.NoContent(http.StatusNoContent)
})
Expand Down
55 changes: 44 additions & 11 deletions backend/handler/passcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwt"
zeroLogger "github.com/rs/zerolog/log"
"github.com/sethvargo/go-limiter"
"github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/crypto"
"github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/dto/webhook"
"github.com/teamhanko/hanko/backend/mail"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
Expand Down Expand Up @@ -189,22 +191,53 @@ func (h *PasscodeHandler) Init(c echo.Context) error {
}

lang := c.Request().Header.Get("Accept-Language")
str, err := h.renderer.Render("loginTextMail", lang, data)
subject := h.renderer.Translate(lang, "email_subject_login", data)
bodyPlain, err := h.renderer.Render("loginTextMail", lang, data)
if err != nil {
return fmt.Errorf("failed to render email template: %w", err)
}

message := gomail.NewMessage()
message.SetAddressHeader("To", email.Address, "")
message.SetAddressHeader("From", h.emailConfig.FromAddress, h.emailConfig.FromName)
webhookData := webhook.EmailSend{
Subject: subject,
BodyPlain: bodyPlain,
ToEmailAddress: email.Address,
DeliveredByHanko: true,
AcceptLanguage: lang,
Type: webhook.EmailTypePasscode,
Data: webhook.PasscodeData{
ServiceName: h.cfg.Service.Name,
OtpCode: passcode,
TTL: h.TTL,
ValidUntil: passcodeModel.CreatedAt.Add(time.Duration(h.TTL) * time.Second).UTC().Unix(),
},
}

message.SetHeader("Subject", h.renderer.Translate(lang, "email_subject_login", data))
if h.cfg.EmailDelivery.Enabled {
message := gomail.NewMessage()
message.SetAddressHeader("To", email.Address, "")
message.SetAddressHeader("From", h.emailConfig.FromAddress, h.emailConfig.FromName)

message.SetBody("text/plain", str)
message.SetHeader("Subject", subject)

err = h.mailer.Send(message)
if err != nil {
return fmt.Errorf("failed to send passcode: %w", err)
message.SetBody("text/plain", bodyPlain)

err = h.mailer.Send(message)
if err != nil {
return fmt.Errorf("failed to send passcode: %w", err)
}

err = utils.TriggerWebhooks(c, events.EmailSend, webhookData)

if err != nil {
zeroLogger.Warn().Err(err).Msg("failed to trigger webhook")
}
} else {
webhookData.DeliveredByHanko = false
err = utils.TriggerWebhooks(c, events.EmailSend, webhookData)

if err != nil {
return fmt.Errorf(fmt.Sprintf("failed to trigger webhook: %s", err))
}
}

err = h.auditLogger.Create(c, models.AuditLogPasscodeLoginInitSucceeded, user, nil)
Expand Down Expand Up @@ -326,7 +359,7 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
}

wasUnverified := false
hasEmails := len(user.Emails) >= 1 // check if we need to trigger a UserCreate webhook or a EmailCreate one
hasEmails := len(user.Emails) >= 1 // check if we need to trigger a UserCreate webhook or a UserEmailCreate one

if !passcode.Email.Verified {
wasUnverified = true
Expand Down Expand Up @@ -394,7 +427,7 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
var evt events.Event

if hasEmails {
evt = events.EmailCreate
evt = events.UserEmailCreate
} else {
evt = events.UserCreate
}
Expand Down
4 changes: 2 additions & 2 deletions backend/handler/public_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister, promet
webauthnCredentials.DELETE("/:id", webauthnHandler.DeleteCredential)

passcode := g.Group("/passcode")
passcodeLogin := passcode.Group("/login")
passcodeLogin := passcode.Group("/login", webhookMiddlware)
passcodeLogin.POST("/initialize", passcodeHandler.Init)
passcodeLogin.POST("/finalize", passcodeHandler.Finish, webhookMiddlware)
passcodeLogin.POST("/finalize", passcodeHandler.Finish)

email := g.Group("/emails", sessionMiddleware, webhookMiddlware)
email.GET("", emailHandler.List)
Expand Down
16 changes: 16 additions & 0 deletions backend/json_schema/hanko.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@
"smtp": {
"$ref": "#/$defs/SMTP"
},
"email_delivery": {
"$ref": "#/$defs/EmailDelivery"
},
"passcode": {
"$ref": "#/$defs/Passcode"
},
Expand Down Expand Up @@ -302,6 +305,19 @@
"additionalProperties": false,
"type": "object"
},
"EmailDelivery": {
"properties": {
"enabled": {
"type": "boolean",
"default": true
}
},
"additionalProperties": false,
"type": "object",
"required": [
"enabled"
]
},
"Emails": {
"properties": {
"require_verification": {
Expand Down
1 change: 0 additions & 1 deletion backend/rate_limiter/rate_limiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ func Limit(store limiter.Store, userId uuid.UUID, c echo.Context) error {
}

resetTime := int(math.Floor(time.Unix(0, int64(reset)).UTC().Sub(time.Now().UTC()).Seconds()))
log.Println(resetTime)

// Set headers (we do this regardless of whether the request is permitted).
c.Response().Header().Set(httplimit.HeaderRateLimitLimit, strconv.FormatUint(limit, 10))
Expand Down
3 changes: 3 additions & 0 deletions backend/test/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ var DefaultConfig = config.Config{
Host: "localhost",
Port: "2500",
},
EmailDelivery: config.EmailDelivery{
Enabled: true,
},
Passcode: config.Passcode{
Email: config.Email{
FromAddress: "[email protected]",
Expand Down
2 changes: 1 addition & 1 deletion backend/thirdparty/linking.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func signIn(tx *pop.Connection, cfg *config.Config, p persistence.Persister, use
}

identity.EmailID = email.ID
webhookEvent = events.EmailCreate
webhookEvent = events.UserEmailCreate
}
}

Expand Down
20 changes: 11 additions & 9 deletions backend/webhooks/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import "github.com/teamhanko/hanko/backend/persistence/models"
type Event string

const (
User Event = "user"
UserCreate Event = "user.create"
UserUpdate Event = "user.update"
UserDelete Event = "user.delete"
Email Event = "user.update.email"
EmailCreate Event = "user.update.email.create"
EmailPrimary Event = "user.update.email.primary"
EmailDelete Event = "user.update.email.delete"
User Event = "user"
UserCreate Event = "user.create"
UserUpdate Event = "user.update"
UserDelete Event = "user.delete"
UserEmail Event = "user.update.email"
UserEmailCreate Event = "user.update.email.create"
UserEmailPrimary Event = "user.update.email.primary"
UserEmailDelete Event = "user.update.email.delete"

EmailSend Event = "email.send"
)

func StringIsValidEvent(value string) bool {
Expand All @@ -23,7 +25,7 @@ func StringIsValidEvent(value string) bool {
func IsValidEvent(evt Event) bool {
var isValid bool
switch evt {
case User, UserCreate, UserUpdate, UserDelete, Email, EmailCreate, EmailPrimary, EmailDelete:
case User, UserCreate, UserUpdate, UserDelete, UserEmail, UserEmailCreate, UserEmailPrimary, UserEmailDelete, EmailSend:
isValid = true
default:
isValid = false
Expand Down
Loading
Loading