Skip to content
AyoKoding

Authentication Authorization

Why Authentication & Authorization Matter

Authentication (verifying identity) and authorization (verifying permissions) are critical security controls in production systems. Without proper authentication, anyone can impersonate users. Without authorization, authenticated users can access resources they shouldn't. Understanding Go's authentication patterns prevents security breaches, protects sensitive data, and ensures compliance with security standards.

Core benefits:

  • Identity verification: Ensures users are who they claim to be
  • Access control: Limits what authenticated users can do
  • Audit trails: Tracks who accessed what resources
  • Compliance: Meets regulatory requirements (GDPR, HIPAA, SOC 2)

Problem: Standard library provides basic HTTP authentication but no JWT validation, session management, or OAuth2 flows. Manual implementation is error-prone and insecure.

Solution: Start with net/http Basic Auth to understand fundamentals, identify limitations (no JWT, no sessions), then use production libraries (golang-jwt/jwt for tokens, OAuth2 packages) for comprehensive authentication.

JWT Authentication Flow

sequenceDiagram
    participant Client
    participant LoginHandler
    participant Database
    participant JWTMiddleware
    participant ProtectedHandler
 
    Client->>LoginHandler: POST /login<br/>(username, password)
    LoginHandler->>Database: Validate credentials
    Database-->>LoginHandler: User found + role
    LoginHandler->>LoginHandler: Generate JWT<br/>(sign with secret)
    LoginHandler-->>Client: {"token": "eyJhbGc..."}
 
    Note over Client: Store token<br/>(HttpOnly cookie)
 
    Client->>JWTMiddleware: GET /protected<br/>Authorization: Bearer eyJhbGc...
    JWTMiddleware->>JWTMiddleware: Parse token<br/>Validate signature<br/>Check expiration
    alt Token Valid
        JWTMiddleware->>JWTMiddleware: Extract claims<br/>(username, role)
        JWTMiddleware->>ProtectedHandler: Forward request<br/>(claims in context)
        ProtectedHandler-->>Client: 200 OK<br/>Protected resource
    else Token Invalid/Expired
        JWTMiddleware-->>Client: 401 Unauthorized
    end
 
    style Client fill:#0173B2,stroke:#0173B2,color:#fff
    style LoginHandler fill:#DE8F05,stroke:#DE8F05,color:#fff
    style Database fill:#029E73,stroke:#029E73,color:#fff
    style JWTMiddleware fill:#CC78BC,stroke:#CC78BC,color:#fff
    style ProtectedHandler fill:#CA9161,stroke:#CA9161,color:#fff

Authentication flow steps:

  • Login: Client sends credentials, server validates, generates signed JWT
  • Token storage: Client stores token (HttpOnly cookie for XSS protection)
  • Protected request: Client sends token in Authorization header
  • Validation: Middleware verifies signature, expiration, extracts claims
  • Authorization: Handler checks claims (username, role) for access control

Standard Library: Basic Authentication

Go's net/http supports Basic Authentication through Authorization header parsing.

Pattern from standard library:

package main
 
import (
    "encoding/base64"
    // => Standard library for Base64 decoding
    // => Basic Auth encodes credentials as Base64
    "fmt"
    // => Standard library for formatted output
    "net/http"
    // => Standard library HTTP server
    // => No external dependencies
    "strings"
    // => String manipulation (splitting)
)
 
