The Missing Session Cookie While Using GoLang Gin

  sonic0002        2024-12-02 04:30:06       995        0          English  中文  Tiếng Việt 

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.

  1. 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.
  2. 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.

SESSION  GO  GOLANG  COOKIE  NOT WORKING  GIN  MISSING 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Kafka for real?