1
0
mirror of https://github.com/volatiletech/authboss.git synced 2024-11-24 08:42:17 +02:00

Finish auth module

This commit is contained in:
Aaron L 2018-02-20 08:58:59 -08:00
parent 77987afb8a
commit d4f8d2f292
4 changed files with 592 additions and 375 deletions

View File

@ -41,7 +41,7 @@ func (a *Auth) Init(ab *authboss.Authboss) (err error) {
case "DELETE":
logoutRouteMethod = a.Authboss.Config.Core.Router.Delete
default:
return errors.Errorf("auth wants to register a logout route but is given an invalid method: %s", a.Authboss.Config.Modules.AuthLogoutMethod)
return errors.Errorf("auth wants to register a logout route but was given an invalid method: %s", a.Authboss.Config.Modules.AuthLogoutMethod)
}
a.Authboss.Config.Core.Router.Get("/login", a.Authboss.Core.ErrorHandler.Wrap(a.LoginGet))
@ -59,6 +59,8 @@ func (a *Auth) LoginGet(w http.ResponseWriter, r *http.Request) error {
// LoginPost attempts to validate the credentials passed in
// to log in a user.
func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error {
logger := a.RequestLogger(r)
validatable, err := a.Authboss.Core.BodyReader.Read(PageLogin, r)
if err != nil {
return err
@ -71,6 +73,7 @@ func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error {
pid := creds.GetPID()
pidUser, err := a.Authboss.Storage.Server.Load(r.Context(), pid)
if err == authboss.ErrUserNotFound {
logger.Infof("failed to load user requested by pid: %s", pid)
data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"}
return a.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data)
} else if err != nil {
@ -78,45 +81,43 @@ func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error {
}
authUser := authboss.MustBeAuthable(pidUser)
password, err := authUser.GetPassword(r.Context())
if err != nil {
return err
}
password := authUser.GetPassword()
var handled bool
err = bcrypt.CompareHashAndPassword([]byte(password), []byte(creds.GetPassword()))
if err != nil {
err = a.Authboss.Events.FireAfter(r.Context(), authboss.EventAuthFail)
handled, err = a.Authboss.Events.FireAfter(authboss.EventAuthFail, w, r)
if err != nil {
return err
} else if handled {
return nil
}
logger.Infof("user %s failed to log in", pid)
data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"}
return a.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data)
}
interrupted, err := a.Events.FireBefore(r.Context(), authboss.EventAuth)
handled, err = a.Events.FireBefore(authboss.EventAuth, w, r)
if err != nil {
return err
} else if interrupted != authboss.InterruptNone {
var reason string
switch interrupted {
case authboss.InterruptAccountLocked:
reason = "Your account is locked"
case authboss.InterruptAccountNotConfirmed:
reason = "Your account is not confirmed"
}
data := authboss.HTMLData{authboss.DataErr: reason}
return a.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data)
} else if handled {
return nil
}
logger.Infof("user %s logged in", pid)
authboss.PutSession(w, authboss.SessionKey, pid)
authboss.DelSession(w, authboss.SessionHalfAuthKey)
if err := a.Authboss.Events.FireAfter(r.Context(), authboss.EventAuth); err != nil {
handled, err = a.Authboss.Events.FireAfter(authboss.EventAuth, w, r)
if err != nil {
return err
} else if handled {
return nil
}
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: a.Authboss.Paths.AuthLogoutOK,
}
return a.Authboss.Core.Redirector.Redirect(w, r, ro)
@ -124,11 +125,21 @@ func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error {
// Logout a user
func (a *Auth) Logout(w http.ResponseWriter, r *http.Request) error {
logger := a.RequestLogger(r)
user, err := a.CurrentUser(w, r)
if err != nil {
return err
}
logger.Infof("user %s logged out", user.GetPID())
authboss.DelSession(w, authboss.SessionKey)
authboss.DelSession(w, authboss.SessionLastAction)
authboss.DelSession(w, authboss.SessionHalfAuthKey)
authboss.DelCookie(w, authboss.CookieRemember)
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: a.Authboss.Paths.AuthLogoutOK,
Success: "You have been logged out",
}

View File

@ -1,380 +1,388 @@
package auth
import (
"bytes"
"html/template"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/pkg/errors"
"github.com/volatiletech/authboss"
"github.com/volatiletech/authboss/internal/mocks"
)
func testSetup() (a *Auth, s *mocks.MockStorer) {
s = mocks.NewMockStorer()
ab := authboss.New()
ab.LogWriter = ioutil.Discard
ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`))
ab.Storage.Server = s
ab.XSRFName = "xsrf"
ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string {
return "xsrfvalue"
}
ab.PrimaryID = authboss.StoreUsername
a = &Auth{}
if err := a.Initialize(ab); err != nil {
panic(err)
}
return a, s
}
func testRequest(ab *authboss.Authboss, method string, postFormValues ...string) (*authboss.Context, *httptest.ResponseRecorder, *http.Request, authboss.ClientStorerErr) {
sessionStorer := mocks.NewMockClientStorer()
ctx := ab.NewContext()
r := mocks.MockRequest(method, postFormValues...)
ctx.SessionStorer = sessionStorer
return ctx, httptest.NewRecorder(), r, sessionStorer
}
func TestAuth(t *testing.T) {
t.Parallel()
a, _ := testSetup()
storage := a.Storage()
if storage[a.PrimaryID] != authboss.String {
t.Error("Expected storage KV:", a.PrimaryID, authboss.String)
}
if storage[authboss.StorePassword] != authboss.String {
t.Error("Expected storage KV:", authboss.StorePassword, authboss.String)
}
routes := a.Routes()
if routes["/login"] == nil {
t.Error("Expected route '/login' with handleFunc")
}
if routes["/logout"] == nil {
t.Error("Expected route '/logout' with handleFunc")
}
}
func TestAuth_loginHandlerFunc_GET(t *testing.T) {
t.Parallel()
a, _ := testSetup()
ctx, w, r, _ := testRequest(a.Authboss, "GET")
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected status:", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, "<form") {
t.Error("Should have rendered a form")
}
if !strings.Contains(body, `name="`+a.PrimaryID) {
t.Error("Form should contain the primary ID field:", body)
}
if !strings.Contains(body, `name="password"`) {
t.Error("Form should contain password field:", body)
}
}
func TestAuth_loginHandlerFunc_POST_ReturnsErrorOnCallbackFailure(t *testing.T) {
t.Parallel()
a, storer := testSetup()
storer.Users["john"] = authboss.Attributes{"password": "$2a$10$B7aydtqVF9V8RSNx3lCKB.l09jqLV/aMiVqQHajtL7sWGhCS9jlOu"}
a.Events = authboss.NewCallbacks()
a.Events.Before(authboss.EventAuth, func(_ *authboss.Context) (authboss.Interrupt, error) {
return authboss.InterruptNone, errors.New("explode")
})
ctx, w, r, _ := testRequest(a.Authboss, "POST", "username", "john", "password", "1234")
if err := a.loginHandlerFunc(ctx, w, r); err.Error() != "explode" {
t.Error("Unexpected error:", err)
}
}
func TestAuth_loginHandlerFunc_POST_RedirectsWhenInterrupted(t *testing.T) {
t.Parallel()
a, storer := testSetup()
storer.Users["john"] = authboss.Attributes{"password": "$2a$10$B7aydtqVF9V8RSNx3lCKB.l09jqLV/aMiVqQHajtL7sWGhCS9jlOu"}
a.Events = authboss.NewCallbacks()
a.Events.Before(authboss.EventAuth, func(_ *authboss.Context) (authboss.Interrupt, error) {
return authboss.InterruptAccountLocked, nil
})
ctx, w, r, sessionStore := testRequest(a.Authboss, "POST", "username", "john", "password", "1234")
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusFound {
t.Error("Unexpected status:", w.Code)
}
loc := w.Header().Get("Location")
if loc != a.AuthLoginFailPath {
t.Error("Unexpeced location:", loc)
}
expectedMsg := "Your account has been locked."
if msg, ok := sessionStore.Get(authboss.FlashErrorKey); !ok || msg != expectedMsg {
t.Error("Expected error flash message:", expectedMsg)
}
a.Events = authboss.NewCallbacks()
a.Events.Before(authboss.EventAuth, func(_ *authboss.Context) (authboss.Interrupt, error) {
return authboss.InterruptAccountNotConfirmed, nil
})
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusFound {
t.Error("Unexpected status:", w.Code)
}
loc = w.Header().Get("Location")
if loc != a.AuthLoginFailPath {
t.Error("Unexpeced location:", loc)
}
expectedMsg = "Your account has not been confirmed."
if msg, ok := sessionStore.Get(authboss.FlashErrorKey); !ok || msg != expectedMsg {
t.Error("Expected error flash message:", expectedMsg)
}
}
func TestAuth_loginHandlerFunc_POST_AuthenticationFailure(t *testing.T) {
t.Parallel()
a, _ := testSetup()
log := &bytes.Buffer{}
a.LogWriter = log
ctx, w, r, _ := testRequest(a.Authboss, "POST", "username", "john", "password", "1")
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected status:", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, "invalid username and/or password") {
t.Error("Should have rendered with error")
}
ctx, w, r, _ = testRequest(a.Authboss, "POST", "username", "john", "password", "1234")
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected status:", w.Code)
}
body = w.Body.String()
if !strings.Contains(body, "invalid username and/or password") {
t.Error("Should have rendered with error")
}
ctx, w, r, _ = testRequest(a.Authboss, "POST", "username", "jake", "password", "1")
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected status:", w.Code)
}
body = w.Body.String()
if !strings.Contains(body, "invalid username and/or password") {
t.Error("Should have rendered with error")
}
}
func TestAuth_loginHandlerFunc_POST(t *testing.T) {
t.Parallel()
a, storer := testSetup()
storer.Users["john"] = authboss.Attributes{"password": "$2a$10$B7aydtqVF9V8RSNx3lCKB.l09jqLV/aMiVqQHajtL7sWGhCS9jlOu"}
ctx, w, r, _ := testRequest(a.Authboss, "POST", "username", "john", "password", "1234")
cb := mocks.NewMockAfterCallback()
a.Events = authboss.NewCallbacks()
a.Events.After(authboss.EventAuth, cb.Fn)
a.AuthLoginOKPath = "/dashboard"
sessions := mocks.NewMockClientStorer()
ctx.SessionStorer = sessions
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if _, ok := ctx.Values[authboss.CookieRemember]; !ok {
t.Error("Authboss cookie remember should be set for the callback")
}
if !cb.HasBeenCalled {
t.Error("Expected after callback to have been called")
}
if w.Code != http.StatusFound {
t.Error("Unexpected status:", w.Code)
}
loc := w.Header().Get("Location")
if loc != a.AuthLoginOKPath {
t.Error("Unexpeced location:", loc)
}
val, ok := sessions.Values[authboss.SessionKey]
if !ok {
t.Error("Expected session to be set")
} else if val != "john" {
t.Error("Expected session value to be authed username")
}
}
func TestAuth_loginHandlerFunc_OtherMethods(t *testing.T) {
t.Parallel()
a, _ := testSetup()
methods := []string{"HEAD", "PUT", "DELETE", "TRACE", "CONNECT"}
for i, method := range methods {
r, err := http.NewRequest(method, "/login", nil)
if err != nil {
t.Errorf("%d> Unexpected error '%s'", i, err)
}
w := httptest.NewRecorder()
if err := a.loginHandlerFunc(nil, w, r); err != nil {
t.Errorf("%d> Unexpected error: %s", i, err)
}
if http.StatusMethodNotAllowed != w.Code {
t.Errorf("%d> Expected status code %d, got %d", i, http.StatusMethodNotAllowed, w.Code)
continue
}
}
}
func TestAuth_validateCredentials(t *testing.T) {
func TestAuthInit(t *testing.T) {
t.Parallel()
ab := authboss.New()
storer := mocks.NewMockStorer()
ab.Storage.Server = storer
ctx := ab.NewContext()
storer.Users["john"] = authboss.Attributes{"password": "$2a$10$pgFsuQwdhwOdZp/v52dvHeEi53ZaI7dGmtwK4bAzGGN5A4nT6doqm"}
if _, err := validateCredentials(ctx, "john", "a"); err != nil {
t.Error("Unexpected error:", err)
router := &mocks.Router{}
renderer := &mocks.Renderer{}
errHandler := &mocks.ErrorHandler{}
ab.Config.Core.Router = router
ab.Config.Core.ViewRenderer = renderer
ab.Config.Core.ErrorHandler = errHandler
a := &Auth{}
a.Init(ab)
if err := renderer.HasLoadedViews(PageLogin); err != nil {
t.Error(err)
}
ctx = ab.NewContext()
if valid, err := validateCredentials(ctx, "jake", "a"); err != nil {
t.Error("Expect no error when user not found:", err)
} else if valid {
t.Error("Expect invalid when not user found")
if err := router.HasGets("/login"); err != nil {
t.Error(err)
}
ctx = ab.NewContext()
storer.GetErr = "Failed to load user"
if _, err := validateCredentials(ctx, "", ""); err.Error() != "Failed to load user" {
t.Error("Unexpected error:", err)
if err := router.HasPosts("/login"); err != nil {
t.Error(err)
}
if err := router.HasDeletes("/logout"); err != nil {
t.Error(err)
}
}
func TestAuth_logoutHandlerFunc_GET(t *testing.T) {
func TestAuthGet(t *testing.T) {
t.Parallel()
a, _ := testSetup()
ab := authboss.New()
responder := &mocks.Responder{}
ab.Config.Core.Responder = responder
a.AuthLogoutOKPath = "/dashboard"
a := &Auth{ab}
a.LoginGet(nil, nil)
ctx, w, r, sessionStorer := testRequest(a.Authboss, "GET")
sessionStorer.Put(authboss.SessionKey, "asdf")
sessionStorer.Put(authboss.SessionLastAction, "1234")
cookieStorer := mocks.NewMockClientStorer(authboss.CookieRemember, "qwert")
ctx.CookieStorer = cookieStorer
if err := a.logoutHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
if responder.Page != PageLogin {
t.Error("wanted login page, got:", responder.Page)
}
if val, ok := sessionStorer.Get(authboss.SessionKey); ok {
t.Error("Unexpected session key:", val)
}
if val, ok := sessionStorer.Get(authboss.SessionLastAction); ok {
t.Error("Unexpected last action:", val)
}
if val, ok := cookieStorer.Get(authboss.CookieRemember); ok {
t.Error("Unexpected rm cookie:", val)
}
if http.StatusFound != w.Code {
t.Errorf("Expected status code %d, got %d", http.StatusFound, w.Code)
}
location := w.Header().Get("Location")
if location != "/dashboard" {
t.Errorf("Expected lcoation %s, got %s", "/dashboard", location)
if responder.Status != http.StatusOK {
t.Error("wanted ok status, got:", responder.Status)
}
}
func TestAuth_logoutHandlerFunc_OtherMethods(t *testing.T) {
a, _ := testSetup()
type testHarness struct {
auth *Auth
ab *authboss.Authboss
methods := []string{"HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT"}
bodyReader *mocks.BodyReader
responder *mocks.Responder
redirector *mocks.Redirector
session *mocks.ClientStateRW
storer *mocks.ServerStorer
}
for i, method := range methods {
r, err := http.NewRequest(method, "/logout", nil)
if err != nil {
t.Errorf("%d> Unexpected error '%s'", i, err)
func testSetup() *testHarness {
harness := &testHarness{}
harness.ab = authboss.New()
harness.bodyReader = &mocks.BodyReader{}
harness.redirector = &mocks.Redirector{}
harness.responder = &mocks.Responder{}
harness.session = mocks.NewClientRW()
harness.storer = mocks.NewServerStorer()
harness.ab.Config.Core.BodyReader = harness.bodyReader
harness.ab.Config.Core.Logger = mocks.Logger{}
harness.ab.Config.Core.Responder = harness.responder
harness.ab.Config.Core.Redirector = harness.redirector
harness.ab.Config.Storage.SessionState = harness.session
harness.ab.Config.Storage.Server = harness.storer
harness.auth = &Auth{harness.ab}
return harness
}
func TestAuthPostSuccess(t *testing.T) {
t.Parallel()
setupMore := func(h *testHarness) *testHarness {
h.bodyReader.Return = mocks.Values{
PID: "test@test.com",
Password: "hello world",
}
w := httptest.NewRecorder()
h.storer.Users["test@test.com"] = &mocks.User{
Email: "test@test.com",
Password: "$2a$10$IlfnqVyDZ6c1L.kaA/q3bu1nkAC6KukNUsizvlzay1pZPXnX2C9Ji", // hello world
}
h.session.ClientValues[authboss.SessionHalfAuthKey] = "true"
if err := a.logoutHandlerFunc(nil, w, r); err != nil {
t.Errorf("%d> Unexpected error: %s", i, err)
return h
}
t.Run("normal", func(t *testing.T) {
t.Parallel()
h := setupMore(testSetup())
var beforeCalled, afterCalled bool
h.ab.Events.Before(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
beforeCalled = true
return false, nil
})
h.ab.Events.After(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
afterCalled = true
return false, nil
})
r := mocks.Request("POST")
resp := httptest.NewRecorder()
w := h.ab.NewResponse(resp, r)
if err := h.auth.LoginPost(w, r); err != nil {
t.Error(err)
}
if http.StatusMethodNotAllowed != w.Code {
t.Errorf("%d> Expected status code %d, got %d", i, http.StatusMethodNotAllowed, w.Code)
continue
if resp.Code != http.StatusTemporaryRedirect {
t.Error("code was wrong:", resp.Code)
}
if _, ok := h.session.ClientValues[authboss.SessionHalfAuthKey]; ok {
t.Error("half auth should have been deleted")
}
if pid := h.session.ClientValues[authboss.SessionKey]; pid != "test@test.com" {
t.Error("pid was wrong:", pid)
}
if !beforeCalled {
t.Error("before should have been called")
}
if !afterCalled {
t.Error("after should have been called")
}
})
t.Run("handledBefore", func(t *testing.T) {
t.Parallel()
h := setupMore(testSetup())
var beforeCalled bool
h.ab.Events.Before(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
w.WriteHeader(http.StatusTeapot)
beforeCalled = true
return true, nil
})
r := mocks.Request("POST")
resp := httptest.NewRecorder()
w := h.ab.NewResponse(resp, r)
if err := h.auth.LoginPost(w, r); err != nil {
t.Error(err)
}
if h.responder.Status != 0 {
t.Error("a status should never have been sent back")
}
if _, ok := h.session.ClientValues[authboss.SessionKey]; ok {
t.Error("session key should not have been set")
}
if !beforeCalled {
t.Error("before should have been called")
}
if resp.Code != http.StatusTeapot {
t.Error("should have left the response alone once teapot was sent")
}
})
t.Run("handledAfter", func(t *testing.T) {
t.Parallel()
h := setupMore(testSetup())
var afterCalled bool
h.ab.Events.After(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
w.WriteHeader(http.StatusTeapot)
afterCalled = true
return true, nil
})
r := mocks.Request("POST")
resp := httptest.NewRecorder()
w := h.ab.NewResponse(resp, r)
if err := h.auth.LoginPost(w, r); err != nil {
t.Error(err)
}
if h.responder.Status != 0 {
t.Error("a status should never have been sent back")
}
if _, ok := h.session.ClientValues[authboss.SessionKey]; !ok {
t.Error("session key should have been set")
}
if !afterCalled {
t.Error("after should have been called")
}
if resp.Code != http.StatusTeapot {
t.Error("should have left the response alone once teapot was sent")
}
})
}
func TestAuthPostBadPassword(t *testing.T) {
t.Parallel()
setupMore := func(h *testHarness) *testHarness {
h.bodyReader.Return = mocks.Values{
PID: "test@test.com",
Password: "world hello",
}
h.storer.Users["test@test.com"] = &mocks.User{
Email: "test@test.com",
Password: "$2a$10$IlfnqVyDZ6c1L.kaA/q3bu1nkAC6KukNUsizvlzay1pZPXnX2C9Ji", // hello world
}
return h
}
t.Run("normal", func(t *testing.T) {
h := setupMore(testSetup())
r := mocks.Request("POST")
resp := httptest.NewRecorder()
w := h.ab.NewResponse(resp, r)
var afterCalled bool
h.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
afterCalled = true
return false, nil
})
if err := h.auth.LoginPost(w, r); err != nil {
t.Error(err)
}
if resp.Code != 200 {
t.Error("wanted a 200:", resp.Code)
}
if h.responder.Data[authboss.DataErr] != "Invalid Credentials" {
t.Error("wrong error:", h.responder.Data)
}
if _, ok := h.session.ClientValues[authboss.SessionKey]; ok {
t.Error("user should not be logged in")
}
if !afterCalled {
t.Error("after should have been called")
}
})
t.Run("handledAfter", func(t *testing.T) {
h := setupMore(testSetup())
r := mocks.Request("POST")
resp := httptest.NewRecorder()
w := h.ab.NewResponse(resp, r)
var afterCalled bool
h.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
w.WriteHeader(http.StatusTeapot)
afterCalled = true
return true, nil
})
if err := h.auth.LoginPost(w, r); err != nil {
t.Error(err)
}
if h.responder.Status != 0 {
t.Error("responder should not have been called to give a status")
}
if _, ok := h.session.ClientValues[authboss.SessionKey]; ok {
t.Error("user should not be logged in")
}
if !afterCalled {
t.Error("after should have been called")
}
if resp.Code != http.StatusTeapot {
t.Error("should have left the response alone once teapot was sent")
}
})
}
func TestAuthPostUserNotFound(t *testing.T) {
t.Parallel()
harness := testSetup()
harness.bodyReader.Return = mocks.Values{
PID: "test@test.com",
Password: "world hello",
}
r := mocks.Request("POST")
resp := httptest.NewRecorder()
w := harness.ab.NewResponse(resp, r)
// This event is really the only thing that separates "user not found" from "bad password"
var afterCalled bool
harness.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
afterCalled = true
return false, nil
})
if err := harness.auth.LoginPost(w, r); err != nil {
t.Error(err)
}
if resp.Code != 200 {
t.Error("wanted a 200:", resp.Code)
}
if harness.responder.Data[authboss.DataErr] != "Invalid Credentials" {
t.Error("wrong error:", harness.responder.Data)
}
if _, ok := harness.session.ClientValues[authboss.SessionKey]; ok {
t.Error("user should not be logged in")
}
if afterCalled {
t.Error("after should not have been called")
}
}
func TestAuthLogout(t *testing.T) {
t.Parallel()
h := testSetup()
h.storer.Users["test@test.com"] = &mocks.User{
Email: "test@test.com",
Password: "$2a$10$IlfnqVyDZ6c1L.kaA/q3bu1nkAC6KukNUsizvlzay1pZPXnX2C9Ji", // hello world
}
h.session.ClientValues[authboss.SessionKey] = "test@test.com"
h.session.ClientValues[authboss.SessionHalfAuthKey] = "true"
cookies := mocks.NewClientRW()
cookies.ClientValues[authboss.CookieRemember] = "token"
h.ab.Config.Storage.CookieState = cookies
r := mocks.Request("POST")
resp := httptest.NewRecorder()
w := h.ab.NewResponse(resp, r)
var err error
r, err = h.ab.LoadClientState(w, r)
if err != nil {
t.Error(err)
}
if err := h.auth.Logout(w, r); err != nil {
t.Error(err)
}
if _, ok := h.session.ClientValues[authboss.SessionKey]; ok {
t.Error("want session key gone")
}
if _, ok := h.session.ClientValues[authboss.SessionHalfAuthKey]; ok {
t.Error("want session half auth key gone")
}
if _, ok := h.session.ClientValues[authboss.SessionLastAction]; ok {
t.Error("want session last action")
}
if _, ok := cookies.ClientValues[authboss.CookieRemember]; ok {
t.Error("want remember me cookies gone")
}
}

View File

@ -24,13 +24,13 @@ func NewResponder(renderer authboss.Renderer) *Responder {
// Respond to an HTTP request. It's main job is to merge data that comes in from
// various middlewares via the context with the data sent by the controller and render that.
func (r *Responder) Respond(w http.ResponseWriter, req *http.Request, code int, templateName string, data authboss.HTMLData) error {
func (r *Responder) Respond(w http.ResponseWriter, req *http.Request, code int, page string, data authboss.HTMLData) error {
ctxData := req.Context().Value(authboss.CTXKeyData)
if ctxData != nil {
data.Merge(ctxData.(authboss.HTMLData))
}
rendered, mime, err := r.Renderer.Render(req.Context(), templateName, data)
rendered, mime, err := r.Renderer.Render(req.Context(), page, data)
if err != nil {
return err
}

View File

@ -30,8 +30,8 @@ type User struct {
OAuthExpiry time.Time
}
func (m User) GetUsername() string { return m.Username }
func (m User) GetPID() string { return m.Email }
func (m User) GetUsername() string { return m.Username }
func (m User) GetPassword() string { return m.Password }
func (m User) GetRecoverToken() string { return m.RecoverToken }
func (m User) GetRecoverTokenExpiry() time.Time { return m.RecoverTokenExpiry }
@ -44,21 +44,22 @@ func (m User) GetOAuthToken() string { return m.OAuthToken }
func (m User) GetOAuthRefresh() string { return m.OAuthRefresh }
func (m User) GetOAuthExpiry() time.Time { return m.OAuthExpiry }
func (m *User) SetUsername(username string) { m.Username = username }
func (m *User) SetEmail(email string) { m.Email = email }
func (m *User) SetPassword(password string) { m.Password = password }
func (m *User) SetRecoverToken(recoverToken string) { m.RecoverToken = recoverToken }
func (m *User) SetRecoverTokenExpiry(recoverTokenExpiry time.Time) {
func (m *User) PutPID(email string) { m.Email = email }
func (m *User) PutUsername(username string) { m.Username = username }
func (m *User) PutEmail(email string) { m.Email = email }
func (m *User) PutPassword(password string) { m.Password = password }
func (m *User) PutRecoverToken(recoverToken string) { m.RecoverToken = recoverToken }
func (m *User) PutRecoverTokenExpiry(recoverTokenExpiry time.Time) {
m.RecoverTokenExpiry = recoverTokenExpiry
}
func (m *User) SetConfirmToken(confirmToken string) { m.ConfirmToken = confirmToken }
func (m *User) SetConfirmed(confirmed bool) { m.Confirmed = confirmed }
func (m *User) SetLocked(locked bool) { m.Locked = locked }
func (m *User) SetAttemptNumber(attemptNumber int) { m.AttemptNumber = attemptNumber }
func (m *User) SetAttemptTime(attemptTime time.Time) { m.AttemptTime = attemptTime }
func (m *User) SetOAuthToken(oAuthToken string) { m.OAuthToken = oAuthToken }
func (m *User) SetOAuthRefresh(oAuthRefresh string) { m.OAuthRefresh = oAuthRefresh }
func (m *User) SetOAuthExpiry(oAuthExpiry time.Time) { m.OAuthExpiry = oAuthExpiry }
func (m *User) PutConfirmToken(confirmToken string) { m.ConfirmToken = confirmToken }
func (m *User) PutConfirmed(confirmed bool) { m.Confirmed = confirmed }
func (m *User) PutLocked(locked bool) { m.Locked = locked }
func (m *User) PutAttemptNumber(attemptNumber int) { m.AttemptNumber = attemptNumber }
func (m *User) PutAttemptTime(attemptTime time.Time) { m.AttemptTime = attemptTime }
func (m *User) PutOAuthToken(oAuthToken string) { m.OAuthToken = oAuthToken }
func (m *User) PutOAuthRefresh(oAuthRefresh string) { m.OAuthRefresh = oAuthRefresh }
func (m *User) PutOAuthExpiry(oAuthExpiry time.Time) { m.OAuthExpiry = oAuthExpiry }
// ServerStorer should be valid for any module storer defined in authboss.
type ServerStorer struct {
@ -74,6 +75,23 @@ func NewServerStorer() *ServerStorer {
}
}
// Load a user
func (s *ServerStorer) Load(ctx context.Context, key string) (authboss.User, error) {
user, ok := s.Users[key]
if ok {
return user, nil
}
return nil, authboss.ErrUserNotFound
}
// Save a user
func (s *ServerStorer) Save(ctx context.Context, user authboss.User) error {
u := user.(*User)
s.Users[u.Email] = u
return nil
}
/*
// TODO(aarondl): What is this?
// AddToken for remember me
@ -315,3 +333,183 @@ func NewAfterCallback() *AfterCallback {
return &m
}
// Renderer mock
type Renderer struct {
Pages []string
// Render call variables
Context context.Context
Page string
Data authboss.HTMLData
}
// HasLoadedViews ensures the views were loaded
func (r *Renderer) HasLoadedViews(pages ...string) error {
if len(r.Pages) != len(pages) {
return errors.Errorf("want: %d loaded views, got: %d", len(pages), len(r.Pages))
}
for i, want := range pages {
got := r.Pages[i]
if want != got {
return errors.Errorf("want: %s [%d], got: %s", want, i, got)
}
}
return nil
}
// Load nothing but store the pages that were loaded
func (r *Renderer) Load(pages ...string) error {
r.Pages = append(r.Pages, pages...)
return nil
}
// Render nothing, but record the arguments into the renderer
func (r *Renderer) Render(ctx context.Context, page string, data authboss.HTMLData) ([]byte, string, error) {
r.Context = ctx
r.Page = page
r.Data = data
return nil, "text/html", nil
}
// Responder records how a request was responded to
type Responder struct {
Status int
Page string
Data authboss.HTMLData
}
// Respond stores the arguments in the struct
func (r *Responder) Respond(w http.ResponseWriter, req *http.Request, code int, page string, data authboss.HTMLData) error {
r.Status = code
r.Page = page
r.Data = data
return nil
}
// Redirector stores the redirect options passed to it and writes the Code
// to the ResponseWriter.
type Redirector struct {
Options authboss.RedirectOptions
}
// Redirect a request
func (r *Redirector) Redirect(w http.ResponseWriter, req *http.Request, ro authboss.RedirectOptions) error {
r.Options = ro
http.Redirect(w, req, ro.RedirectPath, ro.Code)
return nil
}
// BodyReader reads the body of a request and returns some values
type BodyReader struct {
Return Values
}
// Read the return values
func (b BodyReader) Read(page string, r *http.Request) (authboss.Validator, error) {
return b.Return, nil
}
// Values is returned from the BodyReader
type Values struct {
PID string
Password string
}
// GetPID from values
func (v Values) GetPID() string {
return v.PID
}
// GetPassword from values
func (v Values) GetPassword() string {
return v.Password
}
// Validate the values
func (v Values) Validate() []error {
return nil
}
// Logger logs to the void
type Logger struct {
}
// Info logging
func (l Logger) Info(string) {}
// Error logging
func (l Logger) Error(string) {}
// Router records the routes that were registered
type Router struct {
Gets []string
Posts []string
Deletes []string
}
// ServeHTTP does nothing
func (Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// Get records the path in the router
func (r *Router) Get(path string, _ http.Handler) {
r.Gets = append(r.Gets, path)
}
// Post records the path in the router
func (r *Router) Post(path string, _ http.Handler) {
r.Posts = append(r.Posts, path)
}
// Delete records the path in the router
func (r *Router) Delete(path string, _ http.Handler) {
r.Deletes = append(r.Deletes, path)
}
// HasGets ensures all gets routes are present
func (r *Router) HasGets(gets ...string) error {
return r.hasRoutes(gets, r.Gets)
}
// HasPosts ensures all gets routes are present
func (r *Router) HasPosts(posts ...string) error {
return r.hasRoutes(posts, r.Posts)
}
// HasDeletes ensures all gets routes are present
func (r *Router) HasDeletes(deletes ...string) error {
return r.hasRoutes(deletes, r.Deletes)
}
func (r *Router) hasRoutes(want []string, got []string) error {
if len(got) != len(want) {
return errors.Errorf("want: %d get routes, got: %d", len(want), len(got))
}
for i, w := range want {
g := got[i]
if w != g {
return errors.Errorf("wanted route: %s [%d], but got: %s", w, i, g)
}
}
return nil
}
// ErrorHandler just holds the last error
type ErrorHandler struct {
Error error
}
// Wrap an http method
func (e *ErrorHandler) Wrap(handler func(w http.ResponseWriter, r *http.Request) error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := handler(w, r); err != nil {
e.Error = err
}
})
}