hmac

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Mar 1, 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"

	authhttp "github.com/dioad/auth/http/context"
	"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("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

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

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, 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/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, 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

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 HMACRoundTripper

type HMACRoundTripper struct {
	Config ClientConfig
	Base   http.RoundTripper
}

HMACRoundTripper is an http.RoundTripper that adds HMAC authentication.

func (*HMACRoundTripper) RoundTrip

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

RoundTrip executes a single HTTP transaction, adding 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

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