Skip to content

Commit

Permalink
Merge pull request #353 from viovanov/vlad/secondary-emails
Browse files Browse the repository at this point in the history
support for secondary recovery emails emails
  • Loading branch information
aarondl committed Sep 28, 2023
2 parents 0c38ddb + 229d87f commit ccfe4d1
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 4 deletions.
43 changes: 43 additions & 0 deletions mocks/secondary_emails_mocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package mocks

import (
"context"

"github.com/volatiletech/authboss/v3"
)

type UserWithSecondaryEmails struct {
User
SecondaryEmails []string
}

// GetSecondaryEmails for the user
func (u *UserWithSecondaryEmails) GetSecondaryEmails() []string {
return u.SecondaryEmails
}

type ServerStorerWithSecondaryEmails struct {
BasicStorer *ServerStorer
}

func (s ServerStorerWithSecondaryEmails) Load(ctx context.Context, key string) (authboss.User, error) {
user, err := s.BasicStorer.Load(ctx, key)
if err != nil {
return user, err
}

mockedUser := user.(*User)

return &UserWithSecondaryEmails{
User: *mockedUser,
SecondaryEmails: []string{"personal@one.com", "personal@two.com"},
}, nil
}

func (s ServerStorerWithSecondaryEmails) Save(ctx context.Context, user authboss.User) error {
if u, ok := user.(*UserWithSecondaryEmails); ok {
user = &u.User
}

return s.BasicStorer.Save(ctx, user)
}
16 changes: 12 additions & 4 deletions recover/recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ func (r *Recover) StartPost(w http.ResponseWriter, req *http.Request) error {
return err
}

ruWithSecondaries, hasSecondaryEmails := authboss.CanBeRecoverableUserWithSecondaryEmails(user)

ru.PutRecoverSelector(selector)
ru.PutRecoverVerifier(verifier)
ru.PutRecoverExpiry(time.Now().UTC().Add(r.Config.Modules.RecoverTokenDuration))
Expand All @@ -126,10 +128,16 @@ func (r *Recover) StartPost(w http.ResponseWriter, req *http.Request) error {
return err
}

recoveryEmailRecipients := []string{ru.GetEmail()}

if hasSecondaryEmails {
recoveryEmailRecipients = append(recoveryEmailRecipients, ruWithSecondaries.GetSecondaryEmails()...)
}

if r.Authboss.Modules.MailNoGoroutine {
r.SendRecoverEmail(req.Context(), ru.GetEmail(), token)
r.SendRecoverEmail(req.Context(), recoveryEmailRecipients, token)
} else {
go r.SendRecoverEmail(req.Context(), ru.GetEmail(), token)
go r.SendRecoverEmail(req.Context(), recoveryEmailRecipients, token)
}

_, err = r.Authboss.Events.FireAfter(authboss.EventRecoverStart, w, req)
Expand All @@ -148,13 +156,13 @@ func (r *Recover) StartPost(w http.ResponseWriter, req *http.Request) error {

// SendRecoverEmail to a specific e-mail address passing along the encodedToken
// in an escaped URL to the templates.
func (r *Recover) SendRecoverEmail(ctx context.Context, to, encodedToken string) {
func (r *Recover) SendRecoverEmail(ctx context.Context, to []string, encodedToken string) {
logger := r.Authboss.Logger(ctx)

mailURL := r.mailURL(encodedToken)

email := authboss.Email{
To: []string{to},
To: to,
From: r.Authboss.Config.Mail.From,
FromName: r.Authboss.Config.Mail.FromName,
Subject: r.Authboss.Config.Mail.SubjectPrefix + "Password Reset",
Expand Down
87 changes: 87 additions & 0 deletions recover/recover_secondary_emails_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package recover

import (
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/volatiletech/authboss/v3"
"github.com/volatiletech/authboss/v3/mocks"
)

func testSetupWithSecondaryEmails() *testHarness {
harness := &testHarness{}

harness.ab = authboss.New()
harness.bodyReader = &mocks.BodyReader{}
harness.mailer = &mocks.Emailer{}
harness.redirector = &mocks.Redirector{}
harness.renderer = &mocks.Renderer{}
harness.responder = &mocks.Responder{}
harness.session = mocks.NewClientRW()
harness.storer = mocks.NewServerStorer()

harness.ab.Paths.RecoverOK = "/recover/ok"
harness.ab.Modules.MailNoGoroutine = true

harness.ab.Config.Core.BodyReader = harness.bodyReader
harness.ab.Config.Core.Logger = mocks.Logger{}
harness.ab.Config.Core.Mailer = harness.mailer
harness.ab.Config.Core.Redirector = harness.redirector
harness.ab.Config.Core.MailRenderer = harness.renderer
harness.ab.Config.Core.Responder = harness.responder
harness.ab.Config.Storage.SessionState = harness.session
harness.ab.Config.Storage.Server = mocks.ServerStorerWithSecondaryEmails{
BasicStorer: harness.storer,
}

harness.recover = &Recover{harness.ab}

return harness
}

func TestSecondaryEmails(t *testing.T) {
t.Parallel()

h := testSetupWithSecondaryEmails()

h.bodyReader.Return = &mocks.Values{
PID: "test@test.com",
}
h.storer.Users["test@test.com"] = &mocks.User{
Email: "test@test.com",
Password: "i can't recall, doesn't seem like something bcrypted though",
}

r := mocks.Request("GET")
w := httptest.NewRecorder()

if err := h.recover.StartPost(w, r); err != nil {
t.Error(err)
}

if w.Code != http.StatusTemporaryRedirect {
t.Error("code was wrong:", w.Code)
}
if h.redirector.Options.RedirectPath != h.ab.Config.Paths.RecoverOK {
t.Error("page was wrong:", h.responder.Page)
}
if len(h.redirector.Options.Success) == 0 {
t.Error("expected a nice success message")
}

if h.mailer.Email.To[0] != "test@test.com" {
t.Error("e-mail to address is wrong:", h.mailer.Email.To)
}
if !strings.HasSuffix(h.mailer.Email.Subject, "Password Reset") {
t.Error("e-mail subject line is wrong:", h.mailer.Email.Subject)
}
if len(h.renderer.Data[DataRecoverURL].(string)) == 0 {
t.Errorf("the renderer's url in data was missing: %#v", h.renderer.Data)
}

if len(h.mailer.Email.To) != 3 {
t.Errorf("should have sent 3 e-mails out, but sent %d", len(h.mailer.Email.To))
}
}
13 changes: 13 additions & 0 deletions user.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ type RecoverableUser interface {
PutRecoverExpiry(expiry time.Time)
}

type RecoverableUserWithSecondaryEmails interface {
RecoverableUser

GetSecondaryEmails() (secondaryEmails []string)
}

// ArbitraryUser allows arbitrary data from the web form through. You should
// definitely only pull the keys you want from the map, since this is unfiltered
// input from a web request and is an attack vector.
Expand Down Expand Up @@ -142,6 +148,13 @@ func MustBeRecoverable(u User) RecoverableUser {
panic(fmt.Sprintf("could not upgrade user to a recoverable user, given type: %T", u))
}

func CanBeRecoverableUserWithSecondaryEmails(u User) (RecoverableUserWithSecondaryEmails, bool) {
if lu, ok := u.(RecoverableUserWithSecondaryEmails); ok {
return lu, true
}
return nil, false
}

// MustBeOAuthable forces an upgrade to an OAuth2User or panic.
func MustBeOAuthable(u User) OAuth2User {
if ou, ok := u.(OAuth2User); ok {
Expand Down

0 comments on commit ccfe4d1

Please sign in to comment.