DEV Community

Wachira
Wachira

Posted on • Originally published at blog.bywachira.com

Create API with Gin in Golang Part 2

Well it has been a while since I last wrote an article, so you know what I did to get motivated I revamped my blog or should I say I just added a dark theme.

This blog is the continuation of the first part where we create a working Golang API powered with a Mongo database. Before you get started on this article ensure you read it, if not here is the

TL;DR

  • We handle basic user authentication nothing more nothing less

In this part we will focus on this:

  • User account verification through email
  • Password reset and request through email
  • Refresh Token endpoint

I plan on having 3 other parts which will focus on key things as higlighted below:

  • Part 3

    • Create the bookmarker endpoints > I wanted to build a TODO API but I think it's kind off cliche so I decided to work on a bookmarker API, will allow users to save links to websites they liked and even save metadata by preloading them and saving meta tag details.
      • Ability to create a bookmark
      • Preload link meta details and save them to the collection
      • Ability to fetch all bookmarks
      • Ability to delete a bookmark
  • Part 4

    • Testing the authentication endpoints
    • Adding session blacklisting(stop users from reusing )
    • Host the project to heroku
  • Part 5

    • Outline how you can improve on this API
    • How many products you can build from this API

Introduction

Prerequisites

You must a know the basics of Golang and a tiny bit of Gin

Getting started

Let's go

Goals

  • A user should be able to verify their account
  • A user should be able to reset their account password
  • A user should be able to refresh token

Setup

Clone the project

# SSH $ git clone git@github.com:werickblog/golang_todo_api.git # HTTP $ git clone https://github.com/werickblog/golang_todo_api.git 
Enter fullscreen mode Exit fullscreen mode

Ensure the project lies in your set $GOPATH/src directory

Next, open the project with your favorite editor and run the app

$ go run app.go 
Enter fullscreen mode Exit fullscreen mode

This will automatically install all packages missing.

Let's hack

lets hack

Password Reset and Request

We are going to start of with the password request and reset controller.

So open the controllers/user.go and create a ResetLink method of the UserController struct.

 // ... // ResetLink handles resending email to user to reset link func (u *UserController) ResetLink(c *gin.Context) { // Defined schema for the request body var data forms.ResendCommand // Ensure the user provides all values from the request.body if (c.BindJSON(&data)) != nil { // Return 400 status if they don't provide the email c.JSON(400, gin.H{"message": "Provided all fields"}) c.Abort() return } // Fetch the account from the database based on the email // provided result, err := userModel.GetUserByEmail(data.Email) // Return 404 status if an account was not found if result.Email == "" { c.JSON(404, gin.H{"message": "User account was not found"}) c.Abort() return } // Return 500 status if something went wrong while fetching // account if err != nil { c.JSON(500, gin.H{"message": "Something wrong happened, try again later"}) c.Abort() return } // Generate the token that will be used to reset the password resetToken, _ := services.GenerateNonAuthToken(result.Email) // The link to be clicked in order to perform a password reset link := "http://localhost:5000/api/v1/password-reset?reset_token=" + resetToken // Define the body of the email body := "Here is your reset <a href='" + link + "'>link</a>" html := "<strong>" + body + "</strong>" // Initialize email sendout email := services.SendMail("Reset Password", body, result.Email, html, result.Name) // If email was sent, return 200 status code if email == true { c.JSON(200, gin.H{"messsage": "Check mail"}) c.Abort() return // Return 500 status when something wrong happened } else { c.JSON(500, gin.H{"message": "An issue occured sending you an email"}) c.Abort() return } } // ... 
Enter fullscreen mode Exit fullscreen mode

The above is the password reset link request, if you run your app, it will fail because we haven't defined certain methods/variables/struct. These are:

  • The request body defined schema in the forms
  • Generate token for password request
  • Send email out

So let's touch on defining the request body schema, first

Password Reset Request Schema

