diff --git a/handler/api_v1_auth_test.go b/handler/api_v1_auth_test.go index 038bbc1..b8143b2 100644 --- a/handler/api_v1_auth_test.go +++ b/handler/api_v1_auth_test.go @@ -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") }) diff --git a/handler/api_v1_clients_test.go b/handler/api_v1_clients_test.go index 03e8625..5e55daa 100644 --- a/handler/api_v1_clients_test.go +++ b/handler/api_v1_clients_test.go @@ -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")) diff --git a/handler/api_v1_oidc.go b/handler/api_v1_oidc.go index 34c9ada..d2f1bff 100644 --- a/handler/api_v1_oidc.go +++ b/handler/api_v1_oidc.go @@ -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) diff --git a/handler/session.go b/handler/session.go index 34a0dda..dc037e1 100644 --- a/handler/session.go +++ b/handler/session.go @@ -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() diff --git a/handler/session_test.go b/handler/session_test.go index 506662f..fde7dda 100644 --- a/handler/session_test.go +++ b/handler/session_test.go @@ -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") })