func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
    // => Middleware pattern: wraps handler
    // => Returns new handler that checks auth first
    // => next is called only if auth succeeds
 
    return func(w http.ResponseWriter, r *http.Request) {
        // => Inner handler checks Authorization header
        // => r.Header.Get() case-insensitive
 
        auth := r.Header.Get("Authorization")
        // => auth is "Basic base64(username:password)"
        // => Example: "Basic YWRtaW46cGFzczEyMzQ="
        // => Empty if header not present
 
        if auth == "" {
            // => No Authorization header
 
            w.Header().Set("WWW-Authenticate", `Basic realm="restricted"`)
            // => WWW-Authenticate tells client to send credentials
            // => realm describes protected area
            // => Browser shows login dialog
 
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            // => 401 Unauthorized response
            // => Client should retry with credentials
            return
            // => Stop processing (don't call next)
        }
 
        if !strings.HasPrefix(auth, "Basic ") {
            // => Authorization header must start with "Basic "
            // => Other schemes: Bearer, Digest, etc.
 
            http.Error(w, "Invalid auth scheme", http.StatusUnauthorized)
            return
        }
 
        payload := auth[len("Basic "):]
        // => payload is "YWRtaW46cGFzczEyMzQ=" (base64 encoded)
        // => Removes "Basic " prefix
        // => Contains base64(username:password)
 
        decoded, err := base64.StdEncoding.DecodeString(payload)
        // => decoded is []byte("admin:pass1234")
        // => Decodes Base64 to username:password
        // => err non-nil if invalid Base64
 
        if err != nil {
            // => Invalid Base64 encoding
 
            http.Error(w, "Invalid encoding", http.StatusUnauthorized)
            return
        }
 
        credentials := string(decoded)
        // => credentials is "admin:pass1234"
        // => Convert bytes to string
 
        parts := strings.SplitN(credentials, ":", 2)
        // => parts is ["admin", "pass1234"]
        // => SplitN limits to 2 parts (username and password)
        // => Password may contain : character
 
        if len(parts) != 2 {
            // => Invalid format (missing colon)
 
            http.Error(w, "Invalid credentials", http.StatusUnauthorized)
            return
        }
 
        username, password := parts[0], parts[1]
        // => username is "admin"
        // => password is "pass1234"
        // => Extract from parts array
 
        if !validateCredentials(username, password) {
            // => Check against stored credentials
            // => validateCredentials queries database
 
            http.Error(w, "Invalid credentials", http.StatusUnauthorized)
            return
        }
 
        // Authentication succeeded
        next(w, r)
        // => Call next handler in chain
        // => User is authenticated at this point
    }
}
 
func validateCredentials(username, password string) bool {
    // => Validates username/password
    // => Production: query database, check bcrypt hash
    // => This is simplified example
 
    return username == "admin" && password == "pass1234"
    // => Hardcoded for demo (NEVER do this in production)
    // => Production: hash password, query database
}
 
func protectedHandler(w http.ResponseWriter, r *http.Request) {
    // => Handler only called if auth succeeds
    // => No auth logic needed here
 
    fmt.Fprintln(w, "Welcome to protected resource!")
    // => Response: Welcome to protected resource!
    // => Only authenticated users see this
}
 
func main() {
    // => Sets up HTTP server with auth
 
    http.HandleFunc("/protected", basicAuthMiddleware(protectedHandler))
    // => Wraps handler with auth middleware
    // => basicAuthMiddleware checks auth before protectedHandler
    // => Middleware pattern (decorates handler)
 
    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
    // => Starts HTTP server
    // => Blocks forever (until interrupted)
}

Testing Basic Auth:

# Without credentials (401)
curl http://localhost:8080/protected
# => Response: Unauthorized
# => Status: 401
 
# With valid credentials (200)
curl -u admin:pass1234 http://localhost:8080/protected
# => Response: Welcome to protected resource!
# => -u flag encodes credentials as Basic Auth
# => Status: 200

Limitations for production authentication:

  • No JWT support (stateless tokens)
  • No session management (stateful sessions)
  • No OAuth2 flows (third-party authentication)
  • No token expiration (credentials never expire)
  • No refresh tokens (long-lived tokens)
  • Credentials sent on every request (performance overhead)
  • No role-based access control (RBAC)

Production Framework: JWT with golang-jwt/jwt

JSON Web Tokens (JWT) provide stateless authentication for APIs. golang-jwt/jwt is the standard library for JWT in Go.

Adding golang-jwt/jwt:

go get github.com/golang-jwt/jwt/v5
# => Installs JWT library
# => v5 is latest version (2023+)

Pattern: JWT Authentication:

package main
 
import (
    "fmt"
    // => Standard library for formatted output
    "net/http"
    // => Standard library HTTP server
    "time"
    // => Standard library for time operations
 
    "github.com/golang-jwt/jwt/v5"
    // => JWT library for token creation/validation
    // => v5 latest version (December 2023+)
)
 
var jwtSecret = []byte("your-secret-key")
// => SECRET KEY for signing tokens
// => CRITICAL: must be random, kept secret
// => Production: load from environment variable
// => Same key signs and validates tokens
 
type Claims struct {
    Username string `json:"username"`
    // => Username embedded in token
    // => Available without database lookup
    // => Public claim (not secret)
 
    Role     string `json:"role"`
    // => User role for authorization
    // => role is "admin", "user", "guest"
    // => Used for RBAC checks
 
    jwt.RegisteredClaims
    // => Embeds standard JWT claims
    // => ExpiresAt, IssuedAt, Issuer, Subject, Audience
    // => Provides expiration validation
}
 
