-
-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Breaking change by renaming `keyfunc.JWKs` to `keyfunc.JWKS`. Renamed other instances of JWKs to JWKS. * Breaking change that makes `keyfunc.Options` a required, non-variadic, argument. (An empty struct is still valid.) * Only recomputing the JWKS when the remote resources changes. * `EdDSA` with an `ed25519` curve is now supported. (`ed448` is not). * JWTs are marked compatible by `kty` header value, not `alg`. * Remote `oct` key types, including HMAC, are not supported. (Still supported through given keys.) * When a JWKS is read, all keys are precomputed or ignored. * A `map[string]interface{}` can be returned. The map key is a key ID, `kid`, to cryptographic keys from the JWKS. It is intended to be read-only.
- Loading branch information
1 parent
f3d1999
commit a7636b7
Showing
25 changed files
with
589 additions
and
401 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package keyfunc_test | ||
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"os" | ||
"path/filepath" | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/golang-jwt/jwt/v4" | ||
|
||
"github.com/MicahParks/keyfunc" | ||
) | ||
|
||
func TestChecksum(t *testing.T) { | ||
|
||
// Create a temporary directory to serve the JWKS from. | ||
tempDir, err := ioutil.TempDir("", "*") | ||
if err != nil { | ||
t.Errorf("Failed to create a temporary directory.\nError: %s", err.Error()) | ||
t.FailNow() | ||
} | ||
defer func() { | ||
if err = os.RemoveAll(tempDir); err != nil { | ||
t.Errorf("Failed to remove temporary directory.\nError: %s", err.Error()) | ||
t.FailNow() | ||
} | ||
}() | ||
|
||
// Create the JWKS file path. | ||
jwksFile := filepath.Join(tempDir, jwksFilePath) | ||
|
||
// Write the JWKS. | ||
if err = ioutil.WriteFile(jwksFile, []byte(jwksJSON), 0600); err != nil { | ||
t.Errorf("Failed to write JWKS file to temporary directory.\nError: %s", err.Error()) | ||
t.FailNow() | ||
} | ||
|
||
// Create the HTTP test server. | ||
server := httptest.NewServer(http.FileServer(http.Dir(tempDir))) | ||
defer server.Close() | ||
|
||
// Create testing options. | ||
testingRefreshErrorHandler := func(err error) { | ||
panic(fmt.Sprintf("Unhandled JWKS error: %s", err.Error())) | ||
} | ||
opts := keyfunc.Options{ | ||
RefreshErrorHandler: testingRefreshErrorHandler, | ||
RefreshUnknownKID: true, | ||
} | ||
|
||
// Set the JWKS URL. | ||
jwksURL := server.URL + jwksFilePath | ||
|
||
jwks, err := keyfunc.Get(jwksURL, opts) | ||
if err != nil { | ||
t.Errorf("Failed to get JWKS from testing URL.\nError: %s", err.Error()) | ||
t.FailNow() | ||
} | ||
defer jwks.EndBackground() | ||
|
||
// Get a map of all interface pointers for the JWKS. | ||
cryptoKeyPointers := make(map[string]interface{}) | ||
for kid, cryptoKey := range jwks.ReadOnlyKeys() { | ||
cryptoKeyPointers[kid] = cryptoKey | ||
} | ||
|
||
// Create a JWT that will not be in the JWKS. | ||
token := jwt.New(jwt.SigningMethodHS256) | ||
token.Header["kid"] = "unknown" | ||
signed, err := token.SignedString([]byte("test")) | ||
if err != nil { | ||
t.Errorf("Failed to sign test JWT.\nError: %s", err.Error()) | ||
t.FailNow() | ||
} | ||
|
||
// Force the JWKS to refresh. | ||
_, _ = jwt.Parse(signed, jwks.Keyfunc) | ||
|
||
// Confirm the keys in the JWKS have not been refreshed. | ||
newKeys := jwks.ReadOnlyKeys() | ||
if len(newKeys) != len(cryptoKeyPointers) { | ||
t.Errorf("The number of keys should not be different.") | ||
t.FailNow() | ||
} | ||
for kid, cryptoKey := range newKeys { | ||
if !reflect.DeepEqual(cryptoKeyPointers[kid], cryptoKey) { | ||
t.Errorf("The JWKS should not have refreshed without a checksum change.") | ||
t.FailNow() | ||
} | ||
} | ||
|
||
// Write a new JWKS to the test file. | ||
_, _, jwksBytes, _, err := keysAndJWKS() | ||
if err != nil { | ||
t.Errorf("Failed to create a test JWKS.\nError: %s", err.Error()) | ||
t.FailNow() | ||
} | ||
|
||
// Write a different JWKS. | ||
if err = ioutil.WriteFile(jwksFile, jwksBytes, 0600); err != nil { | ||
t.Errorf("Failed to write JWKS file to temporary directory.\nError: %s", err.Error()) | ||
t.FailNow() | ||
} | ||
|
||
// Force the JWKS to refresh. | ||
_, _ = jwt.Parse(signed, jwks.Keyfunc) | ||
|
||
// Confirm the keys in the JWKS have been refreshed. | ||
newKeys = jwks.ReadOnlyKeys() | ||
different := false | ||
for kid, cryptoKey := range newKeys { | ||
if !reflect.DeepEqual(cryptoKeyPointers[kid], cryptoKey) { | ||
different = true | ||
break | ||
} | ||
} | ||
if !different { | ||
t.Errorf("A different JWKS checksum should have triggered a JWKS refresh.") | ||
t.FailNow() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package keyfunc | ||
|
||
import ( | ||
"crypto/ed25519" | ||
"encoding/base64" | ||
"fmt" | ||
) | ||
|
||
const ( | ||
|
||
// ktyEC is the key type (kty) in the JWT header for EdDSA. | ||
ktyOKP = "OKP" | ||
) | ||
|
||
// EdDSA parses a jsonWebKey and turns it into a EdDSA public key. | ||
func (j *jsonWebKey) EdDSA() (publicKey ed25519.PublicKey, err error) { | ||
|
||
// Confirm everything needed is present. | ||
if j.X == "" { | ||
return nil, fmt.Errorf("%w: %s", ErrMissingAssets, ktyOKP) | ||
} | ||
|
||
// Decode the public key from Base64. | ||
// | ||
// According to RFC 8037, this is from Base64 URL bytes. | ||
// https://datatracker.ietf.org/doc/html/rfc8037#appendix-A.2 | ||
var publicBytes []byte | ||
if publicBytes, err = base64.RawURLEncoding.DecodeString(j.X); err != nil { | ||
return nil, err | ||
} | ||
|
||
return publicBytes, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.