Skip to content
This repository has been archived by the owner on May 24, 2023. It is now read-only.

Upgrade to github.com/golang-jwt/jwt/v5 and use github.com/MicahParks/keyfunc/v2 for JWK Set client #129

Merged
merged 19 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 86 additions & 72 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
package jwtware

import (
"fmt"
"log"
"strings"
"time"

"github.com/MicahParks/keyfunc/v2"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
)

// KeyRefreshSuccessHandler is a function signature that consumes a set of signing key set.
// Presence of original signing key set allows to update configuration or stop background refresh.
type KeyRefreshSuccessHandler func(j *KeySet)

// KeyRefreshErrorHandler is a function signature that consumes a set of signing key set and an error.
// Presence of original signing key set allows to update configuration or stop background refresh.
type KeyRefreshErrorHandler func(j *KeySet, err error)

// Config defines the config for JWT middleware
type Config struct {
// Filter defines a function to skip middleware.
Expand All @@ -39,47 +34,6 @@ type Config struct {
// Required. This, SigningKey or KeySetUrl(deprecated) or KeySetUrls.
SigningKeys map[string]interface{}

// URL where set of private keys could be downloaded.
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
// Required. This, SigningKey or SigningKeys or KeySetURLs
// Deprecated, use KeySetURLs
KeySetURL string

// URLs where set of private keys could be downloaded.
// Required. This, SigningKey or SigningKeys or KeySetURL(deprecated)
// duplicate key entries are overwritten as encountered across urls
KeySetURLs []string

// KeyRefreshSuccessHandler defines a function which is executed on successful refresh of key set.
// Optional. Default: nil
KeyRefreshSuccessHandler KeyRefreshSuccessHandler

// KeyRefreshErrorHandler defines a function which is executed for refresh key set failure.
// Optional. Default: nil
KeyRefreshErrorHandler KeyRefreshErrorHandler

// KeyRefreshInterval is the duration to refresh the JWKs in the background via a new HTTP request. If this is not nil,
// then a background refresh will be requested in a separate goroutine at this interval until the JWKs method
// EndBackground is called.
// Optional. If set, the value will be used only if `KeySetUrl`(deprecated) or `KeySetUrls` is also present
KeyRefreshInterval *time.Duration

// KeyRefreshRateLimit limits the rate at which refresh requests are granted. Only one refresh request can be queued
// at a time any refresh requests received while there is already a queue are ignored. It does not make sense to
// have RefreshInterval's value shorter than this.
// Optional. If set, the value will be used only if `KeySetUrl`(deprecated) or `KeySetUrls` is also present
KeyRefreshRateLimit *time.Duration

// KeyRefreshTimeout is the duration for the context used to create the HTTP request for a refresh of the JWKs. This
// defaults to one minute. This is only effectual if RefreshInterval is not nil.
// Optional. If set, the value will be used only if `KeySetUrl`(deprecated) or `KeySetUrls` is also present
KeyRefreshTimeout *time.Duration

// KeyRefreshUnknownKID indicates that the JWKs refresh request will occur every time a kid that isn't cached is seen.
// Without specifying a RefreshInterval a malicious client could self-sign X JWTs, send them to this service,
// then cause potentially high network usage proportional to X.
// Optional. If set, the value will be used only if `KeySetUrl`(deprecated) or `KeySetUrls` is also present
KeyRefreshUnknownKID *bool

// Signing method, used to check token signing method.
// Optional. Default: "HS256".
// Possible values: "HS256", "HS384", "HS512", "ES256", "ES384", "ES512", "RS256", "RS384", "RS512"
Expand Down Expand Up @@ -107,16 +61,26 @@ type Config struct {
// Optional. Default: "Bearer".
AuthScheme string

// KeyFunc defines a user-defined function that supplies the public key for a token validation.
// KeyFunc is a function that supplies the public key for JWT cryptographic verification.
// The function shall take care of verifying the signing algorithm and selecting the proper key.
// A user-defined KeyFunc can be useful if tokens are issued by an external party.
// Internally, github.com/MicahParks/keyfunc/v2 package is used project defaults. If you need more customization,
// you can provide a jwt.Keyfunc using that package or make your own implementation.
//
// This option is mutually exclusive with and takes precedence over JWKSetURLs, SigningKeys, and SigningKey.
KeyFunc jwt.Keyfunc // TODO Could be renamed to Keyfunc
efectn marked this conversation as resolved.
Show resolved Hide resolved

// JWKSetURLs is a slice of HTTP URLs that contain the JSON Web Key Set (JWKS) used to verify the signatures of
// JWTs. Use of HTTPS is recommended. The presence of the "kid" field in the JWT header and JWKs is mandatory for
// this feature.
//
// By default, all JWK Sets in this slice will:
// * Refresh every hour.
// * Refresh automatically if a new "kid" is seen in a JWT being verified.
// * Rate limit refreshes to once every 5 minutes.
// * Timeout refreshes after 10 seconds.
//
// When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored.
// This is one of the three options to provide a token validation key.
// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
// Required if neither SigningKeys nor SigningKey is provided.
// Default to an internal implementation verifying the signing algorithm and selecting the proper key.
KeyFunc jwt.Keyfunc
// This field is compatible with the SigningKeys field.
JWKSetURLs []string
}

// makeCfg function will check correctness of supplied configuration
Expand All @@ -138,13 +102,10 @@ func makeCfg(config []Config) (cfg Config) {
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired JWT")
}
}
if cfg.KeySetURL != "" {
cfg.KeySetURLs = append(cfg.KeySetURLs, cfg.KeySetURL)
if cfg.SigningKey == nil && len(cfg.SigningKeys) == 0 && len(cfg.JWKSetURLs) == 0 && cfg.KeyFunc == nil {
panic("Fiber: JWT middleware requires at least one signing key or JWK Set URL")
}
if cfg.SigningKey == nil && len(cfg.SigningKeys) == 0 && len(cfg.KeySetURLs) == 0 && cfg.KeyFunc == nil {
panic("Fiber: JWT middleware requires signing key or url where to download one")
}
if cfg.SigningMethod == "" && len(cfg.KeySetURLs) == 0 {
if cfg.SigningMethod == "" && len(cfg.JWKSetURLs) == 0 {
cfg.SigningMethod = "HS256"
}
if cfg.ContextKey == "" {
Expand All @@ -160,23 +121,76 @@ func makeCfg(config []Config) (cfg Config) {
cfg.AuthScheme = "Bearer"
}
}
if cfg.KeyRefreshTimeout == nil {
cfg.KeyRefreshTimeout = &defaultKeyRefreshTimeout
}

if cfg.KeyFunc == nil {
if len(cfg.KeySetURLs) > 0 {
jwks := &KeySet{
Config: &cfg,
if len(cfg.SigningKeys) > 0 || len(cfg.JWKSetURLs) > 0 {
var givenKeys map[string]keyfunc.GivenKey
if cfg.SigningKeys != nil {
givenKeys = make(map[string]keyfunc.GivenKey, len(cfg.SigningKeys))
for kid, key := range cfg.SigningKeys {
givenKeys[kid] = keyfunc.NewGivenCustom(key, keyfunc.GivenKeyOptions{}) // TODO User supplied alg?
efectn marked this conversation as resolved.
Show resolved Hide resolved
}
}
if len(cfg.JWKSetURLs) > 0 {
var err error
if len(cfg.JWKSetURLs) == 1 {
cfg.KeyFunc, err = oneKeyfunc(cfg, givenKeys)
} else {
cfg.KeyFunc, err = multiKeyfunc(givenKeys, cfg.JWKSetURLs)
}
if err != nil {
panic("Failed to create keyfunc from JWK Set URL: " + err.Error()) // TODO Don't panic?
efectn marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
cfg.KeyFunc = keyfunc.NewGiven(givenKeys).Keyfunc
}
cfg.KeyFunc = jwks.keyFunc()
} else {
cfg.KeyFunc = jwtKeyFunc(cfg)
cfg.KeyFunc = func(token *jwt.Token) (interface{}, error) {
return cfg.SigningKey, nil
}
}
}

return cfg
}

func oneKeyfunc(cfg Config, givenKeys map[string]keyfunc.GivenKey) (jwt.Keyfunc, error) {
jwks, err := keyfunc.Get(cfg.JWKSetURLs[0], keyfuncOptions(givenKeys))
if err != nil {
return nil, fmt.Errorf("failed to get JWK Set URL: %w", err)
}
return jwks.Keyfunc, nil
}

func multiKeyfunc(givenKeys map[string]keyfunc.GivenKey, jwkSetURLs []string) (jwt.Keyfunc, error) {
opts := keyfuncOptions(givenKeys)
multiple := make(map[string]keyfunc.Options, len(jwkSetURLs))
for _, url := range jwkSetURLs {
multiple[url] = opts
}
multiOpts := keyfunc.MultipleOptions{
KeySelector: keyfunc.KeySelectorFirst,
}
multi, err := keyfunc.GetMultiple(multiple, multiOpts)
if err != nil {
return nil, fmt.Errorf("failed to get multiple JWK Set URLs: %w", err)
}
return multi.Keyfunc, nil
}

func keyfuncOptions(givenKeys map[string]keyfunc.GivenKey) keyfunc.Options {
return keyfunc.Options{
GivenKeys: givenKeys,
RefreshErrorHandler: func(err error) {
log.Printf("Failed to perform background refresh of JWK Set: %s.", err)
},
RefreshInterval: time.Hour,
RefreshRateLimit: time.Minute * 5,
RefreshTimeout: time.Second * 10,
RefreshUnknownKID: true,
}
}

// getExtractors function will create a slice of functions which will be used
// for token sarch and will perform extraction of the value
func (cfg *Config) getExtractors() []jwtExtractor {
Expand Down
117 changes: 0 additions & 117 deletions crypto.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
package jwtware

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"fmt"
"math/big"
)

const (
// HS256 represents a public cryptography key generated by a 256 bit HMAC algorithm.
HS256 = "HS256"
Expand Down Expand Up @@ -55,111 +46,3 @@ const (
// PS512 represents a public cryptography key generated by a 512 bit RSA algorithm.
PS512 = "PS512"
)

// getECDSA parses a JSONKey and turns it into an ECDSA public key.
func (j *rawJWK) getECDSA() (publicKey *ecdsa.PublicKey, err error) {
// Check if the key has already been computed.
if j.precomputed != nil {
var ok bool
if publicKey, ok = j.precomputed.(*ecdsa.PublicKey); ok {
return publicKey, nil
}
}

// Confirm everything needed is present.
if j.X == "" || j.Y == "" || j.Curve == "" {
return nil, fmt.Errorf("%w: ecdsa", errMissingAssets)
}

// Decode the X coordinate from Base64.
//
// According to RFC 7518, this is a Base64 URL unsigned integer.
// https://tools.ietf.org/html/rfc7518#section-6.3
var xCoordinate []byte
if xCoordinate, err = base64.RawURLEncoding.DecodeString(j.X); err != nil {
return nil, err
}

// Decode the Y coordinate from Base64.
var yCoordinate []byte
if yCoordinate, err = base64.RawURLEncoding.DecodeString(j.Y); err != nil {
return nil, err
}

// Create the ECDSA public key.
publicKey = &ecdsa.PublicKey{}

// Set the curve type.
var curve elliptic.Curve
switch j.Curve {
case P256:
curve = elliptic.P256()
case P384:
curve = elliptic.P384()
case P521:
curve = elliptic.P521()
}
publicKey.Curve = curve

// Turn the X coordinate into *big.Int.
//
// According to RFC 7517, these numbers are in big-endian format.
// https://tools.ietf.org/html/rfc7517#appendix-A.1
publicKey.X = big.NewInt(0).SetBytes(xCoordinate)

// Turn the Y coordinate into a *big.Int.
publicKey.Y = big.NewInt(0).SetBytes(yCoordinate)

// Keep the public key so it won't have to be computed every time.
j.precomputed = publicKey

return publicKey, nil
}

// getRSA parses a JSONKey and turns it into an RSA public key.
func (j *rawJWK) getRSA() (publicKey *rsa.PublicKey, err error) {
// Check if the key has already been computed.
if j.precomputed != nil {
var ok bool
if publicKey, ok = j.precomputed.(*rsa.PublicKey); ok {
return publicKey, nil
}
}

// Confirm everything needed is present.
if j.Exponent == "" || j.Modulus == "" {
return nil, fmt.Errorf("%w: rsa", errMissingAssets)
}

// Decode the exponent from Base64.
//
// According to RFC 7518, this is a Base64 URL unsigned integer.
// https://tools.ietf.org/html/rfc7518#section-6.3
var exponent []byte
if exponent, err = base64.RawURLEncoding.DecodeString(j.Exponent); err != nil {
return nil, err
}

// Decode the modulus from Base64.
var modulus []byte
if modulus, err = base64.RawURLEncoding.DecodeString(j.Modulus); err != nil {
return nil, err
}

// Create the RSA public key.
publicKey = &rsa.PublicKey{}

// Turn the exponent into an integer.
//
// According to RFC 7517, these numbers are in big-endian format.
// https://tools.ietf.org/html/rfc7517#appendix-A.1
publicKey.E = int(big.NewInt(0).SetBytes(exponent).Uint64())

// Turn the modulus into a *big.Int.
publicKey.N = big.NewInt(0).SetBytes(modulus)

// Keep the public key so it won't have to be computed every time.
j.precomputed = publicKey

return publicKey, nil
}
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
module github.com/gofiber/jwt/v3
module github.com/gofiber/jwt/v4
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved

go 1.16
go 1.18
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved

require (
github.com/MicahParks/keyfunc/v2 v2.0.1
github.com/gofiber/fiber/v2 v2.44.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-jwt/jwt/v5 v5.0.0
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
github.com/MicahParks/keyfunc/v2 v2.0.1 h1:6FrNNvG/20gEKkjxV+5anrkq0VOF666G2zUn8lk8dgk=
github.com/MicahParks/keyfunc/v2 v2.0.1/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8=
github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
Expand Down
Loading