Open forms/user.go file and add the lines below

// .. // ResendCommand defines resend email payload type ResendCommand struct { // We only need the email to initialize an email sendout Email string `json:"email" binding:"required"` } // ... 
Enter fullscreen mode Exit fullscreen mode

On to the next one

Generation of the Token

We don't want our users to use the token they get from logging in, to initialize a reset password due to security reasons, so we will have to create a new method to create non auth tokens and one to decode them. Let's jump into it

jump

Open services/jwt.go and add the following the methods

// ... // Define its own secret key var anotherJwtKey = []byte(os.Getenv("ANOTHER_SECRET_KEY")) // GenerateNonAuthToken handles generation of a jwt code // @returns string -> token and error -> err func GenerateNonAuthToken(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(anotherJwtKey) return tokenString, err } // DecodeNonAuthToken handles decoding a jwt token func DecodeNonAuthToken(tkStr string) (string, error) { claims := &Claims{} // Decode token based on parameters provided, if it fails throw err tkn, err := jwt.ParseWithClaims(tkStr, claims, func(token *jwt.Token) (interface{}, error) { return anotherJwtKey, nil }) if err != nil { if err == jwt.ErrSignatureInvalid { return "", err } return "", err } if !tkn.Valid { return "", err } // Return encoded email return claims.UserID, nil } // ... 
Enter fullscreen mode Exit fullscreen mode

Handle Email sendout

I chose the Sendgrid email service because it is easier to create an account(😂 no credit card required) also setting up your own custom email domain is optional (means you can use your Gmail account).

Thank you Sendgrid

Create a Sendgrid account and generate an API key with permissions to send out emails.

Next we will install a Sendgrid's Go SDK that will ease the process of sending out emails

$ go get github.com/sendgrid/sendgrid-go 
Enter fullscreen mode Exit fullscreen mode

Create a new file to hold our method to send emails. We will also make it reusable, (DRY code, )

Add the following lines of code

// Define the package package services // Import relevant dependecy import ( "fmt" "os" // Import Sendgrid Go library "github.com/sendgrid/sendgrid-go" "github.com/sendgrid/sendgrid-go/helpers/mail" ) // EmailObject defines email payload data type EmailObject struct { To string Body string Subject string } // SendMail method to send email to user func SendMail(subject string, body string, to string, html string, name string) bool { fmt.Println(os.Getenv("SENDGRID_API_KEY")) // The first parameter is how your email name will be from := mail.NewEmail("Just Open it", os.Getenv("SENDGRID_FROM_MAIL")) // The recipient _to := mail.NewEmail(name, to) // Body in plain text plainTextContent := body // Body in html form(You can style a html document convert to string and make it look like the morning brew newsletter) htmlContent := html // Create message message := mail.NewSingleEmail(from, subject, _to, plainTextContent, htmlContent) // initialize client client := sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY")) _, err := client.Send(message) if err != nil { return false } else { return true } } 
Enter fullscreen mode Exit fullscreen mode

Password reset request

Head over to controllers/user.go and let's add the password reset request.

Define the password reset controller method

// ResetLink handles resending email to user to reset link func (u *UserController) ResetLink(c *gin.Context) { var data forms.ResendCommand // Ensure they provide all request body values if (c.BindJSON(&data)) != nil { c.JSON(400, gin.H{"message": "Provided all fields"}) c.Abort() return } // Fetch the user in the database result, err := userModel.GetUserByEmail(data.Email) // If the user doesn't exist return 404 status code if result.Email == "" { c.JSON(404, gin.H{"message": "User account was not found"}) c.Abort() return } // Something went wrong while fetching if err != nil { c.JSON(500, gin.H{"message": "Something wrong happened, try again later"}) c.Abort() return } // Generate reset token to be used resetToken, _ := services.GenerateNonAuthToken(result.Email) // Define the email body link := "http://localhost:5000/api/v1/password-reset?reset_token=" + resetToken body := "Here is your reset <a href='" + link + "'>link</a>" html := "<strong>" + body + "</strong>" // Send the email email := services.SendMail("Reset Password", body, result.Email, html, result.Name) // If email is sent then return 200 HTTP status code if email == true { c.JSON(200, gin.H{"messsage": "Check mail"}) c.Abort() return } else { // Else tell them something went down c.JSON(500, gin.H{"message": "An issue occured sending you an email"}) c.Abort() return } } 
Enter fullscreen mode Exit fullscreen mode

