Creating Login and Private Routes with Refreshing tokens - Part[3/3] of Go Authentication series

mdfaizan7

Faizan

Posted on October 31, 2020

Creating Login and Private Routes with Refreshing tokens - Part[3/3] of Go Authentication series

In this part, we will create 3 routes:

  • A SignIn route from where users can log in either with their username or email along with their password.
  • A secure Route from where any logged-in user can get their details from the database.
  • Another route that returns an access token iff the refresh token is valid.

So let's get started:

We will make all these functions inside the router/user.go file.

First, we will modify the SetupUserRoutes function inside router/user.go file.

// SetupUserRoutes func sets up all the user routes
func SetupUserRoutes() {
    USER.Post("/signup", CreateUser)              // Sign Up a user
++  USER.Post("/signin", LoginUser)               // Sign In a user
++  USER.Get("/get-access-token", GetAccessToken) // returns a new access_token

++  // privUser handles all the private user routes that requires authentication
++  privUser := USER.Group("/private")
++  privUser.Use(util.SecureAuth()) // middleware to secure all routes for this group
++  privUser.Get("/user", GetUserData)
}
Enter fullscreen mode Exit fullscreen mode

You can see that we are using our SecureAuth middleware for the private routes here, to make sure that every route from this group is secured.

Create a SignIn route

Now we will create a new function inside routr/user.go file that will handle the logging in for any user.

We will follow the following steps to log in a user:

  1. First, we will parse the input data into an input struct.
  2. Then, we will check whether or not any user exists according to the provided email or username.
  3. If it exists, then we will compare the password provided with the hashed password inside the database.
  4. If there is a match, we will then generate access and refresh tokens and set their cookies.
  5. Then we return the tokens.

So if we follow all these steps, our function will look like this:

// LoginUser route logins a user in the app
func LoginUser(c *fiber.Ctx) error {
    type LoginInput struct {
        Identity string `json:"identity"`
        Password string `json:"password"`
    }

    input := new(LoginInput)

    if err := c.BodyParser(input); err != nil {
        return c.JSON(fiber.Map{"error": true, "input": "Please review your input"})
    }

    // check if a user exists
    u := new(models.User)
    if res := db.DB.Where(
        &models.User{Email: input.Identity}).Or(
        &models.User{Username: input.Identity},
    ).First(&u); res.RowsAffected <= 0 {
        return c.JSON(fiber.Map{"error": true, "general": "Invalid Credentials."})
    }

    // Comparing the password with the hash
    if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(input.Password)); err != nil {
        return c.JSON(fiber.Map{"error": true, "general": "Invalid Credentials."})
    }

    // setting up the authorization cookies
    accessToken, refreshToken := util.GenerateTokens(u.UUID.String())
    accessCookie, refreshCookie := util.GetAuthCookies(accessToken, refreshToken)
    c.Cookie(accessCookie)
    c.Cookie(refreshCookie)

    return c.Status(fiber.StatusOK).JSON(fiber.Map{
        "access_token":  accessToken,
        "refresh_token": refreshToken,
    })
}
Enter fullscreen mode Exit fullscreen mode

Yay!, we have created both a SignUp route and a SignIn route successfully. But the work is still not over, we will now create a secure route from which a logged-in user can access their data.

Access user data with a secure route

Now, we will create a new function called GetUserData inside the router/user.go file. This function is only triggered when the request passes through our SecureAuth middleware successfully.

// GetUserData returns the details of the user signed in
func GetUserData(c *fiber.Ctx) error {
    id := c.Locals("id")

    u := new(models.User)
    if res := db.DB.Where("uuid = ?", id).First(&u); res.RowsAffected <= 0 {
        return c.JSON(fiber.Map{"error": true, "general": "Cannot find the User"})
    }

    return c.JSON(u)
}
Enter fullscreen mode Exit fullscreen mode

Refreshing Tokens

We know that each of our access tokens expires in 15 minutes. But we don't want our user to log in again and again after 15 minutes.
So this is where the refresh token comes into play. We need to refresh these access tokens. We can do this with the use of refresh tokens we made earlier. So now we have to make a new function called GetAccessToken that will refresh these access tokens.

This function will follow the following steps:

  1. Fetch the refresh token from the cookies of the request.
  2. Parse the refresh token inside a Claim.
  3. Check if the database contains the token belonging to that particular user.
  4. If the token exists in the database, validate that token.
  5. If the token is valid then generate a new access token and create a new cookie for that access token and return it.

So after following the about steps, our function will look like this:

// GetAccessToken generates and sends a new access token iff there is a valid refresh token
func GetAccessToken(c *fiber.Ctx) error {
    refreshToken := c.Cookies("refresh_token")

    refreshClaims := new(models.Claims)
    token, _ := jwt.ParseWithClaims(refreshToken, refreshClaims,
        func(token *jwt.Token) (interface{}, error) {
            return jwtKey, nil
        })

    if res := db.DB.Where(
        "expires_at = ? AND issued_at = ? AND issuer = ?",
        refreshClaims.ExpiresAt, refreshClaims.IssuedAt, refreshClaims.Issuer,
    ).First(&models.Claims{}); res.RowsAffected <= 0 {
        // no such refresh token exist in the database
        c.ClearCookie("access_token", "refresh_token")
        return c.SendStatus(fiber.StatusForbidden)
    }

    if token.Valid {
        if refreshClaims.ExpiresAt < time.Now().Unix() {
            // refresh token is expired
            c.ClearCookie("access_token", "refresh_token")
            return c.SendStatus(fiber.StatusForbidden)
        }
    } else {
        // malformed refresh token
        c.ClearCookie("access_token", "refresh_token")
        return c.SendStatus(fiber.StatusForbidden)
    }

    _, accessToken := util.GenerateAccessClaims(refreshClaims.Issuer)

    c.Cookie(&fiber.Cookie{
        Name:     "access_token",
        Value:    accessToken,
        Expires:  time.Now().Add(24 * time.Hour),
        HTTPOnly: true,
        Secure:   true,
    })

    return c.JSON(fiber.Map{"access_token": accessToken})
}
Enter fullscreen mode Exit fullscreen mode

So now our Go Authentication Boilerplate is complete. If you want to take a look at the source code, here's a GitHub Repository for that. If you like it, please give it a ⭐.

Conclusion

So this is it folks! This was my first experience writing a blog and giving back to a community that has given me so much more. This was a 3 Part series and I really enjoyed writing it.

Thanks for reading! If you liked this article, please let me know and share it!

💖 💪 🙅 🚩
mdfaizan7
Faizan

Posted on October 31, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related