Skip to content

Commit 229d87f

Browse files
committed
support for secondary recovery emails emails
1 parent d38273a commit 229d87f

File tree

4 files changed

+155
-4
lines changed

4 files changed

+155
-4
lines changed

mocks/secondary_emails_mocks.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package mocks
2+
3+
import (
4+
"context"
5+
6+
"github.com/volatiletech/authboss/v3"
7+
)
8+
9+
type UserWithSecondaryEmails struct {
10+
User
11+
SecondaryEmails []string
12+
}
13+
14+
// GetSecondaryEmails for the user
15+
func (u *UserWithSecondaryEmails) GetSecondaryEmails() []string {
16+
return u.SecondaryEmails
17+
}
18+
19+
type ServerStorerWithSecondaryEmails struct {
20+
BasicStorer *ServerStorer
21+
}
22+
23+
func (s ServerStorerWithSecondaryEmails) Load(ctx context.Context, key string) (authboss.User, error) {
24+
user, err := s.BasicStorer.Load(ctx, key)
25+
if err != nil {
26+
return user, err
27+
}
28+
29+
mockedUser := user.(*User)
30+
31+
return &UserWithSecondaryEmails{
32+
User: *mockedUser,
33+
SecondaryEmails: []string{"[email protected]", "[email protected]"},
34+
}, nil
35+
}
36+
37+
func (s ServerStorerWithSecondaryEmails) Save(ctx context.Context, user authboss.User) error {
38+
if u, ok := user.(*UserWithSecondaryEmails); ok {
39+
user = &u.User
40+
}
41+
42+
return s.BasicStorer.Save(ctx, user)
43+
}

recover/recover.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ func (r *Recover) StartPost(w http.ResponseWriter, req *http.Request) error {
118118
return err
119119
}
120120

121+
ruWithSecondaries, hasSecondaryEmails := authboss.CanBeRecoverableUserWithSecondaryEmails(user)
122+
121123
ru.PutRecoverSelector(selector)
122124
ru.PutRecoverVerifier(verifier)
123125
ru.PutRecoverExpiry(time.Now().UTC().Add(r.Config.Modules.RecoverTokenDuration))
@@ -126,10 +128,16 @@ func (r *Recover) StartPost(w http.ResponseWriter, req *http.Request) error {
126128
return err
127129
}
128130

131+
recoveryEmailRecipients := []string{ru.GetEmail()}
132+
133+
if hasSecondaryEmails {
134+
recoveryEmailRecipients = append(recoveryEmailRecipients, ruWithSecondaries.GetSecondaryEmails()...)
135+
}
136+
129137
if r.Authboss.Modules.MailNoGoroutine {
130-
r.SendRecoverEmail(req.Context(), ru.GetEmail(), token)
138+
r.SendRecoverEmail(req.Context(), recoveryEmailRecipients, token)
131139
} else {
132-
go r.SendRecoverEmail(req.Context(), ru.GetEmail(), token)
140+
go r.SendRecoverEmail(req.Context(), recoveryEmailRecipients, token)
133141
}
134142

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

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

154162
mailURL := r.mailURL(encodedToken)
155163

