Skip to main content

Command Palette

Search for a command to run...

It's Simple to Build Your Own Email/Password Authentication REST API

Updated
6 min read
M

Mahad loves building mobile and web applications and is here to take you on a journey, filled with bad decisions and learning from mistakes, through this blog.

Building a REST API for authentication can sound intimidating, but with the right tools and some guidance, it becomes quite simple. In this post, we will walk through how to build an email/password authentication system using Go and the Gin framework.

Setting Up the Gin Router

Let's initialize the go project first

go mod init example-auth-api
go mod tidy

To start, we need to set up a basic Gin router with endpoints for user sign-up and login.

// main.go
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    router.POST("/signup", SignUpUser)
    router.POST("/login", LoginUser)

    userGroup := router.Group("/users")
    userGroup.Use(AuthMiddleware())
    {
        userGroup.GET("/", UserProfile)
    }

    router.Run(":8080")
}

This code creates a simple web server that listens for incoming requests. Now let’s implement the sign-up and login logic.

Sign-Up Endpoint

Here’s the function to handle user sign-ups:

// handlers.go

func SignUpUser(c *gin.Context) {
    var user User

    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
        return
    }

    // check if a user with the current email exists
    existingUser, err := GetUserByEmail(user.Email)
    exists := err == nil || (err != nil && IsNotFoundError(err))
    if exists {
        c.JSON(http.StatusBadRequest, gin.H{"error": "User with this email already exists"})
        return
    }

    // hash the password
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), HASH_COST)
    if  err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to hash password"})
        return
    }

    user.Password = string(bytes)

    // save the user
    if err = CreateUserInDb(user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to create user"})
        return
    }

    c.JSON(http.StatusCreated, gin.H{
        "message": "User created successfully",
        "email": user.Email,
        "password": hashedPassword,
    })
}

This function processes incoming JSON data, hashes the password, and returns a success message. Always remember to hash passwords securely in a real application.

Login Endpoint

Next, we implement the login functionality:

// handlers.go

func LoginUser(c *gin.Context) {
    var user User

    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
        return
    }

    // Get user by email
    user, err := GetUserByEmail(user.Email)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
        return
    }

    if success := user.Authenticate(); success {
        if token, refresh, err := generateToken(user); err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
        } else {
            c.JSON(http.StatusOK, gin.H{
                "message": "Login successful",
                "token": token,
                "refreshToken": refresh,
            })
        }
    } else {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
    }
}

This endpoint verifies the user’s credentials and generates a token. In a production app, use a library to create secure JWT tokens.

Protecting Private Routes

In order to protect our private routes we need to use a middleware that keeps non-authenticated HTTP calls to protected routes. As an example, we have 1 protected route that returns the current user's details. Note: it's not recommended to return the full details in a user, this might include sensitive data such as password hash.

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"})
            c.Abort()
            return
        }

        tokenString := strings.TrimPrefix(authHeader, "Bearer ")
        token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
            return []byte(JWT_SECRET), nil
        })

        if err != nil || !token.Valid {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }

        claims, ok := token.Claims.(jwt.MapClaims)
        // check if claims are not a valid jwt.MapClaims, the user id is missing or the token is not for authentication.
        if !ok || claims["sub"] == nil || claims["typ"] != "authentication" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
            c.Abort()
            return
        }

        c.Set("user_id", claims["sub"])
        c.Next()
    }
}

Complete Example

Here’s the complete code for this simple API:

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

type User struct {
    ID       uint64   `json:"id"`
    Email    string `json:"email"`
    Password string `json:"password"`
}

func main() {
    router := gin.Default()

    router.POST("/signup", SignUpUser)
    router.POST("/login", LoginUser)

    userGroup := router.Group("/users")
    userGroup.Use(AuthMiddleware())
    {
        userGroup.GET("/", UserProfile)
    }

    router.Run(":8080")
}

func SignUpUser(c *gin.Context) {
    var user User

    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
        return
    }

    // check if a user with the current email exists
    existingUser, err := GetUserByEmail(user.Email)
    exists := err == nil || (err != nil && IsNotFoundError(err))
    if exists {
        c.JSON(http.StatusBadRequest, gin.H{"error": "User with this email already exists"})
        return
    }

    // hash the password
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), HASH_COST)
    if  err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to hash password"})
        return
    }

    user.Password = string(bytes)

    // save the user
    if err = CreateUserInDb(user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to create user"})
        return
    }

    c.JSON(http.StatusCreated, gin.H{
        "message": "User created successfully",
        "email": user.Email,
        "password": hashedPassword,
    })
}

func LoginUser(c *gin.Context) {
    var user User

    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
        return
    }

    // Get user by email
    user, err := GetUserByEmail(user.Email)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
        return
    }

    if success := user.Authenticate(); success {
        if token, refresh, err := generateToken(user); err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
        } else {
            c.JSON(http.StatusOK, gin.H{
                "message": "Login successful",
                "token": token,
                "refreshToken": refresh,
            })
        }
    } else {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
    }
}

func UserProfile(c *gin.Context) {
    // example of getting the user_id from the request context.
    id, ok := c.MustGet("user_id").(uint64)
    if !ok || id < 1 {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Access Denied!"})
        return
    }

    user, err := GetUserByID(id)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Access Denied!"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"user": user})
}

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"})
            c.Abort()
            return
        }

        tokenString := strings.TrimPrefix(authHeader, "Bearer ")
        token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
            return []byte(JWT_SECRET), nil
        })

        if err != nil || !token.Valid {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }

        claims, ok := token.Claims.(jwt.MapClaims)
        // check if claims are not a valid jwt.MapClaims, the user id is missing or the token is not for authentication.
        if !ok || claims["sub"] == nil || claims["typ"] != "authentication" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
            c.Abort()
            return
        }

        c.Set("user_id", claims["sub"])
        c.Next()
    }
}

func generateToken(u User) (tokenStr string, refreshStr string, err error) {
    now := time.Now()
    claims := jwt.MapClaims{
        "sub":  u.ID,
        "typ": "authentication",
        "aud":  JWT_AUD_NAME,
        "exp":  now.Add(TOKEN_EXPIRATION_DURATION).Unix(),
        "nbf":  now.Unix(),
        "iat":  now.Unix(),
        "iss":  JWT_ISSUER_NAME,
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
    tokenStr, err = token.SignedString([]byte(JWT_SECRET))
    if err != nil {
        return "", "", err
    }

    claims["typ"] = "refresh"
    claims["exp"] = now.Add(REFRESH_TOKEN_EXPIRATION_DURATION).Unix()

    refresh := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
    refreshStr, err = token.SignedString([]byte(JWT_SECRET))
    if err != nil {
        return "", "", err
    }

    return refreshStr, tokenStr, nil
}

Conclusion

This simple example shows how to build an email/password authentication REST API. You can expand this by integrating a secure database and implement the GetUserByID and GetUserByEmail functions. With these building blocks, you're well on your way to creating a full-featured authentication system.

Warning: Whatever you do, never try to build your own hashing function. You can also secure your API by using multiple layers of security, such as an OS-level firewall that only allows ports you want accessed from outside. Also, you need to set up trusted proxies using gin's built-in function router.SetTrustedProxies; this will reject requests that are coming from URLs that are not yours. Someone can still bypass this spoofed headers, but still, it helps you protect from script kiddies.

Please comment below if you are interested in expanding this to also add social authentication providers such as Google or Facebook, etc.

More from this blog

Tech with Mahad

9 posts

Mahad loves building mobile and web applications and is here to take you on a journey, filled with bad decisions and learning from mistakes, through this blog