In this article, we will implement JWT authentication in GoLang. Today we will create REST API which will implement JWT authentication in GoLang, this API will have few endpoints which will demonstrate the working of the JWT authentication. Here we use MongoDB as a database for storing the user details while registration and we will fetch the data from the MongoDB database for login.
If you already know how JWT works, and just want to get started with the implementation, you can skip, or download the source code from here
Just in case if you don’t know what is JWT and why we use it? Here is a very short description,
1. What is JWT and why we use it?
- When a user logs into a system or registers into a system, the SYstem gives that user a token.
- Once the User receives that token, then the user should always use that token to make any new API calls. In more technical terms, you would send the token in the header of the request.
- Once the Token will be verified from the Backend Logic, then only you will receive the proper response or else you will be considered as logged out user.
A Typical JWT will look like this,
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.6Y-BVfKrjIPXRnE6KAegk_jN4jLQVBb5pD0eokpPjAs
There are three parts in JWT which are separated by the dot(.
).
- The first part of the token is known as
Header
. The Header tells which algorithm is used to generate the token and what is the type of token. - The second part is called the
Payload
. This part consists of the application-specific information, for example, token expiry time or any other data that you would like to store in it. - The last part is called a
Signature
, it is implemented using the first two parts i.e. Header and Payload along with the secret key that you would provide.
If you really wanna dig deep into it, I’ll encourage you to check thejwt.io. You can play with the realtime representation of JWT as you change the Payload or secret key.
2. Implementing the Rest API with JWT
We will create a small JWT Rest API, which will demonstrate the working of the JWT. In this application, we will create below-listed endpoints,
/signin
(POST): The/signin
API will make the user login and along with that will create a new token and give back that token in the response./signup
(POST): The/signup
API will create a new user and along with that will create a new token and give back that token in the response./userDetails
(GET): The/userDetails
API will identify the user with the help of the JWT token and based on that it will fetch the result from the database and it will send the response.
Of course, you will get a full-fledged application when you will download or clone the project but in this article, we will cover only the API implementation.
Since the frontend here is not that big and it doesn’t contain the heavy logics so we won’t be covering that here.
3. Understanding the project structure
Here we will give a very little bit of styling to our web application just to make it look presentable. And we won’t be using any frontend libraries or frameworks, for the sake of simplicity. Inside thepublic
folder, we will write down the Javascript scripts and inside view folder, we will write down the MARKUP.
Apart from the public folder we have only go files, let’s understand the purpose of each file why we need them.
db.go
:The file will have code to connect the Redis database.
routes-handlers.go
:This file will have the actual logic of the application. Here we will write code to store the Hash Key with the actual URL and the Redirection Logic will be handled here only.
routes.go
: As the name suggests, this file will contain the endpoints that we will define in this application.
server.go
: To create the Golang server we will use server.go file.
jwt.go
:This file contains the code related to only JWT, such as creating a JWT token and verifying the JWT tokens.
structs.go
:All the structs
used in this application will go under this file.
4. Creating a GoLang Server
Create aserver.goin the root of the project, which will be our entry point for the project. Here we will make a connection with the MongoDB database and we will define our application routes.
=>Inside the main()
function, First we are printing some information.
=>Then using ConnectDatabse()
function, we will make a MongoDB connection.
=>In the next line, we will create route
variable, which will hold Route instance.
=>Then AddApproutes()
function register application routes.
=>And at the end, using http.ListenAndServe()
we will start our GO server.
server.go:
package main import ( "log" "net/http" "github.com/gorilla/mux" ) func main() { log.Println("Server will start at http://localhost:8000/") ConnectDatabase() route := mux.NewRouter() AddApproutes(route) log.Fatal(http.ListenAndServe(":8000", route)) }
5. Connecting GoLang to MongoDB
Create adb.goin the root of the project, Here we will connect our GoLang app with MongoDB database.
=>In the below code, first we have to include the MongoDB Go driver i.e.go-mongodb-driver
.
=>Then we have created variableClient
, which will hold the MongoDB connection instance. ThisClient
variable will be available inside all the files under themain
package.
=>Inside theConnectDatabse()
function we will create MongoDB connection as shown below,
db.go:
package main import ( "context" "log" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) // Client is exported Mongo Database client var Client *mongo.Client // ConnectDatabase is used to connect the MongoDB database func ConnectDatabase() { log.Println("Database connecting...") // Set client options clientOptions := options.Client().ApplyURI("mongodb://localhost:27017") // Connect to MongoDB client, err := mongo.Connect(context.TODO(), clientOptions) Client = client if err != nil { log.Fatal(err) } // Check the connection err = Client.Ping(context.TODO(), nil) if err != nil { log.Fatal(err) } log.Println("Database Connected.") }
6. Adding GoLang Structs
Create astructs.goin the root of the project, Here we will register all structs used in this application. The reason for creating a separate file is that your code will look more clear and it will become very easy to read.
structs.go:
package main import jwt "github.com/dgrijalva/jwt-go" // ErrorResponse is struct for sending error message with code. type ErrorResponse struct { Code int Message string } // SuccessResponse is struct for sending error message with code. type SuccessResponse struct { Code int Message string Response interface{} } // Claims is a struct that will be encoded to a JWT. // jwt.StandardClaims is an embedded type to provide expiry time type Claims struct { Email string jwt.StandardClaims } // RegistationParams is struct to read the request body type RegistationParams struct { Name string `json:"name"` Email string `json:"email"` Password string `json:"password"` } // LoginParams is struct to read the request body type LoginParams struct { Email string `json:"email"` Password string `json:"password"` } // SuccessfulLoginResponse is struct to send the request response type SuccessfulLoginResponse struct { Email string AuthToken string } // UserDetails is struct used for user details type UserDetails struct { Name string Email string Password string }
7. Creating and Verifying JWT token
Now let’s write code for creating and verifying the JWT token. In this application, we will use thedgrijalva/jwt-go library for implementing and verifying the JWT token. In this file we will have two methods, the first method will create a JWT token and the second method will verify the token.
=> The CreateJWT()
function will create a JWT token and the VerifyToken()
function will verify the token. You will find more explanations of the code and how it works.
=> Create jwt.go file and copy-paste the below code,
jwt.go:
package main import ( "time" "github.com/dgrijalva/jwt-go" ) var jwtSecretKey = []byte("jwt_secret_key") // CreateJWT func will used to create the JWT while signing in and signing out func CreateJWT(email string) (response string, err error) { expirationTime := time.Now().Add(5 * time.Minute) claims := &Claims{ Email: email, StandardClaims: jwt.StandardClaims{ ExpiresAt: expirationTime.Unix(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(jwtSecretKey) if err == nil { return tokenString, nil } return "", err } // VerifyToken func will used to Verify the JWT Token while using APIS func VerifyToken(tokenString string) (email string, err error) { claims := &Claims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { return jwtSecretKey, nil }) if token != nil { return claims.Email, nil } return "", err }
Explanation:
- First, we will create a JWT secret and covert it into a byte array.
- The function
CreateJWT()
will expect the email and parameter and returns the JWT token if everything goes fine. Otherwise, it will return an error. - In the
CreateJWT()
function, first, we create expiration time which will be used to expire the token. - After that, we will create a claim in which we will specify the Email or anything that you want here it can be userid or just a username.
- The function
jwt.NewWithClaims()
expects the algorithm in which the token will be encoded and the claim that you just created. - The method
token.SignedString()
will give you the actual Authentication token - The
VerifyToken()
will expect the token string a parameter and returns the email with which it was generated. If anything goes south then it gives an error. - In
VerifyToken()
method, first, we will create a claims variable that refers to the memory address of the Claims structs. - The method
jwt.ParseWithClaims()
gives the email if we pass the token string and claims that we just created.
8. Adding GoLang routes and implementing the endpoints
Create aroutes.goin the root of the project, Here we will register application routes. Here we will use gorilla/mux
package to register routes.
=>Then AddApproutes()
function will register all the routes in the application. Here we have only one route to add which will be used by the FrontEnd javascript.
routes.go:
package main import ( "log" "github.com/gorilla/mux" ) // AddApproutes will add the routes for the application func AddApproutes(route *mux.Router) { log.Println("Loadeding Routes...") route.HandleFunc("/signin", SignInUser).Methods("POST") route.HandleFunc("/signup", SignUpUser).Methods("POST") route.HandleFunc("/userDetails", GetUserDetails).Methods("GET") log.Println("Routes are Loaded.") }
Now let’s finish the endpoints by creating the handlers of each respective API. We create three methods as follows SignInUser()
, SignUpUser()
and GetUserDetails()
. The name of the above functions are self-explanatory and we will write them inside the routes-handlers.go.
Now create a routes-handlers.gofile and write down the below code,
routes-handlers.go:
// SignInUser Used for Signing In the Users func SignInUser(response http.ResponseWriter, request *http.Request) { var loginRequest LoginParams var result UserDetails var errorResponse = ErrorResponse{ Code: http.StatusInternalServerError, Message: "It's not you it's me.", } decoder := json.NewDecoder(request.Body) decoderErr := decoder.Decode(&loginRequest) defer request.Body.Close() if decoderErr != nil { returnErrorResponse(response, request, errorResponse) } else { errorResponse.Code = http.StatusBadRequest if loginRequest.Email == "" { errorResponse.Message = "Last Name can't be empty" returnErrorResponse(response, request, errorResponse) } else if loginRequest.Password == "" { errorResponse.Message = "Password can't be empty" returnErrorResponse(response, request, errorResponse) } else { collection := Client.Database("test").Collection("users") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) var err = collection.FindOne(ctx, bson.M{ "email": loginRequest.Email, "password": loginRequest.Password, }).Decode(&result) defer cancel() if err != nil { returnErrorResponse(response, request, errorResponse) } else { tokenString, _ := CreateJWT(loginRequest.Email) if tokenString == "" { returnErrorResponse(response, request, errorResponse) } var successResponse = SuccessResponse{ Code: http.StatusOK, Message: "You are registered, login again", Response: SuccessfulLoginResponse{ AuthToken: tokenString, Email: loginRequest.Email, }, } successJSONResponse, jsonError := json.Marshal(successResponse) if jsonError != nil { returnErrorResponse(response, request, errorResponse) } response.Header().Set("Content-Type", "application/json") response.Write(successJSONResponse) } } } } // SignUpUser Used for Signing up the Users func SignUpUser(response http.ResponseWriter, request *http.Request) { var registationRequest RegistationParams var errorResponse = ErrorResponse{ Code: http.StatusInternalServerError, Message: "It's not you it's me.", } decoder := json.NewDecoder(request.Body) decoderErr := decoder.Decode(®istationRequest) defer request.Body.Close() if decoderErr != nil { returnErrorResponse(response, request, errorResponse) } else { errorResponse.Code = http.StatusBadRequest if registationRequest.Name == "" { errorResponse.Message = "First Name can't be empty" returnErrorResponse(response, request, errorResponse) } else if registationRequest.Email == "" { errorResponse.Message = "Last Name can't be empty" returnErrorResponse(response, request, errorResponse) } else if registationRequest.Password == "" { errorResponse.Message = "Country can't be empty" returnErrorResponse(response, request, errorResponse) } else { tokenString, _ := CreateJWT(registationRequest.Email) if tokenString == "" { returnErrorResponse(response, request, errorResponse) } var registrationResponse = SuccessfulLoginResponse{ AuthToken: tokenString, Email: registationRequest.Email, } collection := Client.Database("test").Collection("users") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) _, databaseErr := collection.InsertOne(ctx, bson.M{ "email": registationRequest.Email, "password": registationRequest.Password, "name": registationRequest.Name, }) defer cancel() if databaseErr != nil { returnErrorResponse(response, request, errorResponse) } var successResponse = SuccessResponse{ Code: http.StatusOK, Message: "You are registered, login again", Response: registrationResponse, } successJSONResponse, jsonError := json.Marshal(successResponse) if jsonError != nil { returnErrorResponse(response, request, errorResponse) } response.Header().Set("Content-Type", "application/json") response.WriteHeader(successResponse.Code) response.Write(successJSONResponse) } } } // GetUserDetails Used for getting the user details using user token func GetUserDetails(response http.ResponseWriter, request *http.Request) { var result UserDetails var errorResponse = ErrorResponse{ Code: http.StatusInternalServerError, Message: "It's not you it's me.", } bearerToken := request.Header.Get("Authorization") var authorizationToken = strings.Split(bearerToken, " ")[1] email, _ := VerifyToken(authorizationToken) if email == "" { returnErrorResponse(response, request, errorResponse) } else { collection := Client.Database("test").Collection("users") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) var err = collection.FindOne(ctx, bson.M{ "email": email, }).Decode(&result) defer cancel() if err != nil { returnErrorResponse(response, request, errorResponse) } else { var successResponse = SuccessResponse{ Code: http.StatusOK, Message: "You are logged in successfully", Response: result.Name, } successJSONResponse, jsonError := json.Marshal(successResponse) if jsonError != nil { returnErrorResponse(response, request, errorResponse) } response.Header().Set("Content-Type", "application/json") response.Write(successJSONResponse) } } }
Explanation:
- In the
SignInUser()
function, first, we validate the user’s input and if any required input is missing then return a proper error response with HTTP error code. - After that, we use MongoDB
client
connection object and we check if the user exists in the database or not. If the user not found then again we return a proper error response with HTTP error code. - If the user is found then we use
CreateJWT()
function and return the success response along with the Authentication token. - In the
SignUpUser()
function, first, we again validate the user’s input and if any required input is missing then return a proper error response with HTTP error code. - Then we use
CreateJWT()
function and create an Authentication token. - After that, we use MongoDB
client
connection object and store the user’s input into the MongoDB database collection. - Once all the above mentioned steps are done then we return the success response along with the Authentication token.
- Lastly in
GetUserDetails()
function, first we read the request header with Authorization. This key should contain the Authorization token as shown below,Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJFbWFpbCI6InNoYW5zaGFuazY5QGdtYWlsLmNvbSIsImV4cCI6MTU4MzY5MzU5MX0.bmPs9XkaRlksNkcEEcIe3pOFUlQAFPy0pVA2KKKHhhc
- Then we split it by a space
" "
and get the first index value from the array, which is nothing but our token. - Then using
VerifyToken()
, we verify the token and get the email in return fromVerifyToken()
function. - After that using the email we fetch the result from the MongoDB database just to check if the user exists with that email in our database collection.
- If the user not found with that email then again we return a proper error response with HTTP error code or else will return the success response along with the Users Details
9. Running the application
To run the application, you need to build the application and then run the executable file as shown below,
> Go build > ./JSON_Web_Tokens
Now you can test this application using Postman or just a Simple Curl. for example, the response of /signup
API will be something like,
{ "Code": 200, "Message": "You are registered, login again", "Response": { "Email": "shashank@codershood.info", "AuthToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJFbWFpbCI6InNoYXNoYW5rQGNvZGVyc2hvb2QuaW5mbyIsImV4cCI6MTU4NDA2OTAxNX0.XmA08EAeN0g6PhlW-H5_xWer_1Brb8CSMjbBk-I8fE8" } }
And the respective CURL request is written below,
curl -X POST \ http://localhost:8000/signup \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ -d '{ "name" : "Shashank", "email" : "shashank@codershood.info", "password" : "1234567890" }'
The response of /signup
API,
{ "Code": 200, "Message": "You are logged in successfully", "Response": "Shashank" }
And the respective CURL request is written below,
curl -X GET \ http://localhost:8000/userDetails \ -H 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJFbWFpbCI6InNoYXNoYW5rQGNvZGVyc2hvb2QuaW5mbyIsImV4cCI6MTU4NDA2OTAxNX0.XmA08EAeN0g6PhlW-H5_xWer_1Brb8CSMjbBk-I8fE8' \ -H 'cache-control: no-cache'
10. Conclusion
In this application, we implemented JWT authentication as a rest API in GoLang. But there is one part missing and that is refreshing the Authentication token, which is topic another blog post.
Meanwhile, you can play with code and if something doesn’t work let me know in comments.
In `routes-handlers.go`, why are you not returning from the routes in the event of an error?
E.g. you call `returnErrorResponse()`, which returns the error response via HTTP. After this, you continue with the route function. Why is this?