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:
parent
77987afb8a
commit
d4f8d2f292
47
auth/auth.go
47
auth/auth.go
@ -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",
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user