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
- Basic of
Go
- Basic of
MySQL
- Basic understanding of JWT Tokens
- Should have gone through the previous blog posts for basic understanding
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
);
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>
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
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)
}
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
}
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)
}
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))
}
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)
}
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"}
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
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)
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)