Lesson 2
Protecting All API Methods with Authentication
Introduction to API Authentication

Welcome to this lesson, where we will explore how to protect all API methods using authentication. In previous lessons, you have learned how to implement user registration and login functionalities, which are crucial for managing user access. Now, we will focus on enhancing the security of our ToDo app by ensuring that only authenticated users can access the API endpoints.

Authentication prevents unauthorized users from accessing and manipulating data, which is vital for application integrity and user privacy. By the end of this lesson, you’ll be equipped to safeguard your application methods using authentication techniques.

JWT (JSON Web Tokens) Basics

JSON Web Tokens (JWT) are a popular choice for securing APIs due to their simplicity and effectiveness. A JWT is a compact, URL-safe token that represents claims between two parties. It consists of three parts: the header, the payload, and the signature.

  • Header: Contains metadata about the token, like its type and the signing algorithm.
  • Payload: Carries the claims or data you want to transmit (e.g., username). Claims are statements about an entity (typically, the user) and additional data. There are three types of claims:
    • Registered Claims: Predefined claims such as iss (issuer), exp (expiration time), and sub (subject).
    • Public Claims: Custom claims that you define and register for public use.
    • Private Claims: Custom claims agreed upon between parties sharing the JWT.
  • Signature: Ensures the token hasn't been altered, generated by signing the header and payload.

JWT is favored for API security because it allows stateless authentication, meaning the server doesn’t need to store every token it issues. This makes JWT efficient and scalable for modern applications.

Creating a JWT

To create a JWT, you typically follow these steps:

  1. Define Claims: Start by determining the claims, such as user information or permissions, that you want to include in the payload.
  2. Create the Header: Specify the token's type (JWT) and the signing algorithm, such as HS256.
  3. Sign the Token: Encode the header and payload to Base64Url format. Then, concatenate them with a period (.) separator, and sign the resulting string using a secret key.
  4. Form the JWT: Combine the encoded header, payload, and signature with periods (.) to form the complete JWT.

Let's see how we can extend our Login service so that it makes use of JWT authentication:

Go
1package services 2 3import ( 4 "time" 5 6 "github.com/golang-jwt/jwt/v5" 7) 8 9// Secret key for signing the JWT. 10// This is a simplification for demonstration purposes - in reality, it should be kept safe and secure and not available as plain text in the code. 11var jwtSecret = []byte("your-secret-key") 12 13func LoginUser(db *gorm.DB, username string, password string) (string, error) { 14 // Validate login credentials 15 if err := user_repository.ValidateUser(db, username, password); err != nil { 16 return "", err 17 } 18 // Define claims, which include a username and an expiration time 24 hours from now. 19 claims := jwt.MapClaims{ 20 "username": username, 21 "exp": time.Now().Add(time.Hour * 24).Unix(), // Expiration time 24 hours from current time 22 } 23 24 // Create a new JWT token with the specified claims, using the HS256 signing method. 25 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 26 27 // Sign the token using the secret key and return the signed token string. 28 return token.SignedString(jwtSecret) 29}

Clearly, the controller function attached to the /login endpoint that makes use of this LoginUser service must return the token in the response; that is:

Go
1token, err := services.LoginUser(db, temp.Username, temp.Password) 2c.JSON(http.StatusOK, gin.H{"token": token})
Validating a JWT

The validation of a JWT ensures that it hasn't been tampered with and is issued by a trusted source. During validation, the following process is carried out:

  1. Extract the Token: Retrieve the JWT from the request header.
  2. Decode the JWT: Split the JWT into header, payload, and signature.
  3. Verify the Signature: Recompute the signature using the stored secret key and compare it with the received signature.
  4. Check Claims: Examine the payload claims for validity, such as checking the expiration time (exp) to ensure the token isn't expired.

Validation is a critical step in securing your API endpoints, confirming that the incoming requests are from an authenticated user.

JWT Security Best Practices

Implementing JWT authentication requires attention to security best practices to mitigate potential risks.

  • Keep Secrets Safe: Ensure that your secret keys are stored securely and not exposed in your codebase or logs.
  • Use Strong Algorithms: Prefer algorithms like HS256 or RS256 for signing to provide strong encryption.
  • Set Expiration Times: Include an exp claim to prevent tokens from being used indefinitely and protect against replay attacks.
  • Validate Token Audience and Issuer: Use aud and iss claims to verify the token's audience and issuer, ensuring it was delivered to the intended recipient and issued by a trusted source.
  • Regenerate Tokens Periodically: Regularly rotate and regenerate tokens to limit the window of exposure in case of a compromised token.

