mirror of
https://github.com/volatiletech/authboss.git
synced 2024-11-28 08:58:38 +02:00
Add redirection on pages when logged in.
- Stop logged in users from accessing pages like auth/recover etc. - Ensure that half-authed users are allowed access to auth-like pages. - Make sure that if users have a remember token, it's processed before we decide if a user is logged in or not, preventing or granting access to these pages. - Fix #58
This commit is contained in:
parent
e7eda3c62e
commit
704697472f
@ -71,13 +71,6 @@ func (a *Auth) Storage() authboss.StorageOptions {
|
||||
func (a *Auth) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
|
||||
switch r.Method {
|
||||
case methodGET:
|
||||
if _, ok := ctx.SessionStorer.Get(authboss.SessionKey); ok {
|
||||
if halfAuthed, ok := ctx.SessionStorer.Get(authboss.SessionHalfAuthKey); !ok || halfAuthed == "false" {
|
||||
response.Redirect(ctx, w, r, a.AuthLoginOKPath, "", "", true)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
data := authboss.NewHTMLData(
|
||||
"showRemember", a.IsLoaded("remember"),
|
||||
"showRecover", a.IsLoaded("recover"),
|
||||
|
@ -70,31 +70,6 @@ func TestAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_loginHandlerFunc_GET_RedirectsWhenHalfAuthed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, _ := testSetup()
|
||||
ctx, w, r, sessionStore := testRequest(a.Authboss, "GET")
|
||||
|
||||
sessionStore.Put(authboss.SessionKey, "a")
|
||||
sessionStore.Put(authboss.SessionHalfAuthKey, "false")
|
||||
|
||||
a.AuthLoginOKPath = "/dashboard"
|
||||
|
||||
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
|
||||
t.Error("Unexpeced error:", err)
|
||||
}
|
||||
|
||||
if w.Code != http.StatusFound {
|
||||
t.Error("Unexpcted status:", w.Code)
|
||||
}
|
||||
|
||||
loc := w.Header().Get("Location")
|
||||
if loc != a.AuthLoginOKPath {
|
||||
t.Error("Unexpected redirect:", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_loginHandlerFunc_GET(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -72,7 +72,11 @@ func (a *Authboss) CurrentUser(w http.ResponseWriter, r *http.Request) (interfac
|
||||
ctx.SessionStorer = clientStoreWrapper{a.SessionStoreMaker(w, r)}
|
||||
ctx.CookieStorer = clientStoreWrapper{a.CookieStoreMaker(w, r)}
|
||||
|
||||
_, err = a.Callbacks.FireBefore(EventGetUserSession, ctx)
|
||||
return a.currentUser(ctx, w, r)
|
||||
}
|
||||
|
||||
func (a *Authboss) currentUser(ctx *Context, w http.ResponseWriter, r *http.Request) (interface{}, error) {
|
||||
_, err := a.Callbacks.FireBefore(EventGetUserSession, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -49,10 +49,13 @@ type expireMiddleware struct {
|
||||
// ExpireMiddleware ensures that the user's expiry information is kept up-to-date
|
||||
// on each request. Deletes the SessionKey from the session if the user is
|
||||
// expired (a.ExpireAfter duration since SessionLastAction).
|
||||
// This middleware conflicts with use of the Remember module, don't enable both
|
||||
// at the same time.
|
||||
func (a *Authboss) ExpireMiddleware(next http.Handler) http.Handler {
|
||||
return expireMiddleware{a, next}
|
||||
}
|
||||
|
||||
// ServeHTTP removes the session if it's passed the expire time.
|
||||
func (m expireMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
session := m.ab.SessionStoreMaker(w, r)
|
||||
if _, ok := session.Get(SessionKey); ok {
|
||||
|
47
router.go
47
router.go
@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HandlerFunc augments http.HandlerFunc with a context and error handling.
|
||||
@ -45,22 +46,30 @@ type contextRoute struct {
|
||||
}
|
||||
|
||||
func (c contextRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Instantiate the context
|
||||
ctx, err := c.Authboss.ContextFromRequest(r)
|
||||
if err != nil {
|
||||
fmt.Fprintf(c.LogWriter, "route: Malformed request, could not create context: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.CookieStorer = clientStoreWrapper{c.CookieStoreMaker(w, r)}
|
||||
ctx.SessionStorer = clientStoreWrapper{c.SessionStoreMaker(w, r)}
|
||||
|
||||
// Check to make sure we actually need to visit this route
|
||||
if redirectIfLoggedIn(ctx, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Call the handler
|
||||
err = c.fn(ctx, w, r)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Log the error
|
||||
fmt.Fprintf(c.LogWriter, "Error Occurred at %s: %v", r.URL.Path, err)
|
||||
|
||||
// Do specific error handling for special kinds of errors.
|
||||
switch e := err.(type) {
|
||||
case ErrAndRedirect:
|
||||
if len(e.FlashSuccess) > 0 {
|
||||
@ -86,3 +95,39 @@ func (c contextRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// redirectIfLoggedIn checks a user's existence by using currentUser. This is done instead of
|
||||
// a simple Session cookie check so that the remember module has a chance to log the user in
|
||||
// before they are determined to "not be logged in".
|
||||
//
|
||||
// The exceptional routes are sort of hardcoded in a terrible way in here, later on this could move to some
|
||||
// configuration or something more interesting.
|
||||
func redirectIfLoggedIn(ctx *Context, w http.ResponseWriter, r *http.Request) (handled bool) {
|
||||
// If it's a log out url, always let it pass through.
|
||||
if strings.HasSuffix(r.URL.Path, "/logout") {
|
||||
return false
|
||||
}
|
||||
|
||||
// If it's an auth url, allow them through if they're half-authed.
|
||||
if strings.HasSuffix(r.URL.Path, "/auth") || strings.Contains(r.URL.Path, "/oauth2/") {
|
||||
if halfAuthed, ok := ctx.SessionStorer.Get(SessionHalfAuthKey); ok && halfAuthed == "true" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if cu, err := ctx.currentUser(ctx, w, r); err != nil {
|
||||
fmt.Fprintf(ctx.LogWriter, "error occurred reading current user at %s: %v", r.URL.Path, err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
io.WriteString(w, "500 An error has occurred")
|
||||
return true
|
||||
} else if cu != nil {
|
||||
if redir, ok := ctx.FirstFormValue(FormValueRedirect); ok && len(redir) > 0 {
|
||||
http.Redirect(w, r, redir, http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(w, r, ctx.AuthLoginOKPath, http.StatusFound)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package authboss
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@ -203,3 +204,99 @@ func TestRouter_Redirect(t *testing.T) {
|
||||
t.Error(fail, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_redirectIfLoggedIn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
Path string
|
||||
LoggedIn bool
|
||||
HalfAuthed bool
|
||||
|
||||
ShouldRedirect bool
|
||||
}{
|
||||
// These routes will be accessed depending on logged in and half auth's value
|
||||
{"/auth", false, false, false},
|
||||
{"/auth", true, false, true},
|
||||
{"/auth", true, true, false},
|
||||
{"/oauth2/facebook", false, false, false},
|
||||
{"/oauth2/facebook", true, false, true},
|
||||
{"/oauth2/facebook", true, true, false},
|
||||
{"/oauth2/callback/facebook", false, false, false},
|
||||
{"/oauth2/callback/facebook", true, false, true},
|
||||
{"/oauth2/callback/facebook", true, true, false},
|
||||
// These are logout routes and never redirect
|
||||
{"/logout", true, false, false},
|
||||
{"/logout", true, true, false},
|
||||
{"/oauth2/logout", true, false, false},
|
||||
{"/oauth2/logout", true, true, false},
|
||||
// These routes should always redirect despite half auth
|
||||
{"/register", true, true, true},
|
||||
{"/recover", true, true, true},
|
||||
{"/register", false, false, false},
|
||||
{"/recover", false, false, false},
|
||||
}
|
||||
|
||||
storer := mockStorer{"john@john.com": Attributes{
|
||||
StoreEmail: "john@john.com",
|
||||
StorePassword: "password",
|
||||
}}
|
||||
ab := New()
|
||||
ab.Storer = storer
|
||||
|
||||
for i, test := range tests {
|
||||
session := mockClientStore{}
|
||||
cookies := mockClientStore{}
|
||||
ctx := ab.NewContext()
|
||||
ctx.SessionStorer = session
|
||||
ctx.CookieStorer = cookies
|
||||
|
||||
if test.LoggedIn {
|
||||
session[SessionKey] = "john@john.com"
|
||||
}
|
||||
if test.HalfAuthed {
|
||||
session[SessionHalfAuthKey] = "true"
|
||||
}
|
||||
|
||||
r, _ := http.NewRequest("GET", test.Path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
handled := redirectIfLoggedIn(ctx, w, r)
|
||||
|
||||
if test.ShouldRedirect && (!handled || w.Code != http.StatusFound) {
|
||||
t.Errorf("%d) It should have redirected the request: %q %t %d", i, test.Path, handled, w.Code)
|
||||
} else if !test.ShouldRedirect && (handled || w.Code != http.StatusOK) {
|
||||
t.Errorf("%d) It should have NOT redirected the request: %q %t %d", i, test.Path, handled, w.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type deathStorer struct{}
|
||||
|
||||
func (d deathStorer) Create(key string, attributes Attributes) error { return nil }
|
||||
func (d deathStorer) Put(key string, attributes Attributes) error { return nil }
|
||||
func (d deathStorer) Get(key string) (interface{}, error) { return nil, errors.New("explosion") }
|
||||
|
||||
func TestRouter_redirectIfLoggedInError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ab := New()
|
||||
ab.LogWriter = ioutil.Discard
|
||||
ab.Storer = deathStorer{}
|
||||
|
||||
session := mockClientStore{SessionKey: "john"}
|
||||
cookies := mockClientStore{}
|
||||
ctx := ab.NewContext()
|
||||
ctx.SessionStorer = session
|
||||
ctx.CookieStorer = cookies
|
||||
|
||||
r, _ := http.NewRequest("GET", "/auth", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handled := redirectIfLoggedIn(ctx, w, r)
|
||||
|
||||
if !handled {
|
||||
t.Error("It should have been handled.")
|
||||
}
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Error("It should have internal server error'd:", w.Code)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user