Lesson 3
Managing User Sessions with Cookies
Introduction to User Sessions with Cookies

Welcome back! Building on the previous lesson, where we focused on JWT authentication for securing API endpoints, this lesson will emphasize managing user sessions through cookies. By using cookies, web applications can maintain a user's authenticated state across multiple HTTP requests, enhancing the user experience.

We will explore how to manage user sessions securely using cookies in the Gin framework. This lesson will guide you through creating and verifying sessions, leveraging cookies to store JWTs securely.

Understanding Cookies

Cookies are small data pieces sent from a website and stored on the user's device by the user's web browser. They play a crucial role in preserving user state across sessions in web applications. In this lesson, you'll learn how to effectively utilize cookies to maintain user sessions, especially in the stateless HTTP protocol.

Cookies allow us to store JWTs on the client side, and these can be sent back with every request to facilitate smooth session management. By storing JWTs in cookies, we ensure that user's authenticated state persists across different requests. Let's consider a simple example where we set a cookie using the Gin c.SetCookie method:

Go
1func ExampleSetCookie(c *gin.Context) { 2 c.SetCookie( 3 "example_cookie", // Cookie name 4 "cookie_value", // Cookie value 5 3600, // MaxAge in seconds (1 hour) 6 "/", // Path 7 "localhost", // Domain 8 false, // Secure - HTTPS only 9 true, // HttpOnly - JavaScript access 10 ) 11 c.JSON(http.StatusOK, gin.H{"message": "Cookie has been set"}) 12}

In this example, we are setting a cookie named example_cookie with a value of cookie_value. The cookie will last for 1 hour (3600 seconds), applying it to the root path /, and be sent only to the localhost domain. Please note that using localhost as the domain will limit the functionality to local development environment; for deployment in production or more general environments, you should use a domain that matches your application's host domain. Finally, the cookie is marked as not secure (HTTP allowed) but is HttpOnly, meaning it cannot be accessed via JavaScript, which helps in mitigating XSS attacks. This function returns a JSON response once the cookie is set.

Implementing Login Functionality with Session Management

Let's delve into implementing login functionality coupled with session management.

controllers package:

Go
1package controllers 2 3import ( 4 "net/http" 5 "time" 6 7 "github.com/gin-gonic/gin" 8 "gorm.io/gorm" 9 10 "codesignal.com/todoapp/services" 11) 12 13func LoginWithSession(c *gin.Context, db *gorm.DB) { 14 var credentials struct { 15 Username string `json:"username"` 16 Password string `json:"password"` 17 } 18 if err := c.ShouldBindJSON(&credentials); err != nil { 19 c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"}) 20 return 21 } 22 23 // Check user's credentials 24 if err := services.LoginUser(db, credentials.Username, credentials.Password); err != nil { 25 c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) 26 return 27 } 28 29 // Generate JWT 30 tokenString, err := services.GenerateToken(credentials.Username) 31 if err != nil { 32 c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not generate token"}) 33 return 34 } 35 36 // Sets a cookie named "session_token" with the JWT as the value and with 4 hours validity 37 c.SetCookie("session_token", tokenString, int(4*time.Hour), "/", "localhost", false, true) 38 c.JSON(http.StatusOK, gin.H{"message": "Login successful, session created"}) 39}

services package:

Go
1package services 2 3import ( 4 "time" 5 6 "github.com/golang-jwt/jwt/v5" 7 "gorm.io/gorm" 8 9 "codesignal.com/todoapp/repositories/user_repository" 10) 11 12var jwtSecret = []byte("your_jwt_secret_key") 13 14type Claims struct { 15 Username string `json:"username"` 16 jwt.RegisteredClaims 17} 18 19func GenerateToken(username string) (string, error) { 20 // Generate JWT with 4 hours expiration time 21 expirationTime := time.Now().Add(4 * time.Hour) 22 claims := &Claims{ 23 Username: username, 24 RegisteredClaims: jwt.RegisteredClaims{ 25 ExpiresAt: jwt.NewNumericDate(expirationTime), 26 }, 27 } 28 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 29 return token.SignedString(jwtSecret) 30} 31 32func LoginUser(db *gorm.DB, username, password string) error { 33 // Check if credentials match in the database 34 return user_repository.ValidateUser(db, username, password) 35}

Here's what's happening:

  • The LoginWithSession function manages the login request.
  • User credentials are validated against the user database via the LoginUser service function and the user_repository package.
  • Upon successful validation, a JWT is created using jwt.NewWithClaims in the services.GenerateToken function, incorporating the username and an expiration claim.
  • The JWT is signed and stored in a session_token cookie, effectively initiating a user session. This functionality is crucial for securely maintaining user sessions across different client requests.
Validating User Sessions

To validate user sessions, we'll introduce a CheckSession controller function:

controllers package:

Go
1// ... previous code 2 3func CheckSession(c *gin.Context) { 4 // Retrieve the session token from cookies 5 sessionToken, err := c.Cookie("session_token") 6 if err != nil { 7 c.JSON(http.StatusUnauthorized, gin.H{"error": "Session not found"}) 8 return 9 } 10 11 claims, err := services.ValidateToken(sessionToken) 12 if err != nil { 13 c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid session"}) 14 return 15 } 16 17 c.JSON(http.StatusOK, gin.H{"message": "Session is valid", "username": claims.Username}) 18}

services package:

Go
1/// ... previous code 2 3func ValidateToken(tokenString string) (*Claims, error) { 4 claims := &Claims{} 5 // Validate token with secret key 6 token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { 7 return jwtSecret, nil 8 }) 9 if err != nil || !token.Valid { 10 return nil, err 11 } 12 return claims, nil 13}

Breaking down the above:

  • The CheckSession function retrieves and verifies the session_token cookie via the ValidateToken service function.
  • If the token is missing or invalid, an unauthorized response is returned.
  • Instead, if the token is valid, the JWT is parsed and verified with the jwtSecret.
  • For valid tokens, the server responds with the user’s verified session status.

This process ensures that only authenticated users can maintain active sessions, reinforcing application security.

Pros and Cons of Using Cookies

Pros:

  • State Maintenance: Cookies help maintain user sessions across multiple requests.
  • Browser Support: Well-supported across all modern web browsers.
  • Automatic Handling: Browsers handle cookies automatically, reducing the need for explicit client-side scripting.

Cons:

  • Size Limits: Cookies have size limits, typically around 4KB, which may restrict large session data storage.
  • Potential Security Vulnerabilities: Cookies can be targeted in attacks like XSS if not adequately secured.
  • Client Visibility: Cookies are stored on the client side and can potentially be read or modified by the user.
Enhancing Security and User Experience

To protect user sessions effectively and enhance the user experience, consider the following practices:

  • HttpOnly Flag: Prevents JavaScript access to cookies, mitigating XSS risks.
  • Secure Flag: Ensures cookies are sent only over secure HTTPS connections.
  • Sensitive Data Management: Store sensitive data, like jwtSecret, using environment variables and keep them secure from exposure.
  • Session Expiry Management: Implement user-friendly handling of expired sessions by redirecting users to the login page seamlessly.

Ensuring the security of user sessions is crucial for both user trust and application integrity.

Summary and Next Steps

In this lesson, you learned to manage user sessions using cookies in a Gin-based application. By leveraging JWTs stored in cookies, you can maintain state securely across HTTP requests. Understanding how to implement login functionality with session cookies and validate sessions are key steps in enhancing the security and reliability of your applications.

Dive into the practical exercises ahead to solidify these concepts and continue to build secure web applications.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.