hmac

package
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2026 License: Apache-2.0 Imports: 14 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.

Example

Example demonstrates basic HMAC client-server authentication.

package main

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

	authhttp "github.com/dioad/auth/authctx"
	"github.com/dioad/auth/http/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, _ := authhttp.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(http.MethodPost, 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 func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	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 is the request body size limit applied when
	// ServerConfig.MaxRequestSize is unset.
	DefaultMaxRequestSizeBytes = 10 * 1024 * 1024 // 10 MB
	// DefaultMaxTimestampDiff is the maximum allowed age of a request
	// timestamp applied when ServerConfig.MaxTimestampDiff is unset.
	DefaultMaxTimestampDiff = 5 * time.Minute
)

Variables

This section is empty.

Functions

func CanonicalData

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 Key added in v0.11.0

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

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

func KeyBytes added in v0.11.0

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

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

Types

type ClientAuth

type ClientAuth struct {
	Config ClientConfig
}

ClientAuth implements authentication for an HMAC client.

func (ClientAuth) AddAuth

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/auth/http/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, _ *http.Request) {
		_, _ = fmt.Fprint(w, "ok")
	})))
	defer server.Close()

	req, _ := http.NewRequest(http.MethodPost, 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 func() { _ = 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/auth/http/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, _ *http.Request) {
		_, _ = fmt.Fprint(w, "verified binding")
	})))
	defer server.Close()

	req, _ := http.NewRequest(http.MethodGet, 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 func() { _ = resp.Body.Close() }()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	fmt.Println(string(body))

}
Output:
verified binding

func (ClientAuth) HTTPClient

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

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

type ClientConfig

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

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 Handler

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

Handler implements HMAC-based authentication.

func NewHandler

func NewHandler(cfg ServerConfig) *Handler

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

func (*Handler) AuthRequest

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

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

Wrap wraps handler with HMAC authentication middleware.

type RoundTripper added in v0.11.0

type RoundTripper struct {
	Config ClientConfig
	Base   http.RoundTripper
}

RoundTripper is an http.RoundTripper that adds HMAC authentication.

func (*RoundTripper) RoundTrip added in v0.11.0

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

RoundTrip executes a single HTTP transaction, adding HMAC authentication.

type ServerConfig

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