It's Simple to Build Your Own Email/Password Authentication REST API
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.