Learning Golang - API Security, TLS, and Authentication
Episode 11 of 19

Learning Golang - API Security, TLS, and Authentication

This episode secures your Go API: TLS with certificates, JWT and auth middleware, the OAuth2 flow and sessions, and security practices like input validation, CORS, rate limiting, and security headers to make the API ready for the internet.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

The API you built in episode 10 is still plain: traffic is sent without encryption and anyone can access it. In episode 11, you will secure your Go API layer by layer, from transport to authorization.

Episode 11 covers four security areas: TLS for transport encryption, JWT as stateless authentication tokens, OAuth2 flows and sessions for integration with identity providers, and API security practices such as input validation, CORS, rate limiting, and security headers.

TLS and Certificates

Why HTTPS

Without TLS, all data — including credentials — is sent as plain text and can be read by anyone on the network path. HTTPS requires TLS encryption. For production, get a certificate from Let's Encrypt via certbot or automatically from a cloud platform.

Create a local certificate
openssl req -x509 -newkey rsa:4096 -keyout key.pem \
  -out cert.pem -days 365 -nodes -subj "/CN=localhost"

Serving HTTPS in net/http

http.Server can serve TLS with ListenAndServeTLS, using a certificate and private key:

HTTPS server
package main
 
import (
	"net/http"
)
 
func main() {
	srv := &http.Server{
		Addr:    ":8443",
		Handler: http.DefaultServeMux,
	}
 
	srv.ListenAndServeTLS("cert.pem", "key.pem")
}

In production, never commit private keys to the repository. Platforms like Kubernetes store them as secrets or use a certificate manager for automatic renewal.

Authentication with JWT

JWT Structure

JWT is a three-part JSON token — header, payload, and signature — that is signed so it can't be forged. The payload holds claims like sub for the subject and exp for the expiry. Popular package: github.com/golang-jwt/jwt/v5.

Add golang-jwt
go get github.com/golang-jwt/jwt/v5

Issuing and Verifying Tokens

Creating a JWT
package main
 
import (
	"fmt"
	"time"
 
	"github.com/golang-jwt/jwt/v5"
)
 
var rahasia = []byte("kunci-sangat-rahasia")
 
func buatToken(id string) (string, error) {
	klaim := jwt.RegisteredClaims{
		Subject:   id,
		ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, klaim)
	return token.SignedString(rahasia)
}
 
func main() {
	token, err := buatToken("42")
	if err != nil {
		panic(err)
	}
	fmt.Println(token)
}

The signature is built from the same secret used during verification. Store the secret in an environment variable or a secret manager, not in code.

Authorization Middleware

Token verification happens in middleware: read the Authorization header, check the Bearer format, parse it, and verify the signature before proceeding to the handler.

JWT middleware
func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		raw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
		token, err := jwt.Parse(raw, func(t *jwt.Token) (any, error) {
			return rahasia, nil
		})
		if err != nil || !token.Valid {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

Always limit the token lifetime, don't store tokens in localStorage for web applications, and use the right signature (HS256 for a single server, RS256 for distributed setups). To quickly test a generated token, decode the header and payload parts with jq -R 'split(".")[1] | @base64d' <<< "$TOKEN".

OAuth2 and Sessions

OAuth2 for Identity Delegation

OAuth2 lets users log in with their Google, GitHub, and other accounts without sharing passwords. The golang.org/x/oauth2 package manages the authorization code flow safely. The access token received is used to call the provider, and the refresh token extends the session.

Server-Based Sessions

For applications that need logout and revocation, server-based sessions fit better than JWT. Store sessions in Redis or a database with an expiry, and send the session ID through HttpOnly and Secure cookies. The HttpOnly cookie prevents access from JavaScript, reducing XSS risk.

Secure cookie
http.SetCookie(w, &http.Cookie{
	Name:     "session_id",
	Value:    sid,
	HttpOnly: true,
	Secure:   true,
	Path:     "/",
	SameSite: http.SameSiteStrictMode,
})

API Security Practices

Input Validation

Never trust user input. Validate every field — length, format, and type — before processing. Libraries like go-playground/validator simplify struct validation with tags:

Validation with tags
type LoginRequest struct {
	Email    string `validate:"required,email"`
	Password string `validate:"required,min=8"`
}

CORS and Rate Limiting

CORS restricts which domains may call the API from a browser. Rate limiting restricts the number of requests per client per time window to prevent abuse and brute force. For production, apply centralized rate limiting like Redis together with golang.org/x/time/rate or dedicated middleware.

Security Headers

Always send security headers on every response: Content-Security-Policy to restrict content sources, X-Content-Type-Options: nosniff to prevent MIME sniffing, and Strict-Transport-Security to enforce HTTPS. This combination dramatically raises an application's security level at almost zero cost.

Closing

Episode 11 secured your Go API from transport down to the application layer: TLS with ListenAndServeTLS, JWT with golang-jwt and authorization middleware, OAuth2 flows and secure cookie-based sessions, plus input validation, CORS, rate limiting, and security headers practices.

Key takeaways:

  • TLS encrypts transport; never send credentials without HTTPS.
  • JWT is a signed stateless token with an expiry.
  • Limit token TTLs and store secrets in a secret manager.
  • Server sessions make logout and revocation easy.
  • Validate all input; attach security headers to every response.
  • Rate limiting protects the API from abuse and brute force.

In the next episode we will discuss concurrency, context, and safe parallelism — goroutines, channels, and select as concurrency primitives, context.Context for cancellation and deadlines, and synchronization with sync.Mutex, WaitGroup, Once, and atomic operations. This is Go's most distinctive identity.