detached 5fc37ea

Improve session management

- Implement session validation and verification before OAuth flow
- Remove redundant session reset functionality
- Refactor session validation logic into dedicated handler
commit
5fc37eaf68142a92e0fa30aff629a83b0fa9f76d
author
bnema · · 2023-11-04 09:36 UTC
parent
0a33911
signature
unsigned
5 files · +124 -103
M internal/httpserve/handler/adminroutes.go +1 -2
@@ -5,7 +5,6 @@ import (
55
66 "net/http"
77
8- "github.com/bnema/gordon/internal/httpserve/middleware"
98 "github.com/bnema/gordon/internal/server"
109 "github.com/bnema/gordon/internal/templating/render"
1110 "github.com/labstack/echo/v4"
@@ -18,7 +17,7 @@ var (
1817
1918 // GetLocalizedData returns the data for the localization
2019 func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, error) {
21- lang := c.Get(middleware.LangKey)
20+ lang := c.Get("LangKey")
2221
2322 if lang == nil {
2423 return nil, fmt.Errorf("LangKey not found in context")
@@ -5,7 +5,6 @@ import (
5 5
6 "net/http"6 "net/http"
7 7
8- "github.com/bnema/gordon/internal/httpserve/middleware"
9 "github.com/bnema/gordon/internal/server"8 "github.com/bnema/gordon/internal/server"
10 "github.com/bnema/gordon/internal/templating/render"9 "github.com/bnema/gordon/internal/templating/render"
11 "github.com/labstack/echo/v4"10 "github.com/labstack/echo/v4"
@@ -18,7 +17,7 @@ var (
18 17
19 // GetLocalizedData returns the data for the localization18 // GetLocalizedData returns the data for the localization
20 func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, error) {19 func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, error) {
21- lang := c.Get(middleware.LangKey)20+ lang := c.Get("LangKey")
22 21
23 if lang == nil {22 if lang == nil {
24 return nil, fmt.Errorf("LangKey not found in context")23 return nil, fmt.Errorf("LangKey not found in context")
M internal/httpserve/handler/oauth.go +17 -3
@@ -7,6 +7,7 @@ import (
77 "io"
88 "net/http"
99 "os"
10+ "time"
1011
1112 "github.com/bnema/gordon/internal/db"
1213 "github.com/bnema/gordon/internal/db/queries"
@@ -65,9 +66,22 @@ func StartOAuthGithub(c echo.Context, a *server.App) error {
6566 }
6667
6768 // Check if the session values are valid
68- accountID, ok := sess.Values["accountID"].(string)
69- if ok && accountID != "" {
70- return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path) // StatusSeeOther is HTTP 303
69+ sessionID, accountID, ok := sess.Values["accountID"].(string), sess.Values["sessionID"].(string), sess.Values["authenticated"].(bool)
70+ if ok && accountID != "" && sessionID != "" {
71+ // Check if the session is expired
72+ sessionExpired, err := IsSessionExpiredInDB(a, accountID, sessionID)
73+ if err != nil {
74+ return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
75+ }
76+ if !sessionExpired {
77+ sessionExpiryTime := 30 * time.Minute
78+ // Extend session expiration conditionally, e.g., if it's close to expiring
79+ err = queries.ExtendSessionExpiration(a, accountID, sessionID, time.Now().Add(sessionExpiryTime))
80+ if err != nil {
81+ return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
82+ }
83+ return c.Redirect(http.StatusFound, a.Config.Admin.Path)
84+ }
7185 }
7286
7387 //Initiate the Github OAuth flow
@@ -7,6 +7,7 @@ import (
7 "io"7 "io"
8 "net/http"8 "net/http"
9 "os"9 "os"
10+ "time"
10 11
11 "github.com/bnema/gordon/internal/db"12 "github.com/bnema/gordon/internal/db"
12 "github.com/bnema/gordon/internal/db/queries"13 "github.com/bnema/gordon/internal/db/queries"
@@ -65,9 +66,22 @@ func StartOAuthGithub(c echo.Context, a *server.App) error {
65 }66 }
66 67
67 // Check if the session values are valid68 // Check if the session values are valid
68- accountID, ok := sess.Values["accountID"].(string)69+ sessionID, accountID, ok := sess.Values["accountID"].(string), sess.Values["sessionID"].(string), sess.Values["authenticated"].(bool)
69- if ok && accountID != "" {70+ if ok && accountID != "" && sessionID != "" {
70- return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path) // StatusSeeOther is HTTP 30371+ // Check if the session is expired
72+ sessionExpired, err := IsSessionExpiredInDB(a, accountID, sessionID)
73+ if err != nil {
74+ return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
75+ }
76+ if !sessionExpired {
77+ sessionExpiryTime := 30 * time.Minute
78+ // Extend session expiration conditionally, e.g., if it's close to expiring
79+ err = queries.ExtendSessionExpiration(a, accountID, sessionID, time.Now().Add(sessionExpiryTime))
80+ if err != nil {
81+ return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
82+ }
83+ return c.Redirect(http.StatusFound, a.Config.Admin.Path)
84+ }
71 }85 }
72 86
73 //Initiate the Github OAuth flow87 //Initiate the Github OAuth flow
D internal/httpserve/handler/session.go +0 -26
@@ -1,26 +0,0 @@
1-package handler
2-
3-// ResetSession resets the session cookie and session data
4-// func ResetSession(c echo.Context) error {
5-// // Manually expire the cookie
6-// cookie := new(http.Cookie)
7-// cookie.Name = "session"
8-// cookie.Value = ""
9-// cookie.Path = "/"
10-// cookie.Expires = time.Unix(0, 0)
11-// cookie.MaxAge = -1 // Clear immediately
12-// c.SetCookie(cookie)
13-// // Invalidate the existing session
14-// sess, err := session.Get("session", c)
15-// if err != nil {
16-// log.Println(err)
17-// return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
18-// }
19-// sess.Options.MaxAge = -1
20-// if err := sess.Save(c.Request(), c.Response()); err != nil {
21-// log.Println("Could not delete session:", err)
22-// return echo.NewHTTPError(http.StatusInternalServerError, "Could not delete session")
23-// }
24-
25-// return nil
26-// }
@@ -1,26 +0,0 @@
1-package handler
2-
3-// ResetSession resets the session cookie and session data
4-// func ResetSession(c echo.Context) error {
5-// // Manually expire the cookie
6-// cookie := new(http.Cookie)
7-// cookie.Name = "session"
8-// cookie.Value = ""
9-// cookie.Path = "/"
10-// cookie.Expires = time.Unix(0, 0)
11-// cookie.MaxAge = -1 // Clear immediately
12-// c.SetCookie(cookie)
13-// // Invalidate the existing session
14-// sess, err := session.Get("session", c)
15-// if err != nil {
16-// log.Println(err)
17-// return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
18-// }
19-// sess.Options.MaxAge = -1
20-// if err := sess.Save(c.Request(), c.Response()); err != nil {
21-// log.Println("Could not delete session:", err)
22-// return echo.NewHTTPError(http.StatusInternalServerError, "Could not delete session")
23-// }
24-
25-// return nil
26-// }
A internal/httpserve/handler/sessiondb.go +92 -0
@@ -0,0 +1,92 @@
1+package handler
2+
3+import (
4+ "fmt"
5+ "time"
6+
7+ "github.com/bnema/gordon/internal/db/queries"
8+ "github.com/bnema/gordon/internal/server"
9+ "github.com/labstack/echo-contrib/session"
10+ "github.com/labstack/echo/v4"
11+)
12+
13+func ValidateSessionAndUser(c echo.Context, a *server.App) error {
14+ sessionExpiryTime := 30 * time.Minute
15+
16+ sess, err := session.Get("session", c)
17+ if err != nil {
18+ return err
19+ }
20+
21+ accountID, ok := sess.Values["accountID"].(string)
22+ if !ok {
23+ return fmt.Errorf("accountID not found or invalid type")
24+ }
25+
26+ sessionID, ok := sess.Values["sessionID"].(string)
27+ if !ok {
28+ return fmt.Errorf("sessionID not found or invalid type")
29+ }
30+
31+ accountExists, err := IsAccountIDInDB(a, accountID)
32+ if err != nil || !accountExists {
33+ return fmt.Errorf("account does not exist")
34+ }
35+
36+ sessionExpired, err := IsSessionExpiredInDB(a, accountID, sessionID)
37+ if err != nil || sessionExpired {
38+ return fmt.Errorf("session is expired")
39+ }
40+
41+ // Extend session expiration conditionally, e.g., if it's close to expiring
42+ err = queries.ExtendSessionExpiration(a, accountID, sessionID, time.Now().Add(sessionExpiryTime))
43+ if err != nil {
44+ return fmt.Errorf("failed to extend session: %v", err)
45+ }
46+
47+ return nil
48+}
49+
50+func IsSessionExpiredInDB(a *server.App, userID string, sessionID string) (bool, error) {
51+ currentTime := time.Now()
52+ expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime)
53+ if err != nil {
54+ return false, err
55+ }
56+
57+ return currentTime.After(expirationTime), nil
58+}
59+
60+func IsAccountIDInDB(a *server.App, accountID string) (bool, error) {
61+ accountExists, err := queries.CheckDBAccountExists(a, accountID)
62+ if err != nil {
63+ return false, err
64+ }
65+
66+ return accountExists, nil
67+}
68+
69+// ResetSession resets the session cookie and session data
70+// func ResetSession(c echo.Context) error {
71+// // Manually expire the cookie
72+// cookie := new(http.Cookie)
73+// cookie.Name = "session"
74+// cookie.Value = ""
75+// cookie.Path = "/"
76+// cookie.Expires = time.Unix(0, 0)
77+// cookie.MaxAge = -1 // Clear immediately
78+// c.SetCookie(cookie)
79+// // Invalidate the existing session
80+// sess, err := session.Get("session", c)
81+// if err != nil {
82+// log.Println(err)
83+// return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
84+// }
85+// sess.Options.MaxAge = -1
86+// if err := sess.Save(c.Request(), c.Response()); err != nil {
87+// log.Println("Could not delete session:", err)
88+// return echo.NewHTTPError(http.StatusInternalServerError, "Could not delete session")
89+// }
90+
91+// return nil
92+// }
@@ -0,0 +1,92 @@
1+package handler
2+
3+import (
4+ "fmt"
5+ "time"
6+
7+ "github.com/bnema/gordon/internal/db/queries"
8+ "github.com/bnema/gordon/internal/server"
9+ "github.com/labstack/echo-contrib/session"
10+ "github.com/labstack/echo/v4"
11+)
12+
13+func ValidateSessionAndUser(c echo.Context, a *server.App) error {
14+ sessionExpiryTime := 30 * time.Minute
15+
16+ sess, err := session.Get("session", c)
17+ if err != nil {
18+ return err
19+ }
20+
21+ accountID, ok := sess.Values["accountID"].(string)
22+ if !ok {
23+ return fmt.Errorf("accountID not found or invalid type")
24+ }
25+
26+ sessionID, ok := sess.Values["sessionID"].(string)
27+ if !ok {
28+ return fmt.Errorf("sessionID not found or invalid type")
29+ }
30+
31+ accountExists, err := IsAccountIDInDB(a, accountID)
32+ if err != nil || !accountExists {
33+ return fmt.Errorf("account does not exist")
34+ }
35+
36+ sessionExpired, err := IsSessionExpiredInDB(a, accountID, sessionID)
37+ if err != nil || sessionExpired {
38+ return fmt.Errorf("session is expired")
39+ }
40+
41+ // Extend session expiration conditionally, e.g., if it's close to expiring
42+ err = queries.ExtendSessionExpiration(a, accountID, sessionID, time.Now().Add(sessionExpiryTime))
43+ if err != nil {
44+ return fmt.Errorf("failed to extend session: %v", err)
45+ }
46+
47+ return nil
48+}
49+
50+func IsSessionExpiredInDB(a *server.App, userID string, sessionID string) (bool, error) {
51+ currentTime := time.Now()
52+ expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime)
53+ if err != nil {
54+ return false, err
55+ }
56+
57+ return currentTime.After(expirationTime), nil
58+}
59+
60+func IsAccountIDInDB(a *server.App, accountID string) (bool, error) {
61+ accountExists, err := queries.CheckDBAccountExists(a, accountID)
62+ if err != nil {
63+ return false, err
64+ }
65+
66+ return accountExists, nil
67+}
68+
69+// ResetSession resets the session cookie and session data
70+// func ResetSession(c echo.Context) error {
71+// // Manually expire the cookie
72+// cookie := new(http.Cookie)
73+// cookie.Name = "session"
74+// cookie.Value = ""
75+// cookie.Path = "/"
76+// cookie.Expires = time.Unix(0, 0)
77+// cookie.MaxAge = -1 // Clear immediately
78+// c.SetCookie(cookie)
79+// // Invalidate the existing session
80+// sess, err := session.Get("session", c)
81+// if err != nil {
82+// log.Println(err)
83+// return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
84+// }
85+// sess.Options.MaxAge = -1
86+// if err := sess.Save(c.Request(), c.Response()); err != nil {
87+// log.Println("Could not delete session:", err)
88+// return echo.NewHTTPError(http.StatusInternalServerError, "Could not delete session")
89+// }
90+
91+// return nil
92+// }
M internal/httpserve/middleware/requirelogin.go +14 -72
@@ -1,14 +1,11 @@
11 package middleware
22
33 import (
4- "fmt"
5- "net/http"
64 "time"
75
8- "github.com/bnema/gordon/internal/db/queries"
6+ "github.com/bnema/gordon/internal/httpserve/handler"
97 "github.com/bnema/gordon/internal/server"
108
11- "github.com/labstack/echo-contrib/session"
129 "github.com/labstack/echo/v4"
1310 )
1411
@@ -18,7 +15,7 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc {
1815 const maxRetries = 3
1916 var err error
2017 for i := 0; i < maxRetries; i++ {
21- err = validateSessionAndUser(c, a)
18+ err = handler.ValidateSessionAndUser(c, a)
2219 if err == nil {
2320 return next(c)
2421 }
@@ -30,73 +27,18 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc {
3027 }
3128 }
3229 }
33-func validateSessionAndUser(c echo.Context, a *server.App) error {
34- sessionExpiryTime := 30 * time.Minute
3530
36- sess, err := session.Get("session", c)
37- if err != nil {
38- return err
39- }
40-
41- accountID, ok := sess.Values["accountID"].(string)
42- if !ok {
43- return fmt.Errorf("accountID not found or invalid type")
44- }
45-
46- sessionID, ok := sess.Values["sessionID"].(string)
47- if !ok {
48- return fmt.Errorf("sessionID not found or invalid type")
49- }
50-
51- accountExists, err := isAccountIDInDB(a, accountID)
52- if err != nil || !accountExists {
53- return fmt.Errorf("account does not exist")
54- }
55-
56- sessionExpired, err := isSessionExpiredInDB(a, accountID, sessionID)
57- if err != nil || sessionExpired {
58- return fmt.Errorf("session is expired")
59- }
60-
61- // Extend session expiration conditionally, e.g., if it's close to expiring
62- err = queries.ExtendSessionExpiration(a, accountID, sessionID, time.Now().Add(sessionExpiryTime))
63- if err != nil {
64- return fmt.Errorf("failed to extend session: %v", err)
65- }
66-
67- return nil
68-}
69-
70-func redirectToLogin(c echo.Context, a *server.App) error {
71- return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
72-}
73-
74-func isSessionExpiredInDB(a *server.App, userID string, sessionID string) (bool, error) {
75- currentTime := time.Now()
76- expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime)
77- if err != nil {
78- return false, err
79- }
80-
81- return currentTime.After(expirationTime), nil
82-}
83-
84-func isAccountIDInDB(a *server.App, accountID string) (bool, error) {
85- accountExists, err := queries.CheckDBAccountExists(a, accountID)
86- if err != nil {
87- return false, err
88- }
31+// func redirectToLogin(c echo.Context, a *server.App) error {
32+// return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
33+// }
8934
90- return accountExists, nil
91-}
92-
93-func DeleteCookieAndRedirectToLogin(c echo.Context, a *server.App) error {
94- // Delete session cookie
95- cookie := new(http.Cookie)
96- cookie.Name = "session"
97- cookie.Value = ""
98- cookie.Expires = time.Now().Add(-1 * time.Hour)
99- c.SetCookie(cookie)
35+// func DeleteCookieAndRedirectToLogin(c echo.Context, a *server.App) error {
36+// // Delete session cookie
37+// cookie := new(http.Cookie)
38+// cookie.Name = "session"
39+// cookie.Value = ""
40+// cookie.Expires = time.Now().Add(-1 * time.Hour)
41+// c.SetCookie(cookie)
10042
101- return redirectToLogin(c, a)
102-}
43+// return redirectToLogin(c, a)
44+// }
@@ -1,14 +1,11 @@
1 package middleware1 package middleware
2 2
3 import (3 import (
4- "fmt"
5- "net/http"
6 "time"4 "time"
7 5
8- "github.com/bnema/gordon/internal/db/queries"6+ "github.com/bnema/gordon/internal/httpserve/handler"
9 "github.com/bnema/gordon/internal/server"7 "github.com/bnema/gordon/internal/server"
10 8
11- "github.com/labstack/echo-contrib/session"
12 "github.com/labstack/echo/v4"9 "github.com/labstack/echo/v4"
13 )10 )
14 11
@@ -18,7 +15,7 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc {
18 const maxRetries = 315 const maxRetries = 3
19 var err error16 var err error
20 for i := 0; i < maxRetries; i++ {17 for i := 0; i < maxRetries; i++ {
21- err = validateSessionAndUser(c, a)18+ err = handler.ValidateSessionAndUser(c, a)
22 if err == nil {19 if err == nil {
23 return next(c)20 return next(c)
24 }21 }
@@ -30,73 +27,18 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc {
30 }27 }
31 }28 }
32 }29 }
33-func validateSessionAndUser(c echo.Context, a *server.App) error {
34- sessionExpiryTime := 30 * time.Minute
35 30
36- sess, err := session.Get("session", c)31+// func redirectToLogin(c echo.Context, a *server.App) error {
37- if err != nil {32+// return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
38- return err33+// }
39- }
40-
41- accountID, ok := sess.Values["accountID"].(string)
42- if !ok {
43- return fmt.Errorf("accountID not found or invalid type")
44- }
45-
46- sessionID, ok := sess.Values["sessionID"].(string)
47- if !ok {
48- return fmt.Errorf("sessionID not found or invalid type")
49- }
50-
51- accountExists, err := isAccountIDInDB(a, accountID)
52- if err != nil || !accountExists {
53- return fmt.Errorf("account does not exist")
54- }
55-
56- sessionExpired, err := isSessionExpiredInDB(a, accountID, sessionID)
57- if err != nil || sessionExpired {
58- return fmt.Errorf("session is expired")
59- }
60-
61- // Extend session expiration conditionally, e.g., if it's close to expiring
62- err = queries.ExtendSessionExpiration(a, accountID, sessionID, time.Now().Add(sessionExpiryTime))
63- if err != nil {
64- return fmt.Errorf("failed to extend session: %v", err)
65- }
66-
67- return nil
68-}
69-
70-func redirectToLogin(c echo.Context, a *server.App) error {
71- return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
72-}
73-
74-func isSessionExpiredInDB(a *server.App, userID string, sessionID string) (bool, error) {
75- currentTime := time.Now()
76- expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime)
77- if err != nil {
78- return false, err
79- }
80-
81- return currentTime.After(expirationTime), nil
82-}
83-
84-func isAccountIDInDB(a *server.App, accountID string) (bool, error) {
85- accountExists, err := queries.CheckDBAccountExists(a, accountID)
86- if err != nil {
87- return false, err
88- }
89 34
90- return accountExists, nil35+// func DeleteCookieAndRedirectToLogin(c echo.Context, a *server.App) error {
91-}36+// // Delete session cookie
92-37+// cookie := new(http.Cookie)
93-func DeleteCookieAndRedirectToLogin(c echo.Context, a *server.App) error {38+// cookie.Name = "session"
94- // Delete session cookie39+// cookie.Value = ""
95- cookie := new(http.Cookie)40+// cookie.Expires = time.Now().Add(-1 * time.Hour)
96- cookie.Name = "session"41+// c.SetCookie(cookie)
97- cookie.Value = ""
98- cookie.Expires = time.Now().Add(-1 * time.Hour)
99- c.SetCookie(cookie)
100 42
101- return redirectToLogin(c, a)43+// return redirectToLogin(c, a)
102-}44+// }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close