Files
httpauth-jwt/signer.go
Rene Nochebuena 1faab9a604 fix(httpauth-jwt): WithJSONNumber in Verify, JSON errors in AuthMiddleware, bump httpauth v1.0.2
Pass jwt.WithJSONNumber() to jwt.Parse in hmacSigner.Verify, rsaSigner.Verify, and
rsaPublicVerifier.Verify. JWT numbers (including permission bitmasks) are now decoded
as json.Number instead of float64 — exact parsing for all integers including
math.MaxInt64 (the ADMIN wildcard mask). Float64 would round MaxInt64 to 2^63 and
overflow back to math.MinInt64 on cast to int64.

AuthMiddleware now calls httpauth.WriteJSONError for all 401 responses instead of
http.Error. Standardizes the error format across the full httpauth-* middleware stack:
AuthMiddleware (httpauth-jwt), EnrichmentMiddleware, and AuthzMiddleware (httpauth)
all produce {"code":"UNAUTHENTICATED"|"PERMISSION_DENIED"|"INTERNAL","message":"..."}.

Update two existing tests (TestIssueTokenPair_CustomClaims,
TestRefreshTokenPair_CustomClaimsInNewToken) to assert json.Number instead of float64
— they were correct before WithJSONNumber, now reflect the actual decoded type.

Add TestVerify_JSONNumberPreservesMaxInt64: issues a token with math.MaxInt64 as a
bitmask, verifies it, and asserts the decoded value is exactly math.MaxInt64 via
json.Number.Int64() — proving no float64 round-trip occurs.

Add TestAuthMiddleware_UnauthorizedJSON: asserts the 401 response is
Content-Type: application/json with body {"code":"UNAUTHENTICATED","message":"..."}.

Bump httpauth dependency to v1.0.2.
2026-05-18 14:38:28 -06:00

134 lines
4.0 KiB
Go

package httpauthjwt
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/golang-jwt/jwt/v5"
)
// Verifier validates JWT strings. Use this in services that receive but do not
// issue tokens (e.g. microservices given only the RSA public key).
type Verifier interface {
Verify(tokenString string) (*jwt.Token, error)
}
// Signer signs and verifies JWTs.
// NewHMACSigner and NewRSASigner return implementations backed by HS256 and RS256.
type Signer interface {
Verifier
Sign(claims jwt.Claims) (string, error)
}
// --- HMAC (HS256) ---
type hmacSigner struct{ secret []byte }
// NewHMACSigner returns a Signer backed by HMAC-SHA256.
// secret should be at least 32 bytes; shorter values are accepted but weakened.
func NewHMACSigner(secret []byte) Signer {
return &hmacSigner{secret: secret}
}
func (s *hmacSigner) Sign(claims jwt.Claims) (string, error) {
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.secret)
}
func (s *hmacSigner) Verify(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method %q", t.Header["alg"])
}
return s.secret, nil
}, jwt.WithJSONNumber())
}
// --- RSA (RS256) ---
type rsaSigner struct {
private *rsa.PrivateKey
public *rsa.PublicKey
}
// NewRSASigner returns a Signer backed by RSA-SHA256.
// The public key is derived from the private key — no separate argument needed.
func NewRSASigner(privateKey *rsa.PrivateKey) Signer {
return &rsaSigner{private: privateKey, public: &privateKey.PublicKey}
}
// NewRSASignerFromPEM parses a PKCS#8 or PKCS#1 PEM-encoded RSA private key.
func NewRSASignerFromPEM(pemKey []byte) (Signer, error) {
block, _ := pem.Decode(pemKey)
if block == nil {
return nil, fmt.Errorf("no PEM block found")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
rsaKey, err2 := x509.ParsePKCS1PrivateKey(block.Bytes)
if err2 != nil {
return nil, fmt.Errorf("parse RSA private key: %w", err)
}
return NewRSASigner(rsaKey), nil
}
rsaKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("PEM key is not an RSA private key")
}
return NewRSASigner(rsaKey), nil
}
func (s *rsaSigner) Sign(claims jwt.Claims) (string, error) {
return jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(s.private)
}
func (s *rsaSigner) Verify(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method %q", t.Header["alg"])
}
return s.public, nil
}, jwt.WithJSONNumber())
}
// --- RSA public-key-only verifier ---
type rsaPublicVerifier struct{ public *rsa.PublicKey }
// NewRSAPublicKeyVerifier returns a Verifier backed by an RSA public key.
// Use this in services that verify tokens but never issue them.
func NewRSAPublicKeyVerifier(publicKey *rsa.PublicKey) Verifier {
return &rsaPublicVerifier{public: publicKey}
}
// NewRSAPublicKeyVerifierFromPEM parses a PKIX or PKCS#1 PEM-encoded RSA public key.
func NewRSAPublicKeyVerifierFromPEM(pemKey []byte) (Verifier, error) {
block, _ := pem.Decode(pemKey)
if block == nil {
return nil, fmt.Errorf("no PEM block found")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
rsaPub, err2 := x509.ParsePKCS1PublicKey(block.Bytes)
if err2 != nil {
return nil, fmt.Errorf("parse RSA public key: %w", err)
}
return NewRSAPublicKeyVerifier(rsaPub), nil
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("PEM key is not an RSA public key")
}
return NewRSAPublicKeyVerifier(rsaPub), nil
}
func (v *rsaPublicVerifier) Verify(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method %q", t.Header["alg"])
}
return v.public, nil
}, jwt.WithJSONNumber())
}