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

Implementing JWKs #57

Merged
merged 46 commits into from
Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ff0672f
#48: adding code from
shytikov Aug 30, 2021
df588de
#48: adding `KeySetUrl` configuration
shytikov Aug 30, 2021
1342630
#48: adding JWKs refresh-related configuration
shytikov Aug 30, 2021
20e93c9
#48: correcting error thrown on missing key information
shytikov Aug 30, 2021
67ebf42
#48: adding switch between JWT and JWKs `KeyFunc` implementations
shytikov Aug 30, 2021
3b5a3c7
#48: starting to hide members that should not be public in new library
shytikov Aug 30, 2021
e3c6e52
#48: successfully getting a key and verifying a token
Aug 31, 2021
63296b9
#48: removing unused code
Aug 31, 2021
54457bb
#48: making `NewKeys` function private
Aug 31, 2021
e72678c
#48: removing unused file
Aug 31, 2021
74bbada
#48: renaming types to be more uniform
Aug 31, 2021
cab0148
#48: extracting `Config` to separate file
Aug 31, 2021
36d8755
#48: moving JWT utility functions to separate file
Aug 31, 2021
7f2c17c
#48: remaing `jwt.go` to `main.go`
Aug 31, 2021
b7e901e
#48: remaing `utils.go` to `jwt.go`
Aug 31, 2021
fa04f7d
#48: moving JWT `KeyFunc` from `main.go` to `jwt.go`
Aug 31, 2021
b119992
#48: adding comment to extracted function
Aug 31, 2021
41dee30
#48: moving retrieve JWKs functions to `jwks.go` file
Aug 31, 2021
cfca566
#48: renaming `start/stop` refresh functions
Aug 31, 2021
a858185
#48: removing redundant configurations
Aug 31, 2021
24ca195
#48: moving `jwks.keyFunc` to `jwks.go`
Aug 31, 2021
804da64
#48: merging `rsa.go` and `ecdsa.go` to `crypto.go`
Aug 31, 2021
a7edf11
#48: download keys on first call, not on start of the app, because st…
Aug 31, 2021
3bcfba7
#48: reintroducting success / error handlers for refresh key set oper…
Aug 31, 2021
25ba2ed
#48: giving ability to change configuration or stop refreshing in cas…
Aug 31, 2021
98a6d58
#48: unexporting error variables
Aug 31, 2021
dd573f3
#48: renaming `KeySetUrl` to `KeySetURL`
Aug 31, 2021
e204278
#48: correcting documentation comment for refresh handlers
Aug 31, 2021
21cfa1b
#48: resolving code smells
Aug 31, 2021
478e705
#48: extracting `initConfiguration` function for easier testing
shytikov Sep 2, 2021
5e12339
#48: extracting `initExtractors` function
shytikov Sep 2, 2021
791eb08
#48: adding names of HMAC algorithms
shytikov Sep 2, 2021
626a082
#48: adding first simple tests
shytikov Sep 2, 2021
1048c44
#48: adding documentation comments
shytikov Sep 2, 2021
7e0986c
#48: adding tests for `initExtractors` function
shytikov Sep 2, 2021
92e09e8
#48: reusing `defaultTokenLookup` variable
shytikov Sep 4, 2021
a3868fb
#48: reorganizing configuration for better testing
shytikov Sep 4, 2021
2dc41d2
#48: correcting test that failed after reorganization
shytikov Sep 4, 2021
e931108
8: adding tests for HMAC algorithm
shytikov Sep 5, 2021
5cb8c0f
#48: adding failing `jwks` test
shytikov Sep 6, 2021
20351d7
#48: exposing algorithm names, so they could be used as parameters
Sep 6, 2021
b32409c
#48: adding valid RSA tokens
shytikov Sep 6, 2021
3e0b3bc
#48: adding valid ECDSA tokens
shytikov Sep 6, 2021
26213fe
#48: adding bits of documentation
Sep 6, 2021
e82b5fe
#48: making assignment to pass linting
shytikov Sep 13, 2021
baf53f9
#48: changing code to compy with Go `1.15.x`
shytikov Sep 13, 2021
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ jwtware.New(config ...jwtware.Config) func(*fiber.Ctx) error
| Claims | `jwt.Claim` | Claims are extendable claims data defining token content. | `jwt.MapClaims{}` |
| TokenLookup | `string` | TokenLookup is a string in the form of `<source>:<name>` that is used | `"header:Authorization"` |
| AuthScheme | `string` |AuthScheme to be used in the Authorization header. | `"Bearer"` |
| KeySetURL | `string` |KeySetURL location of JSON file with signing keys. | `""` |
| KeyRefreshSuccessHandler | `func(j *KeySet)` |KeyRefreshSuccessHandler defines a function which is executed for a valid refresh of signing keys.| `nil` |
| KeyRefreshErrorHandler | `func(j *KeySet, err error)` |KeyRefreshErrorHandler defines a function which is executed for an invalid refresh of signing keys. | `nil` |
| KeyRefreshInterval | `*time.Duration` |KeyRefreshInterval is the duration to refresh the JWKs in the background via a new HTTP request. | `nil` |
| KeyRefreshRateLimit | `*time.Duration` |KeyRefreshRateLimit limits the rate at which refresh requests are granted. | `nil` |
| KeyRefreshTimeout | `*time.Duration` |KeyRefreshTimeout is the duration for the context used to create the HTTP request for a refresh of the JWKs. | `1min` |
| KeyRefreshUnknownKID | `bool` |KeyRefreshUnknownKID indicates that the JWKs refresh request will occur every time a kid that isn't cached is seen. | `false` |


