Table of Contents
- Why Go for Authentication Services
- Project Setup and Dependencies
- User Model and Password Hashing
- Registration and Login Handlers
- JWT Authentication and Middleware
- Session Management Patterns
- Integrating H33 Biometric Authentication
- Post-Quantum Security with H33
- Rate Limiting and Brute-Force Protection
- Production Deployment
- Security Best Practices Checklist
Go has become the default language for backend infrastructure. Kubernetes, Docker, Terraform, Prometheus — the tools that run the modern internet are written in Go. Authentication services are no different. Go's goroutine-per-request concurrency model, static binary compilation, and strong standard library make it an ideal choice for building auth services that need to handle thousands of concurrent requests with predictable latency.
But building authentication correctly in Go requires more than spinning up an HTTP server and hashing some passwords. The threat landscape in 2026 includes credential stuffing attacks testing billions of leaked credentials, harvest-now-decrypt-later campaigns by nation-state actors, and an accelerating timeline toward cryptographically relevant quantum computers. Your authentication system needs to be secure against both classical and quantum adversaries.
This tutorial takes a layered approach. We start with the fundamentals — project structure, password hashing with bcrypt, JWT issuance and validation, and HTTP middleware patterns. Then we layer on biometric authentication and post-quantum cryptography through H33's API. Every code example is complete and runnable. No pseudo-code, no hand-waving.
1. Why Go for Authentication Services
Go brings specific advantages to authentication workloads that other languages struggle to match:
Goroutine Concurrency
Each incoming authentication request gets its own goroutine — a lightweight thread that costs roughly 4 KB of stack space. A single Go process can handle tens of thousands of concurrent auth requests without thread pool tuning or callback hell. This is critical for services that proxy to external APIs like H33.
Static Binary Deployment
CGO_ENABLED=0 go build produces a single binary with zero runtime dependencies. No JVM, no Node.js runtime, no Python interpreter. Your auth service deploys as a scratch Docker image under 15 MB. Fewer dependencies means fewer CVEs in your container image.
Context Propagation
Go's context.Context flows through every layer of your auth stack — from the HTTP handler, through middleware, into database queries and external API calls. Timeouts propagate automatically. If a user's request is cancelled, every downstream operation aborts cleanly.
Strong Standard Library
net/http, crypto/bcrypt, crypto/subtle, encoding/json — Go ships with production-grade implementations of every primitive you need for authentication. No npm install required for the basics.
Python and Node.js are fine for prototyping auth flows. But at scale — thousands of concurrent bcrypt hashes, JWT validations, and external API calls — Go's goroutine scheduler and compiled performance deliver 5-10x throughput improvements over interpreted languages. When you add H33 API calls with sub-millisecond latency requirements, that overhead matters. See our Node.js tutorial and Rust tutorial for language-specific comparisons.
2. Project Setup and Dependencies
Start by initializing a Go module and installing the dependencies we will use throughout this tutorial. We use chi as our HTTP router (lightweight, net/http compatible, middleware-friendly), golang-jwt/jwt for JWT operations, and pgx for PostgreSQL.
# Create project directory mkdir go-auth-h33 && cd go-auth-h33 go mod init github.com/yourorg/go-auth-h33 # HTTP router + middleware go get github.com/go-chi/chi/v5 go get github.com/go-chi/chi/v5/middleware go get github.com/go-chi/cors # JWT go get github.com/golang-jwt/jwt/v5 # Password hashing go get golang.org/x/crypto/bcrypt # Database go get github.com/jackc/pgx/v5/pgxpool # Environment and config go get github.com/joho/godotenv # UUID generation go get github.com/google/uuid
go-auth-h33/
cmd/
server/
main.go # Entrypoint
internal/
config/
config.go # Environment and configuration
models/
user.go # User model + password hashing
handlers/
auth.go # Registration, login, logout
h33.go # H33 biometric enrollment + verification
middleware/
jwt.go # JWT validation middleware
ratelimit.go # Rate limiting middleware
store/
postgres.go # Database access layer
.env
go.mod
go.sum
Dockerfile
PORT=8080 DATABASE_URL=postgres://auth_user:password@localhost:5432/auth_db?sslmode=disable JWT_SECRET=your-256-bit-secret-replace-in-production JWT_EXPIRY=15m REFRESH_TOKEN_EXPIRY=168h BCRYPT_COST=12 H33_API_KEY=your-h33-api-key H33_BASE_URL=https://api.h33.ai/v1
Add .env to your .gitignore immediately. In production, inject secrets through environment variables, a secrets manager (AWS Secrets Manager, Vault), or your container orchestrator's secret store. Never embed API keys or JWT secrets in source code.
Now let us set up the configuration loader and the application entrypoint.
package config import ( "os" "strconv" "time" ) type Config struct { Port string DatabaseURL string JWTSecret []byte JWTExpiry time.Duration RefreshTokenExpiry time.Duration BcryptCost int H33APIKey string H33BaseURL string } func Load() *Config { jwtExpiry, _ := time.ParseDuration(getEnv("JWT_EXPIRY", "15m")) refreshExpiry, _ := time.ParseDuration(getEnv("REFRESH_TOKEN_EXPIRY", "168h")) bcryptCost, _ := strconv.Atoi(getEnv("BCRYPT_COST", "12")) return &Config{ Port: getEnv("PORT", "8080"), DatabaseURL: os.Getenv("DATABASE_URL"), JWTSecret: []byte(os.Getenv("JWT_SECRET")), JWTExpiry: jwtExpiry, RefreshTokenExpiry: refreshExpiry, BcryptCost: bcryptCost, H33APIKey: os.Getenv("H33_API_KEY"), H33BaseURL: getEnv("H33_BASE_URL", "https://api.h33.ai/v1"), } } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }
package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/joho/godotenv" "github.com/yourorg/go-auth-h33/internal/config" "github.com/yourorg/go-auth-h33/internal/handlers" authMW "github.com/yourorg/go-auth-h33/internal/middleware" ) func main() { _ = godotenv.Load() cfg := config.Load() r := chi.NewRouter() // Global middleware r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(30 * time.Second)) r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"https://yourdomain.com"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowedHeaders: []string{"Authorization", "Content-Type"}, AllowCredentials: true, MaxAge: 300, })) auth := handlers.NewAuthHandler(cfg) // Public routes r.Post("/api/auth/register", auth.Register) r.Post("/api/auth/login", auth.Login) r.Post("/api/auth/refresh", auth.RefreshToken) // Protected routes r.Group(func(r chi.Router) { r.Use(authMW.JWTAuth(cfg.JWTSecret)) r.Get("/api/auth/me", auth.Me) r.Post("/api/auth/logout", auth.Logout) r.Post("/api/auth/h33/enroll", auth.H33Enroll) r.Post("/api/auth/h33/verify", auth.H33Verify) }) // Health check r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"ok"}`)) }) srv := &http.Server{ Addr: ":" + cfg.Port, Handler: r, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, } // Graceful shutdown go func() { log.Printf("Server starting on :%s", cfg.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server failed: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() srv.Shutdown(ctx) log.Println("Server stopped gracefully") }
Several things to note here. The middleware.Timeout sets a 30-second deadline on every request context. The ReadTimeout and WriteTimeout on the server itself guard against slowloris attacks. The graceful shutdown block ensures in-flight authentication requests complete before the process exits — critical for services behind a load balancer.
3. User Model and Password Hashing
The user model is the foundation of your auth system. Get password hashing wrong and everything else is meaningless. Go's golang.org/x/crypto/bcrypt package provides a battle-tested implementation that handles salt generation, cost factor tuning, and constant-time comparison.
package models import ( "time" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) type User struct { ID uuid.UUID `json:"id" db:"id"` Email string `json:"email" db:"email"` PasswordHash string `json:"-" db:"password_hash"` H33UserID *string `json:"h33_user_id,omitempty" db:"h33_user_id"` H33Enrolled bool `json:"h33_enrolled" db:"h33_enrolled"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` LastLoginAt *time.Time `json:"last_login_at,omitempty" db:"last_login_at"` FailedAttempts int `json:"-" db:"failed_attempts"` LockedUntil *time.Time `json:"-" db:"locked_until"` } // HashPassword generates a bcrypt hash with the configured cost factor. // Cost 12 takes ~250ms on modern hardware — slow enough to resist // brute-force attacks, fast enough for acceptable UX. func HashPassword(password string, cost int) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost) if err != nil { return "", err } return string(bytes), nil } // CheckPassword performs a constant-time comparison between // the provided password and the stored bcrypt hash. func CheckPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } // IsLocked returns true if the account is currently locked // due to too many failed login attempts. func (u *User) IsLocked() bool { if u.LockedUntil == nil { return false } return time.Now().Before(*u.LockedUntil) }
Why bcrypt Cost 12?
The bcrypt cost factor is a power of 2. Cost 10 (the default) takes roughly 65 ms. Cost 12 takes roughly 250 ms. Cost 14 takes roughly 1 second. You want hashing to be slow enough that an attacker with a GPU cluster cannot brute-force hashes economically, but fast enough that your users do not notice a delay on login. Cost 12 hits the sweet spot for 2026 hardware. Revisit this annually as hardware gets faster.
Note the json:"-" tag on PasswordHash. This ensures the hash is never accidentally serialized into an API response. Defense in depth starts at the struct level.
Database Schema
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, h33_user_id TEXT, h33_enrolled BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), last_login_at TIMESTAMPTZ, failed_attempts INTEGER DEFAULT 0, locked_until TIMESTAMPTZ ); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_h33_user_id ON users(h33_user_id);
4. Registration and Login Handlers
Authentication handlers in Go follow a consistent pattern: parse input, validate, perform the operation, and return a structured JSON response. Error handling in Go is explicit — no exceptions, no try-catch. Every error is checked. This verbosity is a feature, not a bug, in security-critical code.
package handlers import ( "encoding/json" "net/http" "strings" "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/yourorg/go-auth-h33/internal/config" "github.com/yourorg/go-auth-h33/internal/models" "github.com/yourorg/go-auth-h33/internal/store" ) type AuthHandler struct { cfg *config.Config store *store.PostgresStore } func NewAuthHandler(cfg *config.Config) *AuthHandler { return &AuthHandler{cfg: cfg} } type RegisterRequest struct { Email string `json:"email"` Password string `json:"password"` } type LoginRequest struct { Email string `json:"email"` Password string `json:"password"` } type AuthResponse struct { Token string `json:"token"` RefreshToken string `json:"refresh_token"` ExpiresAt time.Time `json:"expires_at"` User models.User `json:"user"` } func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { var req RegisterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Validate email format if !isValidEmail(req.Email) { respondError(w, http.StatusBadRequest, "Invalid email format") return } // Enforce password strength if len(req.Password) < 12 { respondError(w, http.StatusBadRequest, "Password must be at least 12 characters") return } // Hash password with configured bcrypt cost hash, err := models.HashPassword(req.Password, h.cfg.BcryptCost) if err != nil { respondError(w, http.StatusInternalServerError, "Registration failed") return } user := models.User{ ID: uuid.New(), Email: strings.ToLower(strings.TrimSpace(req.Email)), PasswordHash: hash, CreatedAt: time.Now(), UpdatedAt: time.Now(), } // Insert user — unique constraint on email handles duplicates if err := h.store.CreateUser(r.Context(), &user); err != nil { if strings.Contains(err.Error(), "duplicate") { respondError(w, http.StatusConflict, "Email already registered") return } respondError(w, http.StatusInternalServerError, "Registration failed") return } // Issue JWT token, expiresAt, err := h.issueToken(user.ID, user.Email) if err != nil { respondError(w, http.StatusInternalServerError, "Token generation failed") return } respondJSON(w, http.StatusCreated, AuthResponse{ Token: token, ExpiresAt: expiresAt, User: user, }) }
The login handler follows the same pattern but adds account lockout logic and constant-time password comparison. Notice we always return the same generic error message for both "user not found" and "wrong password" — this prevents user enumeration attacks.
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } user, err := h.store.GetUserByEmail(r.Context(), req.Email) if err != nil { // Same error for not-found and wrong-password // prevents user enumeration respondError(w, http.StatusUnauthorized, "Invalid credentials") return } // Check account lockout if user.IsLocked() { respondError(w, http.StatusTooManyRequests, "Account locked. Try again later.") return } // Constant-time password comparison via bcrypt if !models.CheckPassword(req.Password, user.PasswordHash) { // Increment failed attempts h.store.IncrementFailedAttempts(r.Context(), user.ID) if user.FailedAttempts+1 >= 5 { lockUntil := time.Now().Add(15 * time.Minute) h.store.LockAccount(r.Context(), user.ID, lockUntil) } respondError(w, http.StatusUnauthorized, "Invalid credentials") return } // Reset failed attempts on successful login h.store.ResetFailedAttempts(r.Context(), user.ID) // Issue JWT token, expiresAt, err := h.issueToken(user.ID, user.Email) if err != nil { respondError(w, http.StatusInternalServerError, "Token generation failed") return } now := time.Now() h.store.UpdateLastLogin(r.Context(), user.ID, now) respondJSON(w, http.StatusOK, AuthResponse{ Token: token, ExpiresAt: expiresAt, User: *user, }) }
Never return different error messages for "user not found" vs "wrong password." An attacker can use the difference to enumerate valid email addresses. Always return the same generic "Invalid credentials" for both cases. Apply the same bcrypt comparison even when the user does not exist (using a dummy hash) to prevent timing-based enumeration.
5. JWT Authentication and Middleware
JSON Web Tokens are the standard for stateless authentication in Go APIs. The golang-jwt/jwt library (the community-maintained successor to dgrijalva/jwt-go) provides secure JWT creation and validation. We use HMAC-SHA256 for symmetric signing, which is appropriate when the same service issues and validates tokens. For microservice architectures where different services validate tokens, switch to RS256 or ES256 (asymmetric).
type Claims struct { UserID uuid.UUID `json:"user_id"` Email string `json:"email"` jwt.RegisteredClaims } func (h *AuthHandler) issueToken( userID uuid.UUID, email string, ) (string, time.Time, error) { expiresAt := time.Now().Add(h.cfg.JWTExpiry) claims := Claims{ UserID: userID, Email: email, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "go-auth-h33", Subject: userID.String(), ID: uuid.New().String(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString(h.cfg.JWTSecret) if err != nil { return "", time.Time{}, err } return signed, expiresAt, nil }
The JWT middleware extracts the token from the Authorization: Bearer header, validates it, and injects the claims into the request context. Downstream handlers access the authenticated user via a type-safe context key.
package middleware import ( "context" "net/http" "strings" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) type contextKey string const UserContextKey contextKey = "user_claims" type UserClaims struct { UserID uuid.UUID Email string } func JWTAuth(secret []byte) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { http.Error(w, `{"error":"Missing authorization header"}`, http.StatusUnauthorized) return } parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { http.Error(w, `{"error":"Invalid authorization format"}`, http.StatusUnauthorized) return } token, err := jwt.Parse(parts[1], func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } return secret, nil }) if err != nil || !token.Valid { http.Error(w, `{"error":"Invalid or expired token"}`, http.StatusUnauthorized) return } claims, ok := token.Claims.(jwt.MapClaims) if !ok { http.Error(w, `{"error":"Invalid token claims"}`, http.StatusUnauthorized) return } userID, _ := uuid.Parse(claims["user_id"].(string)) uc := UserClaims{ UserID: userID, Email: claims["email"].(string), } ctx := context.WithValue(r.Context(), UserContextKey, uc) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // GetUser extracts the authenticated user from the request context. func GetUser(ctx context.Context) (UserClaims, bool) { uc, ok := ctx.Value(UserContextKey).(UserClaims) return uc, ok }
Key Decisions in the JWT Middleware
Signing method validation: We explicitly check that the token uses HMAC (SigningMethodHMAC). Without this check, an attacker could submit a token signed with the "none" algorithm, and the parser would accept it. This is a well-known JWT vulnerability (CVE-2015-9235). The golang-jwt library mitigates this by default, but the explicit check is defense in depth.
Custom context key type: We use a private contextKey type instead of a bare string. This prevents collisions with other middleware that might also store values in context. It is an idiomatic Go pattern for context values.
6. Session Management Patterns
JWTs handle stateless authentication, but most production systems also need session management for token refresh, logout (token invalidation), and device tracking. Go gives you two clean options: server-side sessions with Redis, or a token-pair pattern (short-lived access token + long-lived refresh token stored in the database).
Token Pair Pattern
The approach we use in this tutorial: a 15-minute JWT access token paired with a 7-day refresh token stored in PostgreSQL. When the access token expires, the client submits the refresh token to get a new pair. When the user logs out, we delete the refresh token from the database — instant invalidation.
type RefreshRequest struct { RefreshToken string `json:"refresh_token"` } func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { var req RefreshRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Look up refresh token in database session, err := h.store.GetRefreshToken(r.Context(), req.RefreshToken) if err != nil || session.ExpiresAt.Before(time.Now()) { respondError(w, http.StatusUnauthorized, "Invalid refresh token") return } // Delete old refresh token (rotate on every use) h.store.DeleteRefreshToken(r.Context(), req.RefreshToken) // Issue new token pair token, expiresAt, _ := h.issueToken(session.UserID, session.Email) newRefresh := uuid.New().String() h.store.StoreRefreshToken(r.Context(), newRefresh, session.UserID, session.Email, time.Now().Add(h.cfg.RefreshTokenExpiry)) respondJSON(w, http.StatusOK, AuthResponse{ Token: token, RefreshToken: newRefresh, ExpiresAt: expiresAt, }) } func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { uc, _ := authMW.GetUser(r.Context()) // Delete all refresh tokens for this user h.store.DeleteAllRefreshTokens(r.Context(), uc.UserID) respondJSON(w, http.StatusOK, map[string]string{ "message": "Logged out successfully", }) }
Every time a refresh token is used, we delete it and issue a new one. This is called refresh token rotation and it limits the blast radius of a stolen refresh token. If an attacker steals a refresh token and uses it, the legitimate user's next refresh attempt will fail (because the token was already consumed), alerting them to the compromise. Without rotation, a stolen refresh token is valid for its entire 7-day lifetime.
7. Integrating H33 Biometric Authentication
Classical password + JWT authentication is a solid baseline, but it is not quantum-resistant and it is vulnerable to credential theft. H33's API adds a biometric authentication layer that operates on fully homomorphic encrypted (FHE) biometric templates — your users' biometric data is never exposed in plaintext, not even to H33's servers. Every authentication is attested with post-quantum digital signatures.
The integration follows a two-step pattern: enroll (one-time, capture and store the encrypted biometric template) and verify (on every login, compare a fresh biometric sample against the stored template).
H33 Authentication Pipeline
Each H33 API call executes a three-stage pipeline in a single request: (1) FHE-encrypted biometric comparison at ~50 microseconds per auth, (2) zero-knowledge proof validation via STARK lookup, and (3) post-quantum attestation with CRYSTALS-Dilithium signatures. The result: 1.2 million authentications per second with full post-quantum security. See our benchmarks page for independently verifiable numbers.
H33 Client Setup
package handlers import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) type H33Client struct { baseURL string apiKey string httpClient *http.Client } func NewH33Client(baseURL, apiKey string) *H33Client { return &H33Client{ baseURL: baseURL, apiKey: apiKey, httpClient: &http.Client{ Timeout: 10 * time.Second, }, } } func (c *H33Client) do( ctx context.Context, method, path string, body interface{}, ) ([]byte, error) { var reqBody io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } reqBody = bytes.NewReader(b) } req, err := http.NewRequestWithContext( ctx, method, c.baseURL+path, reqBody) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+c.apiKey) req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("h33 request failed: %w", err) } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) if resp.StatusCode >= 400 { return nil, fmt.Errorf("h33 error %d: %s", resp.StatusCode, string(data)) } return data, nil }
Biometric Enrollment
Enrollment happens once per user, typically after traditional registration. The biometric template (from a face scan, fingerprint, or voice sample captured on the client device) is sent to H33 where it is encrypted under FHE and stored. The plaintext biometric data never persists.
type EnrollRequest struct { BiometricTemplate string `json:"biometric_template"` } type H33EnrollResponse struct { UserID string `json:"user_id"` Enrolled bool `json:"enrolled"` Attestation string `json:"attestation"` } func (h *AuthHandler) H33Enroll(w http.ResponseWriter, r *http.Request) { uc, ok := authMW.GetUser(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Not authenticated") return } var req EnrollRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Call H33 enrollment API data, err := h.h33Client.do(r.Context(), "POST", "/auth/enroll", map[string]string{ "external_id": uc.UserID.String(), "biometric_template": req.BiometricTemplate, }) if err != nil { respondError(w, http.StatusBadGateway, "Biometric enrollment failed") return } var resp H33EnrollResponse json.Unmarshal(data, &resp) // Store H33 user ID in our database h.store.UpdateH33UserID(r.Context(), uc.UserID, resp.UserID) respondJSON(w, http.StatusOK, resp) }
Biometric Verification
On subsequent logins, after the user passes password authentication, the client submits a fresh biometric sample for verification. H33 compares the encrypted sample against the enrolled template using FHE — the comparison happens in the encrypted domain. The response includes a post-quantum attestation signature proving the verification result has not been tampered with.
type VerifyRequest struct { BiometricSample string `json:"biometric_sample"` } type H33VerifyResponse struct { Match bool `json:"match"` Confidence float64 `json:"confidence"` LatencyUS int64 `json:"latency_us"` Attestation string `json:"attestation"` PQSecure bool `json:"pq_secure"` } func (h *AuthHandler) H33Verify(w http.ResponseWriter, r *http.Request) { uc, ok := authMW.GetUser(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Not authenticated") return } // Get H33 user ID from our database user, err := h.store.GetUserByID(r.Context(), uc.UserID) if err != nil || user.H33UserID == nil { respondError(w, http.StatusBadRequest, "Biometric not enrolled. Call /h33/enroll first.") return } var req VerifyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Call H33 verification API data, err := h.h33Client.do(r.Context(), "POST", "/auth/verify", map[string]string{ "user_id": *user.H33UserID, "biometric_sample": req.BiometricSample, }) if err != nil { respondError(w, http.StatusBadGateway, "Biometric verification failed") return } var resp H33VerifyResponse json.Unmarshal(data, &resp) if !resp.Match { respondError(w, http.StatusUnauthorized, "Biometric verification failed") return } // Biometric verified — issue a new token with elevated privileges token, expiresAt, _ := h.issueToken(uc.UserID, uc.Email) respondJSON(w, http.StatusOK, map[string]interface{}{ "verified": true, "token": token, "expires_at": expiresAt, "latency_us": resp.LatencyUS, "pq_secure": resp.PQSecure, "attestation": resp.Attestation, }) }
Traditional biometric systems decrypt the stored template to perform comparison. If the server is compromised, biometric data is exposed — and unlike passwords, biometrics cannot be changed. H33's FHE-based approach performs the comparison on encrypted data. The server never sees the plaintext biometric. Even a complete database breach yields nothing but ciphertexts that are computationally indistinguishable from random noise. For a deeper technical treatment, see our FHE Biometric Authentication guide.
8. Post-Quantum Security with H33
Every JWT you sign today with HS256 or RS256 will be forgeable by a sufficiently powerful quantum computer. Shor's algorithm breaks RSA and ECC in polynomial time. HMAC-SHA256 is more resistant (Grover's algorithm provides only a quadratic speedup, reducing 256-bit HMAC to effectively 128-bit security), but the tokens it protects are transmitted over TLS sessions that use key exchanges vulnerable to quantum attack.
This is the harvest-now, decrypt-later threat. Nation-state adversaries are recording encrypted traffic today, warehousing it until quantum computers are capable enough to crack TLS key exchanges and retroactively decrypt session data — including your authentication tokens.
NIST estimates cryptographically relevant quantum computers could arrive by the early 2030s. If your authentication tokens have long-term value (session recordings, signed documents, audit logs), they are already vulnerable. The time to migrate is before the threat materializes, not after. See our quantum computing threat timeline for the latest estimates.
Post-Quantum Token Attestation
H33's verification response includes a attestation field — a CRYSTALS-Dilithium digital signature over the verification result. This is a NIST-standardized post-quantum signature (FIPS 204, ML-DSA) that remains secure against both classical and quantum adversaries. You can verify this attestation independently to prove that the biometric verification was performed by H33 and was not tampered with in transit.
# The attestation field from the verify response can be # independently checked against H33's public verification key curl -X POST https://api.h33.ai/v1/attestation/verify \ -H "Authorization: Bearer $H33_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "attestation": "base64_attestation_from_verify_response", "expected_result": { "match": true, "user_id": "h33_user_id_here" } }' # Response { "valid": true, "algorithm": "ML-DSA-65", "nist_standard": "FIPS 204", "pq_secure": true }
Comparison: Classical vs Post-Quantum Auth
| Property | Password + JWT (Classical) | Password + JWT + H33 (Post-Quantum) |
|---|---|---|
| Authentication factor | Knowledge (password) | Knowledge + Inherence (biometric) |
| Credential theft | Phishable, reusable | Biometric cannot be phished or reused |
| Quantum resistance | None (RSA/ECC broken by Shor) | Dilithium attestation (FIPS 204) |
| Biometric privacy | N/A | FHE-encrypted, never exposed |
| Verification proof | None | ZK-STARK proof + PQ attestation |
| Per-auth latency | ~250ms (bcrypt) | ~50us (H33) + ~250ms (bcrypt) |
9. Rate Limiting and Brute-Force Protection
Authentication endpoints are the most targeted surfaces in any application. Without rate limiting, an attacker can attempt millions of password guesses per hour. Go's concurrency model makes it easy to handle high request volumes, but that same capability means you need explicit throttling on auth endpoints.
Token Bucket Rate Limiter
We implement a per-IP rate limiter using Go's golang.org/x/time/rate package. This uses a token bucket algorithm: each IP gets a bucket that refills at a steady rate. Requests consume tokens. When the bucket is empty, requests are rejected with 429 Too Many Requests.
package middleware import ( "net/http" "sync" "time" "golang.org/x/time/rate" ) type RateLimiter struct { visitors map[string]*visitor mu sync.RWMutex rate rate.Limit burst int } type visitor struct { limiter *rate.Limiter lastSeen time.Time } func NewRateLimiter(rps float64, burst int) *RateLimiter { rl := &RateLimiter{ visitors: make(map[string]*visitor), rate: rate.Limit(rps), burst: burst, } // Clean up stale entries every minute go rl.cleanup() return rl } func (rl *RateLimiter) getVisitor(ip string) *rate.Limiter { rl.mu.Lock() defer rl.mu.Unlock() v, exists := rl.visitors[ip] if !exists { limiter := rate.NewLimiter(rl.rate, rl.burst) rl.visitors[ip] = &visitor{limiter, time.Now()} return limiter } v.lastSeen = time.Now() return v.limiter } func (rl *RateLimiter) cleanup() { for { time.Sleep(time.Minute) rl.mu.Lock() for ip, v := range rl.visitors { if time.Since(v.lastSeen) > 3*time.Minute { delete(rl.visitors, ip) } } rl.mu.Unlock() } } func (rl *RateLimiter) Limit(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip := r.RemoteAddr limiter := rl.getVisitor(ip) if !limiter.Allow() { http.Error(w, `{"error":"Rate limit exceeded. Try again later."}`, http.StatusTooManyRequests) return } next.ServeHTTP(w, r) }) }
Apply different rate limits to different endpoints. Login endpoints should be more restrictive than general API endpoints:
// In main(), before route definitions: authLimiter := middleware.NewRateLimiter(5, 10) // 5 req/sec, burst 10 apiLimiter := middleware.NewRateLimiter(50, 100) // 50 req/sec, burst 100 // Apply strict rate limiting to auth endpoints r.With(authLimiter.Limit).Post("/api/auth/login", auth.Login) r.With(authLimiter.Limit).Post("/api/auth/register", auth.Register) // Less restrictive for general API r.With(apiLimiter.Limit).Group(func(r chi.Router) { r.Use(authMW.JWTAuth(cfg.JWTSecret)) r.Get("/api/auth/me", auth.Me) // ... other protected routes })
10. Production Deployment
Go's static binary compilation makes deployment straightforward. A multi-stage Docker build produces a minimal image with just the binary — no build tools, no source code, no attack surface.
# Stage 1: Build FROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build \ -ldflags="-s -w" \ -o /auth-server ./cmd/server # Stage 2: Run FROM scratch COPY --from=builder /etc/ssl/certs/ca-certificates.crt \ /etc/ssl/certs/ COPY --from=builder /auth-server /auth-server EXPOSE 8080 ENTRYPOINT ["/auth-server"]
The scratch base image contains literally nothing — no shell, no package manager, no libc. Your Go binary (compiled with CGO_ENABLED=0) is completely self-contained. The result is a Docker image under 15 MB with zero CVEs from base image dependencies. We copy in CA certificates so the binary can make HTTPS calls to H33's API.
Production Checklist
Always terminate TLS at your load balancer or reverse proxy (nginx, Caddy, ALB). Never run Go's ListenAndServeTLS directly in production — let a dedicated TLS terminator handle certificate rotation and OCSP stapling.
The /health endpoint returns 200 OK for load balancer health checks. In production, extend this to ping your database and H33 API to verify downstream dependencies are healthy.
Replace log.Printf with a structured logger like slog (Go 1.21+) or zerolog. Log every authentication event (registration, login, failed attempt, lockout, biometric enrollment, biometric verification) with structured fields for audit trails.
Never store JWT_SECRET or H33_API_KEY in environment files that get committed. Use AWS Secrets Manager, HashiCorp Vault, or your orchestrator's secret store. Rotate secrets on a regular schedule.
Use pgxpool instead of raw pgx connections. Set MaxConns to match your expected concurrency (a good starting point is 2x your CPU count). Set MaxConnLifetime to avoid stale connections behind a PgBouncer or RDS proxy.
The signal handler in our main.go gives in-flight requests 15 seconds to complete before the process exits. This prevents dropped authentication requests during rolling deployments.
systemd Service File
For bare-metal or VM deployments without Docker:
[Unit]
Description=Go Auth H33 Service
After=network.target postgresql.service
[Service]
Type=simple
User=authservice
Group=authservice
WorkingDirectory=/opt/go-auth-h33
ExecStart=/opt/go-auth-h33/auth-server
EnvironmentFile=/opt/go-auth-h33/.env
Restart=always
RestartSec=5
LimitNOFILE=65535
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadOnlyPaths=/opt/go-auth-h33
[Install]
WantedBy=multi-user.target
11. Security Best Practices Checklist
Authentication is a system, not a feature. Every layer needs to be correct. Use this checklist to audit your Go authentication service before it reaches production.
Password Security
- bcrypt with cost >= 12
- Minimum 12-character password requirement
- Constant-time comparison (bcrypt handles this)
- Never log or return passwords in API responses
json:"-"tag on password hash fields- Account lockout after 5 failed attempts
Token Security
- Short-lived access tokens (15 minutes max)
- Refresh token rotation on every use
- Validate signing method in JWT parser
- Include
jticlaim for token revocation - Set
iss,sub,exp,nbfclaims - Store refresh tokens server-side (DB or Redis)
Transport Security
- TLS everywhere (no HTTP fallback)
- HSTS headers with long max-age
- Secure, HttpOnly, SameSite cookies for web clients
- CORS restricted to known origins
- Rate limiting on all auth endpoints
- Request size limits to prevent DoS
Post-Quantum Readiness
- H33 biometric enrollment for high-value accounts
- PQ attestation on verification results
- Audit logs with Dilithium signatures
- Monitor quantum threat timeline
- Plan migration path from HMAC-JWT to PQ tokens
- Test with PQ TLS in staging
Utility Functions
For completeness, here are the JSON response helpers and email validation used throughout the handlers:
package handlers import ( "encoding/json" "net/http" "regexp" ) var emailRegex = regexp.MustCompile( `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) func isValidEmail(email string) bool { return emailRegex.MatchString(email) } func respondJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } func respondError(w http.ResponseWriter, status int, msg string) { respondJSON(w, status, map[string]string{"error": msg}) }
What We Built
This tutorial walked through building a production-grade authentication service in Go, layer by layer:
- Project structure — Chi router, environment config, graceful shutdown
- User model — bcrypt password hashing at cost 12, account lockout after failed attempts
- Registration and login — Input validation, user enumeration prevention, constant-time comparison
- JWT middleware — Token issuance and validation with signing method enforcement
- Session management — Refresh token rotation, server-side invalidation on logout
- H33 biometric integration — FHE-encrypted enrollment and verification with post-quantum attestation
- Rate limiting — Per-IP token bucket with automatic cleanup
- Production deployment — Multi-stage Docker, scratch base image, systemd hardening
The classical layers (bcrypt + JWT) give you a solid baseline that works today. The H33 integration layers (FHE biometrics + Dilithium attestation) make it quantum-resistant. The two layers complement each other — classical auth handles the everyday UX, while H33 provides cryptographic guarantees that survive into the post-quantum era.
For the complete, runnable source code, see the H33 API documentation. For language-specific alternatives, see our Node.js tutorial, Rust tutorial, and Python tutorial.
Ready to Go Quantum-Secure?
Start protecting your users with post-quantum authentication today. 10,000 free API calls/month, no credit card required.
Get Free API Key