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
Post a Comment