DEV Community

arcade
arcade

Posted on • Updated on

User Authentication in Go APIs: JWT, Password Hashing, and MySQL

In this blog post, you'll learn how to build user authentication in Go APIs. We'll create user, store there password securely using bcrypt in MySQL database and create protected routes by using JWT auth tokens using go-chi.

PreRequisites

Code

Full code of this blog can be found in my Github repo the-arcade-01/go-auth-api

So, the first step is to create the database with the User table.

For the sake of simplicity, we are only storing 3 fields for now.

Password entered by user will be hashed using bcrypt and will be stored in hash field.

CREATE DATABASE IF NOT EXISTS `Practice`;

CREATE TABLE IF NOT EXISTS `Practice`.`User` (
    `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `email` VARCHAR(255) NOT NULL,
    `hash` VARCHAR(255) NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

Now create an environment file, add your MySQl <user>, <password> and <database> into it.
.env

PORT=:5000
DATABASE_URL=<user>:<password>@tcp(localhost)/<database>?parseTime=true
DB_DRIVER=mysql
JWT_SECRET_KEY=<secret_key>
Enter fullscreen mode Exit fullscreen mode

Now, this been done we will install some external libraries that we will be using in this project

go get -u github.com/go-chi/chi/v5        // library for creating APIs in Go
go get golang.org/x/crypto/bcrypt         // for hashing password
go get github.com/joho/godotenv           // loading .env variables
go get github.com/go-sql-driver/mysql     // DB driver for MySQL
go get github.com/go-chi/jwtauth/v5       // JWT library
Enter fullscreen mode Exit fullscreen mode

main.go

package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "strconv"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
    "github.com/go-chi/jwtauth/v5"
    "github.com/joho/godotenv"
    "golang.org/x/crypto/bcrypt"

    _ "github.com/go-sql-driver/mysql"
)

var (
    DATABASE_URL, DB_DRIVER, JWT_SECRET_KEY, PORT string
)

/*
 *  Loads enviroment variables for .env
*/
func init() {
    err := godotenv.Load()
    if err != nil {
        log.Fatalln("Coudn't load env file!!")
    }

    DATABASE_URL = os.Getenv("DATABASE_URL")
    DB_DRIVER = os.Getenv("DB_DRIVER")
    PORT = os.Getenv("PORT")
    JWT_SECRET_KEY = os.Getenv("JWT_SECRET_KEY")
}

/*
 *  DB connection is created in this function
*/
func DBClient() (*sql.DB, error) {
    db, err := sql.Open(DB_DRIVER, DATABASE_URL)
    if err != nil {
        return nil, err
    }

    if err := db.Ping(); err != nil {
        return nil, err
    }
    fmt.Println("Connected to DB!!")
    return db, nil
}

/*
 *  This funtion returns the JWT auth structure
 *  We passed our own JWT_SECRET_KEY to generate it
*/
func GenerateAuthToken() *jwtauth.JWTAuth {
    tokenAuth := jwtauth.New("HS256", []byte(JWT_SECRET_KEY), nil)
    return tokenAuth
}

type Server struct {
    Router    *chi.Mux
    DB        *sql.DB
    AuthToken *jwtauth.JWTAuth
}

func CreateServer(db *sql.DB) *Server {
    server := &Server{
        Router:    chi.NewRouter(),
        DB:        db,
        AuthToken: GenerateAuthToken(),
    }
    return server
}

/*
 *  go-chi's middleware library provides us with different inbuilt functionality
 *  like Clean path, logger, cors etc. read here (https://go-chi.io/#/pages/middleware)
 * Here we're using Logger which will log inbound requests
*/
func (server *Server) MountMiddleware() {
    server.Router.Use(middleware.Logger)
}

/*
 * Base url: http:localhost:5000/user
*/
func (server *Server) MountHandlers() {
    server.Router.Route("/user", func(userRouter chi.Router) {

    /*  These endpoins will be accessible to user without auth
     *  POST /user/login    =   login user
     *  POST /user          =   create user
    */
        userRouter.Post("/login", server.LoginUser)
        userRouter.Post("/", server.CreateUser)

        userRouter.Group(func(r chi.Router) {
      /*  We mount the Verifier and Authenticator to our request
       *  All APIs inside this group will require JWT Token in Auth Headers
      */
            r.Use(jwtauth.Verifier(server.AuthToken))
            r.Use(jwtauth.Authenticator)

      /*  GET /user/{id}    =   get user with specific ID
      */
            r.Get("/{id}", server.GetUser)
        })
    })
}

func main() {
    db, err := DBClient()
    if err != nil {
        log.Fatal(err)
    }

    server := CreateServer(db)
    server.MountMiddleware()
    server.MountHandlers()
    fmt.Printf("server running on port%v\n", PORT)
    http.ListenAndServe(PORT, server.Router)
}
Enter fullscreen mode Exit fullscreen mode

Now, we'll create neccessary schemas that we'll use in this project.

type User struct {
    Id    int    `json:"id"`
    Email string `json:"email"`
    Hash  string `json:"hash"`
}

type UserRequestBody struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

type Response struct {
    Id int `json:"id"`
}

func ScanRow(rows *sql.Rows) (*User, error) {
    user := new(User)
    err := rows.Scan(&user.Id, &user.Email, &user.Hash)
    if err != nil {
        return nil, err
    }
    return user, nil
}
Enter fullscreen mode Exit fullscreen mode

Now, the actual implementation and the most important parts, the routes for creating user, login user and get users details.

Lets start with create user route first.

/*
 * We pass user's password into this function and
 * call bcrypts function to give us the hashed password
*/
func getHashPassword(password string) (string, error) {
    bytePassword := []byte(password)
    hash, err := bcrypt.GenerateFromPassword(bytePassword, bcrypt.DefaultCost)
    if err != nil {
        return "", err
    }
    return string(hash), nil
}

/*
 * We parse the user request body, takes the email, password
 * hashes the password and then runs insert query to insert in DB
 * on success returns the Id of the record
*/
func (server *Server) CreateUser(w http.ResponseWriter, r *http.Request) {
    userReqBody := new(UserRequestBody)
    if err := json.NewDecoder(r.Body).Decode(userReqBody); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("Please provide the correct input!!"))
        return
    }

    hashPassword, err := getHashPassword(userReqBody.Password)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte("Something bad happened on the server :("))
        return
    }

    query := `INSERT INTO User (email, hash) VALUES (?, ?)`
    result, err := server.DB.Exec(query, userReqBody.Email, hashPassword)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte("Something bad happened on the server :("))
        return
    }
    recordId, _ := result.LastInsertId()
    response := Response{
        Id: int(recordId),
    }

    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(response)
}
Enter fullscreen mode Exit fullscreen mode

Now, we'll write the Login user endpoint

/*
 * We compare the hashed password in DB with the password entered by user
*/
func checkPassword(hashPassword, password string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hashPassword), []byte(password))
    return err == nil
}

/*
 * We parse the request body, fetch user details based on email entered,
 * check the hashed password with user's entered password
 * generate claims and create token and return it in response
*/
func (server *Server) LoginUser(w http.ResponseWriter, r *http.Request) {
    userReqBody := new(UserRequestBody)
    if err := json.NewDecoder(r.Body).Decode(userReqBody); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("Please provide the correct input!!"))
        return
    }
    query := `SELECT * FROM User where email = ?`
    rows, err := server.DB.Query(query, userReqBody.Email)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("Please provide the correct input!!"))
        return
    }
    var user *User
    for rows.Next() {
        user, err = ScanRow(rows)
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            w.Write([]byte("Something bad happened on the server :("))
            return
        }
    }

    if !checkPassword(user.Hash, userReqBody.Password) {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("Incorrect password please check again"))
        return
    }

  /* After verify password, we want to generate user specific token
   * for that we mask few of the user details in the jwt token
   * these are called as claims and are used for verifying user w.r.t token
  */
    claims := map[string]interface{}{"id": user.Id, "email": user.Email}
    _, tokenString, err := server.AuthToken.Encode(claims)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte("Something bad happened on the server :("))
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte(tokenString))
}
Enter fullscreen mode Exit fullscreen mode

Now, lets write the final endpoint get user by his id

/* This GET /user/{id} will require the JWT token
 * generated from POST /user/login in auth headers
*/
func (server *Server) GetUser(w http.ResponseWriter, r *http.Request) {

  // We get the 'id' from URL parameters of the request
  id := chi.URLParam(r, "id")
    userId, err := strconv.Atoi(id)

  if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("Please provide the correct input!!"))
        return
    }

  /* After the Verifier and Authenticator have successful validated this request
   * We destructure the claims from the request and get the userId from claims
   * We then check whether the userId from claims is same as the userId for which
   * the request has been hit (from url params), if not that means user is using
   * different JWT token and hence unauthorized.
  */
    _, claims, _ := jwtauth.FromContext(r.Context())
    userIdFromClaims := int(claims["id"].(float64))

    if userId != userIdFromClaims {
        w.WriteHeader(http.StatusUnauthorized)
        w.Write([]byte("You're not authorized >("))
        return
    }

    query := `SELECT * FROM User WHERE id = ?`

    rows, err := server.DB.Query(query, userId)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("Please provide the correct input!!"))
        return
    }

    var user *User
    for rows.Next() {
        user, err = ScanRow(rows)
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            w.Write([]byte("Something bad happened on the server :("))
            return
        }
    }
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(user)
}
Enter fullscreen mode Exit fullscreen mode

Now, thats been done, we can finally hit our endpoints and see the response

  go-auth-api git:(main)  curl -X POST 'http://localhost:5000/user' -d '{"email": "test@test.com", "password": "test"}'

{"id":1}

  go-auth-api git:(main)  curl -X POST 'http://localhost:5000/user/login' -d '{"email": "test@test.com", "password": "test"}'

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpZCI6Mn0.M3MPy4rxJVDn_lxSnIXpfZavWC-gPr3325WnLT26NHw

  go-auth-api git:(main)  curl -X GET 'http://localhost:5000/user/2' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpZCI6Mn0.M3MPy4rxJVDn_lxSnIXpfZavWC-gPr3325WnLT26NHw'

{"id":1,"email":"test@test.com","hash":"$2a$10$P7.mUSvVrjx3N6mPEAD19.N9RoGEtGXNuz.XfGzeKmB9fsFqaZUbq"}
Enter fullscreen mode Exit fullscreen mode

Logs will look like this as we used go-chi middleware Logger

  go-auth-api git:(main)  make run
Connected to DB!!
server running on port:5000
2023/09/10 01:05:28 "POST http://localhost:5000/user HTTP/1.1" from 127.0.0.1:43244 - 200 9B in 146.277249ms
2023/09/10 01:06:55 "POST http://localhost:5000/user/login HTTP/1.1" from 127.0.0.1:49428 - 200 124B in 78.764985ms
2023/09/10 01:10:18 "GET http://localhost:5000/user/2 HTTP/1.1" from 127.0.0.1:42164 - 200 103B in 705.163µs
Enter fullscreen mode Exit fullscreen mode

I encourage you to create another user and check whether he is able to get different users details with his own token.

Same can be seen inside your MySQL db.

mysql> select * from User;
+----+---------------+--------------------------------------------------------------+
| id | email         | hash                                                         |
+----+---------------+--------------------------------------------------------------+
|  1 | test@test.com | $2a$10$P7.mUSvVrjx3N6mPEAD19.N9RoGEtGXNuz.XfGzeKmB9fsFqaZUbq |
+----+---------------+--------------------------------------------------------------+
1 row in set (0.00 sec)

Enter fullscreen mode Exit fullscreen mode

Conclusion

That's it, we covered a lot of stuff in this blog, I encourage you to write code own your own and explore different implementations as well.

Github: https://github.com/the-arcade-01/go-auth-api

Thanks for reading till the end, really appreciate it!!

Top comments (0)