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.
Search for a command to run...
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.
No comments yet. Be the first to comment.
Introduction Hosting your email server is the first step to secure your privacy It saves you money if you own multiple domains What You'll Need [ ] Brand new VPS or any Linux Server (My recommended VPS provider is Linode) [ ] Ubuntu 22.04 LTS i...
Introduction AWS has a MySQL compatible database but it's expensive and sometimes the cost doesn't justify the convenience we get from having such a service for personal projects or projects that don't make money. The alternative solution is to have ...
How to Setup EC2 for Dockerized Next.js Deployment Introduction Next.js is great serve-side oriented frontend framework that makes building SEO-friendly websites easier. Deploying it is trickier than it needs to be, I understand Vercel needs to make ...
Sometimes you need to allow for self-hosted subscription of your SaaS whether it's because your client has their own air-gapped system that is not connected to the internet or they want to host it in their on-premise servers. Having the option is alw...
I'm running windows 11 on a virtual machine to do few tests and I wanted to change the theme but in order to do that you need to activate your copy of Windows. I don't think it's feasible to buy a license to test few things in a virtual machine. If y...
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.
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.
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.
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.
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()
}
}
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
}
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.