func generateToken(username, role string) (string, error) {
    // => Creates JWT token for authenticated user
    // => Returns signed token string
    // => Token includes username, role, expiration
 
    claims := Claims{
        Username: username,
        // => Username stored in token
        Role:     role,
        // => Role stored in token
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            // => Token expires in 24 hours
            // => After expiration, token invalid
            // => Client must request new token
 
            IssuedAt: jwt.NewNumericDate(time.Now()),
            // => Token creation timestamp
            // => Used to track token age
 
            Issuer: "auth-service",
            // => Identifies token issuer
            // => Validates token origin
        },
    }
    // => Claims object with user info and metadata
 
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    // => Creates token with HS256 signing method
    // => HS256 uses HMAC with SHA-256
    // => Symmetric key (same key signs and validates)
    // => Token not yet signed
 
    return token.SignedString(jwtSecret)
    // => Signs token with secret key
    // => Returns token string (3 parts: header.payload.signature)
    // => Example: eyJhbGc...eyJ1c2V...SflKxw
    // => err non-nil if signing fails
}
 
func loginHandler(w http.ResponseWriter, r *http.Request) {
    // => Handles POST /login requests
    // => Validates credentials, returns JWT
 
    username := r.FormValue("username")
    // => username from form data
    // => Example: "admin"
 
    password := r.FormValue("password")
    // => password from form data
    // => Production: never log passwords
 
    if !validateCredentials(username, password) {
        // => Check credentials against database
        // => Production: bcrypt compare
 
        http.Error(w, "Invalid credentials", http.StatusUnauthorized)
        return
    }
 
    role := getUserRole(username)
    // => Query database for user role
    // => role is "admin", "user", "guest"
    // => Used for authorization decisions
 
    token, err := generateToken(username, role)
    // => Create signed JWT
    // => token is "eyJhbGc..."
 
    if err != nil {
        http.Error(w, "Token generation failed", http.StatusInternalServerError)
        return
    }
 
    w.Header().Set("Content-Type", "application/json")
    // => JSON response
 
    fmt.Fprintf(w, `{"token": "%s"}`, token)
    // => Response: {"token": "eyJhbGc..."}
    // => Client stores token (localStorage, cookie)
    // => Client sends token on subsequent requests
}
 
func jwtMiddleware(next http.HandlerFunc) http.HandlerFunc {
    // => Middleware validates JWT on each request
    // => Extracts claims, adds to request context
 
    return func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        // => authHeader is "Bearer eyJhbGc..."
        // => Bearer scheme indicates JWT
 
        if authHeader == "" {
            http.Error(w, "Missing token", http.StatusUnauthorized)
            return
        }
 
        tokenString := authHeader[len("Bearer "):]
        // => tokenString is "eyJhbGc..."
        // => Removes "Bearer " prefix
 
        claims := &Claims{}
        // => Empty claims object to receive parsed claims
 
        token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
            // => Validation function called during parsing
            // => Must return signing key
            // => Validates signing method
 
            if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                // => Ensure token signed with HMAC (HS256)
                // => Rejects tokens signed with other methods
                // => Prevents algorithm confusion attacks
 
                return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
            }
 
            return jwtSecret, nil
            // => Return secret key for signature validation
            // => Same key used for signing
        })
        // => token is parsed JWT object
        // => err non-nil if signature invalid or token expired
 
        if err != nil || !token.Valid {
            // => Invalid signature or expired token
            // => token.Valid checks expiration
 
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
 
        // Token valid, user authenticated
        // => claims.Username and claims.Role available
        // => Use for authorization checks
 
        next(w, r)
        // => Call next handler
        // => Handler can access claims (add to context in production)
    }
}
 
func protectedHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Access granted to protected resource")
}
 
func getUserRole(username string) string {
    // => Query database for user role
    // => Production: SELECT role FROM users WHERE username = ?
 
    if username == "admin" {
        return "admin"
    }
    return "user"
}
 
