I just started learning to Golang recently, and I must say the Golang is not that difficult. To fully grasp the syntax as a web developer I opted to create an API.
In this blog, we are going to create an Authentication API with the help of the gin
web framework and use a MongoDB database with the help of mgo.v2
This blog will be a two-part series covering all authentication endpoints:
- The first part focuses on basic Signup and Login of a user.
- The second part will focus on using Account verification via email with the help of SendGrid, Token blacklisting, and Password reset request.
Introduction
Prerequisites
To be able to grasp the contents and syntax of this blog you should have used golang before and setup the directory for development.
Getting started
Goals
By the end of this article we should be able to:
- Use the API to sign up
- Use the API to log in
Setup
Create the folder to hold the Golang project inside the set $GOPATH
directory
$ mkdir golang_api_with_gin && cd $_
Our project directory setup will look something similar to this:
|-- db # hold files interacting with db connection
|-- controllers # holds files that handle controllers
|-- models # holds functions that interact with the database
|-- helpers # holds helper functions such as token generation
|-- forms # holds files with request body struct
|-- app.go # Will hold the app routes and server
Let's run go mod init
on the terminal to track the libraries we are using. This will create a go.mod
file.
We need to install the gin
framework library, godotenv
for tracking variables in our .env file and mgo.v2
to interact with mongo
$ go get github.com/gin-gonic/gin # the web framework
$ go get github.com/joho/godotenv # environment variables
$ go get gopkg.in/mgo.v2 # mongo driver
Write some code
Let's create a controller to return a JSON response with a message attribute of value "Hello world"
Go into the controller's folders and create hello.go file
$ cd controllers && touch hello.go
Let's write our hello world controller code
package controllers
import (
// Import the Gin library
"github.com/gin-gonic/gin"
)
// HelloWorldController will hold the methods to the
type HelloWorldController struct{}
// Default controller handles returning the hello world JSON response
func (h *HelloWorldController) Default(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello world, climate change is real"})
}
Let's add some code to our app.go
file
package main
import (
// Log items to the terminal
"log"
// Import gin for route definition
"github.com/gin-gonic/gin"
// Import godotenv for .env variables
"github.com/joho/godotenv"
// Import our app controllers
"github.com/tesh254/golang_todo_api/controllers"
)
// init gets called before the main function
func init() {
// Log error if .env file does not exist
if err := godotenv.Load(); err != nil {
log.Printf("No .env file found")
}
}
func main() {
// Init gin router
router := gin.Default()
// Its great to version your API's
v1 := router.Group("/api/v1")
{
// Define the hello controller
hello := new(controllers.HelloWorldController)
// Define a GET request to call the Default
// method in controllers/hello.go
v1.GET("/hello", hello.Default)
}
// Handle error response when a route is not defined
router.NoRoute(func(c *gin.Context) {
// In gin this is how you return a JSON response
c.JSON(404, gin.H{"message": "Not found"})
})
// Init our server
router.Run(":5000")
}
If you run your app
$ go run app.go
You should see something similar to this on your terminal
2020/02/07 20:17:24 No .env file found
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /api/v1/hello --> github.com/tesh254/golang_todo_api/controllers.(*HelloWorldController).Default-fm (3 handlers)
[GIN-debug] Listening and serving HTTP on :5000
Send a GET
request to our defined hello
endpoint.
$ curl http://localhost:5000/api/v1/hello
The response should be this
{"message": "Hello world, climate change is real"}
That's one of the so many ways to create an endpoint, its a bit long, you might just write your whole API in one file, but splitting your code into files makes your code:
- readable
- maintainable
Database connection
Next, we are going to handle database connection in the db
folder inside the db.go
file
// Define the package interacting with the database
package db
import (
"os"
"time"
"gopkg.in/mgo.v2"
)
// DBConnection defines the connection structure
type DBConnection struct {
session *mgo.Session
}
// NewConnection handles connecting to a mongo database
func NewConnection(host string, dbName string) (conn *DBConnection) {
info := &mgo.DialInfo{
// Address if its a local db then the value host=localhost
Addrs: []string{host},
// Timeout when a failure to connect to db
Timeout: 60 * time.Second,
// Database name
Database: dbName,
// Database credentials if your db is protected
Username: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PWD"),
}
session, err := mgo.DialWithInfo(info)
if err != nil {
panic(err)
}
session.SetMode(mgo.Monotonic, true)
conn = &DBConnection{session}
return conn
}
// Use handles connect to a certain collection
func (conn *DBConnection) Use(dbName, tableName string) (collection *mgo.Collection) {
// This returns method that interacts with a specific collection and table
return conn.session.DB(dbName).C(tableName)
}
// Close handles closing a database connection
func (conn *DBConnection) Close() {
// This closes the connection
conn.session.Close()
return
}
Create our Models
First of all, we will create a file in the models
folder containing methods to perform CRUD(Create Read Update Delete) interactions with the database.
We will create config.go
file to hold a couple of db global variables
$ cd models && touch config.go
Add this into the file
package models
import (
"os"
"github.com/tesh254/golang_todo_api/db"
)
// Mongo server ip -> localhost -> 127.0.0.1 -> 0.0.0.0
var server = os.Getenv("DATABASE")
// Database name
var databaseName = os.Getenv("DATABASE_NAME")
// Create a connection
var dbConnect = db.NewConnection(server, databaseName)
Since we are creating an Authentication API we will create a models/user.go
file.
Let's create the file
$ cd models && touch user.go
Now we add some lines of code into it.
package models
import (
"gopkg.in/mgo.v2/bson"
)
// User defines user object structure
type User struct {
ID bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
Name string `json:"name" bson:"name"`
Email string `json:"email" bson:"email"`
Password string `json:"password" bson:"password"`
IsVerified bool `json:"is_verified" bson:"is_verified"`
}
// UserModel defines the model structure
type UserModel struct{}
// Signup handles registering a user
func (u *UserModel) Signup(data forms.SignupUserCommand) error {
// Connect to the user collection
collection := dbConnect.Use(databaseName, "user")
// Assign result to error object while saving user
err := collection.Insert(bson.M{
"name": data.Name,
"email": data.Email,
"password": data.Password,
// This will come later when adding verification
"is_verified": false,
})
return err
}
You might have noticed the forms.SignupUserCommand
that defines the type of data retrieved from the controller. We we have to define the struct
with the types.
User Sign up
Let's go ahead and create a file to hold authentication body structs.
$ cd forms && touch user.go
Next we add the SignupUserCommand
struct
package forms
// SignupUserCommand defines user form struct
type SignupUserCommand struct {
// binding:"required" ensures that the field is provided
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
We will have to create the user controller to interact with the database via the API requests.
We will do so by creating a new file to hold User authentication controllers controllers/user.go
.
$ cd controllers && touch user.go
Let's add some code inside the file
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/tesh254/golang_todo_api/forms"
"github.com/tesh254/golang_todo_api/models"
)
// Import the userModel from the models
var userModel = new(models.UserModel)
// UserController defines the user controller methods
type UserController struct{}
// Signup controller handles registering a user
func (u *UserController) Signup(c *gin.Context) {
var data forms.SignupUserCommand
// Bind the data from the request body to the SignupUserCommand Struct
// Also check if all fields are provided
if c.BindJSON(&data) != nil {
// specified response
c.JSON(406, gin.H{"message": "Provide relevant fields"})
// abort the request
c.Abort()
// return nothing
return
}
/*
You can add your validation logic
here such as email
if regexMethodChecker(data.Email) {
c.JSON(400, gin.H{"message": "Email is invalid"})
c.Abort()
return
}
*/
err := userModel.Signup(data)
// Check if there was an error when saving user
if err != nil {
c.JSON(400, gin.H{"message": "Problem creating an account"})
c.Abort()
return
}
c.JSON(201, gin.H{"message": "New user account registered"})
}
Now that we have a signup controller let's tie it to an endpoint. Go back to app.go
and add a signup controller.
...
// Define the user controller
user := new(controllers.UserController)
// Create the signup endpoint
v1.POST("/signup", user.Signup)
Let's create a .env
file to hold our environment variables
$ touch .env
Add this to the file
export DATABASE=localhost
export DATABASE_NAME=golangtodoapi
If godotenv
library does not work with the .env file create a Makefile
to export your variables and run your app. This will make it easier to run your app instead of type two commands each time while you could with one.
run:
@echo ":::: App is startin up ::::"
@echo "CONFIG:: 😁 Exporting environemnt variables"
# This might vary depending on your unix os
# some might use source by default
/bin/sh .env
@echo "SUCCESS: ✔ Environment variables exported"
@echo "INIT:::: ⚡ Running server"
go run app.go
Before you run the server ensure you have a MongoDB instance running. Follow this link to install MongoDB community version on your computer based on your operating system.
Run your server
$ go run app.go
Let's try to add a new user
$ curl -d '{"name": "Erick Wachira", "email": "ewachira254@gmail.com", "password": "Wachira254"}' -H "Content-Type: application/json" -X POST http://localhost:5000/api/v1/signup
Your response should be something similar
{"message":"New user account registered"}
If you go and check the user's collection contents using a GUI/Mongo cli. The document we just saved has the password saved in plain text. Well, we both know that is not secure. We can fix that by using the bcrypt
library.
We need to first install it before we start coding.
$ go get golang.org/x/crypto/bcrypt
After installing the library we need to create a helpers package to house helping functions for our API, this will include our password hasher and compare.
$ cd helpers && touch bcrypt.go
Let us add a few lines to the file.
package helpers
// Allows us to hash and compare passwords
import "golang.org/x/crypto/bcrypt"
// GeneratePasswordHash handles generating password hash
// bcrypt hashes password of type byte
func GeneratePasswordHash(password []byte) string {
// default cost is 10
hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
// If there was an error panic
if err != nil {
panic(err)
}
// return stringified password
return string(hashedPassword)
}
// PasswordCompare handles password hash compare
func PasswordCompare(password []byte, hashedPassword []byte) error {
err := bcrypt.CompareHashAndPassword(hashedPassword, password)
return err
}
Let's update the user model function to save a hashed password.
...
err := collection.Insert(bson.M{
"name": data.Name,
"email": data.Email,
"password": helpers.GeneratePasswordHash([]byte(data.Password)),
// This will come later when adding verification
"is_verified": false,
})
...
Let's try that again, kill and then run your server, then send a signup request again.
Now with our Makefile defined your will running your app with this command
$ make run
Account will be created with a hashed password, you can confirm by checking the document saved.
Now we have another problem, an account is being created with the same this will cause conflicts if the app will ever be deployed to production. We will have to fix that.
We can do so by introducing a method that finds a user with an email. We will use the result to validate if the user exists or not. Let's jump into that.
Go to the models/user.go
file to add the method.
...
// GetUserByEmail handles fetching user by email
func (u *UserModel) GetUserByEmail(email string) (user User, err error) {
// Connect to the user collection
collection := dbConnect.Use(databaseName, "user")
// Assign result to error object while saving user
err = collection.Find(bson.M{"email": email}).One(&user)
return user, err
}
...
Let's modify the Signup controller to add a new condition
...
result, _ := userModel.GetUserByEmail(data.Email)
// If there happens to be a result respond with a
// descriptive mesage
if result.Email != "" {
c.JSON(403, gin.H{"message": "Email is already in use"})
c.Abort()
return
}
...
You user's controller file should look like this
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/tesh254/golang_todo_api/forms"
"github.com/tesh254/golang_todo_api/models"
)
// Import the userModel from the models
var userModel = new(models.UserModel)
// UserController defines the user controller methods
type UserController struct{}
// Signup controller handles registering a user
func (u *UserController) Signup(c *gin.Context) {
var data forms.SignupUserCommand
// Bind the data from the request body to the SignupUserCommand Struct
// Also check if all fields are provided
if c.BindJSON(&data) != nil {
// specified response
c.JSON(406, gin.H{"message": "Provide relevant fields"})
// abort the request
c.Abort()
// return nothing
return
}
/*
You can add your validation logic
here such as email
if regexMethodChecker(data.Email) {
c.JSON(400, gin.H{"message": "Email is invalid"})
c.Abort()
return
}
*/
result, _ := userModel.GetUserByEmail(data.Email)
// If there happens to be a result respond with a
// descriptive mesage
if result.Email != "" {
c.JSON(403, gin.H{"message": "Email is already in use"})
c.Abort()
return
}
err := userModel.Signup(data)
// Check if there was an error when saving user
if err != nil {
c.JSON(400, gin.H{"message": "Problem creating an account"})
c.Abort()
return
}
c.JSON(201, gin.H{"message": "New user account registered"})
}
Without deleting any document from the user's collection, try the request again.
You should get this response
{"message":"Email is already in use"}
User Login
We have gotten this far and have a solid user sign up endpoint we can now create a login endpoint. There are different ways to authenticate a user this include:
- Sessions
- JWT way
We are going to use the JWT(JSON Web Token) way. It has its advantages over sessions, these are:
No Session to Manage (stateless): The JWT is a self-contained token that has authentication information, expire time information, and other user-defined claims digitally signed.
Portable: A single token can be used with multiple backends.
No Cookies Required, So It's Very Mobile Friendly
Good Performance: It reduces the network round trip time.
Decoupled/Decentralized: The token can be generated anywhere. Authentication can happen on the resource server or easily separated into its own server.
To achieve JWT authentication we need to install a JWT library to generate, and verify our tokens.
$ go get github.com/dgrijalva/jwt-go
jwt-go
library is a great and simple library that will help us achieve this.
Let's create a file inside the services
folder with the jwt.go
file to hold our jwt methods.
package services
import (
"os"
"time"
"github.com/dgrijalva/jwt-go"
)
var jwtKey = []byte(os.Getenv("SECRET_KEY"))
// Claims defines jwt claims
type Claims struct {
UserID string `json:"email"`
jwt.StandardClaims
}
// GenerateToken handles generation of a jwt code
// @returns string -> token and error -> err
func GenerateToken(userID string) (string, error) {
// Define token expiration time
expirationTime := time.Now().Add(1440 * time.Minute)
// Define the payload and exp time
claims := &Claims{
UserID: userID,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}
// Generate token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Sign token with secret key encoding
tokenString, err := token.SignedString(jwtKey)
return tokenString, err
}
// DecodeToken handles decoding a jwt token
func DecodeToken(tkStr string) (string, error) {
claims := &Claims{}
tkn, err := jwt.ParseWithClaims(tkStr, claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
return "", err
}
return "", err
}
if !tkn.Valid {
return "", err
}
return claims.UserID, nil
}
We won't have to create a Login
user model method we can utilize the GetUserByEmail
, following the DRY(Don't Repeat Yourself) rule. We need to add a LoginUserCommand
to define the request body types. Go to forms/user.go
file and add these lines of code
...
// LoginUserCommand defines user login form struct
type LoginUserCommand struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
...
Your forms/user.go
file should look like the code below
package forms
// SignupUserCommand defines user form struct
type SignupUserCommand struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginUserCommand defines user login form struct
type LoginUserCommand struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
Next we create the Login
method controller.
...
// Login allows a user to login a user and get
// access token
func (u *UserController) Login(c *gin.Context) {
var data forms.LoginUserCommand
// Bind the request body data to var data and check if all details are provided
if c.BindJSON(&data) != nil {
c.JSON(406, gin.H{"message": "Provide required details"})
c.Abort()
return
}
result, err := userModel.GetUserByEmail(data.Email)
if result.Email == "" {
c.JSON(404, gin.H{"message": "User account was not found"})
c.Abort()
return
}
if err != nil {
c.JSON(400, gin.H{"message": "Problem logging into your account"})
c.Abort()
return
}
// Get the hashed password from the saved document
hashedPassword := []byte(result.Password)
// Get the password provided in the request.body
password := []byte(data.Password)
err = helpers.PasswordCompare(password, hashedPassword)
if err != nil {
c.JSON(403, gin.H{"message": "Invalid user credentials"})
c.Abort()
return
}
jwtToken, err2 := services.GenerateToken(data.Email)
// If we fail to generate token for access
if err2 != nil {
c.JSON(403, gin.H{"message": "There was a problem logging you in, try again later"})
c.Abort()
return
}
c.JSON(200, gin.H{"message": "Log in success", "token": jwtToken})
}
...
Your controllers/user.go
should look something similar to this
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/tesh254/golang_todo_api/forms"
"github.com/tesh254/golang_todo_api/helpers"
"github.com/tesh254/golang_todo_api/models"
"github.com/tesh254/golang_todo_api/services"
)
// Import the userModel from the models
var userModel = new(models.UserModel)
// UserController defines the user controller methods
type UserController struct{}
// Signup controller handles registering a user
func (u *UserController) Signup(c *gin.Context) {
var data forms.SignupUserCommand
// Bind the data from the request body to the SignupUserCommand Struct
// Also check if all fields are provided
if c.BindJSON(&data) != nil {
// specified response
c.JSON(406, gin.H{"message": "Provide relevant fields"})
// abort the request
c.Abort()
// return nothing
return
}
/*
You can add your validation logic
here such as email
if regexMethodChecker(data.Email) {
c.JSON(400, gin.H{"message": "Email is invalid"})
c.Abort()
return
}
*/
result, _ := userModel.GetUserByEmail(data.Email)
// If there happens to be a result respond with a
// descriptive mesage
if result.Email != "" {
c.JSON(403, gin.H{"message": "Email is already in use"})
c.Abort()
return
}
err := userModel.Signup(data)
// Check if there was an error when saving user
if err != nil {
c.JSON(400, gin.H{"message": "Problem creating an account"})
c.Abort()
return
}
c.JSON(201, gin.H{"message": "New user account registered"})
}
// Login allows a user to login a user and get
// access token
func (u *UserController) Login(c *gin.Context) {
var data forms.LoginUserCommand
// Bind the request body data to var data and check if all details are provided
if c.BindJSON(&data) != nil {
c.JSON(406, gin.H{"message": "Provide required details"})
c.Abort()
return
}
result, err := userModel.GetUserByEmail(data.Email)
if result.Email == "" {
c.JSON(404, gin.H{"message": "User account was not found"})
c.Abort()
return
}
if err != nil {
c.JSON(400, gin.H{"message": "Problem logging into your account"})
c.Abort()
return
}
// Get the hashed password from the saved document
hashedPassword := []byte(result.Password)
// Get the password provided in the request.body
password := []byte(data.Password)
err = helpers.PasswordCompare(password, hashedPassword)
if err != nil {
c.JSON(403, gin.H{"message": "Invalid user credentials"})
c.Abort()
return
}
jwtToken, err2 := services.GenerateToken(data.Email)
// If we fail to generate token for access
if err2 != nil {
c.JSON(403, gin.H{"message": "There was a problem logging you in, try again later"})
c.Abort()
return
}
c.JSON(200, gin.H{"message": "Log in success", "token": jwtToken})
}
Next, we will have to create a login endpoint to call the Login
method controller.
...
// Create the login endpoint
v1.POST("/login", user.Login)
...
We will have to add a new variable to the .env
file, the SECRET_KEY
that will be used to protect our JWT tokens.
...
export const SECRET_KEY=#53cR3Tk3y
...
Let's run our app and test out our endpoint
$ make run
curl -d '{"email": "ewachira254@gmail.com", "password": "Wachira254"}' -H "Content-Type: application/json" -X POST http://localhost:5000/api/v1/login
Your response should be something similar
{
"message":"Log in success","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImV3YWNoaXJhMjU0QGdtYWlsLmNvbSIsImV4cCI6MTU4MTIwNzE2Nn0.XXaDB0R9UJ7aKLQxyDDjIdj1WMNN_bP5Ez9um6_T-SU"
}
Extras
Next
In the next part of this series, we will be able to send out password reset and verification emails. We will also deploy the API to Heroku on the final part of the series. I just started learning to Golang recently, and I must say the Golang is not that difficult. To fully grasp the syntax as a web developer I opted to create an API.
In this blog, we are going to create an Authentication API with the help of the gin
web framework and use a MongoDB database with the help of mgo.v2
This blog will be a two-part series covering all authentication endpoints:
- The first part focuses on basic Signup and Login of a user.
- The second part will focus on using Account verification via email with the help of SendGrid, Token blacklisting, and Password reset request.
Introduction
Prerequisites
To be able to grasp the contents and syntax of this blog you should have used golang before and setup the directory for development.
Getting started
Goals
By the end of this article we should be able to:
- Use the API to sign up
- Use the API to log in
Setup
Create the folder to hold the Golang project inside the set $GOPATH
directory
$ mkdir golang_api_with_gin && cd $_
Our project directory setup will look something similar to this:
|-- db # hold files interacting with db connection
|-- controllers # holds files that handle controllers
|-- models # holds functions that interact with the database
|-- helpers # holds helper functions such as token generation
|-- forms # holds files with request body struct
|-- services
|-- app.go # Will hold the app routes and server
Let's run go mod init
on the terminal to track the libraries we are using. This will create a go.mod
file.
We need to install the gin
framework library, godotenv
for tracking variables in our .env file and mgo.v2
to interact with mongo
$ go get github.com/gin-gonic/gin # the web framework
$ go get github.com/joho/godotenv # environment variables
$ go get gopkg.in/mgo.v2 # mongo driver
Write some code
Let's create a controller to return a JSON response with a message attribute of value "Hello world"
Go into the controllers' folders and create hello.go file
$ cd controllers && touch hello.go
Let's write our hello world controller code
package controllers
import (
// Import the Gin library
"github.com/gin-gonic/gin"
)
// HelloWorldController will hold the methods to the
type HelloWorldController struct{}
// Default controller handles returning the hello world JSON response
func (h *HelloWorldController) Default(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello world, climate change is real"})
}
Let's add some code to our app.go
file
package main
import (
// Log items to the terminal
"log"
// Import gin for route definition
"github.com/gin-gonic/gin"
// Import godotenv for .env variables
"github.com/joho/godotenv"
// Import our app controllers
"github.com/tesh254/golang_todo_api/controllers"
)
// init gets called before the main function
func init() {
// Log error if .env file does not exist
if err := godotenv.Load(); err != nil {
log.Printf("No .env file found")
}
}
func main() {
// Init gin router
router := gin.Default()
// Its great to version your API's
v1 := router.Group("/api/v1")
{
// Define the hello controller
hello := new(controllers.HelloWorldController)
// Define a GET request to call the Default
// method in controllers/hello.go
v1.GET("/hello", hello.Default)
}
// Handle error response when a route is not defined
router.NoRoute(func(c *gin.Context) {
// In gin this is how you return a JSON response
c.JSON(404, gin.H{"message": "Not found"})
})
// Init our server
router.Run(":5000")
}
If you run your app
$ go run app.go
You should see something similar to this on your terminal
2020/02/07 20:17:24 No .env file found
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /api/v1/hello --> github.com/tesh254/golang_todo_api/controllers.(*HelloWorldController).Default-fm (3 handlers)
[GIN-debug] Listening and serving HTTP on :5000
Send a GET
request to our defined hello
endpoint.
$ curl http://localhost:5000/api/v1/hello
The response should be this
{"message": "Hello world, climate change is real"}
That's one of the so many ways to create an endpoint, its a bit long, you might just write your whole API in one file, but splitting your code into files makes your code:
- readable
- maintainable
Database connection
Next, we are going to handle database connection in the db
folder inside the db.go
file
// Define the package interacting with the database
package db
import (
"os"
"time"
"gopkg.in/mgo.v2"
)
// DBConnection defines the connection structure
type DBConnection struct {
session *mgo.Session
}
// NewConnection handles connecting to a mongo database
func NewConnection(host string, dbName string) (conn *DBConnection) {
info := &mgo.DialInfo{
// Address if its a local db then the value host=localhost
Addrs: []string{host},
// Timeout when a failure to connect to db
Timeout: 60 * time.Second,
// Database name
Database: dbName,
// Database credentials if your db is protected
Username: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PWD"),
}
session, err := mgo.DialWithInfo(info)
if err != nil {
panic(err)
}
session.SetMode(mgo.Monotonic, true)
conn = &DBConnection{session}
return conn
}
// Use handles connect to a certain collection
func (conn *DBConnection) Use(dbName, tableName string) (collection *mgo.Collection) {
// This returns method that interacts with a specific collection and table
return conn.session.DB(dbName).C(tableName)
}
// Close handles closing a database connection
func (conn *DBConnection) Close() {
// This closes the connection
conn.session.Close()
return
}
Create our Models
First of all, we will create a file in the models
folder containing methods to perform CRUD(Create Read Update Delete) interactions with the database.
We will create config.go
file to hold a couple of DB global variables
$ cd models && touch config.go
Add this into the file
package models
import (
"os"
"github.com/tesh254/golang_todo_api/db"
)
// Mongo server ip -> localhost -> 127.0.0.1 -> 0.0.0.0
var server = os.Getenv("DATABASE")
// Database name
var databaseName = os.Getenv("DATABASE_NAME")
// Create a connection
var dbConnect = db.NewConnection(server, databaseName)
Since we are creating an Authentication API we will create a models/user.go
file.
Let's create the file
$ cd models && touch user.go
Now we add some lines of code into it.
package models
import (
"gopkg.in/mgo.v2/bson"
)
// User defines user object structure
type User struct {
ID bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
Name string `json:"name" bson:"name"`
Email string `json:"email" bson:"email"`
Password string `json:"password" bson:"password"`
IsVerified bool `json:"is_verified" bson:"is_verified"`
}
// UserModel defines the model structure
type UserModel struct{}
// Signup handles registering a user
func (u *UserModel) Signup(data forms.SignupUserCommand) error {
// Connect to the user collection
collection := dbConnect.Use(databaseName, "user")
// Assign result to error object while saving user
err := collection.Insert(bson.M{
"name": data.Name,
"email": data.Email,
"password": data.Password,
// This will come later when adding verification
"is_verified": false,
})
return err
}
You might have noticed the forms.SignupUserCommand
that defines the type of data retrieved from the controller. We have to define the struct
with the types.
User Sign up
Let's go ahead and create a file to hold authentication body structs.
$ cd forms && touch user.go
Next we add the SignupUserCommand
struct
package forms
// SignupUserCommand defines user form struct
type SignupUserCommand struct {
// binding:"required" ensures that the field is provided
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
We will have to create the user controller to interact with the database via the API requests.
We will do so by creating a new file to hold User authentication controllers controllers/user.go
.
$ cd controllers && touch user.go
Let's add some code inside the file
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/tesh254/golang_todo_api/forms"
"github.com/tesh254/golang_todo_api/models"
)
// Import the userModel from the models
var userModel = new(models.UserModel)
// UserController defines the user controller methods
type UserController struct{}
// Signup controller handles registering a user
func (u *UserController) Signup(c *gin.Context) {
var data forms.SignupUserCommand
// Bind the data from the request body to the SignupUserCommand Struct
// Also check if all fields are provided
if c.BindJSON(&data) != nil {
// specified response
c.JSON(406, gin.H{"message": "Provide relevant fields"})
// abort the request
c.Abort()
// return nothing
return
}
/*
You can add your validation logic
here such as email
if regexMethodChecker(data.Email) {
c.JSON(400, gin.H{"message": "Email is invalid"})
c.Abort()
return
}
*/
err := userModel.Signup(data)
// Check if there was an error when saving user
if err != nil {
c.JSON(400, gin.H{"message": "Problem creating an account"})
c.Abort()
return
}
c.JSON(201, gin.H{"message": "New user account registered"})
}
Now that we have a signup controller let's tie it to an endpoint. Go back to app.go
and add a signup controller.
...
// Define the user controller
user := new(controllers.UserController)
// Create the signup endpoint
v1.POST("/signup", user.Signup)
Let's create a .env
file to hold our environment variables
$ touch .env
Add this to the file
export DATABASE=localhost
export DATABASE_NAME=golangtodoapi
If godotenv
library does not work with the .env file create a Makefile
to export your variables and run your app. This will make it easier to run your app instead of type two commands each time while you could with one.
run:
@echo ":::: App is startin up ::::"
@echo "CONFIG:: 😁 Exporting environemnt variables"
# This might vary depending on your unix os
# some might use source by default
/bin/sh .env
@echo "SUCCESS: ✔ Environment variables exported"
@echo "INIT:::: ⚡ Running server"
go run app.go
Before you run the server ensure you have a MongoDB instance running. Follow this link to install the MongoDB community version on your computer based on your operating system.
Run your server
$ go run app.go
Let's try to add a new user
$ curl -d '{"name": "Erick Wachira", "email": "ewachira254@gmail.com", "password": "Wachira254"}' -H "Content-Type: application/json" -X POST http://localhost:5000/api/v1/signup
Your response should be something similar
{"message":"New user account registered"}
If you go and check the user's collection contents using a GUI/Mongo cli. The document we just saved has the password saved in plain text. Well, we both know that it is not secure. We can fix that by using the bcrypt
library.
We need to first install it before we start coding.
$ go get golang.org/x/crypto/bcrypt
After installing the library we need to create a helpers package to house helping functions for our API, this will include our password hasher and compare.
$ cd helpers && touch bcrypt.go
Let us add a few lines to the file.
package helpers
// Allows us to hash and compare passwords
import "golang.org/x/crypto/bcrypt"
// GeneratePasswordHash handles generating password hash
// bcrypt hashes password of type byte
func GeneratePasswordHash(password []byte) string {
// default cost is 10
hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
// If there was an error panic
if err != nil {
panic(err)
}
// return stringified password
return string(hashedPassword)
}
// PasswordCompare handles password hash compare
func PasswordCompare(password []byte, hashedPassword []byte) error {
err := bcrypt.CompareHashAndPassword(hashedPassword, password)
return err
}
Let's update the user model function to save a hashed password.
...
err := collection.Insert(bson.M{
"name": data.Name,
"email": data.Email,
"password": helpers.GeneratePasswordHash([]byte(data.Password)),
// This will come later when adding verification
"is_verified": false,
})
...
Let's try that again, kill and then run your server, then send a signup request again.
Now with our Makefile defined your will running your app with this command
$ make run
The account will be created with a hashed password, you can confirm by checking the document saved.
Now we have another problem, an account is being created with the same this will cause conflicts if the app will ever be deployed to production. We will have to fix that.
We can do so by introducing a method that finds a user with an email. We will use the result to validate if the user exists or not. Let's jump into that.
Go to the models/user.go
file to add the method.
...
// GetUserByEmail handles fetching user by email
func (u *UserModel) GetUserByEmail(email string) (user User, err error) {
// Connect to the user collection
collection := dbConnect.Use(databaseName, "user")
// Assign result to error object while saving user
err = collection.Find(bson.M{"email": email}).One(&user)
return user, err
}
...
Let's modify the Signup controller to add a new condition
...
result, _ := userModel.GetUserByEmail(data.Email)
// If there happens to be a result respond with a
// descriptive mesage
if result.Email != "" {
c.JSON(403, gin.H{"message": "Email is already in use"})
c.Abort()
return
}
...
You user's controller file should look like this
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/tesh254/golang_todo_api/forms"
"github.com/tesh254/golang_todo_api/models"
)
// Import the userModel from the models
var userModel = new(models.UserModel)
// UserController defines the user controller methods
type UserController struct{}
// Signup controller handles registering a user
func (u *UserController) Signup(c *gin.Context) {
var data forms.SignupUserCommand
// Bind the data from the request body to the SignupUserCommand Struct
// Also check if all fields are provided
if c.BindJSON(&data) != nil {
// specified response
c.JSON(406, gin.H{"message": "Provide relevant fields"})
// abort the request
c.Abort()
// return nothing
return
}
/*
You can add your validation logic
here such as email
if regexMethodChecker(data.Email) {
c.JSON(400, gin.H{"message": "Email is invalid"})
c.Abort()
return
}
*/
result, _ := userModel.GetUserByEmail(data.Email)
// If there happens to be a result respond with a
// descriptive mesage
if result.Email != "" {
c.JSON(403, gin.H{"message": "Email is already in use"})
c.Abort()
return
}
err := userModel.Signup(data)
// Check if there was an error when saving user
if err != nil {
c.JSON(400, gin.H{"message": "Problem creating an account"})
c.Abort()
return
}
c.JSON(201, gin.H{"message": "New user account registered"})
}
Without deleting any document from the user's collection, try the request again.
You should get this response
{"message":"Email is already in use"}
User Login
We have gotten this far and have a solid user sign up endpoint we can now create a login endpoint. There are different ways to authenticate a user this include:
- Sessions
- JWT way
We are going to use the JWT(JSON Web Token) way. It has its advantages over sessions, these are:
No Session to Manage (stateless): The JWT is a self-contained token that has authentication information, expire time information, and other user-defined claims digitally signed.
Portable: A single token can be used with multiple backends.
No Cookies Required, So It's Very Mobile Friendly
Good Performance: It reduces the network round trip time.
Decoupled/Decentralized: The token can be generated anywhere. Authentication can happen on the resource server or easily separated into its own server.
To achieve JWT authentication we need to install a JWT library to generate and verify our tokens.
$ go get github.com/dgrijalva/jwt-go
jwt-go
library is a great and simple library that will help us achieve this.
Let's create a file inside the services
folder with the jwt.go
file to hold our jwt methods.
package services
import (
"os"
"time"
"github.com/dgrijalva/jwt-go"
)
var jwtKey = []byte(os.Getenv("SECRET_KEY"))
// Claims defines jwt claims
type Claims struct {
UserID string `json:"email"`
jwt.StandardClaims
}
// GenerateToken handles generation of a jwt code
// @returns string -> token and error -> err
func GenerateToken(userID string) (string, error) {
// Define token expiration time
expirationTime := time.Now().Add(1440 * time.Minute)
// Define the payload and exp time
claims := &Claims{
UserID: userID,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}
// Generate token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Sign token with secret key encoding
tokenString, err := token.SignedString(jwtKey)
return tokenString, err
}
// DecodeToken handles decoding a jwt token
func DecodeToken(tkStr string) (string, error) {
claims := &Claims{}
tkn, err := jwt.ParseWithClaims(tkStr, claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
return "", err
}
return "", err
}
if !tkn.Valid {
return "", err
}
return claims.UserID, nil
}
We won't have to create a Login
user model method we can utilize the GetUserByEmail
, following the DRY(Don't Repeat Yourself) rule. We need to add a LoginUserCommand
to define the request body types. Go to forms/user.go
file and add these lines of code
...
// LoginUserCommand defines user login form struct
type LoginUserCommand struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
...
Your forms/user.go
file should look like the code below
package forms
// SignupUserCommand defines user form struct
type SignupUserCommand struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginUserCommand defines user login form struct
type LoginUserCommand struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
Next, we create a Login
method controller.
...
// Login allows a user to login a user and get
// access token
func (u *UserController) Login(c *gin.Context) {
var data forms.LoginUserCommand
// Bind the request body data to var data and check if all details are provided
if c.BindJSON(&data) != nil {
c.JSON(406, gin.H{"message": "Provide required details"})
c.Abort()
return
}
result, err := userModel.GetUserByEmail(data.Email)
if result.Email == "" {
c.JSON(404, gin.H{"message": "User account was not found"})
c.Abort()
return
}
if err != nil {
c.JSON(400, gin.H{"message": "Problem logging into your account"})
c.Abort()
return
}
// Get the hashed password from the saved document
hashedPassword := []byte(result.Password)
// Get the password provided in the request.body
password := []byte(data.Password)
err = helpers.PasswordCompare(password, hashedPassword)
if err != nil {
c.JSON(403, gin.H{"message": "Invalid user credentials"})
c.Abort()
return
}
jwtToken, err2 := services.GenerateToken(data.Email)
// If we fail to generate token for access
if err2 != nil {
c.JSON(403, gin.H{"message": "There was a problem logging you in, try again later"})
c.Abort()
return
}
c.JSON(200, gin.H{"message": "Log in success", "token": jwtToken})
}
...
Your controllers/user.go
should look something similar to this
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/tesh254/golang_todo_api/forms"
"github.com/tesh254/golang_todo_api/helpers"
"github.com/tesh254/golang_todo_api/models"
"github.com/tesh254/golang_todo_api/services"
)
// Import the userModel from the models
var userModel = new(models.UserModel)
// UserController defines the user controller methods
type UserController struct{}
// Signup controller handles registering a user
func (u *UserController) Signup(c *gin.Context) {
var data forms.SignupUserCommand
// Bind the data from the request body to the SignupUserCommand Struct
// Also check if all fields are provided
if c.BindJSON(&data) != nil {
// specified response
c.JSON(406, gin.H{"message": "Provide relevant fields"})
// abort the request
c.Abort()
// return nothing
return
}
/*
You can add your validation logic
here such as email
if regexMethodChecker(data.Email) {
c.JSON(400, gin.H{"message": "Email is invalid"})
c.Abort()
return
}
*/
result, _ := userModel.GetUserByEmail(data.Email)
// If there happens to be a result respond with a
// descriptive mesage
if result.Email != "" {
c.JSON(403, gin.H{"message": "Email is already in use"})
c.Abort()
return
}
err := userModel.Signup(data)
// Check if there was an error when saving user
if err != nil {
c.JSON(400, gin.H{"message": "Problem creating an account"})
c.Abort()
return
}
c.JSON(201, gin.H{"message": "New user account registered"})
}
// Login allows a user to login a user and get
// access token
func (u *UserController) Login(c *gin.Context) {
var data forms.LoginUserCommand
// Bind the request body data to var data and check if all details are provided
if c.BindJSON(&data) != nil {
c.JSON(406, gin.H{"message": "Provide required details"})
c.Abort()
return
}
result, err := userModel.GetUserByEmail(data.Email)
if result.Email == "" {
c.JSON(404, gin.H{"message": "User account was not found"})
c.Abort()
return
}
if err != nil {
c.JSON(400, gin.H{"message": "Problem logging into your account"})
c.Abort()
return
}
// Get the hashed password from the saved document
hashedPassword := []byte(result.Password)
// Get the password provided in the request.body
password := []byte(data.Password)
err = helpers.PasswordCompare(password, hashedPassword)
if err != nil {
c.JSON(403, gin.H{"message": "Invalid user credentials"})
c.Abort()
return
}
jwtToken, err2 := services.GenerateToken(data.Email)
// If we fail to generate token for access
if err2 != nil {
c.JSON(403, gin.H{"message": "There was a problem logging you in, try again later"})
c.Abort()
return
}
c.JSON(200, gin.H{"message": "Log in success", "token": jwtToken})
}
Next, we will have to create a login endpoint to call the Login
method controller.
...
// Create the login endpoint
v1.POST("/login", user.Login)
...
We will have to add a new variable to the .env
file, the SECRET_KEY
that will be used to protect our JWT tokens.
...
export const SECRET_KEY=#53cR3Tk3y
...
Let's run our app and test out our endpoint
$ make run
curl -d '{"email": "ewachira254@gmail.com", "password": "Wachira254"}' -H "Content-Type: application/json" -X POST http://localhost:5000/api/v1/login
Your response should be something similar
{
"message":"Log in success","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImV3YWNoaXJhMjU0QGdtYWlsLmNvbSIsImV4cCI6MTU4MTIwNzE2Nn0.XXaDB0R9UJ7aKLQxyDDjIdj1WMNN_bP5Ez9um6_T-SU"
}
Extras
Next
In the next part of this series, we will be able to send out password reset and verification emails. We will also deploy the API to Heroku on the final part of the series.
Top comments (3)
"github.com/tesh254/golang_todo_api/controllers" is a private repository, can not download.
github.com/werickblog/golang_todo_api
Cool!