Persist user sessions across server restarts.

This commit is contained in:
Patrick Fairbank
2019-08-19 20:51:30 -07:00
parent 3fca52b424
commit 33378fe3eb
4 changed files with 58 additions and 33 deletions

View File

@@ -8,27 +8,31 @@ package web
import (
"fmt"
"github.com/Team254/cheesy-arena/model"
"github.com/google/uuid"
"net/http"
"time"
)
// Shows the login form.
func (web *Web) loginHandler(w http.ResponseWriter, r *http.Request) {
var errorMessage string
if username := web.cookieAuth.Authorize(r); username != "" {
// If redirected here but already logged in, the user must have insufficient privileges; show a useful message.
errorMessage = fmt.Sprintf("User '%s' has insufficient privileges for the requested page. Try logging in as a"+
" different user.", username)
}
web.renderLogin(w, r, errorMessage)
web.renderLogin(w, r, "")
}
// Processes the login request.
func (web *Web) loginPostHandler(w http.ResponseWriter, r *http.Request) {
if err := web.cookieAuth.Login(w, r.PostFormValue("username"), r.PostFormValue("password")); err != nil {
username := r.PostFormValue("username")
if err := web.checkAuthPassword(username, r.PostFormValue("password")); err != nil {
web.renderLogin(w, r, err.Error())
return
}
session := model.UserSession{Token: uuid.New().String(), Username: username, CreatedAt: time.Now()}
if err := web.arena.Database.CreateUserSession(&session); err != nil {
handleWebErr(w, err)
return
}
http.SetCookie(w, &http.Cookie{Name: sessionTokenCookie, Value: session.Token})
redirectUrl := r.URL.Query().Get("redirect")
if redirectUrl == "" {
redirectUrl = "/"
@@ -52,3 +56,35 @@ func (web *Web) renderLogin(w http.ResponseWriter, r *http.Request, errorMessage
return
}
}
// Returns true if the given user is authorized for admin operations. Used for HTTP cookie authentication.
func (web *Web) userIsAdmin(w http.ResponseWriter, r *http.Request) bool {
if web.arena.EventSettings.AdminPassword == "" {
// Disable auth if there is no password configured.
return true
}
session := web.getUserSessionFromCookie(r)
if session != nil && session.Username == adminUser {
return true
} else {
http.Redirect(w, r, "/login?redirect="+r.URL.Path, 307)
return false
}
}
func (web *Web) getUserSessionFromCookie(r *http.Request) *model.UserSession {
token, err := r.Cookie(sessionTokenCookie)
if err != nil {
return nil
}
session, _ := web.arena.Database.GetUserSessionByToken(token.Value)
return session
}
func (web *Web) checkAuthPassword(user, password string) error {
if user == adminUser && password == web.arena.EventSettings.AdminPassword {
return nil
} else {
return fmt.Errorf("Invalid login credentials.")
}
}

View File

@@ -24,19 +24,19 @@ func TestLoginDisplay(t *testing.T) {
// Check logging in with the wrong username and right password.
recorder = web.postHttpResponse("/login?redirect=/match_play", "username=blorpy&password=reader")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Bad username or password")
assert.Contains(t, recorder.Body.String(), "Invalid login credentials.")
// Check logging in with the right username and wrong password.
recorder = web.postHttpResponse("/login?redirect=/match_play", "username=admin&password=blorpy")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Bad username or password")
assert.Contains(t, recorder.Body.String(), "Invalid login credentials.")
// Check logging in with the right username and password.
recorder = web.postHttpResponse("/login?redirect=/match_play", "username=admin&password=admin")
assert.Equal(t, 303, recorder.Code)
assert.Equal(t, "/match_play", recorder.Header().Get("Location"))
cookie := recorder.Header().Get("Set-Cookie")
assert.Contains(t, cookie, "Authorization=")
assert.Contains(t, cookie, "session_token=")
// Check that hitting the reader-level protected page works now.
recorder = web.getHttpResponseWithHeaders("/match_play", map[string]string{"Cookie": cookie})

View File

@@ -39,6 +39,7 @@ func (web *Web) settingsPostHandler(w http.ResponseWriter, r *http.Request) {
if len(eventSettings.Name) < 1 && eventSettings.Name != previousEventName {
eventSettings.Name = previousEventName
}
previousAdminPassword := eventSettings.AdminPassword
numAlliances, _ := strconv.Atoi(r.PostFormValue("numElimAlliances"))
if numAlliances < 2 || numAlliances > 16 {
@@ -80,6 +81,14 @@ func (web *Web) settingsPostHandler(w http.ResponseWriter, r *http.Request) {
return
}
if eventSettings.AdminPassword != previousAdminPassword {
// Delete any existing user sessions to force a logout.
if err := web.arena.Database.TruncateUserSessions(); err != nil {
handleWebErr(w, err)
return
}
}
http.Redirect(w, r, "/setup/settings", 303)
}

View File

@@ -6,7 +6,6 @@
package web
import (
"bitbucket.org/rj/httpauth-go"
"fmt"
"github.com/Team254/cheesy-arena/field"
"github.com/Team254/cheesy-arena/model"
@@ -18,18 +17,17 @@ import (
)
const (
adminUser = "admin"
sessionTokenCookie = "session_token"
adminUser = "admin"
)
type Web struct {
arena *field.Arena
cookieAuth *httpauth.Cookie
templateHelpers template.FuncMap
}
func NewWeb(arena *field.Arena) *Web {
web := &Web{arena: arena}
web.cookieAuth = httpauth.NewCookie("Cheesy Arena", "", web.checkAuthPassword)
// Helper functions that can be used inside templates.
web.templateHelpers = template.FuncMap{
@@ -93,24 +91,6 @@ func (web *Web) indexHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Returns true if the given user is authorized for admin operations. Used for HTTP cookie authentication.
func (web *Web) userIsAdmin(w http.ResponseWriter, r *http.Request) bool {
if web.arena.EventSettings.AdminPassword == "" {
// Disable auth if there is no password configured.
return true
}
if web.cookieAuth.Authorize(r) == adminUser {
return true
} else {
http.Redirect(w, r, "/login?redirect="+r.URL.Path, 307)
return false
}
}
func (web *Web) checkAuthPassword(user, password string) bool {
return user == adminUser && password == web.arena.EventSettings.AdminPassword
}
// Sets up the mapping between URLs and handlers.
func (web *Web) newHandler() http.Handler {
router := mux.NewRouter()