Building a Secure Golang REST API with JWT, PostgreSQL, and the Chi Framework
In this post, I’ll walk you through building a RESTful API in Golang with secure authentication, data persistence using PostgreSQL, and a clean, efficient architecture. We’ll implement the following components:
- JWT (JSON Web Tokens) for authentication and authorization.
- PostgreSQL as the database to store data.
- Chi as the router for handling HTTP requests.
- Repository Pattern for managing database interactions without using an ORM.
Setting Up the Environment
Before diving into the code, let's first ensure that you have the required tools set up:
- Golang (latest version)
- PostgreSQL (for the database)
- Chi (for routing)
To get started, make sure you have PostgreSQL up and running. For Golang, install it from the official website or use your package manager.
1. JWT Authentication and Authorization
The first and most important feature in our API is authentication. We’ll use JWT (JSON Web Tokens) for securely transmitting information between the client and server.
JWT allows us to verify the user’s identity by encoding data into a token, which is then sent back with subsequent API requests for validation. This eliminates the need to store user sessions in memory or a database, making the system more scalable.
Key Features of JWT in the API:
- Sign In: Users authenticate with their username and password.
- Token Issuance: Upon successful authentication, a JWT is issued to the user, which will be used to make subsequent requests.
- Token Refresh: When the token expires, we provide an endpoint to refresh it using a refresh token, ensuring seamless user experience.
2. PostgreSQL Database
The API uses PostgreSQL as the database to store user data, and any other application-related data. Unlike some of the more popular Go ORMs like GORM, we are not using an ORM here. Instead, we are directly interacting with the database via raw SQL queries, giving us more control over the queries and reducing any overhead that comes with an ORM.
Database Structure:
- Users table to store user credentials and other essential data.
- Other necessary tables depending on the domain (e.g., user profiles, logs, etc.).
You can structure your queries to match the business logic you need while keeping your codebase flexible and easy to maintain.
3. Chi Framework
We are using Chi, a lightweight yet powerful router for Golang, to handle our HTTP routes. Chi is a great choice because of its simplicity, flexibility, and high performance. It provides an easy-to-use middleware stack and supports a modular design.
In our API, we have defined routes for:
- User Authentication: Sign In, Sign Out, and Refresh Token.
- Resource Protection: Routes that require valid JWT tokens to access.
4. Repository Pattern Without an ORM
One of the most significant architectural decisions I made was to implement the Repository Pattern for managing database interactions. The Repository Pattern helps decouple the database logic from the rest of the application code, promoting better organization, testability, and scalability.
In this implementation, we don’t use an ORM like GORM. Instead, we define raw SQL queries for interacting with the database. This gives us fine-grained control over the SQL and ensures better performance since we avoid the overhead of an ORM.
Benefits of Using the Repository Pattern:
- Clean Architecture: Keeps database code separate from business logic.
- Testability: Easier to mock database interactions for unit tests.
- Flexibility: You can change the underlying database without affecting the rest of your application.
5. Resource Protection
One of the key concerns in any REST API is ensuring that certain resources are protected from unauthorized access. Using JWT, we can protect routes by checking the validity of the token before allowing access to specific resources.
For instance, if you have a route that allows users to access their profile, the server will validate the token in the request header and only allow access if the token is valid.
If the token has expired, the client will receive a response indicating they need to refresh the token.
6. Code Implementation Overview
Here’s a simplified view of how the pieces fit together:
JWT Middleware
We use a middleware to validate the token on protected routes:
mux.Route("/admin", func(mux chi.Router){
mux.Use(app.authRequired)
mux.Get("/movies", app.MovieCatalog)
mux.Get("/movies/{id}", app.MovieForEdit)
mux.Put("/movies/0", app.InsertMovie)
mux.Patch("/movies/{id}", app.UpdateMovie)
mux.Delete("/movies/{id}", app.DeleteMovie)
})
Token Refresh Endpoint
We create an endpoint that issues a new token when the old one expires:
func (app *application) refreshToken(w http.ResponseWriter, r *http.Request) {
for _, cookie := range r.Cookies() {
if cookie.Name == app.auth.CookieName {
claims := &Claims{}
refreshToken := cookie.Value
// parse the token to get the claims
_, err := jwt.ParseWithClaims(refreshToken, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(app.JWTSecret), nil
})
if err != nil {
app.errorJSON(w, errors.New("unauthorized"), http.StatusUnauthorized)
return
}
// get the user id from the token claims
userID, err := strconv.Atoi(claims.Subject)
if err != nil {
app.errorJSON(w, errors.New("unknown user"), http.StatusUnauthorized)
return
}
user, err := app.DB.GetUserByID(userID)
if err != nil {
app.errorJSON(w, errors.New("unknown user"), http.StatusUnauthorized)
return
}
u := jwtUser{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
}
tokenPairs, err := app.auth.GenerateTokenPair(&u)
if err != nil {
app.errorJSON(w, errors.New("error generating tokens"), http.StatusUnauthorized)
return
}
http.SetCookie(w, app.auth.GetRefreshCookie(tokenPairs.RefreshToken))
app.writeJSON(w, http.StatusOK, tokenPairs)
}
}
}