Remove remember-me (#21)
This commit is contained in:
parent
2b36d33b89
commit
8e996878ac
|
|
@ -185,7 +185,7 @@ func TestAPIGetMe_WithSession(t *testing.T) {
|
|||
// Create a route that first creates a session and then calls APIGetMe
|
||||
env.echo.GET("/setup-and-getme", func(c echo.Context) error {
|
||||
// Create a session for admin user
|
||||
createSession(c, "admin", true, uint32(0), false)
|
||||
createSession(c, "admin", true, uint32(0))
|
||||
return c.String(http.StatusOK, "session created")
|
||||
})
|
||||
env.echo.GET("/api/v1/auth/me", APIGetMe(env.db))
|
||||
|
|
@ -247,7 +247,7 @@ func TestAPIGetMe_WithAuthenticatedUser(t *testing.T) {
|
|||
|
||||
// Create session
|
||||
env.echo.GET("/setup-session", func(c echo.Context) error {
|
||||
createSession(c, "realuser", false, crc, false)
|
||||
createSession(c, "realuser", false, crc)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
env.echo.GET("/api/v1/auth/me2", APIGetMe(env.db))
|
||||
|
|
@ -309,7 +309,7 @@ func TestAPIAuth_WithValidSession(t *testing.T) {
|
|||
|
||||
// Create session
|
||||
env.echo.GET("/create-api-session", func(c echo.Context) error {
|
||||
createSession(c, "admin", true, uint32(12345), true)
|
||||
createSession(c, "admin", true, uint32(12345))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
@ -363,7 +363,7 @@ func TestAPIGetMe_EmptyUsername(t *testing.T) {
|
|||
|
||||
// Create a session and then clear the username to empty string
|
||||
env.echo.GET("/setup-empty-username", func(c echo.Context) error {
|
||||
createSession(c, "", false, uint32(0), false)
|
||||
createSession(c, "", false, uint32(0))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
@ -407,7 +407,7 @@ func TestAPIGetMe_DBError(t *testing.T) {
|
|||
}()
|
||||
|
||||
env.echo.GET("/setup-doomed-session", func(c echo.Context) error {
|
||||
createSession(c, "doomed", false, crc, false)
|
||||
createSession(c, "doomed", false, crc)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1140,7 +1140,7 @@ func TestCurrentUserEmail_UserNotFound(t *testing.T) {
|
|||
var email string
|
||||
env.echo.GET("/test-email-nf", func(c echo.Context) error {
|
||||
// session has a username that doesn't exist in DB
|
||||
createSession(c, "nonexistent", false, uint32(0), false)
|
||||
createSession(c, "nonexistent", false, uint32(0))
|
||||
email = currentUserEmail(c, env.db)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
|
@ -1163,7 +1163,7 @@ func TestCurrentUserEmail_UserExists(t *testing.T) {
|
|||
|
||||
var email string
|
||||
env.echo.GET("/test-email-found", func(c echo.Context) error {
|
||||
createSession(c, "emailuser", false, uint32(0), false)
|
||||
createSession(c, "emailuser", false, uint32(0))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
env.echo.GET("/read-email", func(c echo.Context) error {
|
||||
|
|
@ -1640,7 +1640,7 @@ func TestNonAdmin_ClientAccess(t *testing.T) {
|
|||
|
||||
// Register ALL routes before first ServeHTTP
|
||||
env.echo.GET("/na-setup", func(c echo.Context) error {
|
||||
createSession(c, "naviewer", false, crc, false)
|
||||
createSession(c, "naviewer", false, crc)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
env.echo.GET("/na-get/:id", APIGetClient(env.db))
|
||||
|
|
@ -1743,7 +1743,7 @@ func TestAPIEmailClient_NonAdmin_OtherClient(t *testing.T) {
|
|||
|
||||
// Register routes before any ServeHTTP
|
||||
env.echo.GET("/email-na-setup", func(c echo.Context) error {
|
||||
createSession(c, "emailna", false, crc, false)
|
||||
createSession(c, "emailna", false, crc)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
env.echo.POST("/email-deny/:id", APIEmailClient(env.db, mailer, "Subject", "Body"))
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ func APIHandleOIDCCallback(oidcProvider *OIDCProvider, db store.IStore) echo.Han
|
|||
}
|
||||
|
||||
// create session using shared helper (respects SessionMaxDuration config)
|
||||
createSession(c, user.Username, user.Admin, util.GetDBUserCRC32(user), true)
|
||||
createSession(c, user.Username, user.Admin, util.GetDBUserCRC32(user))
|
||||
|
||||
auditLogEvent(c, "user.login", "user", user.Username, map[string]string{"email": user.Email})
|
||||
log.Infof("OIDC login successful for user: %s", user.Username)
|
||||
|
|
|
|||
|
|
@ -13,38 +13,6 @@ import (
|
|||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if !isValidSession(c) {
|
||||
nextURL := c.Request().URL
|
||||
if nextURL != nil && c.Request().Method == http.MethodGet {
|
||||
return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath+"/login?next=%s", c.Request().URL))
|
||||
} else {
|
||||
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login")
|
||||
}
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshSession must only be used after ValidSession middleware
|
||||
// RefreshSession checks if the session is eligible for the refresh, but doesn't check if it's fully valid
|
||||
func RefreshSession(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
doRefreshSession(c)
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if !isAdmin(c) {
|
||||
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/")
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
func isValidSession(c echo.Context) bool {
|
||||
if util.DisableLogin {
|
||||
return true
|
||||
|
|
@ -86,55 +54,6 @@ func isValidSession(c echo.Context) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Refreshes a "remember me" session when the user visits web pages (not API)
|
||||
// Session must be valid before calling this function
|
||||
// Refresh is performed at most once per 24h
|
||||
func doRefreshSession(c echo.Context) {
|
||||
if util.DisableLogin {
|
||||
return
|
||||
}
|
||||
|
||||
sess, _ := session.Get("session", c)
|
||||
maxAge := getMaxAge(sess)
|
||||
if maxAge <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
oldCookie, err := c.Cookie("session_token")
|
||||
if err != nil || sess.Values["session_token"] != oldCookie.Value {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh no sooner than 24h
|
||||
createdAt := getCreatedAt(sess)
|
||||
updatedAt := getUpdatedAt(sess)
|
||||
expiration := updatedAt + int64(getMaxAge(sess))
|
||||
now := time.Now().UTC().Unix()
|
||||
if updatedAt > now || expiration < now || now-updatedAt < 86_400 || createdAt+util.SessionMaxDuration < now {
|
||||
return
|
||||
}
|
||||
|
||||
cookiePath := util.GetCookiePath()
|
||||
|
||||
sess.Values["updated_at"] = now
|
||||
sess.Options = &sessions.Options{
|
||||
Path: cookiePath,
|
||||
MaxAge: maxAge,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
sess.Save(c.Request(), c.Response())
|
||||
|
||||
cookie := new(http.Cookie)
|
||||
cookie.Name = "session_token"
|
||||
cookie.Path = cookiePath
|
||||
cookie.Value = oldCookie.Value
|
||||
cookie.MaxAge = maxAge
|
||||
cookie.HttpOnly = true
|
||||
cookie.SameSite = http.SameSiteLaxMode
|
||||
c.SetCookie(cookie)
|
||||
}
|
||||
|
||||
// Get time in seconds this session is valid without updating
|
||||
func getMaxAge(sess *sessions.Session) int {
|
||||
if util.DisableLogin {
|
||||
|
|
@ -223,14 +142,10 @@ func isAdmin(c echo.Context) bool {
|
|||
}
|
||||
|
||||
// createSession establishes a new authenticated session for the user
|
||||
func createSession(c echo.Context, username string, admin bool, userCRC32 uint32, rememberMe bool) {
|
||||
maxAge := 0
|
||||
if rememberMe {
|
||||
if util.SessionMaxDuration > 0 {
|
||||
maxAge = int(util.SessionMaxDuration)
|
||||
} else {
|
||||
maxAge = 86400 * 7
|
||||
}
|
||||
func createSession(c echo.Context, username string, admin bool, userCRC32 uint32) {
|
||||
maxAge := int(util.SessionMaxDuration)
|
||||
if maxAge <= 0 {
|
||||
maxAge = 86400 // 1 day default
|
||||
}
|
||||
|
||||
cookiePath := util.GetCookiePath()
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ func TestCreateSession(t *testing.T) {
|
|||
|
||||
var sessionCreated bool
|
||||
env.echo.GET("/create-session", func(c echo.Context) error {
|
||||
createSession(c, "testuser", true, uint32(12345), false)
|
||||
createSession(c, "testuser", true, uint32(12345))
|
||||
sessionCreated = true
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
|
@ -300,134 +300,39 @@ func TestCreateSession(t *testing.T) {
|
|||
assert.True(t, found, "session_token cookie should be set")
|
||||
}
|
||||
|
||||
func TestCreateSession_WithRememberMe(t *testing.T) {
|
||||
func TestCreateSession_MaxAgeFromSessionMaxDuration(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
origMaxDuration := util.SessionMaxDuration
|
||||
util.SessionMaxDuration = 86400 * 7 // 7 days
|
||||
defer func() {
|
||||
util.DisableLogin = origDisable
|
||||
util.SessionMaxDuration = origMaxDuration
|
||||
}()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
env.echo.GET("/create-session-remember", func(c echo.Context) error {
|
||||
createSession(c, "testuser", false, uint32(999), true)
|
||||
env.echo.GET("/create-session-maxage", func(c echo.Context) error {
|
||||
createSession(c, "testuser", false, uint32(999))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req, rec := jsonRequest(http.MethodGet, "/create-session-remember", nil)
|
||||
req, rec := jsonRequest(http.MethodGet, "/create-session-maxage", nil)
|
||||
env.echo.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
// Verify session_token cookie has MaxAge > 0
|
||||
// Verify session_token cookie has MaxAge from SessionMaxDuration
|
||||
cookies := rec.Result().Cookies()
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "session_token" {
|
||||
assert.Greater(t, cookie.MaxAge, 0, "remember-me cookie should have positive MaxAge")
|
||||
assert.Equal(t, int(util.SessionMaxDuration), cookie.MaxAge,
|
||||
"session cookie should have MaxAge equal to SessionMaxDuration")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ValidSession tests ---
|
||||
|
||||
func TestValidSession_DisabledLogin(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = true
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = true
|
||||
|
||||
called := false
|
||||
env.echo.GET("/test-valid-session", ValidSession(func(c echo.Context) error {
|
||||
called = true
|
||||
return c.String(http.StatusOK, "ok")
|
||||
}))
|
||||
|
||||
req, rec := jsonRequest(http.MethodGet, "/test-valid-session", nil)
|
||||
env.echo.ServeHTTP(rec, req)
|
||||
assert.True(t, called)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
func TestValidSession_NoSession_Redirects(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
env.echo.GET("/protected", ValidSession(func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "ok")
|
||||
}))
|
||||
|
||||
req, rec := jsonRequest(http.MethodGet, "/protected", nil)
|
||||
env.echo.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, rec.Code)
|
||||
assert.Contains(t, rec.Header().Get("Location"), "/login")
|
||||
}
|
||||
|
||||
// --- NeedsAdmin tests ---
|
||||
|
||||
func TestNeedsAdmin_DisabledLogin(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = true
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = true
|
||||
|
||||
called := false
|
||||
env.echo.GET("/admin-only", NeedsAdmin(func(c echo.Context) error {
|
||||
called = true
|
||||
return c.String(http.StatusOK, "admin ok")
|
||||
}))
|
||||
|
||||
req, rec := jsonRequest(http.MethodGet, "/admin-only", nil)
|
||||
env.echo.ServeHTTP(rec, req)
|
||||
assert.True(t, called)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
func TestNeedsAdmin_NotAdmin_Redirects(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
env.echo.GET("/admin-only", NeedsAdmin(func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "admin ok")
|
||||
}))
|
||||
|
||||
req, rec := jsonRequest(http.MethodGet, "/admin-only", nil)
|
||||
env.echo.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, rec.Code)
|
||||
}
|
||||
|
||||
// --- RefreshSession tests ---
|
||||
|
||||
func TestRefreshSession_DisabledLogin(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = true
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = true
|
||||
|
||||
called := false
|
||||
env.echo.GET("/refresh-test", RefreshSession(func(c echo.Context) error {
|
||||
called = true
|
||||
return c.String(http.StatusOK, "ok")
|
||||
}))
|
||||
|
||||
req, rec := jsonRequest(http.MethodGet, "/refresh-test", nil)
|
||||
env.echo.ServeHTTP(rec, req)
|
||||
assert.True(t, called)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// --- setUser tests ---
|
||||
|
||||
func TestSetUser(t *testing.T) {
|
||||
|
|
@ -468,166 +373,6 @@ func TestClearSession(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// --- doRefreshSession tests ---
|
||||
|
||||
func TestDoRefreshSession_DisabledLogin(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = true
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = true
|
||||
|
||||
env.echo.GET("/do-refresh", func(c echo.Context) error {
|
||||
doRefreshSession(c) // should be a no-op
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req, rec := jsonRequest(http.MethodGet, "/do-refresh", nil)
|
||||
env.echo.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
func TestDoRefreshSession_NoRememberMe(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
env.echo.GET("/do-refresh-no-remember", func(c echo.Context) error {
|
||||
doRefreshSession(c) // should be a no-op since no remember-me
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req, rec := jsonRequest(http.MethodGet, "/do-refresh-no-remember", nil)
|
||||
env.echo.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
func TestDoRefreshSession_EligibleForRefresh(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
origMaxDuration := util.SessionMaxDuration
|
||||
util.SessionMaxDuration = 86400 * 90 // 90 days in seconds
|
||||
defer func() {
|
||||
util.DisableLogin = origDisable
|
||||
util.SessionMaxDuration = origMaxDuration
|
||||
}()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
// Step 1: Create a remember-me session with manipulated timestamps
|
||||
env.echo.GET("/create-old-session", func(c echo.Context) error {
|
||||
// Create a session with remember-me
|
||||
createSession(c, "admin", true, uint32(12345), true)
|
||||
|
||||
// Now manipulate the session to be >24h old
|
||||
sess, _ := session.Get("session", c)
|
||||
now := time.Now().UTC().Unix()
|
||||
sess.Values["created_at"] = now - 172800 // created 2 days ago
|
||||
sess.Values["updated_at"] = now - 86401 // updated >24h ago
|
||||
sess.Save(c.Request(), c.Response())
|
||||
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req1, rec1 := jsonRequest(http.MethodGet, "/create-old-session", nil)
|
||||
env.echo.ServeHTTP(rec1, req1)
|
||||
require.Equal(t, http.StatusOK, rec1.Code)
|
||||
|
||||
// Step 2: Call doRefreshSession with the session cookies
|
||||
// Deduplicate cookies: keep only the LAST cookie for each name
|
||||
allCookies := rec1.Result().Cookies()
|
||||
lastCookie := make(map[string]*http.Cookie)
|
||||
for _, cookie := range allCookies {
|
||||
lastCookie[cookie.Name] = cookie
|
||||
}
|
||||
|
||||
env.echo.GET("/trigger-refresh", func(c echo.Context) error {
|
||||
doRefreshSession(c)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req2, rec2 := jsonRequest(http.MethodGet, "/trigger-refresh", nil)
|
||||
for _, cookie := range lastCookie {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
env.echo.ServeHTTP(rec2, req2)
|
||||
assert.Equal(t, http.StatusOK, rec2.Code)
|
||||
}
|
||||
|
||||
func TestDoRefreshSession_SuccessfulRefresh_VerifyUpdatedAt(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
origMaxDuration := util.SessionMaxDuration
|
||||
util.SessionMaxDuration = 86400 * 90 // 90 days
|
||||
defer func() {
|
||||
util.DisableLogin = origDisable
|
||||
util.SessionMaxDuration = origMaxDuration
|
||||
}()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
// Create a remember-me session, then in the same handler, manipulate it
|
||||
// to look like it was updated 2 days ago (>24h threshold).
|
||||
// IMPORTANT: we must only forward the LAST session cookie to avoid
|
||||
// gorilla/sessions reading the first (unmanipulated) one.
|
||||
env.echo.GET("/create-refresh-session", func(c echo.Context) error {
|
||||
createSession(c, "refreshme", true, uint32(99999), true)
|
||||
|
||||
sess, _ := session.Get("session", c)
|
||||
now := time.Now().UTC().Unix()
|
||||
sess.Values["created_at"] = now - 259200 // 3 days ago
|
||||
sess.Values["updated_at"] = now - 172800 // 2 days ago (well past 24h)
|
||||
sess.Save(c.Request(), c.Response())
|
||||
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req1, rec1 := jsonRequest(http.MethodGet, "/create-refresh-session", nil)
|
||||
env.echo.ServeHTTP(rec1, req1)
|
||||
require.Equal(t, http.StatusOK, rec1.Code)
|
||||
|
||||
// Deduplicate cookies: keep only the LAST cookie for each name
|
||||
// (gorilla/sessions picks the first match, and createSession + sess.Save
|
||||
// both write a "session" cookie; we need the manipulated one)
|
||||
allCookies := rec1.Result().Cookies()
|
||||
lastCookie := make(map[string]*http.Cookie)
|
||||
for _, cookie := range allCookies {
|
||||
lastCookie[cookie.Name] = cookie
|
||||
}
|
||||
|
||||
// Now call the RefreshSession middleware wrapping a simple handler
|
||||
var handlerCalled bool
|
||||
env.echo.GET("/refresh-middleware-test", RefreshSession(func(c echo.Context) error {
|
||||
handlerCalled = true
|
||||
return c.String(http.StatusOK, "refreshed")
|
||||
}))
|
||||
|
||||
req2, rec2 := jsonRequest(http.MethodGet, "/refresh-middleware-test", nil)
|
||||
for _, cookie := range lastCookie {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
env.echo.ServeHTTP(rec2, req2)
|
||||
assert.Equal(t, http.StatusOK, rec2.Code)
|
||||
assert.True(t, handlerCalled, "next handler should be called after refresh")
|
||||
|
||||
// Verify the session was refreshed: response should contain a session_token cookie
|
||||
refreshCookies := rec2.Result().Cookies()
|
||||
foundRefresh := false
|
||||
for _, cookie := range refreshCookies {
|
||||
if cookie.Name == "session_token" {
|
||||
foundRefresh = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, foundRefresh, "doRefreshSession should emit a refreshed session_token cookie")
|
||||
}
|
||||
|
||||
// --- Integration tests: createSession + isValidSession ---
|
||||
|
||||
func TestCreateAndValidateSession(t *testing.T) {
|
||||
|
|
@ -650,7 +395,7 @@ func TestCreateAndValidateSession(t *testing.T) {
|
|||
|
||||
// Create a session
|
||||
env.echo.GET("/create", func(c echo.Context) error {
|
||||
createSession(c, "admin", true, uint32(12345), true)
|
||||
createSession(c, "admin", true, uint32(12345))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
@ -689,7 +434,7 @@ func TestIsValidSession_MismatchedCRC32(t *testing.T) {
|
|||
util.DBUsersToCRC32Mutex.Unlock()
|
||||
|
||||
env.echo.GET("/create-mismatch", func(c echo.Context) error {
|
||||
createSession(c, "admin", true, uint32(12345), true)
|
||||
createSession(c, "admin", true, uint32(12345))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
@ -721,300 +466,6 @@ func TestIsValidSession_MismatchedCRC32(t *testing.T) {
|
|||
assert.False(t, valid, "Session should be invalid when CRC32 mismatches")
|
||||
}
|
||||
|
||||
func TestValidSession_WithValidSession_PassesThrough(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
// Populate CRC32 map
|
||||
util.DBUsersToCRC32Mutex.Lock()
|
||||
util.DBUsersToCRC32["admin"] = uint32(12345)
|
||||
util.DBUsersToCRC32Mutex.Unlock()
|
||||
defer func() {
|
||||
util.DBUsersToCRC32Mutex.Lock()
|
||||
delete(util.DBUsersToCRC32, "admin")
|
||||
util.DBUsersToCRC32Mutex.Unlock()
|
||||
}()
|
||||
|
||||
// Create a session
|
||||
env.echo.GET("/make-session", func(c echo.Context) error {
|
||||
createSession(c, "admin", true, uint32(12345), true)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req1, rec1 := jsonRequest(http.MethodGet, "/make-session", nil)
|
||||
env.echo.ServeHTTP(rec1, req1)
|
||||
|
||||
// Now access a ValidSession-protected route
|
||||
called := false
|
||||
env.echo.GET("/protected-route", ValidSession(func(c echo.Context) error {
|
||||
called = true
|
||||
return c.String(http.StatusOK, "protected")
|
||||
}))
|
||||
|
||||
cookies := rec1.Result().Cookies()
|
||||
req2, rec2 := jsonRequest(http.MethodGet, "/protected-route", nil)
|
||||
for _, cookie := range cookies {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
env.echo.ServeHTTP(rec2, req2)
|
||||
assert.True(t, called, "ValidSession should pass through for valid session")
|
||||
assert.Equal(t, http.StatusOK, rec2.Code)
|
||||
}
|
||||
|
||||
func TestDoRefreshSession_WithSession_NoRefreshNeeded(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
// Create a remember-me session
|
||||
env.echo.GET("/create-for-refresh", func(c echo.Context) error {
|
||||
createSession(c, "admin", true, uint32(12345), true)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req1, rec1 := jsonRequest(http.MethodGet, "/create-for-refresh", nil)
|
||||
env.echo.ServeHTTP(rec1, req1)
|
||||
|
||||
// Try to refresh immediately (should not refresh since <24h since creation)
|
||||
env.echo.GET("/do-refresh-with-session", func(c echo.Context) error {
|
||||
doRefreshSession(c)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
cookies := rec1.Result().Cookies()
|
||||
req2, rec2 := jsonRequest(http.MethodGet, "/do-refresh-with-session", nil)
|
||||
for _, cookie := range cookies {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
env.echo.ServeHTTP(rec2, req2)
|
||||
assert.Equal(t, http.StatusOK, rec2.Code)
|
||||
}
|
||||
|
||||
func TestValidSession_POST_NoNextURL(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
env.echo.POST("/protected-post", ValidSession(func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "ok")
|
||||
}))
|
||||
|
||||
req, rec := jsonRequest(http.MethodPost, "/protected-post", nil)
|
||||
env.echo.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, rec.Code)
|
||||
// POST redirect should go to /login without ?next= parameter
|
||||
location := rec.Header().Get("Location")
|
||||
assert.Contains(t, location, "/login")
|
||||
}
|
||||
|
||||
// --- doRefreshSession: session token mismatch ---
|
||||
|
||||
func TestDoRefreshSession_TokenMismatch(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
// Create a remember-me session
|
||||
env.echo.GET("/create-for-mismatch", func(c echo.Context) error {
|
||||
createSession(c, "admin", true, uint32(12345), true)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req1, rec1 := jsonRequest(http.MethodGet, "/create-for-mismatch", nil)
|
||||
env.echo.ServeHTTP(rec1, req1)
|
||||
require.Equal(t, http.StatusOK, rec1.Code)
|
||||
|
||||
// Now tamper with the session_token cookie
|
||||
env.echo.GET("/do-refresh-mismatch", func(c echo.Context) error {
|
||||
doRefreshSession(c)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
cookies := rec1.Result().Cookies()
|
||||
req2, rec2 := jsonRequest(http.MethodGet, "/do-refresh-mismatch", nil)
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "session_token" {
|
||||
cookie.Value = "tampered-token"
|
||||
}
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
env.echo.ServeHTTP(rec2, req2)
|
||||
assert.Equal(t, http.StatusOK, rec2.Code)
|
||||
// The handler still returns OK, but the session was not refreshed
|
||||
}
|
||||
|
||||
// --- doRefreshSession: no cookie at all ---
|
||||
|
||||
func TestDoRefreshSession_NoCookie(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
env.echo.GET("/do-refresh-no-cookie", func(c echo.Context) error {
|
||||
doRefreshSession(c)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req, rec := jsonRequest(http.MethodGet, "/do-refresh-no-cookie", nil)
|
||||
env.echo.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// --- doRefreshSession: session past max duration ---
|
||||
|
||||
func TestDoRefreshSession_PastMaxDuration(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
origMaxDuration := util.SessionMaxDuration
|
||||
util.SessionMaxDuration = 100 // 100 seconds
|
||||
defer func() {
|
||||
util.DisableLogin = origDisable
|
||||
util.SessionMaxDuration = origMaxDuration
|
||||
}()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
// Create a session and manipulate it to be past max duration
|
||||
env.echo.GET("/create-expired", func(c echo.Context) error {
|
||||
createSession(c, "admin", true, uint32(12345), true)
|
||||
|
||||
// Manipulate session: created long ago, past max duration
|
||||
sess, _ := session.Get("session", c)
|
||||
now := time.Now().UTC().Unix()
|
||||
sess.Values["created_at"] = now - 200 // created 200s ago, max is 100s
|
||||
sess.Values["updated_at"] = now - 90 // updated 90s ago
|
||||
sess.Save(c.Request(), c.Response())
|
||||
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req1, rec1 := jsonRequest(http.MethodGet, "/create-expired", nil)
|
||||
env.echo.ServeHTTP(rec1, req1)
|
||||
require.Equal(t, http.StatusOK, rec1.Code)
|
||||
|
||||
env.echo.GET("/do-refresh-expired", func(c echo.Context) error {
|
||||
doRefreshSession(c)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
cookies := rec1.Result().Cookies()
|
||||
req2, rec2 := jsonRequest(http.MethodGet, "/do-refresh-expired", nil)
|
||||
for _, cookie := range cookies {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
env.echo.ServeHTTP(rec2, req2)
|
||||
assert.Equal(t, http.StatusOK, rec2.Code)
|
||||
}
|
||||
|
||||
// --- doRefreshSession: updatedAt is in the future (corrupted) ---
|
||||
|
||||
func TestDoRefreshSession_FutureUpdatedAt(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
origMaxDuration := util.SessionMaxDuration
|
||||
util.SessionMaxDuration = 86400 * 90
|
||||
defer func() {
|
||||
util.DisableLogin = origDisable
|
||||
util.SessionMaxDuration = origMaxDuration
|
||||
}()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
env.echo.GET("/create-future", func(c echo.Context) error {
|
||||
createSession(c, "admin", true, uint32(12345), true)
|
||||
|
||||
sess, _ := session.Get("session", c)
|
||||
now := time.Now().UTC().Unix()
|
||||
sess.Values["created_at"] = now - 172800
|
||||
sess.Values["updated_at"] = now + 3600 // future timestamp
|
||||
sess.Save(c.Request(), c.Response())
|
||||
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req1, rec1 := jsonRequest(http.MethodGet, "/create-future", nil)
|
||||
env.echo.ServeHTTP(rec1, req1)
|
||||
require.Equal(t, http.StatusOK, rec1.Code)
|
||||
|
||||
env.echo.GET("/do-refresh-future", func(c echo.Context) error {
|
||||
doRefreshSession(c)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
cookies := rec1.Result().Cookies()
|
||||
req2, rec2 := jsonRequest(http.MethodGet, "/do-refresh-future", nil)
|
||||
for _, cookie := range cookies {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
env.echo.ServeHTTP(rec2, req2)
|
||||
assert.Equal(t, http.StatusOK, rec2.Code)
|
||||
}
|
||||
|
||||
// --- doRefreshSession: session expired (updatedAt + maxAge < now) ---
|
||||
|
||||
func TestDoRefreshSession_Expired(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
origMaxDuration := util.SessionMaxDuration
|
||||
util.SessionMaxDuration = 86400 * 90
|
||||
defer func() {
|
||||
util.DisableLogin = origDisable
|
||||
util.SessionMaxDuration = origMaxDuration
|
||||
}()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
env.echo.GET("/create-for-expire-test", func(c echo.Context) error {
|
||||
createSession(c, "admin", true, uint32(12345), true)
|
||||
|
||||
// Manipulate session so that it's expired: updatedAt + maxAge < now
|
||||
sess, _ := session.Get("session", c)
|
||||
now := time.Now().UTC().Unix()
|
||||
maxAge := sess.Values["max_age"].(int)
|
||||
sess.Values["created_at"] = now - int64(maxAge) - 200
|
||||
sess.Values["updated_at"] = now - int64(maxAge) - 100 // expired 100s ago
|
||||
sess.Save(c.Request(), c.Response())
|
||||
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req1, rec1 := jsonRequest(http.MethodGet, "/create-for-expire-test", nil)
|
||||
env.echo.ServeHTTP(rec1, req1)
|
||||
require.Equal(t, http.StatusOK, rec1.Code)
|
||||
|
||||
env.echo.GET("/do-refresh-expire-test", func(c echo.Context) error {
|
||||
doRefreshSession(c)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
cookies := rec1.Result().Cookies()
|
||||
req2, rec2 := jsonRequest(http.MethodGet, "/do-refresh-expire-test", nil)
|
||||
for _, cookie := range cookies {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
env.echo.ServeHTTP(rec2, req2)
|
||||
assert.Equal(t, http.StatusOK, rec2.Code)
|
||||
}
|
||||
|
||||
// --- createSession with custom SessionMaxDuration ---
|
||||
|
||||
func TestCreateSession_WithSessionMaxDuration(t *testing.T) {
|
||||
|
|
@ -1031,7 +482,7 @@ func TestCreateSession_WithSessionMaxDuration(t *testing.T) {
|
|||
util.DisableLogin = false
|
||||
|
||||
env.echo.GET("/create-with-duration", func(c echo.Context) error {
|
||||
createSession(c, "duruser", true, uint32(44444), true)
|
||||
createSession(c, "duruser", true, uint32(44444))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
@ -1043,7 +494,7 @@ func TestCreateSession_WithSessionMaxDuration(t *testing.T) {
|
|||
for _, cookie := range rec.Result().Cookies() {
|
||||
if cookie.Name == "session_token" {
|
||||
assert.Equal(t, int(util.SessionMaxDuration), cookie.MaxAge,
|
||||
"Cookie MaxAge should equal SessionMaxDuration when rememberMe is true")
|
||||
"Cookie MaxAge should equal SessionMaxDuration")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -1060,7 +511,7 @@ func TestIsAdmin_WithNonAdminSession(t *testing.T) {
|
|||
util.DisableLogin = false
|
||||
|
||||
env.echo.GET("/setup-nonadmin", func(c echo.Context) error {
|
||||
createSession(c, "regular", false, uint32(55555), false)
|
||||
createSession(c, "regular", false, uint32(55555))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
@ -1093,7 +544,7 @@ func TestIsAdmin_WithAdminSession(t *testing.T) {
|
|||
util.DisableLogin = false
|
||||
|
||||
env.echo.GET("/setup-admin-check", func(c echo.Context) error {
|
||||
createSession(c, "adminuser", true, uint32(66666), false)
|
||||
createSession(c, "adminuser", true, uint32(66666))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
@ -1132,7 +583,7 @@ func TestIsValidSession_ExpiredTimeBounds(t *testing.T) {
|
|||
|
||||
// Create a session with timestamps that will fail time bounds
|
||||
env.echo.GET("/create-expired-session", func(c echo.Context) error {
|
||||
createSession(c, "timeuser", true, uint32(77777), true)
|
||||
createSession(c, "timeuser", true, uint32(77777))
|
||||
|
||||
// Manipulate: created 200s ago (past max duration of 100s)
|
||||
sess, _ := session.Get("session", c)
|
||||
|
|
@ -1184,7 +635,7 @@ func TestIsValidSession_UserRemovedFromDB(t *testing.T) {
|
|||
util.DBUsersToCRC32Mutex.Unlock()
|
||||
|
||||
env.echo.GET("/create-temp-session", func(c echo.Context) error {
|
||||
createSession(c, "tempuser", false, uint32(54321), true)
|
||||
createSession(c, "tempuser", false, uint32(54321))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
@ -1233,7 +684,7 @@ func TestIsValidSession_TemporarySession(t *testing.T) {
|
|||
|
||||
// Create session without remember-me (maxAge=0)
|
||||
env.echo.GET("/create-temp-sess", func(c echo.Context) error {
|
||||
createSession(c, "tempsess", false, uint32(11111), false)
|
||||
createSession(c, "tempsess", false, uint32(11111))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
@ -1276,7 +727,7 @@ func TestClearSession_ThenInvalid(t *testing.T) {
|
|||
}()
|
||||
|
||||
env.echo.GET("/create-clear-session", func(c echo.Context) error {
|
||||
createSession(c, "clearme", true, uint32(22222), true)
|
||||
createSession(c, "clearme", true, uint32(22222))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
@ -1306,40 +757,6 @@ func TestClearSession_ThenInvalid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNeedsAdmin_WithAdminSession(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
defer func() { util.DisableLogin = origDisable }()
|
||||
|
||||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
// Create an admin session
|
||||
env.echo.GET("/setup-admin", func(c echo.Context) error {
|
||||
createSession(c, "admin", true, uint32(12345), false)
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req1, rec1 := jsonRequest(http.MethodGet, "/setup-admin", nil)
|
||||
env.echo.ServeHTTP(rec1, req1)
|
||||
|
||||
// Access admin-only route
|
||||
called := false
|
||||
env.echo.GET("/admin-page", NeedsAdmin(func(c echo.Context) error {
|
||||
called = true
|
||||
return c.String(http.StatusOK, "admin page")
|
||||
}))
|
||||
|
||||
cookies := rec1.Result().Cookies()
|
||||
req2, rec2 := jsonRequest(http.MethodGet, "/admin-page", nil)
|
||||
for _, cookie := range cookies {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
env.echo.ServeHTTP(rec2, req2)
|
||||
assert.True(t, called)
|
||||
assert.Equal(t, http.StatusOK, rec2.Code)
|
||||
}
|
||||
|
||||
func TestCurrentUser_WithRealSession(t *testing.T) {
|
||||
origDisable := util.DisableLogin
|
||||
util.DisableLogin = false
|
||||
|
|
@ -1350,7 +767,7 @@ func TestCurrentUser_WithRealSession(t *testing.T) {
|
|||
|
||||
// Create a session first
|
||||
env.echo.GET("/setup-user", func(c echo.Context) error {
|
||||
createSession(c, "testuser", false, uint32(111), false)
|
||||
createSession(c, "testuser", false, uint32(111))
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue