Skip to content

Commit

Permalink
Merge pull request #63 from MicahParks/alg_check
Browse files Browse the repository at this point in the history
Perform Algorithm Verification
  • Loading branch information
MicahParks authored Nov 1, 2022
2 parents a939f14 + a22a0a6 commit 5a2fb27
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 20 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jwksURL := os.Getenv("JWKS_URL")

// Confirm the environment variable is not empty.
if jwksURL == "" {
log.Fatalln("JWKS_URL environment variable must be populated.")
log.Fatalln("JWKS_URL environment variable must be populated.")
}
```

Expand All @@ -81,7 +81,7 @@ Via HTTP:
// Create the JWKS from the resource at the given URL.
jwks, err := keyfunc.Get(jwksURL, keyfunc.Options{}) // See recommended options in the examples directory.
if err != nil {
log.Fatalf("Failed to get the JWKS from the given URL.\nError: %s", err)
log.Fatalf("Failed to get the JWKS from the given URL.\nError: %s", err)
}
```
Via JSON:
Expand All @@ -92,7 +92,7 @@ var jwksJSON = json.RawMessage(`{"keys":[{"kid":"zXew0UJ1h6Q4CCcd_9wxMzvcp5cEBif
// Create the JWKS from the resource at the given URL.
jwks, err := keyfunc.NewJSON(jwksJSON)
if err != nil {
log.Fatalf("Failed to create JWKS from JSON.\nError: %s", err)
log.Fatalf("Failed to create JWKS from JSON.\nError: %s", err)
}
```
Via a given key:
Expand All @@ -103,7 +103,7 @@ uniqueKeyID := "myKeyID"

// Create the JWKS from the HMAC key.
jwks := keyfunc.NewGiven(map[string]keyfunc.GivenKey{
uniqueKeyID: keyfunc.NewGivenHMAC(key),
uniqueKeyID: keyfunc.NewGivenHMAC(key),
})
```

Expand All @@ -117,7 +117,7 @@ features mentioned at the bottom of this `README.md`.
// Parse the JWT.
token, err := jwt.Parse(jwtB64, jwks.Keyfunc)
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
return nil, fmt.Errorf("failed to parse token: %w", err)
}
```

Expand Down Expand Up @@ -180,6 +180,11 @@ base64url the same as RFC 7515 Section 2:
However, this package will remove trailing padding on base64url encoded keys to account for improper implementations of
JWKS.

This package will check the `alg` in each JWK. If present, it will confirm the same `alg` is in a given JWT's header
before returning the key for signature verification. If the `alg`s do not match, `keyfunc.ErrJWKAlgMismatch` will
prevent the key being used for signature verification. If the `alg` is not present in the JWK, this check will not
occur.

## References
This project was built and tested using various RFCs and services. The services are listed below:
* [Keycloak](https://www.keycloak.org/)
Expand Down
25 changes: 25 additions & 0 deletions alg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package keyfunc_test

import (
"encoding/json"
"errors"
"testing"

"github.com/golang-jwt/jwt/v4"

"github.com/MicahParks/keyfunc"
)

func TestAlgMismatch(t *testing.T) {
const jwtB64 = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6IkM2NXEwRUtReWhwZDFtNGZyN1NLTzJIZV9uQXhnQ3RBZHdzNjRkMkJMdDgifQ.eyJleHAiOjE2MTU0MDcwMjYsImlhdCI6MTYxNTQwNjk2NiwianRpIjoiMzg1NjE4ODItOTA5MS00ODY3LTkzYmYtMmE3YmU4NTc3YmZiIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJhZDEyOGRmMS0xMTQwLTRlNGMtYjA5Ny1hY2RjZTcwNWJkOWIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0b2tlbmRlbG1lIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SG9zdCI6IjE3Mi4yMC4wLjEiLCJjbGllbnRJZCI6InRva2VuZGVsbWUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC10b2tlbmRlbG1lIiwiY2xpZW50QWRkcmVzcyI6IjE3Mi4yMC4wLjEifQ.Cmgz3aC_b_kpOmGM-_nRisgQul0d9Jg7BpMLe5F_fdryRhwhW5fQBZtz6FipQ0Tc4jggI6L3Dx1jS2kn823aWCR0x-OAFCawIXnwgAKuM1m2NL7Y6LKC07nytdB_qU4GknAl3jEG-tZIJBHQwYP-K6QKmAT9CdF1ZPbc9u8RgRCPN8UziYcOpvStiG829BO7cTzCt7tp5dJhem8_CnRWBKzelP1fs_z4fAQtW2sgyhX9SUYb5WON-4zrn4i01FlYUwZV-AC83zP6BuHIiy3XpAuTiTp2BjZ-1nzCLWBRpIm_lOObFeo-3AQqWPxzLVAmTFQMKReUF9T8ehL2Osr1XQ"

jwks, err := keyfunc.NewJSON(json.RawMessage(jwksJSON))
if err != nil {
t.Fatalf("Failed to create JWKS from JSON: %v", err)
}

_, err = jwt.Parse(jwtB64, jwks.Keyfunc)
if !errors.Is(err, keyfunc.ErrJWKAlgMismatch) {
t.Fatalf("Expected ErrJWKAlgMismatch, got %v", err)
}
}
39 changes: 25 additions & 14 deletions jwks.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
)

var (
// ErrJWKAlgMismatch indicates that the given JWK was found, but its "alg" parameter's value did not match that of
// the JWT.
ErrJWKAlgMismatch = errors.New(`the given JWK was found, but its "alg" parameter's value did not match the expected algorithm`)

// ErrJWKUseWhitelist indicates that the given JWK was found, but its "use" parameter's value was not whitelisted.
ErrJWKUseWhitelist = errors.New(`the given JWK was found, but its "use" parameter's value was not whitelisted`)

Expand Down Expand Up @@ -39,21 +43,23 @@ type JWKUse string

// jsonWebKey represents a JSON Web Key inside a JWKS.
type jsonWebKey struct {
Curve string `json:"crv"`
Exponent string `json:"e"`
K string `json:"k"`
ID string `json:"kid"`
Modulus string `json:"n"`
Type string `json:"kty"`
Use string `json:"use"`
X string `json:"x"`
Y string `json:"y"`
Algorithm string `json:"alg"`
Curve string `json:"crv"`
Exponent string `json:"e"`
K string `json:"k"`
ID string `json:"kid"`
Modulus string `json:"n"`
Type string `json:"kty"`
Use string `json:"use"`
X string `json:"x"`
Y string `json:"y"`
}

// parsedJWK represents a JSON Web Key parsed with fields as the correct Go types.
type parsedJWK struct {
use JWKUse
public interface{}
algorithm string
public interface{}
use JWKUse
}

// JWKS represents a JSON Web Key Set (JWK Set).
Expand Down Expand Up @@ -124,8 +130,9 @@ func NewJSON(jwksBytes json.RawMessage) (jwks *JWKS, err error) {
}

jwks.keys[key.ID] = parsedJWK{
use: JWKUse(key.Use),
public: keyInter,
algorithm: key.Algorithm,
use: JWKUse(key.Use),
public: keyInter,
}
}

Expand Down Expand Up @@ -181,7 +188,7 @@ func (j *JWKS) ReadOnlyKeys() map[string]interface{} {
}

// getKey gets the jsonWebKey from the given KID from the JWKS. It may refresh the JWKS if configured to.
func (j *JWKS) getKey(kid string) (jsonKey interface{}, err error) {
func (j *JWKS) getKey(alg, kid string) (jsonKey interface{}, err error) {
j.mux.RLock()
pubKey, ok := j.keys[kid]
j.mux.RUnlock()
Expand Down Expand Up @@ -221,5 +228,9 @@ func (j *JWKS) getKey(kid string) (jsonKey interface{}, err error) {
}
}

if pubKey.algorithm != "" && pubKey.algorithm != alg {
return nil, fmt.Errorf(`%w: JWK "alg" parameter value %q does not match token "alg" parameter value %q`, ErrJWKAlgMismatch, pubKey.algorithm, alg)
}

return pubKey.public, nil
}
9 changes: 8 additions & 1 deletion keyfunc.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ func (j *JWKS) Keyfunc(token *jwt.Token) (interface{}, error) {
return nil, fmt.Errorf("%w: could not convert kid in JWT header to string", ErrKID)
}

return j.getKey(kid)
alg, ok := token.Header["alg"].(string)
if !ok {
// For test coverage purposes, this should be impossible to reach because the JWT package rejects a token
// without an alg parameter in the header before calling jwt.Keyfunc.
return nil, fmt.Errorf(`%w: the JWT header did not contain the "alg" parameter, which is required by RFC 7515 section 4.1.1`, ErrJWKAlgMismatch)
}

return j.getKey(alg, kid)
}

// base64urlTrailingPadding removes trailing padding before decoding a string from base64url. Some non-RFC compliant
Expand Down

0 comments on commit 5a2fb27

Please sign in to comment.