Authentication in a Go Web Application

Authentication in a Go Web Application

Authentication is a crucial aspect of web applications to ensure that only authorized users can access certain resources and perform specific actions. In this blog post, we'll explore how to implement authentication in a Go web application using JSON Web Tokens (JWTs) and some best practices. We'll break down the code into different sections to understand each part of the authentication process.

https://github.com/vizvasrj/my-api-project

Setting up the Environment

Before diving into the code, let's ensure that you have the required packages and dependencies installed. In this example, we're using Gorilla Mux for routing and the github.com/golang-jwt/jwt package for JWT handling. Make sure to install these packages using go get.

import (
    // ...
    "github.com/dgrijalva/jwt-go"
    "github.com/gorilla/context"
)

Middleware for Authentication

We start by creating a middleware that handles authentication. This middleware checks incoming requests for JWT tokens in the Authorization header.

package middleware

import (
    // ...
)


// Authentication Middleware
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Extract the JWT token from the Authorization header
        tokenString := r.Header.Get("Authorization")
        if tokenString == "" {
            http.Error(w, "Authorization header is null", http.StatusUnauthorized)
            return
        }

        // Parse and validate the JWT token
        claims, err := helpers.ValidateToken(tokenString)
        if err != nil {
            fmt.Println(err)
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        // Store user information in the request context
        context.Set(r, "uid", claims.Uid)
        context.Set(r, "username", claims.Username)

        // Continue to the next middleware or handler
        next.ServeHTTP(w, r)
    }
}

This middleware ensures that incoming requests have a valid JWT token. If the token is missing or invalid, it responds with an Unauthorized status.

User Registration

In your web application, you need a way for users to register. Here's a handler for user registration:

package handlers

import (
    // ...
)

type MyHandler struct {
    DB *sql.DB
}

func (h MyHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
    // Parse JSON request body
    var newUser data.UserRegister
    err := json.NewDecoder(r.Body).Decode(&newUser)
    if err != nil {
        http.Error(w, "Invalid request data", http.StatusBadRequest)
        return
    }

    // Hash the user's password
    if newUser.Password != newUser.ConfirmPassword {
        http.Error(w, "Password did not match", http.StatusBadRequest)
        return
    }
    hashedPassword, err := helpers.HashPassword(newUser.Password)
    if err != nil {
        http.Error(w, "Failed to hash password", http.StatusInternalServerError)
        return
    }

    // Insert user into the database with the hashed password
    _, err = h.DB.Exec("INSERT INTO users (username, password_hash, role) VALUES ($1, $2, $3)",
        newUser.Username, hashedPassword, "user")
    if err != nil {
        http.Error(w, "Failed to insert user", http.StatusInternalServerError)
        return
    }

    // Respond with a success message or status code
    w.WriteHeader(http.StatusCreated)
    fmt.Fprintf(w, "User registration successful")
}

This handler processes user registration requests by hashing the password and storing user information in a database.

User Login

To allow users to log in, create a handler for user login:

func (h MyHandler) LoginUser(w http.ResponseWriter, r *http.Request) {
    // Parse JSON request body
    var loginRequest data.UserLogin
    err := json.NewDecoder(r.Body).Decode(&loginRequest)
    if err != nil {
        http.Error(w, "Invalid request data", http.StatusBadRequest)
        return
    }

    // Query the database to retrieve the user's hashed password
    var user data.User
    err = h.DB.QueryRow("SELECT id, username, password_hash, role FROM users WHERE username = $1", loginRequest.Username).Scan(
        &user.ID,
        &user.Username,
        &user.PasswordHash,
        &user.Role,
    )
    if err != nil {
        if err == sql.ErrNoRows {
            http.Error(w, "User not found", http.StatusUnauthorized)
            return
        }
        http.Error(w, "Database error", http.StatusInternalServerError)
        return
    }

    // Verify the provided password against the stored hash
    if err := helpers.VerifyPassword(user.PasswordHash, loginRequest.Password); err != nil {
        http.Error(w, "Invalid password", http.StatusUnauthorized)
        return
    }

    // Authentication successful
    // Generate a JWT token
    tokenString, refreshToken, err := helpers.GenerateTokens(user.ID, user.Username)
    if err != nil {
        http.Error(w, "Failed to generate JWT token", http.StatusInternalServerError)
        return
    }

    // Include the token in the response
    response := map[string]string{"token": tokenString, "refresh_token": refreshToken}

    // Respond with the token and a success status code
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(response)
}

This handler handles user login by checking the provided credentials, generating JWT tokens upon successful authentication, and returning them in the response.

Password Hashing and Verification

To securely handle user passwords, we use password hashing and verification functions. These functions ensure that passwords are stored securely in the database and can be verified when users log in.

func HashPassword(password string) (string, error) {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return "", err
    }
    return string(hashedPassword), nil
}

func VerifyPassword(hashedPassword string, password string) error {
    err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
    return err
}

These functions are used in the user registration and login handlers to hash and verify passwords.

JWT Token Handling

JWT tokens are generated, validated, and refreshed using the following functions:

// GenerateTokens generates JWT tokens for a user.
func GenerateTokens(uid string, username string) (signedToken string, signedRefreshToken string, err error) {
    // Define the token expiration time
    accessTokenExpiration := time.Now().Add(time.Hour * time.Duration(1)).Unix()
    refreshTokenExpiration := time.Now().Add(time.Hour * time.Duration(24)).Unix()

    // Create claims for access token
    accessTokenClaims := jwt.MapClaims{
        "uid":       uid,
        "username":  username,
        "exp":       accessTokenExpiration,
    }

    // Create claims for refresh token
    refreshTokenClaims := jwt.MapClaims{
        "uid":       uid,
        "username":  username,
        "exp":       refreshTokenExpiration,
    }

    // Get the secret key from environment variable
    SECRET_KEY := os.Getenv("secret_key")
    if SECRET_KEY == "" {
        return "", "", fmt.Errorf("secret_key not set")
    }

    // Create and sign the access token
    accessTokenToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessTokenClaims)
    access_token, err := accessTokenToken.SignedString([]byte(SECRET_KEY))
    if err != nil {
        return "", "", err
    }

    // Create and sign the refresh token
    refreshTokenToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshTokenClaims)
    refresh_token, err := refreshTokenToken.SignedString([]byte(SECRET_KEY))
    if err != nil {
        return "", "", err
    }

    return access_token, refresh_token, nil
}

// ValidateToken validates a JWT token and returns the claims if valid.
func ValidateToken(signedToken string) (claims jwt.MapClaims, err error) {
    // Get the secret key from environment variable
    SECRET_KEY := os.Getenv("secret_key")
    if SECRET_KEY == "" {
        return nil, fmt.Errorf("secret_key not set")
    }

    // Parse and validate the token
    token, err := jwt.Parse(signedToken, func(token *jwt.Token) (interface{}, error) {
        // Check the signing method
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method")
        }
        return []byte(SECRET_KEY), nil
    })
    if err != nil {
        return nil, err
    }

    // Check if the token is valid
    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        return claims, nil
    }

    return nil, fmt.Errorf("invalid token")
}

I've rewritten the GenerateTokens and ValidateToken functions to make them clearer and more explicit. The token expiration time is defined explicitly, and error handling for missing secret key or invalid tokens is improved.

These functions enable the creation and validation of JWT tokens, ensuring the security of user authentication.

Conclusion

Implementing authentication in a Go web application is crucial for securing your application's resources and data. By using JWT tokens, password hashing, and middleware, you can create a robust authentication system that protects your users and their data. Remember to store sensitive information securely and follow best practices for web application security.

Comments