Add Subtitle
Introduction
In this tutorial, we will build a simple web application using Go and MongoDB that implements JWT (JSON Web Token) authentication. This application will allow users to register, log in, and access a protected route. We will use the Gin framework to build the web server and the MongoDB driver to interact with the database.
Prerequisites
Before we begin, ensure you have the following installed:
Go (version 1.16 or later)
MongoDB Atlas Cluster
A code editor (e.g., Visual Studio Code
The source code for this project can be found on GitHub.
Project Structure
Create a new directory for your project and set up the following structure:
go-mongodb-auth/
├── controllers/
│ ├── handler.go
├── database/
│ ├── database.go
├── jwt/
│ ├── claims.go
│ ├── jwt.go
├── middleware/
│ ├── auth.go
├── models/
│ ├── user.go
├── utils/
│ ├── helper.go
│ ├── password.go
├── main.go
├── .env
├── go.mod
└── go.sum
Step 1: Initialize the Go Module
Navigate to your project directory and initialize a new Go module:
go mod init go-mongodb-auth
Step 2: Install Dependencies
Install the required packages:
go get github.com/gin-gonic/gin
go get go.mongodb.org/mongo-driver/mongo
go get github.com/golang-jwt/jwt/v5
go get github.com/google/uuid
go get github.com/joho/godotenv
go get github.com/joho/godotenv
go get golang.org/x/crypto/bcrypt
Step 3: Set Up a MongoDB Atlas Cluster
To store our data, we will use MongoDB Atlas. A database connection URL is required to connect the database. Follow the guide to set up a MongoDB Atlas cluster and get a connection URL.
Next, in the .env
file, add the connection URL as an environment variable.
Step 4: Create the .env File
Create a .env
file in the root of your project to store environment variables:
MONGODB_URI= <mongodb_URI>
MONGODB_DB_NAME= <db_name>
MONGODB_COLLECTION= <collection>
// jwt token
SECRET_KEY= <token>
Replace your_db_name
and your_secret_key
with your desired database name and a secure secret key.
Step 5: Set Up the Database Connection
In database/database.go
, set up the MongoDB connection:
package database
import (
"context"
"log"
"os"
_ "github.com/joho/godotenv/autoload"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type Config struct {
MongoDBURI string
MongoDBName string
MongoDBCollection string
}
var config *Config
var DB *mongo.Database
var Client *mongo.Client
func init() {
config = &Config{
MongoDBURI: os.Getenv("MONGODB_URI"),
MongoDBName: os.Getenv("MONGODB_DB_NAME"),
MongoDBCollection: os.Getenv("MONGODB_COLLECTION"),
}
}
func NewDBInstance() error {
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(config.MongoDBURI))
if err != nil {
log.Fatal(err)
}
DB = client.Database(config.MongoDBName)
Client = client
return nil
}
func GetDBCollection() *mongo.Collection {
return DB.Collection(config.MongoDBCollection)
}
func Close() error {
return Client.Disconnect(context.Background())
}
Step 6: Create User Model
In models/user.go
, define the user model and functions for user operations:
package models
import (
"context"
"go-mongodb-auth/database"
"go-mongodb-auth/utils"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type User struct {
ID primitive.ObjectID `bson:"_id" json:"id"`
UUID string `bson:"uuid" json:"uuid"`
Email string `bson:"email" json:"email"`
Username string `bson:"username" json:"username"`
Password string `bson:"password" json:"-"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
type RegisterUser struct {
Email string `json:"email" binding:"required"`
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func NewUser (user RegisterUser ) error {
collection := database.GetDBCollection()
_, err := collection.InsertOne(context.TODO(), &User {
ID: primitive.NewObjectID(),
UUID: utils.GetUUID(),
Username: user.Username,
Email: user.Email,
Password: user.Password,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
return err
}
func GetUser ByEmail(email string) (*User , error) {
collection := database.GetDBCollection()
var user *User
filter := bson.D{primitive.E{Key: "email", Value: email}}
err := collection.FindOne(context.TODO(), filter).Decode(&user)
if err != nil {
return &User {}, err
}
return user, nil
}
func CheckUser (email string) bool {
user, _ := GetUser ByEmail(email)
return !user.ID.IsZero()
}
Step 7: Implement JWT Functionality
In jwt/claims.go
, define the claims structure:
package jwt
import (
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
type Claims struct {
Email string `json:"email"`
jwt.RegisteredClaims
}
func NewClaims(email string) (*Claims, error) {
tokenID, err := uuid.NewRandom()
if err != nil {
return &Claims{}, err
}
return &Claims{
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ID: tokenID.String(),
Issuer: email,
Subject: email,
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 12)),
},
}, nil
}
In jwt/jwt.go
, implement token creation and verification:
package jwt
import (
"fmt"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
func CreateToken(email string) (string, *Claims, error) {
claims, err := NewClaims(email)
if err != nil {
return "", &Claims{}, err
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY")))
if err != nil {
return "", &Claims{}, err
}
return tokenString, claims, nil
}
func VerifyToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("invalid token signing method")
}
return []byte(os.Getenv("SECRET_KEY")), nil
})
if err != nil {
return nil, fmt.Errorf("error parsing token: %w", err)
}
claims, ok := token.Claims.(*Claims)
if !ok {
return nil, fmt.Errorf("invalid token claims")
}
if time.Now().After(claims.ExpiresAt.Time) {
return nil, fmt.Errorf("token expired")
}
return claims, nil
}
Step 8: Create Authentication Middleware
In middleware/auth.go
, implement the authentication middleware:
package middleware
import (
"go-mongodb-auth/jwt"
"go-mongodb-auth/models"
"go-mongodb-auth/utils"
"net/http"
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
cookie, err := c.Cookie("Authorization")
if err != nil || cookie == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"status": http.StatusUnauthorized,
"message": http.StatusText(http.StatusUnauthorized),
})
return
}
token := utils.RetrieveToken(cookie)
claims, err := jwt.VerifyToken(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError,
"message": err.Error(),
})
return
}
user, err := models.GetUser ByEmail(claims.Email)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError,
"message": err.Error(),
})
return
}
c.Set("user", user)
c.Next()
}
}
Step 9: Create Controllers
In controllers/handler.go
, implement the login and registration controllers:
package controllers
import (
"go-mongodb-auth/jwt"
"go-mongodb-auth/models"
"go-mongodb-auth/utils"
"net/http"
"github.com/gin-gonic/gin"
)
func LoginController(c *gin.Context) {
var user models.LoginUser
if err := c.BindJSON(&user); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"status": http.StatusUnprocessableEntity,
"message": err.Error(),
})
return
}
if exists := models.CheckUser (user.Email); !exists {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"status": http.StatusUnprocessableEntity,
"message": "user does not exist",
})
return
}
authUser , _ := models.GetUser ByEmail(user.Email)
if err := utils.CheckPassword(user.Password, authUser .Password); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"status": http.StatusUnprocessableEntity,
"message": "Password is incorrect",
})
return
}
token, _, err := jwt.CreateToken(user.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError,
"message": err.Error(),
})
return
}
utils.GenerateAndStoreCookie(c, token)
c.JSON(http.StatusOK, gin.H{
"status": http.StatusOK,
"data": gin.H{
"accessToken": token,
"user": gin.H{
"username": authUser .Username,
"email": authUser .Email,
},
},
})
}
func RegisterController(c *gin.Context) {
var user models.RegisterUser
if err := c.BindJSON(&user); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"status": http.StatusUnprocessableEntity,
"message": err.Error(),
})
return
}
if exists := models.CheckUser (user.Email); exists {
c.JSON(http.StatusConflict, gin.H{
"status": http.StatusConflict,
"message": "user already exists",
})
return
}
user.Password, _ = utils.HashPassword(user.Password)
err := models.NewUser (user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError,
"message": err.Error(),
})
return
}
token, _, err := jwt.CreateToken(user.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError,
"message": err.Error(),
})
return
}
utils.GenerateAndStoreCookie(c, token)
c.JSON(http.StatusCreated, gin.H{
"status": http.StatusCreated,
"data": gin.H{
"accessToken": token,
"user": gin.H{
"username": user.Username,
"email": user.Email,
},
},
})
}
func UserAuthController(c *gin.Context) {
data, exists := c.Get("user")
if !exists {
c.JSON(http.StatusBadRequest, gin.H{
"status": http.StatusBadRequest,
"message": "Cannot retrieve user",
})
return
}
user := data.(*models.User)
c.JSON(http.StatusOK, user)
}
Step 10: Implement Utility Functions
In utils/helper.go
, implement helper functions:
package utils
import (
"fmt"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/joho/godotenv"
)
func LoadEnv() error {
err := godotenv.Load()
return err
}
func GetUUID() string {
tokenID, _ := uuid.NewRandom()
return tokenID.String()
}
func RetrieveToken(cookie string) string {
data := strings.Split(cookie, " ")
return data[1]
}
func GenerateAndStoreCookie(c *gin.Context, token string) {
token = fmt.Sprintf("Bearer %s", token)
c.SetCookie("Authorization", token, 24*7*3600, "/", "", true, true)
}
In utils/password.go
, implement password hashing functions:
package utils
import "golang.org/x/crypto/bcrypt"
func HashPassword(password string) (string, error) {
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashed), nil
}
func CheckPassword(password, hashedPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
Step 11: Create the Main Function
In main.go
, set up the server and routes:
package main
import (
"context"
"fmt"
"go-mongodb-auth/controllers"
"go-mongodb-auth/database"
"go-mongodb-auth/middleware"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
database.NewDBInstance()
done := make(chan bool)
router := gin.Default()
router.POST("/login", controllers.LoginController)
router.POST("/register", controllers.RegisterController)
router.GET("/user", middleware.AuthMiddleware(), controllers.UserAuthController)
server := &http.Server{
Addr: ":8005",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 120 * time.Second,
}
go ShutdownServer(server, done)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Println(err)
panic(err)
}
<-done
}
func ShutdownServer(server *http.Server, done chan bool) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Println("Server shutdown error", err)
return
}
if err := database.Close(); err != nil {
log.Println("Database shutdown error", err)
}
<-ctx.Done()
log.Println("Server exiting...")
done <- true
}
Step 12: Run the Application
To run your application, execute the following command in your terminal:
go run main.go
Your server should now be running on http://localhost:8005
.
Step 13: Testing the API
You can use tools like Postman or cURL to test the API endpoints:
Register a User:
Endpoint:
POST /register
Body:
JSON
{ "email": "user@example.com", "username": "username", "password": "password" }
Login a User:
Endpoint:
POST /login
Body:
JSON
{ "email": "user@example.com", "password": "password" }
Access Protected Route:
Endpoint:
GET /user
Headers:
Authorization
: Bearer token (from the login response)
Conclusion
In this tutorial, we have built a simple JWT authentication system using Go and MongoDB. We covered user registration, login, and protected routes using JWTs. You can expand this application by adding more features like user roles, password reset functionality, and more robust error handling.