Next we need to define the endpoint that will initialize the request.

Head over to app.go and add this lines

// Send reset link v1.PUT("/reset-link", user.ResetLink) 
Enter fullscreen mode Exit fullscreen mode

Password reset change

Next we have to add new controller to handle password change based on the user gotten from decoding the token from the url.

Let's jump into it

Head over to the controllers/user.go and lets add out controllers.

// PasswordReset handles user password request func (u *UserController) PasswordReset(c *gin.Context) { var data forms.PasswordResetCommand // Ensure they provide data based on the schema if c.BindJSON(&data) != nil { c.JSON(406, gin.H{"message": "Provide relevant fields"}) c.Abort() return } // Ensures that the password provided matches the confirm if data.Password != data.Confirm { c.JSON(400, gin.H{"message": "Passwords do not match"}) c.Abort() return } // Get token from link query sent to your email resetToken, _ := c.GetQuery("reset_token") // Decode the token userID, _ := services.DecodeNonAuthToken(resetToken) // Fetch the user result, err := userModel.GetUserByEmail(userID) if err != nil { // Return response when we get an error while fetching user c.JSON(500, gin.H{"message": "Something wrong happened, try again later"}) c.Abort() return } // Check if account exists if result.Email == "" { c.JSON(404, gin.H{"message": "User accoun was not found"}) c.Abort() return } // Hash the new password newHashedPassword := helpers.GeneratePasswordHash([]byte(data.Password)) // Update user account _err := userModel.UpdateUserPass(userID, newHashedPassword) if _err != nil { // Return response if we are not able to update user password c.JSON(500, gin.H{"message": "Somehting happened while updating your password try again"}) c.Abort() return } c.JSON(201, gin.H{"message": "Password has been updated, log in"}) c.Abort() return } 
Enter fullscreen mode Exit fullscreen mode

Next lets add a new endpoint to initialize the controller above

Head over to app.go file and the following lines

// Password reset v1.PUT("/password-reset", user.PasswordReset) 
Enter fullscreen mode Exit fullscreen mode

We have successfully handle password reseting for a user account, next we will look into verify an account.

Account verification

Account verification allows the developer to verify the user of a specific account thus reducing creation of dummy with non-existence emails.

We are going to update the Signup controller to handle sending a verification email and add a new controller to handle resending emails and finally a controller to verify a user account.

Head over to controllers/user.go and let's edit the Signup controller.

// ... // Generate token to hold users details resetToken, _ := services.GenerateNonAuthToken(data.Email) // link to be verify account link := "http://localhost:5000/api/v1/verify-account?verify_token=" + resetToken // Define email body body := "Here is your reset <a href='" + link + "'>link</a>" html := "<strong>" + body + "</strong>" // initialize email send out email := services.SendMail("Verify Account", body, data.Email, html, data.Name) // If email fails while sending if !email { c.JSON(500, gin.H{"message": "An issue occured sending you an email"}) c.Abort() return } // ... 
Enter fullscreen mode Exit fullscreen mode

Next let's create a resend verification email controller. Still on the same file, add this method

