Back to home
engineering2026-04-209 min

Building Secure APIs in Go: Avoiding Vulnerabilities from the First Commit

A practical approach to building secure APIs in Go, covering authentication, secure cookies, middleware, and production-ready best practices.

Building Secure APIs in Go: Avoiding Common Vulnerabilities from the First Commit

Introduction

In many backend projects, security is often treated as an additional layer — something that will be "refactored later." In practice, this approach creates fragile systems that are difficult to scale and vulnerable to critical failures.

During the development of modern APIs — especially those handling authentication, sessions, and sensitive data — small technical decisions can introduce significant risks. Insecure cookies, inconsistent validation, poorly structured authentication, and missing protection middleware are among the most common issues.

With the growing adoption of Go for backend services — particularly for high-performance APIs — there is an increasing need to structure secure services from the beginning.

In this article, I present a practical approach to building secure APIs in Go, focusing on authentication, secure cookies, and architecture best practices.

The goal is not just to show code, but to demonstrate the engineering reasoning behind technical decisions.


The Real Problem

One of the most common implementations in APIs involves login authentication and session storage using cookies.

A simple example in Go using Gin:

func Login(c *gin.Context) {
	var body struct {
		Email string `json:"email"`
		Password string `json:"password"`
	}

	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(400, gin.H{"error": "invalid payload"})
		return
	}

	// Mock authentication
	if body.Email == "admin@email.com" && body.Password == "123456" {
		c.SetCookie("session", "token", 3600, "/", "localhost", false, true)
		c.JSON(200, gin.H{"message": "logged"})
		return
	}

	c.JSON(401, gin.H{"error": "invalid credentials"})
}

Although functional, this code contains several issues.

Common Vulnerabilities

  • Cookie not protected in production
  • Missing proper HttpOnly configuration
  • No SameSite policy defined
  • No HTTPS enforcement
  • No secure expiration handling
  • No real token or signature

This type of implementation is common in early-stage projects but becomes dangerous in real production environments.


The Technical Solution

A more professional approach involves structuring authentication with:

  • Secure JWT
  • Protected cookies
  • Environment-based configuration
  • Authentication middleware
  • Controlled expiration

First, define whether the application is running in production:

secure := os.Getenv("ENV") == "production"

This allows adapting security behavior depending on the environment.

Now, a more secure login implementation:

func Login(c *gin.Context) {
	var body struct {
		Email string `json:"email"`
		Password string `json:"password"`
	}

	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(400, gin.H{"error": "invalid payload"})
		return
	}

	if body.Email != "admin@email.com" || body.Password != "123456" {
		c.JSON(401, gin.H{"error": "invalid credentials"})
		return
	}

	token := "secure-token-example"

	secure := os.Getenv("ENV") == "production"

	c.SetCookie(
		"session",
		token,
		3600,
		"/",
		"",
		secure,
		true,
	)

	c.JSON(200, gin.H{
		"message": "authenticated",
	})
}

Improvements Applied

  • HttpOnly cookie
  • Secure cookie in production
  • Defined expiration
  • Token separated from authentication logic
  • More predictable and maintainable code

But we can still improve further.


Authentication Middleware

A professional API should automatically protect routes.

Creating middleware:

func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		token, err := c.Cookie("session")

		if err != nil || token == "" {
			c.AbortWithStatusJSON(401, gin.H{
				"error": "unauthorized",
			})
			return
		}

		// Token validation example
		if token != "secure-token-example" {
			c.AbortWithStatusJSON(401, gin.H{
				"error": "invalid token",
			})
			return
		}

		c.Next()
	}
}

Now protecting routes:

r := gin.Default()

auth := r.Group("/api")
auth.Use(AuthMiddleware())

auth.GET("/profile", func(c *gin.Context) {
	c.JSON(200, gin.H{
		"user": "authenticated",
	})
})

This approach makes security scalable and maintainable.


Best Practices Discovered

During implementation, several best practices became evident:

1. Security Should Not Be Optional

Projects that start insecure rarely get properly refactored later.

2. Separate Authentication from Business Logic

This prevents duplication and improves maintainability.

3. Environment-Based Configuration

Development and production environments have different requirements.

4. Middleware Is Essential

Allows consistent security enforcement across routes.

5. Treat Tokens as Sensitive Data

Never expose tokens in logs or unnecessary responses.


Mistakes Avoided

Some common mistakes were avoided with this approach:

  • Storing tokens in LocalStorage
  • Cookies without HttpOnly
  • No expiration verification
  • Manual authentication in every route
  • Predictable tokens

These problems are common in early-stage APIs and can quickly compromise systems.


Real-World Application

This structure can be applied to:

  • SaaS APIs
  • Internal systems
  • Admin dashboards
  • Subscription platforms
  • Multi-user systems

Practical benefits:

Security

Protection against XSS attacks and session hijacking.

Scalability

Middleware allows expansion without rewriting logic.

Performance

Go maintains low latency even with validation layers.

Maintainability

Modular code structure simplifies evolution.


Architecture Evolution

A more advanced version may include:

  • Signed JWT
  • Refresh tokens
  • Rate limiting
  • Structured logging
  • Login auditing
  • Token rotation

JWT example:

token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
	"user": user.ID,
	"exp": time.Now().Add(time.Hour * 24).Unix(),
})

This makes the system significantly more robust.


Impact on Professional Development

Implementing security from the start changes how we think about software.

Instead of writing only functional code, we begin to:

  • Think about threats
  • Design resilient architectures
  • Build scalable systems
  • Avoid technical debt

This mindset differentiates developers who build features from those who build real products.


Conclusion

Building secure APIs is not just a concern for large companies. It is an essential skill for any software engineer aiming to develop reliable systems.

By structuring authentication with secure cookies, middleware, and proper validation, we create APIs that are resilient, secure, and production-ready.

More than implementing security, the goal is to develop an engineering mindset — where every technical decision considers impact, risk, and scalability.

This approach has guided my backend projects, particularly in building secure APIs and reliable architectures.

In future projects, the natural evolution includes:

  • Multi-tenant systems
  • Distributed authentication
  • Event-driven architectures
  • Infrastructure-level security

Building secure software from the first commit is not just a best practice — it is a strategic decision.

And in software engineering, strategic decisions are what truly differentiate ordinary systems from solid products.

GoGolangSecurityBackendAPIAuthenticationEngineering