Introduction
Authorization and authentication are crucial components of any application that requires secure access to resources. In this article, we will discuss how to implement authentication and authorization in a Go web application.
Authentication is the process of verifying the identity of a user or system. It is the first step in granting access to a resource. Authorization, on the other hand, is the process of determining what actions a user or system is allowed to perform once they have been authenticated.
JSON Web Tokens (JWTs) are a popular way to implement authentication in web applications. JWTs are a compact, URL-safe means of representing claims to be transferred between two parties. They consist of three parts: a header, a payload, and a signature.
Go provides several libraries for implementing authentication and authorization. We will be using the gorilla/mux package for handling HTTP requests, and the golang.org/x/crypto/bcrypt package for hashing passwords.
Setting up the project
First, let's create a new Go module and install the necessary packages. Run the following commands in your terminal:
mkdir authApp
cd authApp
go mod init authApp
Then, install the following packages:
go get github.com/gorilla/mux
go get github.com/dgrijalva/jwt-go
go get github.com/mattn/go-sqlite3
go get golang.org/x/crypto/bcrypt
go get github.com/google/uuid
Importing Packages
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"strings"
"github.com/dgrijalva/jwt-go"
"github.com/google/uuid"
"github.com/gorilla/mux"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt"
)
I will give a brief description of the packages imported, We import the following packages:
context: provides a way to carry request-scoped values, such as the authenticated user ID.
database/sql : provides an interface to interact with the database
fmt : provides basic formatting functions,
log : provides a logging mechanism
net/http : provides the HTTP client and server implementations
strings : provides string manipulation functions, such as
strings.Replace()
github.com/gorilla/mux : provides a powerful router for HTTP requests.
github.com/mattn/go-sqlite3 : provides the SQLite3 driver for the database/sql package
github.com/google/uuid : Create a unique Id
golang.org/x/crypto/bcrypt : provides the hashing functions for passwords.
Creating Data Models
Let's start by creating a simple web application that requires authentication and authorization. We will create an API that allows users to create and retrieve articles. Only authenticated users will be able to create articles, and only the user who created an article will be able to retrieve it.
First, we will create a struct to represent our article
type Article struct {
ID string `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
AuthorID string `json:"author_id"`
}
Next, we will create a struct to represent our user:
type User struct {
ID string `json:"user_id"`
Username string `json:"username"`
Password string `json:"password"`
}
Creating Database
We will use bcrypt to hash the password before storing it in the database.
Now, let's create our database. We will be using SQLite for this example. We will create a file called database.db
in the root directory of our project.
This function (createDatabase) creates two tables in our database: users
and articles
.
func createDatabase() error {
db, err := sql.Open("sqlite3", "./database.db")
if err != nil {
return err
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT,
password TEXT
);
CREATE TABLE IF NOT EXISTS articles (
id TEXT PRIMARY KEY,
title TEXT,
body TEXT,
author_id TEXT,
FOREIGN KEY (author_id) REFERENCES users(id)
);
`)
return err
}
Creating the AuthMiddleware functions
Now, let's create our authentication middleware:
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}
tokenString = strings.Replace(tokenString, "Bearer ", "", 1)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return []byte("secret"), nil
})
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
if !token.Valid {
w.WriteHeader(http.StatusUnauthorized)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
userID, ok := claims["user_id"].(string)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "user_id", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Creating Utils Functions
Here we are going to create the functions upon which other functions shall depend.
createUser(): This function would create a user in our database
func createUser(user User) error {
db, err := sql.Open("sqlite3", "./database.db")
if err != nil {
return err
}
defer db.Close()
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
_, err = db.Exec("INSERT INTO users (id,username, password) VALUES (?,?, ?)", user.ID, user.Username, hashedPassword)
if err != nil {
return err
}
return nil
}
createDatabase() : This function would create Our database.db in the root folder of our project in which our articles and User would be stored
func createDatabase() error {
db, err := sql.Open("sqlite3", "./database.db")
if err != nil {
return err
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT,
password TEXT
);
CREATE TABLE IF NOT EXISTS articles (
id TEXT PRIMARY KEY,
title TEXT,
body TEXT,
author_id TEXT,
FOREIGN KEY (author_id) REFERENCES users(id)
);
`)
return err
}
getUserByUserName(): This Function would fetch User from our sqlite3 database
func getUserByUsername(username string) (User, error) {
db, _ := sql.Open("sqlite3", "./database.db")
var user User
row := db.QueryRow("SELECT id, username, password FROM users WHERE username = ?", username)
err := row.Scan(&user.ID, &user.Username, &user.Password)
if err != nil {
return User{}, err
}
return user, nil
}
createToken(): This function would create a jwt_token for a logged in user, and the token generated should be manually added to the header properties of your request to be able to access the authorize(Private) Routes.
func createToken(userID, username string) (string, error) {
claims := jwt.MapClaims{}
claims["user_id"] = userID
claims["username"] = username
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("secret"))
}
getUserIDFromContext(): This function get the user_id from the context for query purposes, there are different way to achieve this, i just find this handy for this article sake.
func getUserIDFromContext(ctx context.Context) (string, error) {
userID, ok := ctx.Value("user_id").(string)
if !ok {
return "", fmt.Errorf("user ID not found in context")
}
return userID, nil
}
createArticle(): This function creates an article in the database.
func createArticle(title, body, authorID string) (string, error) {
db, _ := sql.Open("sqlite3", "./database.db")
id := uuid.New().String()
_, err := db.Exec("INSERT INTO articles (id, title, body, author_id) VALUES (?, ?, ?, ?)", id, title, body, authorID)
if err != nil {
return "", err
}
return id, nil
}
getArticleByID(): This function fetches an article by it's ID.
func getArticleByID(id int64) (Article, error) {
db, _ := sql.Open("sqlite3", "./database.db")
var article Article
row := db.QueryRow("SELECT id, title, body, author_id FROM articles WHERE id = ?", id)
err := row.Scan(&article.ID, &article.Title, &article.Body, &article.AuthorID)
if err != nil {
return Article{}, err
}
return article, nil
}
Configuring Endpoint handlers
We have three endpoints which are /login,/articles and /articles/{id}, therefore their respective handlers are
func handleLogin(w http.ResponseWriter, r *http.Request) {
var loginReq User
if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
user, err := getUserByUsername(loginReq.Username)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "invalid username or password", http.StatusUnauthorized)
} else {
http.Error(w, "server error", http.StatusInternalServerError)
}
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginReq.Password)); err != nil {
http.Error(w, "invalid username or password", http.StatusUnauthorized)
return
}
token, err := createToken(user.ID, user.Username)
if err != nil {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": token,
})
}
func handleCreateArticle(w http.ResponseWriter, r *http.Request) {
var article Article
if err := json.NewDecoder(r.Body).Decode(&article); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
userID, err := getUserIDFromContext(r.Context())
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if _, err := createArticle(article.Title, article.Body, userID); err != nil {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
func handleGetArticle(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
articleID, err := strconv.ParseInt(vars["id"], 10, 64)
if err != nil {
http.Error(w, "invalid article ID", http.StatusBadRequest)
return
}
article, err := getArticleByID(articleID)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "article not found", http.StatusNotFound)
} else {
http.Error(w, "server error", http.StatusInternalServerError)
}
return
}
userID, err := getUserIDFromContext(r.Context())
if err != nil || article.AuthorID != userID {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(article)
}
Main Function : The entry point of out application is
func main() {
err := createDatabase()
if err != nil {
log.Fatal(err)
}
user := User{
ID: "user_1",
Username: "folajimi",
Password: "password123",
}
createUser(user)
router := mux.NewRouter()
// Create a new router without the auth middleware
router.HandleFunc("/login", handleLogin).Methods("POST")
//defining authenticated route
privateRouter := router.PathPrefix("/").Subrouter()
privateRouter.Use(authMiddleware)
// Register the article-related routes on the main router with the auth middleware
privateRouter.HandleFunc("/articles", handleCreateArticle).Methods("POST")
privateRouter.HandleFunc("/articles/{id}", handleGetArticle).Methods("GET")
fmt.Println("Server Listening on port 8080")
//start Server
http.ListenAndServe(":8080", router)
}
Putting it all together
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/google/uuid"
"github.com/gorilla/mux"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt"
)
type Article struct {
ID string `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
AuthorID string `json:"author_id"`
}
type User struct {
ID string `json:"user_id"`
Username string `json:"username"`
Password string `json:"password"`
}
func main() {
err := createDatabase()
if err != nil {
log.Fatal(err)
}
user := User{
ID: "user_1",
Username: "folajimi",
Password: "password123",
}
createUser(user)
router := mux.NewRouter()
// Create a new router without the auth middleware
router.HandleFunc("/login", handleLogin).Methods("POST")
//defining authenticated route
privateRouter := router.PathPrefix("/").Subrouter()
privateRouter.Use(authMiddleware)
// Register the article-related routes on the main router with the auth middleware
privateRouter.HandleFunc("/articles", handleCreateArticle).Methods("POST")
privateRouter.HandleFunc("/articles/{id}", handleGetArticle).Methods("GET")
fmt.Println("Server Listening on port 8080")
//start Server
http.ListenAndServe(":8080", router)
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}
tokenString = strings.Replace(tokenString, "Bearer ", "", 1)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return []byte("secret"), nil
})
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
if !token.Valid {
w.WriteHeader(http.StatusUnauthorized)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
userID, ok := claims["user_id"].(string)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "user_id", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func createUser(user User) error {
db, err := sql.Open("sqlite3", "./database.db")
if err != nil {
return err
}
defer db.Close()
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
_, err = db.Exec("INSERT INTO users (id,username, password) VALUES (?,?, ?)", user.ID, user.Username, hashedPassword)
if err != nil {
return err
}
return nil
}
func createDatabase() error {
db, err := sql.Open("sqlite3", "./database.db")
if err != nil {
return err
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT,
password TEXT
);
CREATE TABLE IF NOT EXISTS articles (
id TEXT PRIMARY KEY,
title TEXT,
body TEXT,
author_id TEXT,
FOREIGN KEY (author_id) REFERENCES users(id)
);
`)
return err
}
func getUserByUsername(username string) (User, error) {
db, _ := sql.Open("sqlite3", "./database.db")
var user User
row := db.QueryRow("SELECT id, username, password FROM users WHERE username = ?", username)
err := row.Scan(&user.ID, &user.Username, &user.Password)
if err != nil {
return User{}, err
}
return user, nil
}
func createToken(userID, username string) (string, error) {
claims := jwt.MapClaims{}
claims["user_id"] = userID
claims["username"] = username
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("secret"))
}
func getUserIDFromContext(ctx context.Context) (string, error) {
userID, ok := ctx.Value("user_id").(string)
if !ok {
return "", fmt.Errorf("user ID not found in context")
}
return userID, nil
}
func createArticle(title, body, authorID string) (string, error) {
db, _ := sql.Open("sqlite3", "./database.db")
id := uuid.New().String()
_, err := db.Exec("INSERT INTO articles (id, title, body, author_id) VALUES (?, ?, ?, ?)", id, title, body, authorID)
if err != nil {
return "", err
}
return id, nil
}
func getArticleByID(id int64) (Article, error) {
db, _ := sql.Open("sqlite3", "./database.db")
var article Article
row := db.QueryRow("SELECT id, title, body, author_id FROM articles WHERE id = ?", id)
err := row.Scan(&article.ID, &article.Title, &article.Body, &article.AuthorID)
if err != nil {
return Article{}, err
}
return article, nil
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
var loginReq User
if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
user, err := getUserByUsername(loginReq.Username)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "invalid username or password", http.StatusUnauthorized)
} else {
http.Error(w, "server error", http.StatusInternalServerError)
}
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginReq.Password)); err != nil {
http.Error(w, "invalid username or password", http.StatusUnauthorized)
return
}
token, err := createToken(user.ID, user.Username)
if err != nil {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": token,
})
}
func handleCreateArticle(w http.ResponseWriter, r *http.Request) {
var article Article
if err := json.NewDecoder(r.Body).Decode(&article); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
userID, err := getUserIDFromContext(r.Context())
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if _, err := createArticle(article.Title, article.Body, userID); err != nil {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
func handleGetArticle(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
articleID, err := strconv.ParseInt(vars["id"], 10, 64)
if err != nil {
http.Error(w, "invalid article ID", http.StatusBadRequest)
return
}
article, err := getArticleByID(articleID)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "article not found", http.StatusNotFound)
} else {
http.Error(w, "server error", http.StatusInternalServerError)
}
return
}
userID, err := getUserIDFromContext(r.Context())
if err != nil || article.AuthorID != userID {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(article)
}
Conclusion
This article is just a glimpse of how authentication and authorization is achieved with Jwt and Go. Feel free to ask any question , i would always answer . Thank you for reading