### HS256 Example
Expand Down Expand Up @@ -233,3 +240,6 @@ func restricted(c *fiber.Ctx) error {

### RS256 Test
The RS256 is actually identical to the HS256 test above.

### JWKs Test
The tests are identical to basic `JWT` tests above, with exception that `KeySetURL` to valid public keys collection in JSON format should be supplied.
179 changes: 179 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package jwtware

import (
"strings"
"time"

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

// 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.
// Optional. Default: nil
Filter func(*fiber.Ctx) bool

// SuccessHandler defines a function which is executed for a valid token.
// Optional. Default: nil
SuccessHandler fiber.Handler

// ErrorHandler defines a function which is executed for an invalid token.
// It may be used to define a custom JWT error.
// Optional. Default: 401 Invalid or expired JWT
ErrorHandler fiber.ErrorHandler

// Signing key to validate token. Used as fallback if SigningKeys has length 0.
// Required. This, SigningKeys or KeySetUrl.
SigningKey interface{}

// Map of signing keys to validate token with kid field usage.
// Required. This, SigningKey or KeySetUrl.
SigningKeys map[string]interface{}

// URL where set of private keys could be downloaded.
// Required. This, SigningKey or SigningKeys.
KeySetURL 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` 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` 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` 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` 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"
SigningMethod string

// Context key to store user information from the token into context.
// Optional. Default: "user".
ContextKey string

// Claims are extendable claims data defining token content.
// Optional. Default value jwt.MapClaims
Claims jwt.Claims

// TokenLookup is a string in the form of "<source>:<name>" that is used
// to extract token from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "param:<name>"
// - "cookie:<name>"
TokenLookup string

// AuthScheme to be used in the Authorization header.
// Optional. Default: "Bearer".
AuthScheme string

keyFunc jwt.Keyfunc
}

// makeCfg function will check correctness of supplied configuration
// and will complement it with default values instead of missing ones
func makeCfg(config []Config) (cfg Config) {
if len(config) > 0 {
cfg = config[0]
}
if cfg.SuccessHandler == nil {
cfg.SuccessHandler = func(c *fiber.Ctx) error {
return c.Next()
}
}
if cfg.ErrorHandler == nil {
cfg.ErrorHandler = func(c *fiber.Ctx, err error) error {
if err.Error() == "Missing or malformed JWT" {
return c.Status(fiber.StatusBadRequest).SendString("Missing or malformed JWT")
}
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired JWT")
}
}
if cfg.SigningKey == nil && len(cfg.SigningKeys) == 0 && cfg.KeySetURL == "" {
panic("Fiber: JWT middleware requires signing key or url where to download one")
}
if cfg.SigningMethod == "" && cfg.KeySetURL == "" {
cfg.SigningMethod = "HS256"
}
if cfg.ContextKey == "" {
cfg.ContextKey = "user"
}
if cfg.Claims == nil {
cfg.Claims = jwt.MapClaims{}
}
if cfg.TokenLookup == "" {
cfg.TokenLookup = defaultTokenLookup
}
if cfg.AuthScheme == "" {
cfg.AuthScheme = "Bearer"
}
if cfg.KeyRefreshTimeout == nil {
cfg.KeyRefreshTimeout = &defaultKeyRefreshTimeout
}
if cfg.KeySetURL != "" {
jwks := &KeySet{
Config: &cfg,
}
cfg.keyFunc = jwks.keyFunc()
} else {
cfg.keyFunc = jwtKeyFunc(cfg)
}
return cfg
}

// 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 {
// Initialize
extractors := make([]jwtExtractor, 0)
rootParts := strings.Split(cfg.TokenLookup, ",")
for _, rootPart := range rootParts {
parts := strings.Split(strings.TrimSpace(rootPart), ":")

switch parts[0] {
case "header":
extractors = append(extractors, jwtFromHeader(parts[1], cfg.AuthScheme))
case "query":
extractors = append(extractors, jwtFromQuery(parts[1]))
case "param":
extractors = append(extractors, jwtFromParam(parts[1]))
case "cookie":
extractors = append(extractors, jwtFromCookie(parts[1]))
}
}
return extractors
}
84 changes: 84 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package jwtware

import (
"testing"
)

func TestPanicOnMissingConfiguration(t *testing.T) {
t.Parallel()

defer func() {
// Assert
if err := recover(); err == nil {
t.Fatalf("Middleware should panic on missing configuration")
}
}()

// Arrange
config := make([]Config, 0)

// Act
makeCfg(config)
}

func TestDefaultConfiguration(t *testing.T) {
t.Parallel()

defer func() {
// Assert
if err := recover(); err != nil {
t.Fatalf("Middleware should not panic")
}
}()

// Arrange
config := append(make([]Config, 0), Config{
SigningKey: "",
})

// Act
cfg := makeCfg(config)

// Assert
if cfg.SigningMethod != HS256 {
t.Fatalf("Default signing method should be 'HS256'")
}
if cfg.ContextKey != "user" {
t.Fatalf("Default context key should be 'user'")
}
if cfg.Claims == nil {
t.Fatalf("Default claims should not be 'nil'")
}

if cfg.TokenLookup != defaultTokenLookup {
t.Fatalf("Default token lookup should be '%v'", defaultTokenLookup)
}
if cfg.AuthScheme != "Bearer" {
t.Fatalf("Default auth scheme should be 'Bearer'")
}
}

func TestExtractorsInitialization(t *testing.T) {
t.Parallel()

defer func() {
// Assert
if err := recover(); err != nil {
t.Fatalf("Middleware should not panic")
}
}()

// Arrange
cfg := Config{
SigningKey: "",
TokenLookup: defaultTokenLookup + ",query:token,param:token,cookie:token,something:something",
}

// Act
extractors := cfg.getExtractors()

// Assert
if len(extractors) != 4 {
t.Fatalf("Extractors should not be created for invalid lookups")
}
}
Loading