hmac

package
v0.53.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 19, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package hmac provides HMAC-based authentication middleware.

This implementation follows the principles of HTTP Message Signatures (RFC 9421), providing a robust way to authenticate requests using a shared secret. It includes protection against: - Tampering: The HTTP method, path, query parameters, timestamp, principal, and selected headers are signed. - Replay Attacks: A mandatory timestamp is included in the signature and verified by the server. - Principal Spoofing: The principal ID is included in the signature. - Request Binding: Arbitrary headers (like access tokens) can be included in the signature.

Package hmac provides HMAC-based authentication middleware.

Example

Example demonstrates basic HMAC client-server authentication.

package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	authctx "github.com/dioad/net/http/auth/context"
	"github.com/dioad/net/http/auth/hmac"
)

func main() {
	const sharedSecret = "shared-secret-key"
	const userID = "user@example.com"
	const requestBody = `{"action": "update"}`

	// Create a server with HMAC authentication
	handler := hmac.NewHandler(hmac.ServerConfig{
		CommonConfig: hmac.CommonConfig{
			SharedKey: sharedSecret,
		},
	})

	// Wrap the API handler
	api := handler.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		user := authctx.AuthenticatedPrincipalFromContext(r.Context())
		fmt.Printf("Authenticated user: %s\n", user)
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "success")
	}))

	server := httptest.NewServer(api)
	defer server.Close()

	// Create HMAC client
	clientAuth := hmac.ClientAuth{
		Config: hmac.ClientConfig{
			CommonConfig: hmac.CommonConfig{
				SharedKey: sharedSecret,
			},
			Principal: userID,
		},
	}

	req, err := http.NewRequest("POST", server.URL+"/api", bytes.NewBufferString(requestBody))
	if err != nil {
		fmt.Printf("Error creating request: %v\n", err)
		return
	}
	if err := clientAuth.AddAuth(req); err != nil {
		fmt.Printf("Error adding auth: %v\n", err)
		return
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Printf("Response: %s\n", string(body))

}
Output:

Authenticated user: user@example.com
Response: success

Index

Examples

Constants

View Source
const (
	// DefaultTimestampHeader is the default header used for the request timestamp.
	// This aligns with custom implementations; RFC 9421 uses the 'created' parameter.
	DefaultTimestampHeader = "X-Timestamp"
	// DefaultSignedHeadersHeader is the header used to list which headers are signed.
	// This aligns with custom implementations; RFC 9421 uses 'Signature-Input'.
	DefaultSignedHeadersHeader = "X-Signed-Headers"
	// AuthScheme is the scheme used in the Authorization header.
	AuthScheme                 = "HMAC"
	DefaultMaxRequestSizeBytes = 10 * 1024 * 1024 // 10 MB
	DefaultMaxTimestampDiff    = 5 * time.Minute
)

Variables

This section is empty.

Functions

func CanonicalData added in v0.53.0

func CanonicalData(r *http.Request, principal string, timestamp string, signedHeaders []string, body []byte) string

CanonicalData generates the string to be signed based on the request. It follows a strict format to ensure both client and server produce the same string: 1. HTTP Method (e.g., POST) 2. HTTP Path with query parameters (e.g., /api/data?id=123) 3. Timestamp (decimal string) 4. Principal ID 5. Comma-separated list of signed header names 6. Each signed header as "name:value" (header values are trimmed of leading/trailing whitespace) 7. Request body

func HMACKey

func HMACKey(sharedKey, data []byte) (string, error)

HMACKey generates an HMAC-SHA256 signature as a hex-encoded string.

func HMACKeyBytes

func HMACKeyBytes(sharedKey, data []byte) ([]byte, error)

HMACKeyBytes generates an HMAC-SHA256 signature as bytes using the shared key and data.

Types

type ClientAuth added in v0.37.0

type ClientAuth struct {
	Config ClientConfig
}

ClientAuth implements authentication for an HMAC client.

func (ClientAuth) AddAuth added in v0.37.0

func (a ClientAuth) AddAuth(req *http.Request) error

AddAuth adds the HMAC token to the request's Authorization header. The token is generated using the SharedKey, Method, Path, Timestamp, Principal, and specified headers, which allows servers to detect tampering and replay attacks.

Example

ExampleClientAuth_AddAuth demonstrates signing a single request with custom headers.

package main

import (
	"bytes"
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/dioad/net/http/auth/hmac"
)