156164
email := authboss.Email{
157-
To: []string{to},
165+
To: to,
158166
From: r.Authboss.Config.Mail.From,
159167
FromName: r.Authboss.Config.Mail.FromName,
160168
Subject: r.Authboss.Config.Mail.SubjectPrefix + "Password Reset",
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package recover
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"strings"
7+
"testing"
8+
9+
"github.com/volatiletech/authboss/v3"
10+
"github.com/volatiletech/authboss/v3/mocks"
11+
)
12+
13+
func testSetupWithSecondaryEmails() *testHarness {
14+
harness := &testHarness{}
15+
16+
harness.ab = authboss.New()
17+
harness.bodyReader = &mocks.BodyReader{}
18+
harness.mailer = &mocks.Emailer{}
19+
harness.redirector = &mocks.Redirector{}
20+
harness.renderer = &mocks.Renderer{}
21+
harness.responder = &mocks.Responder{}
22+
harness.session = mocks.NewClientRW()
23+
harness.storer = mocks.NewServerStorer()
24+
25+
harness.ab.Paths.RecoverOK = "/recover/ok"
26+
harness.ab.Modules.MailNoGoroutine = true
27+
28+
harness.ab.Config.Core.BodyReader = harness.bodyReader
29+
harness.ab.Config.Core.Logger = mocks.Logger{}
30+
harness.ab.Config.Core.Mailer = harness.mailer
31+
harness.ab.Config.Core.Redirector = harness.redirector
32+
harness.ab.Config.Core.MailRenderer = harness.renderer
33+
harness.ab.Config.Core.Responder = harness.responder
34+
harness.ab.Config.Storage.SessionState = harness.session
35+
harness.ab.Config.Storage.Server = mocks.ServerStorerWithSecondaryEmails{
36+
BasicStorer: harness.storer,
37+
}
38+
39+
harness.recover = &Recover{harness.ab}
40+
41+
return harness
42+
}
43+
44+
func TestSecondaryEmails(t *testing.T) {
45+
t.Parallel()
46+
47+
h := testSetupWithSecondaryEmails()
48+
49+
h.bodyReader.Return = &mocks.Values{
50+
51+
}
52+
h.storer.Users["[email protected]"] = &mocks.User{
53+
54+
Password: "i can't recall, doesn't seem like something bcrypted though",
55+
}
56+
57+
r := mocks.Request("GET")
58+
w := httptest.NewRecorder()
59+
60+
if err := h.recover.StartPost(w, r); err != nil {
61+
t.Error(err)
62+
}
63+
64+
if w.Code != http.StatusTemporaryRedirect {
65+
t.Error("code was wrong:", w.Code)
66+
}
67+
if h.redirector.Options.RedirectPath != h.ab.Config.Paths.RecoverOK {
68+
t.Error("page was wrong:", h.responder.Page)
69+
}
70+
if len(h.redirector.Options.Success) == 0 {
71+
t.Error("expected a nice success message")
72+
}
73+
74+
if h.mailer.Email.To[0] != "[email protected]" {
75+
t.Error("e-mail to address is wrong:", h.mailer.Email.To)
76+
}
77+
if !strings.HasSuffix(h.mailer.Email.Subject, "Password Reset") {
78+
t.Error("e-mail subject line is wrong:", h.mailer.Email.Subject)
79+
}
80+
if len(h.renderer.Data[DataRecoverURL].(string)) == 0 {
81+
t.Errorf("the renderer's url in data was missing: %#v", h.renderer.Data)
82+
}
83+
84+
if len(h.mailer.Email.To) != 3 {
85+
t.Errorf("should have sent 3 e-mails out, but sent %d", len(h.mailer.Email.To))
86+
}
87+
}

user.go

+13
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ type RecoverableUser interface {
7373
PutRecoverExpiry(expiry time.Time)
7474
}
7575

76+
type RecoverableUserWithSecondaryEmails interface {
77+
RecoverableUser
78+
79+
GetSecondaryEmails() (secondaryEmails []string)
80+
}
81+
7682
// ArbitraryUser allows arbitrary data from the web form through. You should
7783
// definitely only pull the keys you want from the map, since this is unfiltered
7884
// input from a web request and is an attack vector.
@@ -142,6 +148,13 @@ func MustBeRecoverable(u User) RecoverableUser {
142148
panic(fmt.Sprintf("could not upgrade user to a recoverable user, given type: %T", u))
143149
}
144150

151+
func CanBeRecoverableUserWithSecondaryEmails(u User) (RecoverableUserWithSecondaryEmails, bool) {
152+
if lu, ok := u.(RecoverableUserWithSecondaryEmails); ok {
153+
return lu, true
154+
}
155+
return nil, false
156+
}
157+
145158
// MustBeOAuthable forces an upgrade to an OAuth2User or panic.
146159
func MustBeOAuthable(u User) OAuth2User {
147160
if ou, ok := u.(OAuth2User); ok {

0 commit comments

Comments
 (0)