MicahParks/keyfunc

Question: Generating a jwks.json for use in testing

cal-pratt opened this issue ยท 5 comments

I'm currently writing an Authentication middleware which uses this library to fetch a remote JWKS (so far so good! thank you!).
Now I'm looking to write tests, but I'm not sure how to generate my mock signing key and JWKS values.

I notice that example_jwks.json is hardcoded in this project. Is the generation of this file documented anywhere?

I'd like a simple way to have my test suite generate test tokens with a generated JWKS to use for the decoding/validation. Ideally these can be generated on the fly with a random seed, rather than having to hardcode in any static content. My plan afterwards is to create a mock HTTP server to expose the generated JWKS to keyfunc.Get.

I see you have https://github.com/MicahParks/jwkset Could probably combine these projects somehow for the unit tests?

@cal-pratt, thank you for opening this issue.

It has been on my TODO list for a long time to move the JWK Set client functionality from this project, github.com/MicahParks/keyfunc, to github.com/MicahParks/jwkset. This would allow me to write tests for keyfunc with dynamic JWK Sets and write other JWK Set clients/servers for other popular Golang JWT packages, not just the one from the golang-jwt organization. I haven't done this yet, as you can see, but it is something I intend on doing eventually ๐Ÿ™‚

As for your specific use case and separate project, my suggested way would be to use github.com/MicahParks/jwkset directly in your tests. This would involve using the in-memory storage implementation of jwkset, generating then storing keys, hosting via httptest, then using github.com/golang-jwt/jwt/v5 to create/sign tokens, then verifying the tokens.

I'll write an example sometime today and attach to this issue.

Awesome, thank you very much! ๐Ÿ˜„

Here is a full example. Feel free to reopen the issue if there's something missing.

package example_test

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/MicahParks/keyfunc/v2"
	"github.com/golang-jwt/jwt/v5"

	"github.com/MicahParks/jwkset"
)

func TestExample(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
	defer cancel()

	const (
		keyID = "my-key-id"
	)

	_, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("Failed to generate EdDSA key.\nError: %s.", err)
	}

	jwks := jwkset.NewMemory[any]()
	meta := jwkset.KeyWithMeta[any]{
		ALG:    jwkset.AlgEdDSA,
		Custom: nil,
		Key:    priv,
		KeyID:  keyID,
	}
	err = jwks.Store.WriteKey(ctx, meta)
	if err != nil {
		t.Fatalf("Failed to write EdDSA key to JWK Set.\nError: %s.", err)
	}

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		raw, err := jwks.JSONPublic(r.Context())
		if err != nil {
			t.Logf("Failed to get JWK Set JSON.\nError: %s.", err)
			http.Error(w, "Failed to get JWK Set JSON.", http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write(raw)
	}))

	unsigned := jwt.New(jwt.SigningMethodEdDSA)
	unsigned.Header["kid"] = keyID
	tokenString, err := unsigned.SignedString(priv)
	if err != nil {
		t.Fatalf("Failed to sign JWT.\nError: %s.", err)
	}

	opt := keyfunc.Options{
		Ctx: ctx,
	}
	jwksClient, err := keyfunc.Get(server.URL, opt)
	if err != nil {
		t.Fatalf("Failed to create JWK Set client.\nError: %s.", err)
	}

	signed, err := jwt.Parse(tokenString, jwksClient.Keyfunc)
	if err != nil {
		t.Fatalf("Failed to parse JWT.\nError: %s.", err)
	}
	if !signed.Valid {
		t.Fatal("JWT is invalid.")
	}

	t.Log("JWT is valid.")
}

Amazing. This was exactly what I was looking for. Thank you again!

Just to follow up, this worked perfectly!

Wrote a small test utility that lets me sign tokens with different usernames and claims, and then allows spawning the HTTP server for use with the middleware. Ends up with like three lines of code per test to try out different configurations :)