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 ¶
- Constants
- func CanonicalData(r *http.Request, principal string, timestamp string, signedHeaders []string, ...) string
- func HMACKey(sharedKey, data []byte) (string, error)
- func HMACKeyBytes(sharedKey, data []byte) ([]byte, error)
- type ClientAuth
- type ClientConfig
- type CommonConfig
- type HMACRoundTripper
- type Handler
- type ServerConfig
Examples ¶
Constants ¶
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 HMACKeyBytes ¶
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"`
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.
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
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.
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.