Tutorial · 28 min read

Go Authentication with H33:
Building Secure Services

From zero to post-quantum secure. Build production-grade authentication in Go — starting with classical best practices like bcrypt and JWTs, then upgrading to biometric and post-quantum auth with H33's API. Complete, runnable code at every step.

5 min
Setup
~50µs
Per Auth
1.2M
Auth/sec
PQ
Secure

Table of Contents

  1. Why Go for Authentication Services
  2. Project Setup and Dependencies
  3. User Model and Password Hashing
  4. Registration and Login Handlers
  5. JWT Authentication and Middleware
  6. Session Management Patterns
  7. Integrating H33 Biometric Authentication
  8. Post-Quantum Security with H33
  9. Rate Limiting and Brute-Force Protection
  10. Production Deployment
  11. 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.

Go vs Other Languages for Auth

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.

Shell Project initialization
# 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
Shell Directory structure
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
Env .env
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
Never Commit Secrets

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.

Go internal/config/config.go
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
}
Go cmd/server/main.go
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.

Go internal/models/user.go
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

SQL migrations/001_create_users.sql
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.

Go internal/handlers/auth.go
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.

Go internal/handlers/auth.go (continued)
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,
    })
}
Security: User Enumeration

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).

Go internal/handlers/auth.go (token issuance)
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.

Go internal/middleware/jwt.go
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.

Go internal/handlers/auth.go (refresh + logout)
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",
    })
}
Refresh Token Rotation

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

Go internal/handlers/h33.go
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.

Go internal/handlers/h33.go (enrollment)
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.

Go internal/handlers/h33.go (verification)
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,
    })
}
Why FHE Biometrics?

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.

The Quantum Timeline

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.

Shell · curl Verify attestation independently
# 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.

Go internal/middleware/ratelimit.go
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:

Go cmd/server/main.go (rate limit wiring)
// 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.

Dockerfile Dockerfile
# 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"]
Why scratch?

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

TLS Termination

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.

Health Checks

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.

Structured Logging

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.

Secrets Management

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.

Database Connection Pooling

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.

Graceful Shutdown

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:

INI /etc/systemd/system/go-auth-h33.service
[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 jti claim for token revocation
  • Set iss, sub, exp, nbf claims
  • 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

Utility Functions

For completeness, here are the JSON response helpers and email validation used throughout the handlers:

Go internal/handlers/helpers.go
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:

  1. Project structure — Chi router, environment config, graceful shutdown
  2. User model — bcrypt password hashing at cost 12, account lockout after failed attempts
  3. Registration and login — Input validation, user enumeration prevention, constant-time comparison
  4. JWT middleware — Token issuance and validation with signing method enforcement
  5. Session management — Refresh token rotation, server-side invalidation on logout
  6. H33 biometric integration — FHE-encrypted enrollment and verification with post-quantum attestation
  7. Rate limiting — Per-IP token bucket with automatic cleanup
  8. 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

Build With Post-Quantum Security

Enterprise-grade FHE, ZKP, and post-quantum cryptography. One API call. Sub-millisecond latency.

Get Free API Key → Read the Docs
Free tier · 10,000 API calls/month · No credit card required
Verify It Yourself