func main() {
    http.HandleFunc("/login", loginHandler)
    // => POST /login returns JWT
 
    http.HandleFunc("/protected", jwtMiddleware(protectedHandler))
    // => GET /protected requires valid JWT
 
    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

Testing JWT Authentication:

# Login to get token
TOKEN=$(curl -X POST -d "username=admin&password=pass1234" \
    http://localhost:8080/login | jq -r '.token')
# => TOKEN is "eyJhbGc..."
# => Stores token in variable
 
# Access protected resource with token
curl -H "Authorization: Bearer $TOKEN" \
    http://localhost:8080/protected
# => Response: Access granted to protected resource
# => Status: 200

Pattern: Role-Based Access Control (RBAC):

package main
 
import (
    "fmt"
    "net/http"
 
    "github.com/golang-jwt/jwt/v5"
)
 
func requireRole(role string) func(http.HandlerFunc) http.HandlerFunc {
    // => Higher-order function: returns middleware
    // => Configurable middleware (role parameter)
    // => Checks if user has required role
 
    return func(next http.HandlerFunc) http.HandlerFunc {
        // => Middleware function
        // => Wraps next handler
 
        return func(w http.ResponseWriter, r *http.Request) {
            // => Extract claims from token (JWT middleware runs first)
            // => Production: get claims from request context
 
            authHeader := r.Header.Get("Authorization")
            tokenString := authHeader[len("Bearer "):]
 
            claims := &Claims{}
            jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
                return jwtSecret, nil
            })
            // => Parses token to extract claims
            // => Production: get from context (avoid re-parsing)
 
            if claims.Role != role {
                // => User doesn't have required role
                // => claims.Role is "user", role is "admin"
 
                http.Error(w, "Forbidden: insufficient permissions", http.StatusForbidden)
                // => 403 Forbidden (authenticated but not authorized)
                // => Different from 401 (not authenticated)
                return
            }
 
            // Authorization passed
            next(w, r)
            // => Call next handler
            // => User has required role
        }
    }
}
 
func adminHandler(w http.ResponseWriter, r *http.Request) {
    // => Handler only for admin role
 
    fmt.Fprintln(w, "Admin access granted")
}
 
func main() {
    http.HandleFunc("/admin",
        jwtMiddleware(requireRole("admin")(adminHandler)))
    // => Middleware chain: jwtMiddleware → requireRole("admin") → adminHandler
    // => First validates JWT, then checks role
 
    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

Production Framework: OAuth2

OAuth2 enables third-party authentication (Google, GitHub, Facebook) without handling passwords.

Adding OAuth2 library:

go get golang.org/x/oauth2
# => Official Go OAuth2 client library
# => Handles OAuth2 flows

Pattern: OAuth2 Google Login:

package main
 
import (
    "context"
    "fmt"
    "net/http"
 
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
    // => Google OAuth2 configuration
    // => Predefined endpoints
)
 
var googleOAuthConfig = &oauth2.Config{
    ClientID:     "your-client-id",
    // => OAuth2 client ID from Google Console
    // => Public identifier
 
    ClientSecret: "your-client-secret",
    // => OAuth2 client secret from Google Console
    // => CRITICAL: keep secret, load from env
 
    RedirectURL:  "http://localhost:8080/callback",
    // => URL Google redirects to after auth
    // => Must match registered URL in Google Console
 
    Scopes: []string{
        "https://www.googleapis.com/auth/userinfo.email",
        // => Scope: access user email
        // => Scopes define permissions requested
    },
 
    Endpoint: google.Endpoint,
    // => Google OAuth2 endpoints
    // => AuthURL and TokenURL
}
 
func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
    // => Redirects user to Google login page
    // => Step 1 of OAuth2 flow
 
    url := googleOAuthConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
    // => Builds authorization URL
    // => "state-token" prevents CSRF attacks
    // => AccessTypeOffline requests refresh token
    // => url is "https://accounts.google.com/o/oauth2/auth?..."
 
    http.Redirect(w, r, url, http.StatusTemporaryRedirect)
    // => Redirects browser to Google
    // => User authenticates with Google
    // => Google redirects back to /callback
}
 
func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
    // => Handles redirect from Google
    // => Step 2 of OAuth2 flow
 
    code := r.URL.Query().Get("code")
    // => Authorization code from Google
    // => code is one-time-use code
    // => Exchange for access token
 
    state := r.URL.Query().Get("state")
    // => state is "state-token" (from AuthCodeURL)
    // => Verify to prevent CSRF
 
    if state != "state-token" {
        // => State mismatch (possible CSRF attack)
 
        http.Error(w, "Invalid state", http.StatusBadRequest)
        return
    }
 
    token, err := googleOAuthConfig.Exchange(context.Background(), code)
    // => Exchange authorization code for access token
    // => Makes HTTP POST to Google token endpoint
    // => token contains AccessToken, RefreshToken, Expiry
 
    if err != nil {
        http.Error(w, "Token exchange failed", http.StatusInternalServerError)
        return
    }
 
    // token.AccessToken can access Google APIs
    // => Use token to call Google APIs (userinfo, gmail, etc.)
    // => Store token in session or database
 
    fmt.Fprintf(w, "Login successful! Token: %s", token.AccessToken)
    // => In production: create session, redirect to app
}
 