By adhering to these practices, you enhance the security of your application while using JWTs for authentication.

Implementing Authentication Middleware

In this section, we will build on your existing knowledge of middleware by implementing an authentication middleware using JWT for our Gin application. This middleware will ensure that all incoming requests are authenticated before they reach the endpoint logic.

Here's a step-by-step implementation of the authentication middleware, located in middleware/auth.go:

Go
1package auth 2 3import ( 4 "net/http" 5 "strings" 6 7 "github.com/gin-gonic/gin" 8 "github.com/golang-jwt/jwt/v5" // Import JWT package for token handling 9 10 "codesignal.com/todoapp/services" 11) 12 13func AuthMiddleware() gin.HandlerFunc { 14 return func(c *gin.Context) { 15 // Retrieve the Authorization header from the request 16 authHeader := c.GetHeader("Authorization") 17 if authHeader == "" { 18 // Respond with unauthorized if header is empty 19 c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is empty"}) 20 c.Abort() 21 return 22 } 23 24 // Split the header's Bearer token 25 tokenParts := strings.Split(authHeader, "Bearer ") 26 if len(tokenParts) != 2 { 27 // Respond with unauthorized if token format is incorrect 28 c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"}) 29 c.Abort() 30 return 31 } 32 33 // Extract the token string 34 tokenString := tokenParts[1] 35 claims := &services.Claims{} 36 37 // Parse and validate the JWT using the provided secret key 38 token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { 39 return services.JWTSecret, nil 40 }) 41 if err != nil || !token.Valid { 42 // Respond with unauthorized if token is invalid 43 c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 44 c.Abort() 45 return 46 } 47 48 // Set the username in the context for access in subsequent handlers 49 c.Set("username", claims.Username) 50 c.Next() // Proceed to the next handler if token is valid 51 } 52}

In this code, we perform the following steps using specific functions:

  • Gather the Authorization header using c.GetHeader("Authorization").
  • Split the header to extract the token with strings.Split() and validate its presence.
  • Parse the token using jwt.ParseWithClaims(), which verifies its authenticity and extracts the claims.
  • If the token is invalid or missing, respond with an unauthorized error using c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}).
  • If valid, set the user's username in the context using c.Set("username", claims.Username) to make it accessible to other handlers.
Securing API Routes with Middleware

With our middleware ready, we'll now apply it to our API routes to safeguard them. Here’s how the routes are defined and registered in todoapp/router/router.go:

Go
1package router 2 3import ( 4 "github.com/gin-gonic/gin" 5 6 "codesignal.com/todoapp/controllers" 7 "codesignal.com/todoapp/middleware/auth" 8 "codesignal.com/todoapp/repositories/db" 9) 10 11func RegisterRoutes() *gin.Engine { 12 database := db.ConnectDatabase() 13 14 router := gin.Default() 15 router.POST("/register", func(c *gin.Context) { controllers.Register(c, database) }) 16 router.POST("/login", func(c *gin.Context) { controllers.Login(c, database) }) 17 18 protected := router.Group("/api", auth.AuthMiddleware()) 19 { 20 protected.GET("/todos", func(c *gin.Context) { controllers.GetTodos(c, database) }) 21 protected.POST("/todos", func(c *gin.Context) { controllers.CreateTodo(c, database) }) 22 } 23 return router 24}

Here's what happens in the code:

  • A protected group is created using router.Group. In Gin, a Group is a way to apply common middleware or path prefixes to a set of routes; for example, in the code snippet both routes share the common /api prefix as well as the AuthMiddleware authentication middleware as they belong to the same protected group.
  • We define our GET /todos and POST /todos routes within this protected group, ensuring they are only accessible to authenticated users.
  • Unauthorized requests will be blocked at the middleware stage, never reaching the business logic of these endpoints.
Summary and Next Steps

Congratulations on completing this lesson! You have learned how to implement JWT-based authentication to protect your API methods using the Gin framework. By creating authentication middleware and integrating it with your route definitions, you set a strong foundation for safeguarding your application.

As you move forward, practical exercises will reinforce this knowledge by having you implement and test these concepts. Continue to explore how authentication can fortify the integrity and security of your applications.

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