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.
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:
Go1func 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.
Let's delve into implementing login functionality coupled with session management.
controllers
package:
Go1package 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:
Go1package 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 theuser_repository
package. - Upon successful validation, a JWT is created using
jwt.NewWithClaims
in theservices.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.
To validate user sessions, we'll introduce a CheckSession
controller function:
controllers
package:
Go1// ... 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:
Go1/// ... 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 thesession_token
cookie via theValidateToken
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:
- 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.
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.
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.