func main() {
    http.HandleFunc("/login/google", handleGoogleLogin)
    // => Initiates OAuth2 flow
 
    http.HandleFunc("/callback", handleGoogleCallback)
    // => Handles OAuth2 redirect
 
    fmt.Println("Server starting on :8080")
    fmt.Println("Visit http://localhost:8080/login/google")
    http.ListenAndServe(":8080", nil)
}

Trade-offs: When to Use Each

Comparison table:

ApproachToken TypeStatefulnessUse Case
Basic AuthCredentialsStatelessInternal tools, simple APIs
JWTSigned tokenStatelessMicroservices, mobile apps, SPAs
OAuth2Access tokenStatefulThird-party login, delegated auth
SessionsSession IDStatefulTraditional web apps, high security

When to use Basic Auth:

  • Internal tools (admin panels)
  • Simple APIs (no token management)
  • Legacy systems (backward compatibility)
  • Low security requirements

When to use JWT:

  • Microservices (stateless authentication)
  • Mobile apps (long-lived tokens)
  • Single-page applications (token in localStorage)
  • API gateways (token validation without database)

When to use OAuth2:

  • Third-party authentication (Google, GitHub, Facebook)
  • Delegated authorization (access user's Gmail)
  • Avoid password handling (OAuth2 provider manages credentials)
  • Social login features

When to use sessions:

  • Traditional web applications (server-rendered HTML)
  • High security requirements (revoke sessions immediately)
  • Short-lived sessions (banking, healthcare)
  • Session storage available (Redis, database)

Production Best Practices

Never store JWT in localStorage (XSS vulnerability):

// GOOD: HttpOnly cookie (JavaScript can't access)
http.SetCookie(w, &http.Cookie{
    Name:     "token",
    Value:    token,
    HttpOnly: true,  // Prevents JavaScript access
    Secure:   true,  // HTTPS only
    SameSite: http.SameSiteStrictMode,  // CSRF protection
})
 
// BAD: returning token in JSON (client stores in localStorage)
// => localStorage accessible by JavaScript (XSS risk)

Use strong secrets (256-bit random):

// GOOD: load from environment variable
import "os"
 
var jwtSecret = []byte(os.Getenv("JWT_SECRET"))
// => 256-bit random string
// => Generate with: openssl rand -base64 32
 
// BAD: hardcoded secret
var jwtSecret = []byte("secret123")  // Predictable, insecure

Implement token refresh:

// GOOD: short-lived access token + long-lived refresh token
type TokenPair struct {
    AccessToken  string `json:"access_token"`   // 15 minutes
    RefreshToken string `json:"refresh_token"`  // 7 days
}
 
// BAD: long-lived access token
// => If stolen, attacker has access until expiration

Validate token expiration:

// GOOD: jwt library validates expiration automatically
token, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
if err != nil {
    // Handles expired tokens
}
 
// BAD: manual expiration check (error-prone)
if time.Now().After(claims.ExpiresAt.Time) {
    // Custom expiration logic (use library instead)
}

Summary

Authentication verifies identity, authorization verifies permissions. Standard library provides Basic Auth through Authorization header parsing, but lacks JWT validation, session management, and OAuth2 support. Production systems use golang-jwt/jwt for stateless tokens, OAuth2 for third-party authentication, and role-based access control for authorization. Use JWT for microservices and APIs, OAuth2 for social login, sessions for traditional web apps. Always use strong secrets, HttpOnly cookies, and short-lived tokens.

Key takeaways:

  • Basic Auth sends credentials on every request (stateless)
  • JWT provides stateless authentication with signed tokens
  • OAuth2 enables third-party authentication without handling passwords
  • RBAC checks user roles for authorization decisions
  • Store JWT in HttpOnly cookies (not localStorage)
  • Use 256-bit random secrets for JWT signing
  • Implement token refresh with short-lived access tokens

Last updated February 3, 2026

Command Palette

Search for a command to run...