func main() {
	const sharedSecret = "my-secret"
	const userID = "alice"

	handler := hmac.NewHandler(hmac.ServerConfig{
		CommonConfig: hmac.CommonConfig{
			SharedKey:     sharedSecret,
			SignedHeaders: []string{"X-Custom-Header"},
		},
	})

	server := httptest.NewServer(handler.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "ok")
	})))
	defer server.Close()

	req, _ := http.NewRequest("POST", server.URL, bytes.NewBufferString("data"))
	req.Header.Set("X-Custom-Header", "important-value")

	clientAuth := hmac.ClientAuth{
		Config: hmac.ClientConfig{
			CommonConfig: hmac.CommonConfig{
				SharedKey:     sharedSecret,
				SignedHeaders: []string{"X-Custom-Header"},
			},
			Principal: userID,
		},
	}
	if err := clientAuth.AddAuth(req); err != nil {
		fmt.Printf("Error adding auth: %v\n", err)
		return
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer resp.Body.Close()
	fmt.Println(resp.StatusCode)

}
Output:

200
Example (RequestBinding)

ExampleClientAuth_AddAuth_requestBinding demonstrates using HMAC to bind to an existing token.

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/dioad/net/http/auth/hmac"
)

func main() {
	const sharedSecret = "secret"
	const jwtToken = "eyXXX.YYY.ZZZ"

	// Server configures HMAC to sign the X-JWT-Token header
	handler := hmac.NewHandler(hmac.ServerConfig{
		CommonConfig: hmac.CommonConfig{
			SharedKey:     sharedSecret,
			SignedHeaders: []string{"X-JWT-Token"},
		},
	})

	server := httptest.NewServer(handler.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "verified binding")
	})))
	defer server.Close()

	req, _ := http.NewRequest("GET", server.URL, nil)
	// Add the token we want to bind
	req.Header.Set("X-JWT-Token", jwtToken)

	clientAuth := hmac.ClientAuth{
		Config: hmac.ClientConfig{
			CommonConfig: hmac.CommonConfig{
				SharedKey:     sharedSecret,
				SignedHeaders: []string{"X-JWT-Token"},
			},
			Principal: "client-app",
		},
	}
	if err := clientAuth.AddAuth(req); err != nil {
		fmt.Printf("Error adding auth: %v\n", err)
		return
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))

}
Output:

verified binding

func (ClientAuth) HTTPClient added in v0.40.0

func (a ClientAuth) HTTPClient() *http.Client

HTTPClient returns an http.Client that automatically adds the HMAC token to requests.

type ClientConfig added in v0.37.0

type ClientConfig struct {
	CommonConfig `mapstructure:",squash"`
	// Principal ID to use for authentication
	Principal string `mapstructure:"principal"`
}

ClientConfig contains configuration for an HMAC authentication client.

type CommonConfig added in v0.37.0

type CommonConfig struct {
	AllowInsecureHTTP bool `mapstructure:"allow-insecure-http"`
	// Inline shared key used to HMAC with value from HTTPHeader
	SharedKey string `mapstructure:"shared-key"`
	// HTTP Headers to include in the HMAC calculation.
	SignedHeaders []string `mapstructure:"signed-headers"`
	// HTTP Header to use for the timestamp (default: X-Timestamp)
	TimestampHeader string `mapstructure:"timestamp-header"`
}

CommonConfig contains shared configuration for HMAC authentication.

type HMACRoundTripper added in v0.53.0

type HMACRoundTripper struct {
	Config ClientConfig
	Base   http.RoundTripper
}

HMACRoundTripper is an http.RoundTripper that adds HMAC authentication.

func (*HMACRoundTripper) RoundTrip added in v0.53.0

func (t *HMACRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip executes a single HTTP transaction, adding HMAC authentication.

type Handler added in v0.37.0

type Handler struct {
	// contains filtered or unexported fields
}

Handler implements HMAC-based authentication.

func NewHandler added in v0.37.0

func NewHandler(cfg ServerConfig) *Handler

NewHandler creates a new HMAC authentication handler with the provided configuration.

func (*Handler) AuthRequest added in v0.37.0

func (a *Handler) AuthRequest(r *http.Request) (stdcontext.Context, error)

AuthRequest authenticates an HTTP request using HMAC. It expects an Authorization header in the format "HMAC principal:signature". It also verifies the request timestamp to prevent replay attacks.

func (*Handler) Wrap added in v0.37.0

func (a *Handler) Wrap(handler http.Handler) http.Handler

type ServerConfig added in v0.37.0

type ServerConfig struct {
	CommonConfig `mapstructure:",squash"`
	// Maximum allowed time difference for the timestamp (default: 5m)
	MaxTimestampDiff time.Duration `mapstructure:"max-timestamp-diff"`
	// Maximum allowed time difference for future timestamps (default: 30s)
	// This should be smaller than MaxTimestampDiff to prevent pre-signed replay attacks.
	// A smaller value is appropriate since it only needs to account for clock skew.
	MaxFutureTimestampDiff time.Duration `mapstructure:"max-future-timestamp-diff"`
	// Maximum request size in bytes (default: 10 MB)
	MaxRequestSize int `mapstructure:"max-request-size"`
}

ServerConfig contains configuration for an HMAC authentication server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL