mirror of
https://github.com/sot-tech/mochi.git
synced 2026-07-12 11:38:11 -07:00
update JWT middleware
This commit is contained in:
+43
-84
@@ -9,11 +9,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MicahParks/keyfunc"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/MicahParks/jwkset"
|
||||
"github.com/MicahParks/keyfunc/v3"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/sot-tech/mochi/bittorrent"
|
||||
"github.com/sot-tech/mochi/middleware"
|
||||
@@ -63,8 +65,9 @@ type Config struct {
|
||||
}
|
||||
|
||||
type hook struct {
|
||||
cfg Config
|
||||
jwks *keyfunc.JWKS
|
||||
cfg Config
|
||||
jwks keyfunc.Keyfunc
|
||||
parser *jwt.Parser
|
||||
}
|
||||
|
||||
func build(config conf.MapConfig, _ storage.PeerStorage) (h middleware.Hook, err error) {
|
||||
@@ -83,23 +86,32 @@ func build(config conf.MapConfig, _ storage.PeerStorage) (h middleware.Hook, err
|
||||
Msg("falling back to default configuration")
|
||||
}
|
||||
|
||||
var jwks *keyfunc.JWKS
|
||||
var jwks keyfunc.Keyfunc
|
||||
if cfg.HandleAnnounce || cfg.HandleScrape {
|
||||
jwks, err = keyfunc.Get(cfg.JWKSetURL, keyfunc.Options{
|
||||
Ctx: context.Background(),
|
||||
RefreshErrorHandler: func(err error) {
|
||||
logger.Error().Err(err).Msg("error occurred while updating JWKs")
|
||||
},
|
||||
RefreshInterval: cfg.JWKUpdateInterval,
|
||||
RefreshUnknownKID: true,
|
||||
})
|
||||
var jwkURL *url.URL
|
||||
jwkURL, err = url.Parse(cfg.JWKSetURL)
|
||||
if err == nil {
|
||||
var httpStorage jwkset.Storage
|
||||
httpStorage, err = jwkset.NewStorageFromHTTP(jwkURL, jwkset.HTTPClientStorageOptions{
|
||||
NoErrorReturnFirstHTTPReq: true,
|
||||
RefreshErrorHandler: func(_ context.Context, err error) {
|
||||
logger.Error().Err(err).Msg("error occurred while updating JWKs")
|
||||
},
|
||||
RefreshInterval: cfg.JWKUpdateInterval,
|
||||
Storage: nil,
|
||||
})
|
||||
if err == nil {
|
||||
jwks, err = keyfunc.New(keyfunc.Options{Storage: httpStorage})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Warn().Msg("both announce and scrape handle disabled")
|
||||
}
|
||||
if err == nil {
|
||||
h = &hook{
|
||||
cfg: cfg,
|
||||
jwks: jwks,
|
||||
cfg: cfg,
|
||||
jwks: jwks,
|
||||
parser: jwt.NewParser(jwt.WithAudience(cfg.Audience), jwt.WithIssuer(cfg.Issuer), hmacAlgorithms),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -109,36 +121,8 @@ func build(config conf.MapConfig, _ storage.PeerStorage) (h middleware.Hook, err
|
||||
return
|
||||
}
|
||||
|
||||
func (h *hook) Close() error {
|
||||
logger.Debug().Msg("attempting to shutdown JWT middleware")
|
||||
if h.jwks != nil {
|
||||
h.jwks.EndBackground()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type verifiableClaims interface {
|
||||
jwt.Claims
|
||||
VerifyIssuer(iss string, req bool) bool
|
||||
GetIssuer() string
|
||||
VerifyAudience(aud string, req bool) bool
|
||||
GetAudience() []string
|
||||
}
|
||||
|
||||
type registeredClaimsWrapper struct {
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func (rc registeredClaimsWrapper) GetIssuer() string {
|
||||
return rc.Issuer
|
||||
}
|
||||
|
||||
func (rc registeredClaimsWrapper) GetAudience() []string {
|
||||
return rc.Audience
|
||||
}
|
||||
|
||||
type announceClaims struct {
|
||||
registeredClaimsWrapper
|
||||
jwt.RegisteredClaims
|
||||
InfoHash string `json:"infohash,omitempty"`
|
||||
}
|
||||
|
||||
@@ -152,13 +136,7 @@ func (h *hook) HandleAnnounce(ctx context.Context, req *bittorrent.AnnounceReque
|
||||
err = ErrMissingJWT
|
||||
} else {
|
||||
claims := new(announceClaims)
|
||||
if errs := h.validateBaseJWT(jwtParam, claims); len(errs) > 0 {
|
||||
logger.Info().
|
||||
Errs("errors", errs).
|
||||
Object("source", req.RequestPeer).
|
||||
Msg("JWT validation failed")
|
||||
err = ErrInvalidJWT
|
||||
} else {
|
||||
if _, jwtErr := h.parser.ParseWithClaims(jwtParam, claims, h.jwks.Keyfunc); jwtErr == nil {
|
||||
var claimIH bittorrent.InfoHash
|
||||
if claimIH, err = bittorrent.NewInfoHashString(claims.InfoHash); err != nil {
|
||||
logger.Info().
|
||||
@@ -175,6 +153,12 @@ func (h *hook) HandleAnnounce(ctx context.Context, req *bittorrent.AnnounceReque
|
||||
Msg("unequal 'infohash' claim when validating JWT")
|
||||
err = ErrInvalidJWT
|
||||
}
|
||||
} else {
|
||||
logger.Info().
|
||||
Err(jwtErr).
|
||||
Object("source", req.RequestPeer).
|
||||
Msg("JWT validation failed")
|
||||
err = ErrInvalidJWT
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +166,7 @@ func (h *hook) HandleAnnounce(ctx context.Context, req *bittorrent.AnnounceReque
|
||||
}
|
||||
|
||||
type scrapeClaims struct {
|
||||
registeredClaimsWrapper
|
||||
jwt.RegisteredClaims
|
||||
InfoHashes []string `json:"infohashes,omitempty"`
|
||||
}
|
||||
|
||||
@@ -197,13 +181,7 @@ func (h *hook) HandleScrape(ctx context.Context, req *bittorrent.ScrapeRequest,
|
||||
err = ErrMissingJWT
|
||||
} else {
|
||||
claims := new(scrapeClaims)
|
||||
if errs := h.validateBaseJWT(jwtParam, claims); len(errs) > 0 {
|
||||
logger.Info().
|
||||
Errs("errors", errs).
|
||||
Array("source", &req.RequestAddresses).
|
||||
Msg("JWT validation failed")
|
||||
err = ErrInvalidJWT
|
||||
} else {
|
||||
if _, jwtErr := h.parser.ParseWithClaims(jwtParam, claims, h.jwks.Keyfunc); jwtErr == nil {
|
||||
var claimIHs bittorrent.InfoHashes
|
||||
for _, s := range claims.InfoHashes {
|
||||
if providedIh, err := bittorrent.NewInfoHashString(s); err == nil {
|
||||
@@ -239,6 +217,12 @@ func (h *hook) HandleScrape(ctx context.Context, req *bittorrent.ScrapeRequest,
|
||||
Msg("unequal 'infohashes' claim when validating JWT")
|
||||
err = ErrInvalidJWT
|
||||
}
|
||||
} else {
|
||||
logger.Info().
|
||||
Err(jwtErr).
|
||||
Array("source", &req.RequestAddresses).
|
||||
Msg("JWT validation failed")
|
||||
err = ErrInvalidJWT
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,28 +240,3 @@ func (h *hook) getJWTString(params bittorrent.Params) (jwt string) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *hook) validateBaseJWT(jwtParam string, claims verifiableClaims) (errs []error) {
|
||||
if _, err := jwt.ParseWithClaims(jwtParam, claims, h.jwks.Keyfunc, hmacAlgorithms); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := claims.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !claims.VerifyIssuer(h.cfg.Issuer, true) {
|
||||
logger.Debug().
|
||||
Str("provided", claims.GetIssuer()).
|
||||
Str("required", h.cfg.Issuer).
|
||||
Msg("unequal or missing issuer when validating JWT")
|
||||
errs = append(errs, jwt.ErrTokenInvalidIssuer)
|
||||
}
|
||||
if !claims.VerifyAudience(h.cfg.Audience, true) {
|
||||
logger.Debug().
|
||||
Strs("provided", claims.GetAudience()).
|
||||
Str("required", h.cfg.Audience).
|
||||
Msg("unequal or missing audience when validating JWT")
|
||||
errs = append(errs, jwt.ErrTokenInvalidAudience)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
+28
-30
@@ -3,7 +3,6 @@ package jwt
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
cr "crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -14,7 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/minio/sha256-simd"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -71,7 +70,12 @@ func init() {
|
||||
}
|
||||
infoHash, _ = bittorrent.NewInfoHash(ihBytes)
|
||||
s2 := sha256.New()
|
||||
s2.Write(elliptic.Marshal(privKey.PublicKey.Curve, privKey.PublicKey.X, privKey.PublicKey.Y))
|
||||
ecdhPubKey, err := privKey.PublicKey.ECDH()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s2.Write(ecdhPubKey.Bytes())
|
||||
//s2.Write(elliptic.Marshal(privKey.PublicKey.Curve, privKey.PublicKey.X, privKey.PublicKey.Y))
|
||||
jwksData = JWKSKeys{Keys: []JWKSKey{
|
||||
{
|
||||
KeyType: "EC",
|
||||
@@ -92,15 +96,13 @@ func TestHook_HandleAnnounceValid(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, announceClaims{
|
||||
registeredClaimsWrapper: registeredClaimsWrapper{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "CN=test",
|
||||
Subject: "CN=test",
|
||||
Audience: []string{"test"},
|
||||
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(time.Hour)},
|
||||
NotBefore: &jwt.NumericDate{Time: time.Now().Add(-time.Hour)},
|
||||
ID: strconv.FormatInt(rand.Int63(), 16),
|
||||
},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "CN=test",
|
||||
Subject: "CN=test",
|
||||
Audience: []string{"test"},
|
||||
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(time.Hour)},
|
||||
NotBefore: &jwt.NumericDate{Time: time.Now().Add(-time.Hour)},
|
||||
ID: strconv.FormatInt(rand.Int63(), 16),
|
||||
},
|
||||
InfoHash: infoHash.String(),
|
||||
})
|
||||
@@ -137,15 +139,13 @@ func TestHook_HandleAnnounceInvalid(t *testing.T) {
|
||||
// now we wll use HMAC-SHA256 with invalid random key
|
||||
// all errors should be nil except announce request
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, announceClaims{
|
||||
registeredClaimsWrapper: registeredClaimsWrapper{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "CN=test",
|
||||
Subject: "CN=test",
|
||||
Audience: []string{"test"},
|
||||
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(time.Hour)},
|
||||
NotBefore: &jwt.NumericDate{Time: time.Now().Add(-time.Hour)},
|
||||
ID: strconv.FormatInt(rand.Int63(), 16),
|
||||
},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "CN=test",
|
||||
Subject: "CN=test",
|
||||
Audience: []string{"test"},
|
||||
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(time.Hour)},
|
||||
NotBefore: &jwt.NumericDate{Time: time.Now().Add(-time.Hour)},
|
||||
ID: strconv.FormatInt(rand.Int63(), 16),
|
||||
},
|
||||
InfoHash: infoHash.String(),
|
||||
})
|
||||
@@ -194,15 +194,13 @@ func TestHook_HandleScrapeValid(t *testing.T) {
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, scrapeClaims{
|
||||
registeredClaimsWrapper: registeredClaimsWrapper{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "CN=test",
|
||||
Subject: "CN=test",
|
||||
Audience: []string{"test"},
|
||||
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(time.Hour)},
|
||||
NotBefore: &jwt.NumericDate{Time: time.Now().Add(-time.Hour)},
|
||||
ID: strconv.FormatInt(rand.Int63(), 16),
|
||||
},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "CN=test",
|
||||
Subject: "CN=test",
|
||||
Audience: []string{"test"},
|
||||
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(time.Hour)},
|
||||
NotBefore: &jwt.NumericDate{Time: time.Now().Add(-time.Hour)},
|
||||
ID: strconv.FormatInt(rand.Int63(), 16),
|
||||
},
|
||||
InfoHashes: ihss,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user