1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-01-24 05:17:10 +02:00
authboss/recover/recover_test.go
Aaron L 020487826a Add MailNoGoroutine option
This change allows users to write Mailer implementations that either
spawn their own goroutines or honor proper context usage. The current
behavior of the modules with this setting turned to 'false' creates a race
condition between the original http request's context authboss was handed being
cancelled by the http server, and the use of that context by the mailer
implementation which is being run in a goroutine.
2020-02-07 14:24:40 -08:00

487 lines
12 KiB
Go

package recover
import (
"bytes"
"crypto/sha512"
"encoding/base64"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/volatiletech/authboss"
"github.com/volatiletech/authboss/mocks"
)
const (
testSelector = `rnaGE8TDilrINHPxq/2xNU1FUTzsUSX8FvN5YzooyyWKk88fw1DjjbKBRGFtGew9OeZ+xeCC4mslfvQQMYspIg==`
testVerifier = `W1Mz30QhavVM4d8jKaFtxGBfb4GX+fOn7V0Pc1WeftgtyOtY5OX7sY9gIeY5CIY4n8LvfWy14W7/6rs2KO9pgA==`
testToken = `w5OZ51E61Q6wsJOVr9o7KmyepP7Od5VBHQ1ADDUBkiGGMjKfnMFPjtvNpLjLKJqffw72KWZzNLj0Cs8wqywdEQ==`
)
func TestInit(t *testing.T) {
t.Parallel()
ab := authboss.New()
router := &mocks.Router{}
renderer := &mocks.Renderer{}
mailRenderer := &mocks.Renderer{}
errHandler := &mocks.ErrorHandler{}
ab.Config.Core.Router = router
ab.Config.Core.ViewRenderer = renderer
ab.Config.Core.MailRenderer = mailRenderer
ab.Config.Core.ErrorHandler = errHandler
r := &Recover{}
if err := r.Init(ab); err != nil {
t.Fatal(err)
}
if err := renderer.HasLoadedViews(PageRecoverStart, PageRecoverEnd); err != nil {
t.Error(err)
}
if err := mailRenderer.HasLoadedViews(EmailRecoverHTML, EmailRecoverTxt); err != nil {
t.Error(err)
}
if err := router.HasGets("/recover", "/recover/end"); err != nil {
t.Error(err)
}
if err := router.HasPosts("/recover", "/recover/end"); err != nil {
t.Error(err)
}
}
type testHarness struct {
recover *Recover
ab *authboss.Authboss
bodyReader *mocks.BodyReader
mailer *mocks.Emailer
redirector *mocks.Redirector
renderer *mocks.Renderer
responder *mocks.Responder
session *mocks.ClientStateRW
storer *mocks.ServerStorer
}
func testSetup() *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 = harness.storer
harness.recover = &Recover{harness.ab}
return harness
}
func TestStartGet(t *testing.T) {
t.Parallel()
h := testSetup()
r := mocks.Request("GET")
w := httptest.NewRecorder()
if err := h.recover.StartGet(w, r); err != nil {
t.Error(err)
}
if w.Code != http.StatusOK {
t.Error("code was wrong:", w.Code)
}
if h.responder.Page != PageRecoverStart {
t.Error("page was wrong:", h.responder.Page)
}
if h.responder.Data != nil {
t.Error("expected no data:", h.responder.Data)
}
}
func TestStartPostSuccess(t *testing.T) {
t.Parallel()
h := testSetup()
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)
}
}
func TestStartPostFailure(t *testing.T) {
t.Parallel()
h := testSetup()
h.bodyReader.Return = &mocks.Values{
PID: "test@test.com",
}
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 len(h.mailer.Email.To) != 0 {
t.Error("should not have sent an e-mail out!")
}
}
func TestEndGet(t *testing.T) {
t.Parallel()
h := testSetup()
h.bodyReader.Return = &mocks.Values{
Token: "abcd",
}
r := mocks.Request("GET")
w := httptest.NewRecorder()
if err := h.recover.EndGet(w, r); err != nil {
t.Error(err)
}
if w.Code != http.StatusOK {
t.Error("code was wrong:", w.Code)
}
if h.responder.Page != PageRecoverEnd {
t.Error("page was wrong:", h.responder.Page)
}
if h.responder.Data[DataRecoverToken].(string) != "abcd" {
t.Errorf("recovery token is wrong: %#v", h.responder.Data)
}
}
func TestEndPostSuccess(t *testing.T) {
t.Parallel()
h := testSetup()
h.bodyReader.Return = &mocks.Values{
Token: testToken,
}
h.storer.Users["test@test.com"] = &mocks.User{
Email: "test@test.com",
Password: "to-overwrite",
RecoverSelector: testSelector,
RecoverVerifier: testVerifier,
RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, 1),
}
r := mocks.Request("POST")
w := httptest.NewRecorder()
if err := h.recover.EndPost(w, r); err != nil {
t.Error(err)
}
if w.Code != http.StatusTemporaryRedirect {
t.Error("code was wrong:", w.Code)
}
if p := h.redirector.Options.RedirectPath; p != h.ab.Paths.RecoverOK {
t.Error("path was wrong:", p)
}
if len(h.session.ClientValues[authboss.SessionKey]) != 0 {
t.Error("should not have logged in the user")
}
if !strings.Contains(h.redirector.Options.Success, "updated password") {
t.Error("should talk about recovering the password")
}
if strings.Contains(h.redirector.Options.Success, "logged in") {
t.Error("should not talk about logging in")
}
}
func TestEndPostSuccessLogin(t *testing.T) {
t.Parallel()
h := testSetup()
h.ab.Config.Modules.RecoverLoginAfterRecovery = true
h.bodyReader.Return = &mocks.Values{
Token: testToken,
}
h.storer.Users["test@test.com"] = &mocks.User{
Email: "test@test.com",
Password: "to-overwrite",
RecoverSelector: testSelector,
RecoverVerifier: testVerifier,
RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, 1),
}
r := mocks.Request("POST")
w := httptest.NewRecorder()
if err := h.recover.EndPost(h.ab.NewResponse(w), r); err != nil {
t.Error(err)
}
if w.Code != http.StatusTemporaryRedirect {
t.Error("code was wrong:", w.Code)
}
if p := h.redirector.Options.RedirectPath; p != h.ab.Paths.RecoverOK {
t.Error("path was wrong:", p)
}
if len(h.session.ClientValues[authboss.SessionKey]) == 0 {
t.Error("it should have logged in the user")
}
if !strings.Contains(h.redirector.Options.Success, "logged in") {
t.Error("should talk about logging in")
}
}
func TestEndPostValidationFailure(t *testing.T) {
t.Parallel()
h := testSetup()
h.bodyReader.Return = &mocks.Values{
Errors: []error{errors.New("password is not sufficiently complex")},
}
h.storer.Users["test@test.com"] = &mocks.User{
Email: "test@test.com",
Password: "to-overwrite",
RecoverSelector: testSelector,
RecoverVerifier: testVerifier,
RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, 1),
}
r := mocks.Request("POST")
w := httptest.NewRecorder()
if err := h.recover.EndPost(w, r); err != nil {
t.Error(err)
}
if w.Code != http.StatusOK {
t.Error("code was wrong:", w.Code)
}
if h.responder.Page != PageRecoverEnd {
t.Error("rendered the wrong page")
}
if m, ok := h.responder.Data[authboss.DataValidation].(map[string][]string); !ok {
t.Error("expected validation errors")
} else if m[""][0] != "password is not sufficiently complex" {
t.Error("error message data was not correct:", m[""])
}
if len(h.session.ClientValues[authboss.SessionKey]) != 0 {
t.Error("should not have logged in the user")
}
}
func TestEndPostInvalidBase64(t *testing.T) {
t.Parallel()
h := testSetup()
h.bodyReader.Return = &mocks.Values{
Token: "a",
}
r := mocks.Request("GET")
w := httptest.NewRecorder()
if err := h.recover.EndPost(w, r); err != nil {
t.Error(err)
}
invalidCheck(t, h, w)
}
func TestEndPostExpiredToken(t *testing.T) {
t.Parallel()
h := testSetup()
h.bodyReader.Return = &mocks.Values{
Token: testToken,
}
h.storer.Users["test@test.com"] = &mocks.User{
Email: "test@test.com",
Password: "to-overwrite",
RecoverSelector: testSelector,
RecoverVerifier: testVerifier,
RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, -1),
}
r := mocks.Request("GET")
w := httptest.NewRecorder()
if err := h.recover.EndPost(w, r); err != nil {
t.Error(err)
}
invalidCheck(t, h, w)
}
func TestEndPostUserNotExist(t *testing.T) {
t.Parallel()
h := testSetup()
h.bodyReader.Return = &mocks.Values{
Token: testToken,
}
r := mocks.Request("GET")
w := httptest.NewRecorder()
if err := h.recover.EndPost(w, r); err != nil {
t.Error(err)
}
invalidCheck(t, h, w)
}
func TestMailURL(t *testing.T) {
t.Parallel()
h := testSetup()
h.ab.Config.Paths.RootURL = "https://api.test.com:6343"
h.ab.Config.Paths.Mount = "/v1/auth"
want := "https://api.test.com:6343/v1/auth/recover/end?token=abc"
if got := h.recover.mailURL("abc"); got != want {
t.Error("want:", want, "got:", got)
}
h.ab.Config.Mail.RootURL = "https://test.com:3333/testauth"
want = "https://test.com:3333/testauth/recover/end?token=abc"
if got := h.recover.mailURL("abc"); got != want {
t.Error("want:", want, "got:", got)
}
}
func invalidCheck(t *testing.T, h *testHarness, w *httptest.ResponseRecorder) {
t.Helper()
if w.Code != http.StatusOK {
t.Error("code was wrong:", w.Code)
}
if h.responder.Page != PageRecoverEnd {
t.Error("page was wrong:", h.responder.Page)
}
if h.responder.Data[authboss.DataValidation].(map[string][]string)[""][0] != "recovery token is invalid" {
t.Error("expected a vague error to mislead")
}
}
func TestGenerateRecoverCreds(t *testing.T) {
t.Parallel()
selector, verifier, token, err := GenerateRecoverCreds()
if err != nil {
t.Error(err)
}
if verifier == selector {
t.Error("the verifier and selector should be different")
}
// base64 length: n = 64; 4*(64/3) = 85.3; round to nearest 4: 88
if len(verifier) != 88 {
t.Errorf("verifier length was wrong (%d): %s", len(verifier), verifier)
}
// base64 length: n = 64; 4*(64/3) = 85.3; round to nearest 4: 88
if len(selector) != 88 {
t.Errorf("selector length was wrong (%d): %s", len(selector), selector)
}
// base64 length: n = 64; 4*(64/3) = 85.33; round to nearest 4: 88
if len(token) != 88 {
t.Errorf("token length was wrong (%d): %s", len(token), token)
}
rawToken, err := base64.URLEncoding.DecodeString(token)
if err != nil {
t.Error(err)
}
rawSelector, err := base64.StdEncoding.DecodeString(selector)
if err != nil {
t.Error(err)
}
rawVerifier, err := base64.StdEncoding.DecodeString(verifier)
if err != nil {
t.Error(err)
}
checkSelector := sha512.Sum512(rawToken[:recoverTokenSplit])
if 0 != bytes.Compare(checkSelector[:], rawSelector) {
t.Error("expected selector to match")
}
checkVerifier := sha512.Sum512(rawToken[recoverTokenSplit:])
if 0 != bytes.Compare(checkVerifier[:], rawVerifier) {
t.Error("expected verifier to match")
}
}