In this article, you’ll learn how to implement RS256 JWT Authentication and Authorization with Golang, Gin Gonic, MongoDB-Go-driver, and Docker-compose.
CRUD RESTful API with Golang + MongoDB Series:
- API with Golang + MongoDB + Redis + Gin Gonic: Project Setup
- Golang & MongoDB: JWT Authentication and Authorization
- API with Golang + MongoDB: Send HTML Emails with Gomail
- API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
- Build Golang gRPC Server and Client: SignUp User & Verify Email
- Build Golang gRPC Server and Client: Access & Refresh Tokens
- Build CRUD RESTful API Server with Golang, Gin, and MongoDB
Related Articles:
- Node.js + TypeScript + MongoDB: JWT Authentication
- Node.js + TypeScript + MongoDB: JWT Refresh Token
- API with Node.js + PostgreSQL + TypeORM: JWT Authentication
Golang & MongoDB JWT Authentication Overview
You can import the Postman collection used in testing the Golang Gin RESTful API into your own Postman to make your life easier.
-Register a new user
-Login the registered user
After the user has been authenticated by the Golang Gin server, some cookies will be returned to the user’s client or browser.
You can click on the Cookies tab in Postman to see the different cookies returned by the Gin server.
-Get the currently logged-in user credentials
-Refresh the access token when it expires
-Logout the authenticated user
JWT Authentication Example with Golang and MongoDB
With this Golang JSON Web Token authentication API, the user will be able to do the following:
- Register for a new account
- Login with the registered credentials
- Refresh the access token when expired
- Get profile information only if authenticated.
RESOURCE | HTTP METHOD | ROUTE | DESCRIPTION |
---|---|---|---|
users | GET | /api/users/me | return the logged-in user’s information |
auth | POST | /api/auth/register | Register a new user |
auth | POST | /api/auth/login | Login registered user |
auth | GET | /api/auth/refresh | Refresh expired access token |
auth | GET | /api/auth/logout | Logout user |
How to Generate Public and Private Keys
To generate the public and private keys, navigate to this website and click on the “Generate New Keys” button.
Next, copy the private key and visit this website to encode it in base64. Later we’ll use the base64 Golang package to decode it back to ASCII string.
On the Base64 Decode and Encode website, paste the copied private key into the input field and click on the “Encode” button.
Copy the encoded private key and add it to the app.env
file asACCESS_TOKEN_PRIVATE_KEY
.
Go back to the website where you generated the keys and copy the corresponding public key.
Visit the Base64 Decode and Encode website and encode it to base64 before adding it to the app.env
file as ACCESS_TOKEN_PUBLIC_KEY
.
Repeat the process for the refresh token.
Update Environment Variables
You can copy and paste the environment variables below into your app.env
file if you find it difficult to encode the private and public keys.
I also added the access and refresh tokens expiration time and max-age to the app.env
file.
app.env
PORT=8000
MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=password123
MONGODB_LOCAL_URI=mongodb://root:password123@localhost:6000
REDIS_URL=localhost:6379
ACCESS_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCUEFJQkFBSkJBTzVIKytVM0xrWC91SlRvRHhWN01CUURXSTdGU0l0VXNjbGFFKzlaUUg5Q2VpOGIxcUVmCnJxR0hSVDVWUis4c3UxVWtCUVpZTER3MnN3RTVWbjg5c0ZVQ0F3RUFBUUpCQUw4ZjRBMUlDSWEvQ2ZmdWR3TGMKNzRCdCtwOXg0TEZaZXMwdHdtV3Vha3hub3NaV0w4eVpSTUJpRmI4a25VL0hwb3piTnNxMmN1ZU9wKzVWdGRXNApiTlVDSVFENm9JdWxqcHdrZTFGY1VPaldnaXRQSjNnbFBma3NHVFBhdFYwYnJJVVI5d0loQVBOanJ1enB4ckhsCkUxRmJxeGtUNFZ5bWhCOU1HazU0Wk1jWnVjSmZOcjBUQWlFQWhML3UxOVZPdlVBWVd6Wjc3Y3JxMTdWSFBTcXoKUlhsZjd2TnJpdEg1ZGdjQ0lRRHR5QmFPdUxuNDlIOFIvZ2ZEZ1V1cjg3YWl5UHZ1YStxeEpXMzQrb0tFNXdJZwpQbG1KYXZsbW9jUG4rTkVRdGhLcTZuZFVYRGpXTTlTbktQQTVlUDZSUEs0PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ==
ACCESS_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBTzVIKytVM0xrWC91SlRvRHhWN01CUURXSTdGU0l0VQpzY2xhRSs5WlFIOUNlaThiMXFFZnJxR0hSVDVWUis4c3UxVWtCUVpZTER3MnN3RTVWbjg5c0ZVQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==
ACCESS_TOKEN_EXPIRED_IN=15m
ACCESS_TOKEN_MAXAGE=15
REFRESH_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT1FJQkFBSkJBSWFJcXZXeldCSndnYjR1SEhFQ01RdHFZMTI5b2F5RzVZMGlGcG51a0J1VHpRZVlQWkE4Cmx4OC9lTUh3Rys1MlJGR3VxMmE2N084d2s3TDR5dnY5dVY4Q0F3RUFBUUpBRUZ6aEJqOUk3LzAxR285N01CZUgKSlk5TUJLUEMzVHdQQVdwcSswL3p3UmE2ZkZtbXQ5NXNrN21qT3czRzNEZ3M5T2RTeWdsbTlVdndNWXh6SXFERAplUUloQVA5UStrMTBQbGxNd2ZJbDZtdjdTMFRYOGJDUlRaZVI1ZFZZb3FTeW40YmpBaUVBaHVUa2JtZ1NobFlZCnRyclNWZjN0QWZJcWNVUjZ3aDdMOXR5MVlvalZVRlVDSUhzOENlVHkwOWxrbkVTV0dvV09ZUEZVemhyc3Q2Z08KU3dKa2F2VFdKdndEQWlBdWhnVU8yeEFBaXZNdEdwUHVtb3hDam8zNjBMNXg4d012bWdGcEFYNW9uUUlnQzEvSwpNWG1heWtsaFRDeWtXRnpHMHBMWVdkNGRGdTI5M1M2ZUxJUlNIS009Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t
REFRESH_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBSWFJcXZXeldCSndnYjR1SEhFQ01RdHFZMTI5b2F5Rwo1WTBpRnBudWtCdVR6UWVZUFpBOGx4OC9lTUh3Rys1MlJGR3VxMmE2N084d2s3TDR5dnY5dVY4Q0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==
REFRESH_TOKEN_EXPIRED_IN=60m
REFRESH_TOKEN_MAXAGE=60
Next, update the config/default.go
file with the environment variables provided in the app.env
file for Viper to load and make them available in the project.
config/default.go
package config
import (
"time"
"github.com/spf13/viper"
)
type Config struct {
DBUri string `mapstructure:"MONGODB_LOCAL_URI"`
RedisUri string `mapstructure:"REDIS_URL"`
Port string `mapstructure:"PORT"`
AccessTokenPrivateKey string `mapstructure:"ACCESS_TOKEN_PRIVATE_KEY"`
AccessTokenPublicKey string `mapstructure:"ACCESS_TOKEN_PUBLIC_KEY"`
RefreshTokenPrivateKey string `mapstructure:"REFRESH_TOKEN_PRIVATE_KEY"`
RefreshTokenPublicKey string `mapstructure:"REFRESH_TOKEN_PUBLIC_KEY"`
AccessTokenExpiresIn time.Duration `mapstructure:"ACCESS_TOKEN_EXPIRED_IN"`
RefreshTokenExpiresIn time.Duration `mapstructure:"REFRESH_TOKEN_EXPIRED_IN"`
AccessTokenMaxAge int `mapstructure:"ACCESS_TOKEN_MAXAGE"`
RefreshTokenMaxAge int `mapstructure:"REFRESH_TOKEN_MAXAGE"`
}
func LoadConfig(path string) (config Config, err error) {
viper.AddConfigPath(path)
viper.SetConfigType("env")
viper.SetConfigName("app")
viper.AutomaticEnv()
err = viper.ReadInConfig()
if err != nil {
return
}
err = viper.Unmarshal(&config)
return
}
Creating the User models with structs
In the JSON web Token authentication flow, we need to start by signing up the user. To achieve that let’s define a struct that specifies the fields required to register a user.
To define the user struct, you need to specify the field name followed by the field type and an optional tag which will be used by Gin Gonic and MongoDB for validation, marshaling, and unmarshaling.
Now create a SignUpInput
struct to specify the fields required to register a new user.
The bson
tag will be used by MongoDB since MongoDB stores data as BSON documents. Also, the json
tag will be used by Gin Gonic for validating user inputs.
Lastly, the binding
tag specifies additional validation rules that will be used by Gin Gonic to validate user inputs.
models/user.model.go
package models
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type SignUpInput struct {
Name string `json:"name" bson:"name" binding:"required"`
Email string `json:"email" bson:"email" binding:"required"`
Password string `json:"password" bson:"password" binding:"required,min=8"`
PasswordConfirm string `json:"passwordConfirm" bson:"passwordConfirm,omitempty" binding:"required"`
Role string `json:"role" bson:"role"`
Verified bool `json:"verified" bson:"verified"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
With the user login logic, we’ll only require the user to provide an email and password. To achieve that create a SignInInput
struct with email and password fields and make them required.
models/user.model.go
package models
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// ? SignUpInput struct
type SignInInput struct {
Email string `json:"email" bson:"email" binding:"required"`
Password string `json:"password" bson:"password" binding:"required"`
}
Next, create a DBResponse
struct to define the fields that will be returned by MongoDB.
models/user.model.go
package models
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// ? SignUpInput struct
// ? SignInInput struct
type DBResponse struct {
ID primitive.ObjectID `json:"id" bson:"_id"`
Name string `json:"name" bson:"name"`
Email string `json:"email" bson:"email"`
Password string `json:"password" bson:"password"`
PasswordConfirm string `json:"passwordConfirm,omitempty" bson:"passwordConfirm,omitempty"`
Role string `json:"role" bson:"role"`
Verified bool `json:"verified" bson:"verified"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
Lastly, create a struct to specify the fields that should be included in the JSON response and a function to filter out the sensitive fields.
models/user.model.go
package models
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// ? SignUpInput struct
// ? SignInInput struct
// ? DBResponse struct
type UserResponse struct {
ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`
Name string `json:"name,omitempty" bson:"name,omitempty"`
Email string `json:"email,omitempty" bson:"email,omitempty"`
Role string `json:"role,omitempty" bson:"role,omitempty"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
func FilteredResponse(user *DBResponse) UserResponse {
return UserResponse{
ID: user.ID,
Email: user.Email,
Name: user.Name,
Role: user.Role,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
Below is the complete code for the user model
models/user.model.go
package models
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// ? SignUpInput struct
type SignUpInput struct {
Name string `json:"name" bson:"name" binding:"required"`
Email string `json:"email" bson:"email" binding:"required"`
Password string `json:"password" bson:"password" binding:"required,min=8"`
PasswordConfirm string `json:"passwordConfirm" bson:"passwordConfirm,omitempty" binding:"required"`
Role string `json:"role" bson:"role"`
Verified bool `json:"verified" bson:"verified"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
// ? SignInInput struct
type SignInInput struct {
Email string `json:"email" bson:"email" binding:"required"`
Password string `json:"password" bson:"password" binding:"required"`
}
// ? DBResponse struct
type DBResponse struct {
ID primitive.ObjectID `json:"id" bson:"_id"`
Name string `json:"name" bson:"name"`
Email string `json:"email" bson:"email"`
Password string `json:"password" bson:"password"`
PasswordConfirm string `json:"passwordConfirm,omitempty" bson:"passwordConfirm,omitempty"`
Role string `json:"role" bson:"role"`
Verified bool `json:"verified" bson:"verified"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
// ? UserResponse struct
type UserResponse struct {
ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`
Name string `json:"name,omitempty" bson:"name,omitempty"`
Email string `json:"email,omitempty" bson:"email,omitempty"`
Role string `json:"role,omitempty" bson:"role,omitempty"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
func FilteredResponse(user *DBResponse) UserResponse {
return UserResponse{
ID: user.ID,
Email: user.Email,
Name: user.Name,
Role: user.Role,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
Creating an Auth and User Interfaces
In a typical Golang RESTful API, there are controllers and services. The controllers don’t have direct access to the database instead they call the services to either mutate or query the database.
Authentication Interface
Now, let’s define an AuthService
interface that specifies a SignUpUser
and SignInUser
methods.
services/auth.service.go
package services
import "github.com/wpcodevo/golang-mongodb/models"
type AuthService interface {
SignUpUser(*models.SignUpInput) (*models.DBResponse, error)
SignInUser(*models.SignInInput) (*models.DBResponse, error)
}
User Interface
Also, define a UserService
interface that has FindUserById
and FindUserByEmail
methods.
services/user.service.go
package services
import "github.com/wpcodevo/golang-mongodb/models"
type UserService interface {
FindUserById(string) (*models.DBResponse, error)
FindUserByEmail(string) (*models.DBResponse, error)
}
Create utility functions to hash and verify password
Another important part of authentication is to encrypt the plain password provided by the user before saving it to the database.
To secure the password, we use a hashing algorithm to transform the plain password into a hashed string.
In the event of a compromised database, the hacker can not easily decrypt the hashed password to get the original plain password.
When two strings are hashed the outputs are the same so we use salt to ensure that no two users having the same password end up with the same hashed password.
There are different ways of hashing a string but in this tutorial, I will use the Golang Bcrypt package to hash the user’s password.
To hash a string with Bcrypt, we specify a Cost Factor which is the amount of time needed to calculate a single hash.
The higher the Cost Factor the longer the hashing time and the more difficult it is to brute force. Modern computers now have powerful CPUs so using a Cost Factor of 12 should be fine.
Now, let’s define two functions to hash and verify the user’s password.
utils/password.go
package utils
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("could not hash password %w", err)
}
return string(hashedPassword), nil
}
func VerifyPassword(hashedPassword string, candidatePassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(candidatePassword))
}
Create services that interact with the database
Next, let’s define a SignUpUser
service that will be called by a controller to create a new user in the database.
Auth interface implementation
I included the MongoDB Collection struct and the Context interface of the context package in the AuthServiceImpl
struct to help us perform the basic CRUD operations against the MongoDB database.
I then used the NewAuthService
constructor function to instantiate the MongoDB collection struct and the Context interface. Also, the NewAuthService
constructor implements the AuthService
interface we defined above.
services/auth.service.impl.go
package services
import (
"context"
"errors"
"strings"
"time"
"github.com/example/golang-test/models"
"github.com/example/golang-test/utils"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type AuthServiceImpl struct {
collection *mongo.Collection
ctx context.Context
}
func NewAuthService(collection *mongo.Collection, ctx context.Context) AuthService {
return &AuthServiceImpl{collection, ctx}
}
func (uc *AuthServiceImpl) SignUpUser(user *models.SignUpInput) (*models.DBResponse, error) {
user.CreatedAt = time.Now()
user.UpdatedAt = user.CreatedAt
user.Email = strings.ToLower(user.Email)
user.PasswordConfirm = ""
user.Verified = true
user.Role = "user"
hashedPassword, _ := utils.HashPassword(user.Password)
user.Password = hashedPassword
res, err := uc.collection.InsertOne(uc.ctx, &user)
if err != nil {
if er, ok := err.(mongo.WriteException); ok && er.WriteErrors[0].Code == 11000 {
return nil, errors.New("user with that email already exist")
}
return nil, err
}
// Create a unique index for the email field
opt := options.Index()
opt.SetUnique(true)
index := mongo.IndexModel{Keys: bson.M{"email": 1}, Options: opt}
if _, err := uc.collection.Indexes().CreateOne(uc.ctx, index); err != nil {
return nil, errors.New("could not create index for email")
}
var newUser *models.DBResponse
query := bson.M{"_id": res.InsertedID}
err = uc.collection.FindOne(uc.ctx, query).Decode(&newUser)
if err != nil {
return nil, err
}
return newUser, nil
}
func (uc *AuthServiceImpl) SignInUser(*models.SignInInput) (*models.DBResponse, error) {
return nil, nil
}
Here is a summary of what I did in the SignUpUser
function:
- First, I added the new user to the database with the
InsertOne()
function - Next, I added a unique index on the email field to ensure that no two users can have the same email address.
- Lastly, I used the
FindOne()
function to find and return the user that was added to the database.
User interface implementation
In order to implement the UserService
interface, I defined the FindUserById()
and FindUserByEmail()
function receivers.
In the FindUserById()
function, I converted the id
string to a MongoDB ObjectId for the query to work.
Also, I converted the email string to lowercase in the FindUserByEmail()
function before calling the FindOne()
function since MongoDB fields are not case-sensitive.
services/user.service.impl.go
package services
import (
"context"
"strings"
"github.com/example/golang-test/models"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type UserServiceImpl struct {
collection *mongo.Collection
ctx context.Context
}
func NewUserServiceImpl(collection *mongo.Collection, ctx context.Context) UserService {
return &UserServiceImpl{collection, ctx}
}
func (us *UserServiceImpl) FindUserById(id string) (*models.DBResponse, error) {
oid, _ := primitive.ObjectIDFromHex(id)
var user *models.DBResponse
query := bson.M{"_id": oid}
err := us.collection.FindOne(us.ctx, query).Decode(&user)
if err != nil {
if err == mongo.ErrNoDocuments {
return &models.DBResponse{}, err
}
return nil, err
}
return user, nil
}
func (us *UserServiceImpl) FindUserByEmail(email string) (*models.DBResponse, error) {
var user *models.DBResponse
query := bson.M{"email": strings.ToLower(email)}
err := us.collection.FindOne(us.ctx, query).Decode(&user)
if err != nil {
if err == mongo.ErrNoDocuments {
return &models.DBResponse{}, err
}
return nil, err
}
return user, nil
}
Create a utility function to sign and verify JWT tokens
User authentication can be done with different strategies but each strategy has a drawback or security flaws.
The concept of authentication can get complex so libraries like Passport provide different strategies to simplify the process.
In this tutorial, I used JSON Web Tokens to sign in and persist the user to enable them to request protected routes without having to log in on every request.
Create Json Web Token
JSON Web Tokens are used to implement stateless authentication but that alone is not enough since the only way the token can be invalidated is when it has expired.
You can read more about the flaws of JSON Web Tokens in this article.
Now let’s define a CreateToken()
function to generate either access or refresh tokens.
utils/token.go
func CreateToken(ttl time.Duration, payload interface{}, privateKey string) (string, error) {
decodedPrivateKey, err := base64.StdEncoding.DecodeString(privateKey)
if err != nil {
return "", fmt.Errorf("could not decode key: %w", err)
}
key, err := jwt.ParseRSAPrivateKeyFromPEM(decodedPrivateKey)
if err != nil {
return "", fmt.Errorf("create: parse key: %w", err)
}
now := time.Now().UTC()
claims := make(jwt.MapClaims)
claims["sub"] = payload
claims["exp"] = now.Add(ttl).Unix()
claims["iat"] = now.Unix()
claims["nbf"] = now.Unix()
token, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key)
if err != nil {
return "", fmt.Errorf("create: sign token: %w", err)
}
return token, nil
}
Here is what I did above:
- First I decoded the private key into an ASCII string
- Next, I parsed the decoded private key and defined the token claims.
- Lastly, I signed the token with the RS256 algorithm and returned it.
Verify JSON Web Token
Next, let’s define a ValidateToken()
function to verify the access or refresh token.
The ValidateToken()
returns the user’s id we stored in the token payload or an error if the token was manipulated or has expired.
utils/token.go
func ValidateToken(token string, publicKey string) (interface{}, error) {
decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKey)
if err != nil {
return nil, fmt.Errorf("could not decode: %w", err)
}
key, err := jwt.ParseRSAPublicKeyFromPEM(decodedPublicKey)
if err != nil {
return "", fmt.Errorf("validate: parse key: %w", err)
}
parsedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected method: %s", t.Header["alg"])
}
return key, nil
})
if err != nil {
return nil, fmt.Errorf("validate: %w", err)
}
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok || !parsedToken.Valid {
return nil, fmt.Errorf("validate: invalid token")
}
return claims["sub"], nil
}
Create the authentication controllers
Now let’s create a AuthController
struct to have access to the services we defined in the AuthService
and UserService
interface with the help of composition.
Signup user controller
controllers/auth.controller.go
type AuthController struct {
authService services.AuthService
userService services.UserService
}
func NewAuthController(authService services.AuthService, userService services.UserService) AuthController {
return AuthController{authService, userService}
}
func (ac *AuthController) SignUpUser(ctx *gin.Context) {
var user *models.SignUpInput
if err := ctx.ShouldBindJSON(&user); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
return
}
if user.Password != user.PasswordConfirm {
ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Passwords do not match"})
return
}
newUser, err := ac.authService.SignUpUser(user)
if err != nil {
if strings.Contains(err.Error(), "email already exist") {
ctx.JSON(http.StatusConflict, gin.H{"status": "error", "message": err.Error()})
return
}
ctx.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": err.Error()})
return
}
ctx.JSON(http.StatusCreated, gin.H{"status": "success", "data": gin.H{"user": models.FilteredResponse(newUser)}})
}
Details of what I did in the SignUpUser
method:
- I validated the user’s input against the
SignUpInput
struct and returned an error if any of the rules were not satisfied. - Next, I checked if the
Password
andPasswordConfirm
values are equal. - Then I called the
SignUpUser
service with theuser
pointer to add the new user to the database. - Lastly, I sent a JSON response to the user assuming there was no error. In an upcoming tutorial, we’ll send an email verification code to the user’s email.
Login user controller
Now let’s create a controller to sign in the registered user.
controllers/auth.controller.go
func (ac *AuthController) SignInUser(ctx *gin.Context) {
var credentials *models.SignInInput
if err := ctx.ShouldBindJSON(&credentials); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
return
}
user, err := ac.userService.FindUserByEmail(credentials.Email)
if err != nil {
if err == mongo.ErrNoDocuments {
ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Invalid email or password"})
return
}
ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
return
}
if err := utils.VerifyPassword(user.Password, credentials.Password); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Invalid email or Password"})
return
}
config, _ := config.LoadConfig(".")
// Generate Tokens
access_token, err := utils.CreateToken(config.AccessTokenExpiresIn, user.ID, config.AccessTokenPrivateKey)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
return
}
refresh_token, err := utils.CreateToken(config.RefreshTokenExpiresIn, user.ID, config.RefreshTokenPrivateKey)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
return
}
ctx.SetCookie("access_token", access_token, config.AccessTokenMaxAge*60, "/", "localhost", false, true)
ctx.SetCookie("refresh_token", refresh_token, config.RefreshTokenMaxAge*60, "/", "localhost", false, true)
ctx.SetCookie("logged_in", "true", config.AccessTokenMaxAge*60, "/", "localhost", false, false)
ctx.JSON(http.StatusOK, gin.H{"status": "success", "access_token": access_token})
}
In the SignInUser
method, I validated the user’s input against the SignInInput
struct and checked if that user exists in the database.
Next, I validated the plain password against the hashed password stored in the database and generated both the access and refresh tokens.
Finally, I sent the access and refresh tokens as cookies to the user’s client or browser.
Refresh access token controller
The access token can be refreshed after every 15 minutes as long as the user has a valid refresh token. This approach is not the best so later we’ll integrate Redis for an extra layer of security.
controllers/auth.controller.go
func (ac *AuthController) RefreshAccessToken(ctx *gin.Context) {
message := "could not refresh access token"
cookie, err := ctx.Cookie("refresh_token")
if err != nil {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"status": "fail", "message": message})
return
}
config, _ := config.LoadConfig(".")
sub, err := utils.ValidateToken(cookie, config.RefreshTokenPublicKey)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"status": "fail", "message": err.Error()})
return
}
user, err := ac.userService.FindUserById(fmt.Sprint(sub))
if err != nil {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"status": "fail", "message": "the user belonging to this token no logger exists"})
return
}
access_token, err := utils.CreateToken(config.AccessTokenExpiresIn, user.ID, config.AccessTokenPrivateKey)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"status": "fail", "message": err.Error()})
return
}
ctx.SetCookie("access_token", access_token, config.AccessTokenMaxAge*60, "/", "localhost", false, true)
ctx.SetCookie("logged_in", "true", config.AccessTokenMaxAge*60, "/", "localhost", false, false)
ctx.JSON(http.StatusOK, gin.H{"status": "success", "access_token": access_token})
}
Logout user controller
The logout controller is the simplest. The LogoutUser
controller sends expired cookies to the user’s browser or client to log them out.
controllers/auth.controller.go
func (ac *AuthController) LogoutUser(ctx *gin.Context) {
ctx.SetCookie("access_token", "", -1, "/", "localhost", false, true)
ctx.SetCookie("refresh_token", "", -1, "/", "localhost", false, true)
ctx.SetCookie("logged_in", "", -1, "/", "localhost", false, true)
ctx.JSON(http.StatusOK, gin.H{"status": "success"})
}
Authentication middleware
Let’s create a DeserializeUser
middleware to extract and validate the access token from either the Cookies object or Authorization header.
Next, check if the user still exists in the database with the user’s id we stored in the token payload and attach the returned user to the Gin context struct with a currentUser
key.
middleware/deserialize-user.go
func DeserializeUser(userService services.UserService) gin.HandlerFunc {
return func(ctx *gin.Context) {
var access_token string
cookie, err := ctx.Cookie("access_token")
authorizationHeader := ctx.Request.Header.Get("Authorization")
fields := strings.Fields(authorizationHeader)
if len(fields) != 0 && fields[0] == "Bearer" {
access_token = fields[1]
} else if err == nil {
access_token = cookie
}
if access_token == "" {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"status": "fail", "message": "You are not logged in"})
return
}
config, _ := config.LoadConfig(".")
sub, err := utils.ValidateToken(access_token, config.AccessTokenPublicKey)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"status": "fail", "message": err.Error()})
return
}
user, err := userService.FindUserById(fmt.Sprint(sub))
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"status": "fail", "message": "The user belonging to this token no logger exists"})
return
}
ctx.Set("currentUser", user)
ctx.Next()
}
}
Create the user controllers
GetMe Controller
Here I extracted the user object we stored in the Gin context struct using the MustGet
method and returned it as a JSON response.
controllers/user.controller.go
type UserController struct {
userService services.UserService
}
func NewUserController(userService services.UserService) UserController {
return UserController{userService}
}
func (uc *UserController) GetMe(ctx *gin.Context) {
currentUser := ctx.MustGet("currentUser").(*models.DBResponse)
ctx.JSON(http.StatusOK, gin.H{"status": "success", "data": gin.H{"user": models.FilteredResponse(currentUser)}})
}
Create routes
The routes are responsible for routing the request to the appropriate controller. You’ll be familiar with this syntax if you’ve worked with Express, Fastify, and FastAPI.
Auth Routes
routes/auth.routes.go
type AuthRouteController struct {
authController controllers.AuthController
}
func NewAuthRouteController(authController controllers.AuthController) AuthRouteController {
return AuthRouteController{authController}
}
func (rc *AuthRouteController) AuthRoute(rg *gin.RouterGroup, userService services.UserService) {
router := rg.Group("/auth")
router.POST("/register", rc.authController.SignUpUser)
router.POST("/login", rc.authController.SignInUser)
router.GET("/refresh", rc.authController.RefreshAccessToken)
router.GET("/logout", middleware.DeserializeUser(userService), rc.authController.LogoutUser)
}
User Routes
routes/user.routes.go
type UserRouteController struct {
userController controllers.UserController
}
func NewRouteUserController(userController controllers.UserController) UserRouteController {
return UserRouteController{userController}
}
func (uc *UserRouteController) UserRoute(rg *gin.RouterGroup, userService services.UserService) {
router := rg.Group("users")
router.Use(middleware.DeserializeUser(userService))
router.GET("/me", uc.userController.GetMe)
}
Update the main file
Run this command to install Redis v8 and CORS packages
go get github.com/go-redis/redis/v8 github.com/gin-contrib/cors
Create all the required variables. Import github.com/go-redis/redis/v8
to avoid getting errors.
main.go
var (
server *gin.Engine
ctx context.Context
mongoclient *mongo.Client
redisclient *redis.Client
userService services.UserService
UserController controllers.UserController
UserRouteController routes.UserRouteController
authCollection *mongo.Collection
authService services.AuthService
AuthController controllers.AuthController
AuthRouteController routes.AuthRouteController
)
Create an init function to:
- Connect to MongoDB database
- Connect to Redis database
- Instantiate the constructors of the controllers, routes, and services.
main.go
func init() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("Could not load environment variables", err)
}
ctx = context.TODO()
// Connect to MongoDB
mongoconn := options.Client().ApplyURI(config.DBUri)
mongoclient, err := mongo.Connect(ctx, mongoconn)
if err != nil {
panic(err)
}
if err := mongoclient.Ping(ctx, readpref.Primary()); err != nil {
panic(err)
}
fmt.Println("MongoDB successfully connected...")
// Connect to Redis
redisclient = redis.NewClient(&redis.Options{
Addr: config.RedisUri,
})
if _, err := redisclient.Ping(ctx).Result(); err != nil {
panic(err)
}
err = redisclient.Set(ctx, "test", "Welcome to Golang with Redis and MongoDB", 0).Err()
if err != nil {
panic(err)
}
fmt.Println("Redis client connected successfully...")
// Collections
authCollection = mongoclient.Database("golang_mongodb").Collection("users")
userService = services.NewUserServiceImpl(authCollection, ctx)
authService = services.NewAuthService(authCollection, ctx)
AuthController = controllers.NewAuthController(authService, userService)
AuthRouteController = routes.NewAuthRouteController(AuthController)
UserController = controllers.NewUserController(userService)
UserRouteController = routes.NewRouteUserController(UserController)
server = gin.Default()
}
Next, create a main function to start the server. Also, include the CORS configurations if you’ll be making requests from a different domain.
main.go
func main() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("Could not load config", err)
}
defer mongoclient.Disconnect(ctx)
value, err := redisclient.Get(ctx, "test").Result()
if err == redis.Nil {
fmt.Println("key: test does not exist")
} else if err != nil {
panic(err)
}
corsConfig := cors.DefaultConfig()
corsConfig.AllowOrigins = []string{"http://localhost:8000", "http://localhost:3000"}
corsConfig.AllowCredentials = true
server.Use(cors.New(corsConfig))
router := server.Group("/api")
router.GET("/healthchecker", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": value})
})
AuthRouteController.AuthRoute(router, userService)
UserRouteController.UserRoute(router, userService)
log.Fatal(server.Run(":" + config.Port))
}
Complete code snippets of the main.go file.
main.go
var (
server *gin.Engine
ctx context.Context
mongoclient *mongo.Client
redisclient *redis.Client
userService services.UserService
UserController controllers.UserController
UserRouteController routes.UserRouteController
authCollection *mongo.Collection
authService services.AuthService
AuthController controllers.AuthController
AuthRouteController routes.AuthRouteController
)
func init() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("Could not load environment variables", err)
}
ctx = context.TODO()
// Connect to MongoDB
mongoconn := options.Client().ApplyURI(config.DBUri)
mongoclient, err := mongo.Connect(ctx, mongoconn)
if err != nil {
panic(err)
}
if err := mongoclient.Ping(ctx, readpref.Primary()); err != nil {
panic(err)
}
fmt.Println("MongoDB successfully connected...")
// Connect to Redis
redisclient = redis.NewClient(&redis.Options{
Addr: config.RedisUri,
})
if _, err := redisclient.Ping(ctx).Result(); err != nil {
panic(err)
}
err = redisclient.Set(ctx, "test", "Welcome to Golang with Redis and MongoDB", 0).Err()
if err != nil {
panic(err)
}
fmt.Println("Redis client connected successfully...")
// Collections
authCollection = mongoclient.Database("golang_mongodb").Collection("users")
userService = services.NewUserServiceImpl(authCollection, ctx)
authService = services.NewAuthService(authCollection, ctx)
AuthController = controllers.NewAuthController(authService, userService)
AuthRouteController = routes.NewAuthRouteController(AuthController)
UserController = controllers.NewUserController(userService)
UserRouteController = routes.NewRouteUserController(UserController)
server = gin.Default()
}
func main() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("Could not load config", err)
}
defer mongoclient.Disconnect(ctx)
value, err := redisclient.Get(ctx, "test").Result()
if err == redis.Nil {
fmt.Println("key: test does not exist")
} else if err != nil {
panic(err)
}
corsConfig := cors.DefaultConfig()
corsConfig.AllowOrigins = []string{"http://localhost:8000", "http://localhost:3000"}
corsConfig.AllowCredentials = true
server.Use(cors.New(corsConfig))
router := server.Group("/api")
router.GET("/healthchecker", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": value})
})
AuthRouteController.AuthRoute(router, userService)
UserRouteController.UserRoute(router, userService)
log.Fatal(server.Run(":" + config.Port))
}
Conclusion
Congrats for reaching the end. In this article, you learned how to implement JWT Authentication and Authorization with Golang, MongoDB database, and Docker-compose.
Golang and Gin Gonic API Source Code
You can find the complete source code on GitHub
Great article… Very explanatory.
The Refresh token and Get me seems not to be working.
Since the Golang API depends on the cookies sent to the client after logging into the API, the Refresh token and Get me routes will block your requests if the cookies are not included.
These are the possible solutions:
withCredentials: true
in the configuration optioncredentials: 'include'
in the configuration optionexample.env
file and rename it toapp.env
before making the requests to the APIWorking perfectly. Thanks for the clarification
great stuff as always
i’m following this tutorial for the second time, it worked just fine the first time but now when i try to login i get this error “Invalid Key: Key must be a PEM encoded PKCS1 or PKCS8 key”. Please is there any workaround for this ?
After generating the private and public key pairs, did you encode them in base64 format before adding them to the
.env
file?yes, i followed the procedure correctly, just tried again and still getting the same error