Gin is a widely used web framework in Go, offering powerful features to streamline website development. Among its many capabilities, handling sessions and cookies is a critical aspect for building functional web applications.
This post highlights a behavior in the Gin framework that may not be immediately obvious or intuitive—the missing session cookie issue.
So, what exactly is the missing session cookie issue? It occurs when a session is created and saved in one route but isn’t accessible in other routes. Additionally, the expected session cookie with the specified key is not present or sent to the browser. Here’s a closer look at how this issue manifests.
We have two pages basically: /signin and /stock. The /signin page is just a simple sign in form with two fields: username and password. Once user enters them and submit the form, the request data will be sent to a backend API and once verified the username and password, a session with the username will be created and saved.
session := sessions.Default(c)
session.Set("username", username)
session.Options(sessions.Options{MaxAge: 60 * 2}) // 2 minutes for testing purpose
session.Save()
After signing in, the user will be redirected to the /stock page which displays some stock information (not important), but it needs to have the session data available (user needs to sign in first) in order for the APIs not being abused while accessing. In /stock API, the session data will be read like
func AuthMiddleware() gin.HandlerFunc {
fmt.Println("Initialize auth middleware")
return func(c *gin.Context) {
session := sessions.Default(c)
username := session.Get("username")
if username == nil && c.Request.URL.Path != "/signin" && !strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.Redirect(http.StatusTemporaryRedirect, "signin")
c.Abort()
return
}
c.Next()
}
}
This is a middleware and it will be ran when every request comes in. The session store itself is initialized when the server starts.
// Initialize the cookie store
store := cookie.NewStore([]byte("secret"))
// Use sessions middleware
r.Use(sessions.Sessions("mysecurity", store))
Initially all these looks quite standard and should work fine. The code is all around the web as well. However, while testing, the /stock page is never redirected and the session cookie is not set properly in the browser, as cannot find the cookie with the name specified above.
To debug, I first confirmed that the signin process itself is okay. The username and password are received correctly by backend API and the session.Set()
and session.Save()
functions are called. It's just the cookie is not properly set.
What is the issue? The issue actually lies at the line
session.Options(sessions.Options{MaxAge: 60 * 2})
With this function call, the cookie option is set with the one specified. The max-age is set, but that's it. Other elements of a HTTP cookie are omitted, such as PATH
. With an empty PATH, some browsers(Microsoft Edge at least) are not accepting the cookie and hence will not save it.
SetCookie: mysecurity=MTczMjcwNzQUJFQUVRQUFBcV80QUFBUVp6ZEhKcGJtY01DZ0FJZFhObGNtNWhiV1VHYzNSeWFXNW5EQW9BQ0hCcGEyVXdNREF5fDuc3NUMqZ5fjluUzVQbDCSpiN-NaHO5BgwXkXCyi01x; Expires=Wed, 27 Nov 2024 11:41:53 GMT; Max-Age=600
Per Gin's implementation, this behavior of setting the cookie is expected, as I have called session.Options()
which basically overwrites the whole sessions.Options
which also contains other fields.
type Options struct {
Path string
Domain string
// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'.
// MaxAge>0 means Max-Age attribute present and given in seconds.
MaxAge int
Secure bool
HttpOnly bool
// rfc-draft to preventing CSRF: https://tools.ietf.org/html/draft-west-first-party-cookies-07
// refer: https://godoc.org/net/http
// https://www.sjoerdlangkemper.nl/2016/04/14/preventing-csrf-with-samesite-cookie-attribute/
SameSite http.SameSite
}
In this case, the Path
is empty now. Later when creating the actual HTTP cookie and sending it. The logic of composing the cookie string is
// Save adds a single session to the response.
func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter,
session *Session) error {
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
s.Codecs...)
if err != nil {
return err
}
http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
return nil
}
func newCookieFromOptions(name, value string, options *Options) *http.Cookie {
return &http.Cookie{
Name: name,
Value: value,
Path: options.Path,
Domain: options.Domain,
MaxAge: options.MaxAge,
Secure: options.Secure,
HttpOnly: options.HttpOnly,
SameSite: options.SameSite,
}
}
func (c *Cookie) String() string {
...
if len(c.Path) > 0 {
b.WriteString("; Path=")
b.WriteString(sanitizeCookiePath(c.Path))
}
...
}
To rectify the issue, I need to set other fields while setting options.
session.Options(sessions.Options{
MaxAge: 60 * 2, // This sets session to expire in 600 seconds (10 minutes)
// Add these settings to ensure proper cookie behavior:
Path: "/", // Make cookie available for all paths
HttpOnly: true, // Protect against XSS
Secure: true, // For HTTPS
SameSite: http.SameSiteStrictMode,
})
Post this, restart and run the flow again and it works. The cookie looks something like
SetCookie: mysecurity=MTczMjcwNzA3MHxEWDhFUVRQUFBcV80QUFBUVp6ZEhKcGJtY01DZ0FJZFhObGNtNWhiV1VHYzNSeWFXNW5EQW9BQ0hCcGEyVXdNREF5fMXu2tVBJMAYJbOxou9ZVNYMuTuU1H7XrV7KU2cQ3S6x; Path=/; Expires=Wed, 27 Nov 2024 11:41:10 GMT; Max-Age=600; HttpOnly; Secure; SameSite=Strict
The Path
and other fields are present.
Although the issue is fixed. Why was the original solution not working? Or at least I expected it to work. This leads to some thoughts about the design of the session
module in Gin.
- Why is there no default value for some of the fields if not set/empty? When the session struct is created, there is default value for those fields though.
- Should it provide WithOptions pattern which can set individual field when necessary, as sometimes I may just want to change the
MaxAge
field.
Hope this helps with those who have experienced the same issue.