// VerifyLink handles resending email to user to reset link func (u *UserController) VerifyLink(c *gin.Context) { var data forms.ResendCommand // Ensure they provide all relevant fields in the request body if (c.BindJSON(&data)) != nil { c.JSON(400, gin.H{"message": "Provided all fields"}) c.Abort() return } // Fetch account from database result, err := userModel.GetUserByEmail(data.Email) // Check if account exist return 404 if not if result.Email == "" { c.JSON(404, gin.H{"message": "User account was not found"}) c.Abort() return } if err != nil { c.JSON(500, gin.H{"message": "Something wrong happened, try again later"}) c.Abort() return } // Generate token to hold user details resetToken, _ := services.GenerateNonAuthToken(result.Email) // Define email body link := "http://localhost:5000/api/v1/verify-account?verify_token=" + resetToken body := "Here is your reset <a href='" + link + "'>link</a>" html := "<strong>" + body + "</strong>" // Initialize email sendout email := services.SendMail("Verify Account", body, result.Email, html, result.Name) // If email send 200 status code if email == true { c.JSON(200, gin.H{"messsage": "Check mail"}) c.Abort() return } else { c.JSON(500, gin.H{"message": "An issue occured sending you an email"}) c.Abort() return } } 
Enter fullscreen mode Exit fullscreen mode

Lets add an endpoint to initialize the above controller, head over to app.go and add the following lines.

// Send verify link v1.PUT("/verify-link", user.VerifyLink) 
Enter fullscreen mode Exit fullscreen mode

We now have to handle the verification of account controller, let's hope on that. Head over to controllers/user.go and add the verify account controller.

// VerifyAccount handles user password request func (u *UserController) VerifyAccount(c *gin.Context) { // Get token from link query verifyToken, _ := c.GetQuery("verify_token") // Decode verify token userID, _ := services.DecodeNonAuthToken(verifyToken) // Fetch user based on details from decoded token result, err := userModel.GetUserByEmail(userID) if err != nil { // Return response when we get an error while fetching user c.JSON(500, gin.H{"message": "Something wrong happened, try again later"}) c.Abort() return } if result.Email == "" { c.JSON(404, gin.H{"message": "User account was not found"}) c.Abort() return } // Update user account _err := userModel.VerifyAccount(userID) if _err != nil { // Return response if we are not able to update user password c.JSON(500, gin.H{"message": "Something happened while verifying you account, try again"}) c.Abort() return } c.JSON(201, gin.H{"message": "Account verified, log in"}) } 
Enter fullscreen mode Exit fullscreen mode

Let's add an endpoint to verify an account

// Verify account v1.PUT("/verify-account", user.VerifyAccount) 
Enter fullscreen mode Exit fullscreen mode

We are almost done

We are left with refresh token. A refresh token is basically a token used to refresh user sessions if the access token happens to expire. Read more about it here

Head over to controllers/user.go and lets add our refresh token controller

// RefreshToken handles refresh token func (u *UserController) RefreshToken(c *gin.Context) { // Get refresh token from header refreshToken := c.Request.Header["Refreshtoken"] // Check if refresh token was provided if refreshToken == nil { c.JSON(403, gin.H{"message": "No refresh token provided"}) c.Abort() return } // Decode token to get data email, err := services.DecodeRefreshToken(refreshToken[0]) if err != nil { c.JSON(500, gin.H{"message": "Problem refreshing your session"}) c.Abort() return } // Create new token accessToken, _refreshToken, _err := services.GenerateToken(email) if _err != nil { c.JSON(500, gin.H{"message": "Problem creating new session"}) c.Abort() return } c.JSON(200, gin.H{"message": "Log in success", "token": accessToken, "refresh_token": _refreshToken}) } 
Enter fullscreen mode Exit fullscreen mode

Now lets add an endpoint in app.go

// Refresh token v1.GET("/refresh", user.RefreshToken) 
Enter fullscreen mode Exit fullscreen mode

And that's it,

Summary

  • We handled password reset request and change
  • We handled account verification
  • We handle refresh token

Extras

  • Repo link here
  • Follow me on twitter here
  • Join Discord server for any questions here

Top comments (0)