Authentication and Authorization with Go Language with JWT.

Authentication and Authorization with Go Language with JWT.

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/dgrijalva/jwt-go : provides support for JWTs

  • 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