Quickstart on Authentication and Authorization with JWT, Golang and MongoDB
Introduction
The most modern system uses REST API architecture to communicate between systems over the internet. REST API has some key principles that must be adhered to, and these principles pose significant threats to our system. Thus, making it vulnerable for attackers to hijack data during exchange over the internet.
Security and access control control has always been a major concern in system and security architecture. We'll dive into the implementation of Authentication and Role-Based access control (RBAC) using Go, JWT and MongoDB for persistence. JWT is a security tool we use to secure our applications that are sent over the internet.
REST API is Stateless
The server does not store the client's state between requests. Each request from a client to a server must contain all the information needed to understand and fulfil the request. This stateless approach of RESTful APIs is widely used in web browsers, mobile applications, and other servers. They provide a scalable and flexible way for different software systems to communicate with each other over the Internet.
Understanding How JSON Web Token(JWT) Handles Stateless Requests
JSON Web Tokens (JWT) is a compact means of representing claims between two parties. It is often used for authentication and information exchange. JWTs are commonly employed for securing communication between the client and the server.
Authentication & Authorization
The client initiates a sign-in request to the server for authorization to protected resources. The server issues unique and ephemeral Base64-encoded values of the Header.Payload.Signature
Header: The header is a JSON object consisting of
alg(algorithm)
and thetyp(type)
of token used.Payload: The payload contains claims or user's data, roles and also
iat(issued date)
,exp(expiring date)
,sub(subject)
and other user details such as ID, email and other detailsSignature: The signature verifies a token's integrity and authenticity is not altered during exchange. Thus, if the signature is valid, the server proceeds to decode the claims within the token.
In conclusion, it's important to note that while the header is visible to anyone with the JWT, the signature is not, as it requires a secret key. The server uses the header to determine how to verify the signature, and the payload contains the actual user's data that the token represents. These create a self-contained and secure token for performing authorization within stateless Restful APIs.
Jump to Git Repo
Creating our Authentication & Authorization Project with Golang
1. Initialize the Project
The commands below are to create directories required by the project and also initialise a new Go project.
mkdir go-jwt-auth && cd go-jwt-auth
mkdir controllers
mkdir internal
mkdir middleware
mkdir models
mkdir repositories
mkdir route
mkdir server
mkdir services
go mod init go-jwt-auth
1.1. Install MongoDB Server
It's mandatory to have MongoDB installed on your local machine or as a remotely accessible server. Use the official MongoDB website to find the installation guide. For the tutorial, I'd recommend the Community Edition.
1.2. Download Required Dependencies
Please ensure pwd
is go-jwt-auth
to download the dependencies.
go get go.mongodb.org/mongo-driver
go get github.com/gorilla/mux
go get github.com/golang-jwt/jwt/v5
go get github.com/joho/godotenv
go get github.com/go-playground/validator/v10
go get github.com/sirupsen/logrus
go get golang.org/x/crypto
The purpose of the dependencies above is as follows:
mongo-driver
Official Go library for MongoDB drivergorilla/mux
A rich, friendly and high-performance HTTP web framework for Go programming.golang-jwt/jwt
Go library for encoding and decoding JWT claimsgodotenv
Go library for environment variables used within the scope of our applicationsirupsen/logrus
Logging library for optimal visibilitycrypto
A Go library for encrypting password hashing function
1.3. Defining the Packages and Directories
Organising our directories likewise organising our server with packages is essential to helping us with better code management. The directories are as follows:
controllers
- This directory has functions that will handle the incoming requests based on the HTTP URI, Method, and Authorization, interact with the models and serve responseroutes
- Within this directory lies aroutes.go
file, responsible for invoking all the public functions defined in thecontrollers
directory. Invocation of this file must be explicitly doneinternal
- This encompasses all external resource handlers, including database connections, and if needed, distributed caching system, message broker connections and othersmiddleware
- This folder houses middleware functions that intercept HTTP requests for purposes such as authentication, logging, or any other required actionsmodels
- This directory is designated for storing the data models relevant to all aspects of the application's business logicrepositories
- This manages the Create-Read-Update-Delete (CRUD) actions for the models in the database. It acts as a middleman among thecontrollers
,services
, andmodels
, ensuring that we refrain from directly accessing the database within the controller or service functionsservices
- The services manage all the application's business logic, interacting with therepositories
and theinternal
database connection, and furnishing responses to the HTTPcontrollers
.server
- Initiate the HTTP server connection and port allocation
1.4. Create.env
File and Add Environment Variables
The env
variables are mandatory to ensure the application starts appropriately. However, BASE_URI_PREFIX
is optional as it is only required if you want a URL versioning within your application
BASE_URI_PREFIX=/v1/api
HTTP_PORT=9000
MONGO_DB_URI=mongodb://localhost:27017
MONGO_DB_NAME=sample_app
2. Building the Project
After initializing our Go project with its required libraries, packages and environment variables. We can get started with building the project and in the end, get it running and run tests with Postman
2.1. Defining Models
To keep things simple, we've created types.go
a which defines all our business logic struct. The file types.go
is defined as follows:
package models
import (
"go.mongodb.org/mongo-driver/bson/primitive"
"time"
)
type (
BaseModel struct {
ID primitive.ObjectID `json:"id"` //ID Generated by Mongo driver
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
DeletedAt time.Time `bson:"deleted_at" json:"deleted_at,omitempty"`
}
Address struct {
Address string
}
User struct {
BaseModel `bson:"-,inline"`
FirstName string `bson:"first_name,omitempty" json:"first_name" validate:"required"`
LastName string `bson:"last_name,omitempty" json:"last_name" validate:"required"`
Email string `bson:"email,omitempty" json:"email,omitempty" validate:"required,email"`
Phone string `bson:"phone,omitempty" json:"phone,omitempty" validate:"required"`
Password string `bson:"password" json:"-"`
PasswordRequestBody string `bson:"-" json:"password,omitempty"`
DateOfBirth time.Time `bson:"date_of_birth,omitempty" json:"date_of_birth,omitempty"`
Roles []string `bson:"roles,omitempty" json:"roles"`
Address Address `bson:"address,inline,omitempty" json:"address,omitempty"`
}
Token struct {
BaseModel `bson:"-,inline"`
AccessToken string `bson:"access_token" json:"access_token"`
RefreshToken string `bson:"refresh_token,omitempty" json:"-"` //Refresh token shouldn't be viewed on the client-side
}
)
func NewBaseModel() BaseModel {
return BaseModel{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
type EnvVar struct {
HttpPort,
MongoDbUri,
MongoDbName,
BaseUrlPrefix,
JwtSecret,
Value string
}
func LoadEnvironmentVariables() EnvVar {
return EnvVar{
HttpPort: os.Getenv("HTTP_PORT"),
MongoDbUri: os.Getenv("MONGO_DB_URI"),
MongoDbName: os.Getenv("MONGO_DB_NAME"),
BaseUrlPrefix: os.Getenv("BASE_URI_PREFIX"),
}
}
2.2. Initializing Database Connection in Internals
It's a good practice to keep a single instance of database connection within the application. As such, this's invoked in a singleton manner within the main.go
file in our application. The file mongodb.go
is defined as follows:
package internal
import (
"context"
"errors"
"fmt"
"github.com/joho/godotenv"
log "github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"quickstart-go-jwt-mongodb/models"
"time"
)
type (
MongoClient struct {
client *mongo.Client
Database *mongo.Database
ctx context.Context
timeOut time.Duration
}
// MongoDatabase Implicitly import mongo.Client functions
MongoDatabase interface {
Client() *mongo.Client
CreateCollection(ctx context.Context, name string, opts ...*options.CreateCollectionOptions) error
Collection(name string, opts ...*options.CollectionOptions) *mongo.Collection
RunCommand(ctx context.Context, runCommand interface{}, opts ...*options.RunCmdOptions) *mongo.SingleResult
}
)
const (
maxRetryCount = 5
appName = "go-jwt-mongo"
)
var (
currentRetryCount = 1
)
// NewMongoDbConn Singleton instance
// Reuse this client. You can use this same client instance to perform multiple tasks, instead of creating a new one each time.
// The client type is safe for concurrent use by multiple goroutines
func NewMongoDbConn(envVar models.EnvVar) *MongoClient {
var (
err error
mongoUri = envVar.MongoDbUri
timeout = 6 * time.Second
client *mongo.Client
ctx, cancel = context.WithTimeout(context.Background(), timeout)
)
defer func() {
cancel()
}()
if err = godotenv.Load(); err != nil {
log.Warn("No .env file found")
}
if mongoUri == "" {
err = errors.New("no `MONGO_DB_URI` found in the environment variable. Please export your connection string to var MONGO_DB_URI to connect to the db")
//panic(err)
}
mongoOption := options.Client()
mongoOption.SetAppName(appName)
mongoOption.ApplyURI(mongoUri)
client, err = mongo.Connect(ctx, mongoOption)
err = client.Ping(ctx, nil)
if err != nil && currentRetryCount <= maxRetryCount {
log.Errorf("[MongoDB] connect attempt failed. ActiveRequest retry %d of %d.\n%v", currentRetryCount, maxRetryCount, err)
currentRetryCount++
NewMongoDbConn(envVar)
}
if currentRetryCount >= maxRetryCount {
//Throw panic if retries exceeded without any successful connection
panic(fmt.Sprintf("Connection retries exceeded limit"))
}
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
panic(fmt.Sprintf("Unable to connect to MongoDB Server with the context timeout %v", timeout))
}
log.Debugf("successfully connected to MongoDB %s", mongoUri)
var database = client.Database(envVar.MongoDbName)
return &MongoClient{
client: client,
Database: database,
ctx: ctx,
timeOut: timeout,
}
}
func (c *MongoClient) CloseClient() {
defer func() {
if r := recover(); r != nil {
log.Warn("No active MongoDB connection to close.")
}
}()
_ = c.client.Disconnect(c.ctx)
}
2.3. Repositories for CRUD Operations in MongoDB
package repositories
import (
"context"
"errors"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"quickstart-go-jwt-mongodb/internal"
)
type Filter struct {
Key string
Value interface{}
}
type CrudOperation interface {
CreateOne(context context.Context, model interface{}) (primitive.ObjectID, error)
CreateMany(context context.Context, model []interface{}) ([]primitive.ObjectID, error)
FindOne(context context.Context, model interface{}, filters ...Filter) bool
FindAll(context context.Context, results interface{}, filters ...Filter) error
FindPaginate(context context.Context, currentPage, perPage int, results interface{}, filters ...Filter) error
}
func filterToBsonFilter(filters ...Filter) bson.D {
f := bson.D{}
for i := range filters {
f = append(f, bson.E{Key: filters[i].Key, Value: filters[i].Value})
}
return f
}
type userRepo struct {
mongoDb internal.MongoDatabase
collection string
timeout context.Context
cancelFun context.CancelFunc
}
func (u *userRepo) FindPaginate(context context.Context, currentPage, perPage int, results interface{}, filters ...Filter) error {
//TODO implement me
panic("implement me")
}
func (u *userRepo) FindAll(context context.Context, results interface{}, filters ...Filter) error {
find, err := u.mongoDb.Collection(u.collection).Find(context, filterToBsonFilter(filters...))
if err != nil {
return err
}
return find.All(context, results)
}
func (u *userRepo) FindOne(context context.Context, model interface{}, filters ...Filter) bool {
singleResult := u.mongoDb.Collection(u.collection).FindOne(context, filterToBsonFilter(filters...))
err := singleResult.Decode(model)
if err != nil {
return false
}
return !errors.Is(singleResult.Err(), mongo.ErrNoDocuments)
}
func (u *userRepo) CreateOne(context context.Context, model interface{}) (primitive.ObjectID, error) {
id, err := u.mongoDb.Collection(u.collection).InsertOne(context, model)
if err != nil {
return primitive.NilObjectID, err
}
return id.InsertedID.(primitive.ObjectID), nil
}
func (u *userRepo) CreateMany(context context.Context, model []interface{}) ([]primitive.ObjectID, error) {
return nil, nil
}
func NewUserRepository(mongoDb internal.MongoDatabase) CrudOperation {
return &userRepo{
mongoDb: mongoDb,
collection: "users",
}
}
2.4. JWT Services Generates and Claims a JWT Token
The jwt_service.go
handle generating ephemeral JWT tokens upon login request. This token shall therefore be passed to the HTTP header of every request.
package services
import (
"context"
"encoding/json"
"errors"
"fmt"
"quickstart-go-jwt-mongodb/models"
"time"
"github.com/golang-jwt/jwt/v5"
log "github.com/sirupsen/logrus"
)
type jwtService struct {
ctx context.Context
jwtSigningKey string
}
type JwtClaim struct {
ExpiringAt time.Duration
Subject string
Payload []byte //JSON bytes
ExtraClaims map[string]string
}
var (
JwtSigningAlg = jwt.SigningMethodHS256
)
type JwtService interface {
GenerateJWT(jwtClaim JwtClaim) (string, error)
ClaimToken(tokenizedString string, obj interface{}) (jwt.Claims, error)
}
func NewJwtService(ctx context.Context, envVar models.EnvVar) JwtService {
return &jwtService{
ctx: ctx,
jwtSigningKey: envVar.JwtSecret,
}
}
// GenerateJWT to generate a JWT Payload
// data must not be a JSON string. This would be done by the GenerateJWT method
func (j *jwtService) GenerateJWT(jwtClaim JwtClaim) (string, error) {
token := jwt.New(JwtSigningAlg)
claims := token.Claims.(jwt.MapClaims)
if jwtClaim.Subject != "" {
claims["sub"] = jwtClaim.Subject
}
if len(jwtClaim.Payload) > 0 {
claims["obj"] = string(jwtClaim.Payload)
}
claims["exp"] = jwt.NewNumericDate(time.Now().Add(jwtClaim.ExpiringAt))
claims["iat"] = jwt.NewNumericDate(time.Now())
for k, v := range jwtClaim.ExtraClaims {
claims[k] = v
}
tokenStr, err := token.SignedString([]byte(j.jwtSigningKey))
if err != nil {
log.Error("error creating jwt token", err)
return "", errors.New("error creating jwt token")
}
return tokenStr, nil
}
func (j *jwtService) ClaimToken(tokenizedString string, obj interface{}) (jwt.Claims, error) {
jwtToken, err := jwt.Parse(tokenizedString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("there was an error in parsing")
}
return j.jwtSigningKey, nil
})
claims, ok := jwtToken.Claims.(jwt.MapClaims)
objClaim, isObj := claims["obj"]
if isObj {
err = json.Unmarshal([]byte(objClaim.(string)), obj)
}
if ok && jwtToken.Valid {
return claims, nil
}
return claims, err
}
2.5. Connect the dots Between the Controllers and Route Function
For this example, we've created a few examples of controllers to handle signup, authentication, refresh token and attempts to access secured URLs likewise non-secured URLs and static pages.
Signup and Authentication Controller
package controllers import ( "context" "encoding/json" "errors" "fmt" log "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" "net/http" "quickstart-go-jwt-mongodb/internal" "quickstart-go-jwt-mongodb/models" "quickstart-go-jwt-mongodb/repositories" "quickstart-go-jwt-mongodb/server" "quickstart-go-jwt-mongodb/services" "sync" "time" ) type auth struct { Username string `json:"username" validate:"required,email"` Password string `json:"password"` } func CreateAccount(database internal.MongoDatabase, ctx context.Context) server.Controller { return server.Controller{ Uri: "/account/create", Method: server.POST, Secure: false, Callback: func(w http.ResponseWriter, req *http.Request) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() var user = models.User{ BaseModel: models.NewBaseModel(), } err := server.ParseReqToJson(req, &user) if err != nil { server.HttpError(w, err) return } userRepository := repositories.NewUserRepository(database) if userRepository.FindOne(ctx, &user, repositories.Filter{Key: "email", Value: user.Email}) { server.HttpError(w, errors.New(fmt.Sprintf("%s already exists", user.Email))) return } password, err := generateHashPassword(user.PasswordRequestBody) if err != nil { server.HttpError(w, errors.New("unable to hash password. Please try again later")) return } user.Password = password objectId, err := userRepository.CreateOne(ctx, &user) if err != nil { server.HttpError(w, err) return } user.ID = objectId user.PasswordRequestBody = "" server.HttpResponse(w, http.StatusCreated, user) return }, } } func Authenticate(database internal.MongoDatabase, ctx context.Context) server.Controller { return server.Controller{ Uri: "/account/auth", Method: server.POST, Secure: false, Callback: func(w http.ResponseWriter, req *http.Request) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() var user models.User var auth auth err := server.ParseReqToJson(req, &auth) if err != nil { server.HttpError(w, err) return } userRepository := repositories.NewUserRepository(database) tokenRepository := repositories.NewTokenRepository(database) findOne := userRepository.FindOne(ctx, &user, repositories.Filter{Key: "email", Value: auth.Username}) if !findOne { server.HttpError(w, errors.New(fmt.Sprintf("Invalid Username. %s does not exists", auth.Username))) return } if !checkPasswordHash(auth.Password, user.Password) { server.HttpError(w, errors.New("invalid Credential supplied. Please check username/password")) return } jwtService := services.NewJwtService(ctx, models.LoadEnvironmentVariables()) var accessTokenStr string var refreshTokenStr string wg := sync.WaitGroup{} wg.Add(2) go func() { defer wg.Done() bytesUser, _ := json.Marshal(user) accessTokenStr, err = jwtService.GenerateJWT(services.JwtClaim{ ExpiringAt: 2 * time.Hour, Subject: user.Email, Payload: bytesUser, ExtraClaims: map[string]string{ "iss": req.Host, }, }) }() go func() { defer wg.Done() refreshTokenStr, err = jwtService.GenerateJWT(services.JwtClaim{ ExpiringAt: 24 * time.Hour, Subject: user.Email, ExtraClaims: map[string]string{ "iss": req.Host, }, }) }() wg.Wait() token := models.Token{ BaseModel: models.NewBaseModel(), AccessToken: accessTokenStr, RefreshToken: refreshTokenStr, } http.SetCookie(w, &http.Cookie{ Name: "jwt", Value: refreshTokenStr, Expires: time.Now().Add(24 * time.Hour), MaxAge: 60 * 60 * 24, Secure: true, HttpOnly: true, SameSite: http.SameSiteNoneMode, }) tokenId, err := tokenRepository.CreateOne(ctx, token) if err != nil { log.Error("Unable to persist token generated", err) } token.ID = tokenId if err != nil { server.HttpError(w, errors.New("unable to generated token. Please try again later")) return } server.HttpResponse(w, http.StatusCreated, token) return }, } } func RefreshToken(database internal.MongoDatabase, ctx context.Context) server.Controller { return server.Controller{ Uri: "/account/refresh-token", Method: server.GET, Secure: false, Callback: func(w http.ResponseWriter, req *http.Request) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cookie, err := req.Cookie("jwt") if err != nil { server.AccessDenied(w, errors.New("'jwt' cookie do not exist in cookie-header")) return } jwtService := services.NewJwtService(ctx, models.LoadEnvironmentVariables()) jwt, err := jwtService.ClaimToken(cookie.Value, nil) if err != nil { server.AccessDenied(w, errors.New("invalid jwt token supplied")) return } expirationTime, _ := jwt.GetExpirationTime() if err != nil { return } if time.Now().After(expirationTime.Time) { server.AccessDenied(w, errors.New("unauthorized. Token expired")) return } userRepository := repositories.NewUserRepository(database) var user models.User subject, _ := jwt.GetSubject() userRepository.FindOne(ctx, &user, repositories.Filter{Key: "email", Value: subject}) //Create a one-time only access token again bytesUser, _ := json.Marshal(user) accessTokenStr, err := jwtService.GenerateJWT(services.JwtClaim{ ExpiringAt: 10 * time.Hour, Payload: bytesUser, ExtraClaims: map[string]string{ "iss": req.Host, }, }) token := models.Token{ BaseModel: models.NewBaseModel(), AccessToken: accessTokenStr, } tokenRepository := repositories.NewTokenRepository(database) id, err := tokenRepository.CreateOne(ctx, token) if err != nil { return } token.ID = id server.HttpResponse(w, http.StatusCreated, token) return }, } } // ListCustomers - Empty '[]PermitRoles' is wildcard access to all users. // By simply excluding the PermitRole filed from the Controller struct, it permits all secured users to // access the page func ListCustomers(database internal.MongoDatabase) server.Controller { return server.Controller{ Uri: "/user/customer-records", Method: server.GET, Secure: true, Callback: func(responseWriter http.ResponseWriter, req *http.Request) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() userRepository := repositories.NewUserRepository(database) var users []models.User err := userRepository.FindAll(ctx, &users) if err != nil { server.HttpError(responseWriter, err) return } server.HttpResponse(responseWriter, http.StatusOK, users) }, } } func checkPasswordHash(password string, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } func generateHashPassword(plainText string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(plainText), bcrypt.DefaultCost) return string(bytes), err }
Secured Page Controller
package controllers import ( "context" "net/http" "quickstart-go-jwt-mongodb/internal" "quickstart-go-jwt-mongodb/server" ) // Provide personal preferences to roles const ( Role1 = "SUPERVISOR" Role2 = "SALES_PERSON" ) func SecuredRole1Only(database internal.MongoDatabase, ctx context.Context) server.Controller { return server.Controller{ Uri: "/secured/role-1", Method: server.GET, Secure: true, PermitRoles: []string{Role1}, Callback: func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte("Access granted!")) }, } } func SecuredRole2Only(database internal.MongoDatabase, ctx context.Context) server.Controller { return server.Controller{ Uri: "/secured/role-2", Method: server.GET, Secure: true, PermitRoles: []string{Role2}, Callback: func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte("Access granted!")) }, } } func SecuredRole1And2Only(database internal.MongoDatabase, ctx context.Context) server.Controller { return server.Controller{ Uri: "/secured/role-1-and-2", Method: server.GET, Secure: true, PermitRoles: []string{Role1, Role2}, Callback: func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte("Access granted!")) }, } }
Static/Non-Secured Page Controler
func Homepage(ctx context.Context) server.Controller { return server.Controller{ Uri: "/", Secure: false, Method: server.GET, Callback: func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte("Hello!")) return }, } } func HealthCheck(ctx context.Context) server.Controller { return server.Controller{ Uri: "/status", Secure: false, Method: server.GET, Callback: func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte("Good!")) return }, } }
Using
routes.go
to connect the functions incontrollers
package andserver/server.go
which enables the requests to be within the*mux.Router.HandleFunc
package route import ( "context" "quickstart-go-jwt-mongodb/controllers" "quickstart-go-jwt-mongodb/internal" "quickstart-go-jwt-mongodb/server" ) func Routes(httpHandler server.RequestHandler, database internal.MongoDatabase, ctx context.Context) { httpHandler.ControllerRegistry(controllers.Homepage(ctx)) httpHandler.ControllerRegistry(controllers.HealthCheck(ctx)) httpHandler.ControllerRegistry(controllers.Authenticate(database, ctx)) httpHandler.ControllerRegistry(controllers.RefreshToken(database, ctx)) httpHandler.ControllerRegistry(controllers.CreateAccount(database, ctx)) httpHandler.ControllerRegistry(controllers.ListCustomers(database)) httpHandler.ControllerRegistry(controllers.ListCustomers(database)) httpHandler.ControllerRegistry(controllers.SecuredRole1Only(database, ctx)) httpHandler.ControllerRegistry(controllers.SecuredRole2Only(database, ctx)) httpHandler.ControllerRegistry(controllers.SecuredRole1And2Only(database, ctx)) }
2.6. Registering ourroutes.go
to Gorillamux.HandleFunc
with our server.go
We've meticulously segregated responsibilities within our application and constructed a route facade for our Gorilla Mux library. Therefore, we need to ensure our server responds to an HTTP URL within the port it serves.
package server
import (
"context"
"encoding/json"
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"net/http"
"quickstart-go-jwt-mongodb/models"
"time"
)
type (
Verb string
Request = http.Request
// Controller Declare how HTTP needs to be handled
Controller struct {
Uri string
Method Verb
Secure bool
PermitRoles []string
Callback func(w http.ResponseWriter, req *http.Request)
}
ResponseBody struct {
IsError bool `json:"is_error"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
Handler struct {
router *mux.Router
port string
httpTimeout context.Context
requestRegistry []Controller
}
RequestHandler interface {
HandleMiddlewares(middlewares ...mux.MiddlewareFunc)
ControllerRegistry(handler ...Controller)
GetControllers() []Controller
Serve()
}
)
func (appRouter *Handler) GetControllers() []Controller {
return appRouter.requestRegistry
}
const (
GET Verb = "GET"
POST Verb = "POST"
PUT Verb = "PUT"
DELETE Verb = "DELETE"
)
func NewHttpRequestHandler(httpTimeoutCtx context.Context, envVar models.EnvVar) RequestHandler {
return &Handler{
router: mux.NewRouter().PathPrefix(envVar.BaseUrlPrefix).Subrouter(),
port: envVar.HttpPort,
httpTimeout: httpTimeoutCtx,
requestRegistry: []Controller{},
}
}
func (appRouter *Handler) HandleMiddlewares(middlewares ...mux.MiddlewareFunc) {
appRouter.router.Use(middlewares...)
}
func (appRouter *Handler) ControllerRegistry(controllers ...Controller) {
for i := range controllers {
appRouter.requestRegistry = append(appRouter.requestRegistry, controllers[i])
}
}
func (appRouter *Handler) Serve() {
for _, request := range appRouter.requestRegistry {
appRouter.router.HandleFunc(request.Uri, request.Callback).Methods(string(request.Method), http.MethodOptions)
}
server := &http.Server{
Addr: fmt.Sprintf("%s:%s", "0.0.0.0", appRouter.port),
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
Handler: appRouter.router,
}
log.Infof("serving HTTP Request on port %s", appRouter.port)
err := server.ListenAndServe()
if err != nil {
log.Error(err)
return
}
}
func ParseReqToJson(req *Request, obj interface{}) error {
defer req.Body.Close()
err := json.NewDecoder(req.Body).Decode(&obj)
if err != nil {
return err
}
return validator.New().Struct(obj)
}
func HttpResponse(w http.ResponseWriter, statusCode int, obj interface{}) {
err := json.NewEncoder(w).Encode(ResponseBody{
IsError: false,
Message: "Request Completed",
Data: obj,
})
if err != nil {
log.Error("error sending server response")
}
w.WriteHeader(statusCode)
}
func HttpError(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusUnauthorized)
if json.NewEncoder(w).Encode(ResponseBody{
IsError: true,
Message: fmt.Sprintf("%s", err),
}) != nil {
log.Error("error sending server response")
}
w.WriteHeader(http.StatusBadRequest)
}
func AccessDenied(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusUnauthorized)
if json.NewEncoder(w).Encode(ResponseBody{
IsError: true,
Message: fmt.Sprintf("%s", err),
}) != nil {
log.Error("error sending server response")
}
}
Conclusion
Implementing authentication and authorization in Golang can be easily achieved. By adhering to the guidelines laid out in this article, you can establish a robust and effective authentication and authorization framework for your Golang application. Whether you're developing a basic web application or a sophisticated enterprise system, Golang furnishes the necessary resources to safeguard your users' data. Employing JWT tokens and cryptographic hashing methods, you can guarantee the dependability, scalability, and manageability of your authentication and authorization mechanism. Armed with the appropriate tools and expertise, you can transform your Golang application into a secure and reliable platform for your users.
GitHub Repository of the Project: https://github.com/xsyro/quickstart-go-jwt-mongodb