diff --git a/.golangci.yaml b/.golangci.yaml index a9a4923a3..17d5a317b 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -109,11 +109,17 @@ linters-settings: # k8s.io - pkg: k8s.io/api/core/v1 alias: corev1 - # OAuth2/OIDC/Fosite + # OAuth2/OIDC/Fosite/JOSE - pkg: github.com/coreos/go-oidc/v3/oidc alias: coreosoidc - pkg: github.com/ory/fosite/handler/oauth2 alias: fositeoauth2 + - pkg: github.com/ory/fosite/token/jwt + alias: fositejwt + - pkg: github.com/go-jose/go-jose/v4/jwt + alias: josejwt + - pkg: github.com/go-jose/go-jose/v3 + alias: oldjosev3 # Generated Pinniped - pkg: go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1 alias: authenticationv1alpha1 diff --git a/go.mod b/go.mod index e0588556c..65d71f15e 100644 --- a/go.mod +++ b/go.mod @@ -26,10 +26,6 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => go.op // This is an indirect dep which has CVE-2023-45142, so replace it with the fixed version. replace go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace => go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.44.0 -// https://github.com/coreos/go-oidc/releases/tag/v3.10.0 starts to use https://github.com/go-jose/go-jose/releases/tag/v4.0.0. -// Unfortunately this has breaking changes. -replace github.com/coreos/go-oidc/v3 => github.com/coreos/go-oidc/v3 v3.9.0 - require ( github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/chromedp/cdproto v0.0.0-20240614221651-cc28c8fb63e7 @@ -40,6 +36,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/felixge/httpsnoop v1.0.4 github.com/go-jose/go-jose/v3 v3.0.3 + github.com/go-jose/go-jose/v4 v4.0.2 github.com/go-ldap/ldap/v3 v3.4.8 github.com/go-logr/logr v1.4.2 github.com/go-logr/stdr v1.2.2 diff --git a/go.sum b/go.sum index 09e67dfa4..ebc64011f 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWH github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= -github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= +github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= +github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -150,6 +150,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go index 529798d1e..56c84a7e4 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go @@ -17,7 +17,7 @@ import ( "time" coreosoidc "github.com/coreos/go-oidc/v3/oidc" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -75,7 +75,18 @@ const ( defaultUsernameClaim = oidcapi.IDTokenClaimUsername defaultGroupsClaim = oidcapi.IDTokenClaimGroups - minimalJWTToTriggerJWKSFetch = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.e30." + // This hardcoded JWT will be used below to trick coreosoidc.RemoteKeySet into immediately fetching the remote JWKS. + // Unfortunately, coreosoidc.RemoteKeySet has no public API to ask it to perform this fetch. However, calling + // VerifySignature() and passing in any valid JWT will cause it to immediately fetch the remote JWKS, and return + // any errors that might have occurred during the fetch. This controller wants to be able to tell the user + // about those fetch errors to aid in user debugging of configuration. It is also a benefit for the in-memory + // cached authenticator to have already preloaded the JWKS, so it is immediately ready to perform authentications. + // Because we are passing a hardcoded JWT that was signed by a randomly generated signing key, the code below + // expects that VerifySignature() should always return a signature error for this JWT (with the side effect of + // fetching the remote JWKS) because no OIDC provider in the world should be using this signing key in its JWKS. + // If somehow this JWT is considered verified by a remote JWKS, then the code below will treat it as an error. + // This JWT and its signing key will not be used for any other purpose. + minimalJWTToTriggerJWKSFetch = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.e30.bqGS21VimxEQES46lfR6-AKt_IQjZKykJGD7uyfer2QAp9d63lzcKZJq8qLj-hnLDJFnvy8F2IU1w26n4RX8s5IWxtkaeS2IZctruZzxg-TngPv4yslOznTPROIx_YOk8TMoz6-qTXWUjVTsu4RcVFnbFtdhRF0V-rfdL7Fah_DGxQM2lteb1nPB0hcN81Q8ony5Kw-NxKIpXXGr977u_SYqnafhlYIyL8W4iMN39xO3F7U3JuiySOUmJjBQPd7jL2XnWQwVcxpiZkjzWpnfEX-jqORMzDMRhbD3EfCBJsc-8NQvC4E9VoIgw2KEsfRHhyPyHITGzYOU7XUA5MnBKg" ) type providerJSON struct { @@ -452,15 +463,14 @@ func (c *jwtCacheFillerController) validateJWKSFetch(ctx context.Context, jwksUR // fetch the keys at all. _, verifyWithKeySetErr := keySet.VerifySignature(ctx, minimalJWTToTriggerJWKSFetch) if verifyWithKeySetErr == nil { - // No unit test. // Since we hard-coded this token we expect there to always be a verification error. // The purpose of this function is really to test if we can get the JWKS, not to actually validate a token. // Therefore, we should never hit this path, nevertheless, lets handle just in case something unexpected happens. - errText := "jwks should not have verified unsigned jwt token" + errText := "remote jwks should not have been able to verify hardcoded test jwt token" conditions = append(conditions, &metav1.Condition{ Type: typeJWKSFetchValid, - Status: metav1.ConditionUnknown, - Reason: reasonUnableToValidate, + Status: metav1.ConditionFalse, + Reason: reasonInvalidCouldNotFetchJWKS, Message: errText, }) return nil, conditions, errors.New(errText) diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go index deff9907b..eb982844d 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go @@ -10,7 +10,10 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "crypto/x509" + _ "embed" "encoding/json" + "encoding/pem" "errors" "fmt" "net/http" @@ -18,10 +21,9 @@ import ( "testing" "time" - "github.com/go-jose/go-jose/v3" - "github.com/go-jose/go-jose/v3/jwt" + "github.com/go-jose/go-jose/v4" + josejwt "github.com/go-jose/go-jose/v4/jwt" "github.com/google/go-cmp/cmp" - fositejwt "github.com/ory/fosite/token/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -46,11 +48,42 @@ import ( "go.pinniped.dev/internal/testutil/tlsserver" ) +// This private key was randomly generated and then used to sign a single JWT. +// That JWT is hardcoded as the constant minimalJWTToTriggerJWKSFetch. +// TestMinimalJWTToTriggerJWKSFetch below shows how that JWT was created. +// This JWT will be used for only one purpose: to trick coreos.RemoteKeySet +// into fetching a remote JWKS immediately. Because we expect that there is no +// OIDC provider in the world that uses this randomly generated signing key, +// then we can safely assume that this JWT should never successfully verify +// using the real JWKS of any OIDC provider. If somehow it did verify, the +// production code in the controller would consider that to be an error and +// would refuse to load that JWTAuthenticator. The production code wants to +// get a signature error, which means that the remote JWKS was successfully +// loaded. +// +// This key was created using: +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// t.Log(string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}))) +// +//go:embed testdata/test.key +var rsaPrivateKeyToSignMinimalJWTToTriggerJWKSFetch string + func TestMinimalJWTToTriggerJWKSFetch(t *testing.T) { - tinyJWT := fositejwt.NewWithClaims(fositejwt.SigningMethodNone, fositejwt.MapClaims{}) - tinyJWTStr, err := tinyJWT.SignedString(fositejwt.UnsafeAllowNoneSignatureType) + hardcodedPEMDecoded, _ := pem.Decode([]byte(rsaPrivateKeyToSignMinimalJWTToTriggerJWKSFetch)) + require.NotNil(t, hardcodedPEMDecoded, "failed to parse hardcoded PEM") + hardcodedPrivateKey, err := x509.ParsePKCS1PrivateKey(hardcodedPEMDecoded.Bytes) require.NoError(t, err) - require.Equal(t, tinyJWTStr, minimalJWTToTriggerJWKSFetch) + + signer, err := jose.NewSigner( + jose.SigningKey{Algorithm: jose.RS256, Key: hardcodedPrivateKey}, + (&jose.SignerOptions{}).WithType("JWT"), + ) + require.NoError(t, err) + + jwt, err := josejwt.Signed(signer).Claims(josejwt.Claims{}).Serialize() + require.NoError(t, err) + require.Equal(t, jwt, minimalJWTToTriggerJWKSFetch) } func TestController(t *testing.T) { @@ -62,6 +95,11 @@ func TestController(t *testing.T) { goodAudience = "some-audience" ) + hardcodedPEMDecoded, _ := pem.Decode([]byte(rsaPrivateKeyToSignMinimalJWTToTriggerJWKSFetch)) + require.NotNil(t, hardcodedPEMDecoded, "failed to parse hardcoded PEM") + hardcodedPrivateKey, err := x509.ParsePKCS1PrivateKey(hardcodedPEMDecoded.Bytes) + require.NoError(t, err) + goodECSigningKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) goodECSigningAlgo := jose.ES256 require.NoError(t, err) @@ -122,12 +160,12 @@ func TestController(t *testing.T) { // A sub (subject) Claim SHOULD NOT be returned from the Claims Provider unless its value // is an identifier for the End-User at the Claims Provider (and not for the OpenID Provider or another party); // this typically means that a sub Claim SHOULD NOT be provided. - claimsWithoutSubject := jwt.Claims{ + claimsWithoutSubject := josejwt.Claims{ Issuer: goodOIDCIssuerServer.URL, Audience: []string{goodAudience}, - Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)), - NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Hour)), - IssuedAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), + Expiry: josejwt.NewNumericDate(time.Now().Add(time.Hour)), + NotBefore: josejwt.NewNumericDate(time.Now().Add(-time.Hour)), + IssuedAt: josejwt.NewNumericDate(time.Now().Add(-time.Hour)), } goodMux.Handle("/claim_source", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Unfortunately we have to set this up pretty early in the test because we can't redeclare @@ -139,12 +177,12 @@ func TestController(t *testing.T) { ) require.NoError(t, err) - builder := jwt.Signed(sig).Claims(claimsWithoutSubject) + builder := josejwt.Signed(sig).Claims(claimsWithoutSubject) builder = builder.Claims(map[string]any{customGroupsClaim: distributedGroups}) builder = builder.Claims(map[string]any{"groups": distributedGroups}) - distributedClaimsJwt, err := builder.CompactSerialize() + distributedClaimsJwt, err := builder.Serialize() require.NoError(t, err) _, err = w.Write([]byte(distributedClaimsJwt)) @@ -160,11 +198,11 @@ func TestController(t *testing.T) { ) require.NoError(t, err) - builder := jwt.Signed(sig).Claims(claimsWithoutSubject) + builder := josejwt.Signed(sig).Claims(claimsWithoutSubject) builder = builder.Claims(map[string]any{"some-other-claim": distributedGroups}) - distributedClaimsJwt, err := builder.CompactSerialize() + distributedClaimsJwt, err := builder.Serialize() require.NoError(t, err) _, err = w.Write([]byte(distributedClaimsJwt)) @@ -172,24 +210,24 @@ func TestController(t *testing.T) { })) badMuxInvalidJWKSURI := http.NewServeMux() - badOIDCIssuerServerInvalidJWKSURI, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + badOIDCIssuerServerInvalidJWKSURIServer, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tlsserver.AssertTLS(t, r, ptls.Default) badMuxInvalidJWKSURI.ServeHTTP(w, r) }), tlsserver.RecordTLSHello) badMuxInvalidJWKSURI.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - _, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, badOIDCIssuerServerInvalidJWKSURI.URL, "https://.café .com/café/café/café/coffee/jwks.json") + _, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, badOIDCIssuerServerInvalidJWKSURIServer.URL, "https://.café .com/café/café/café/coffee/jwks.json") require.NoError(t, err) })) badMuxInvalidJWKSURIScheme := http.NewServeMux() - badOIDCIssuerServerInvalidJWKSURIScheme, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + badOIDCIssuerServerInvalidJWKSURISchemeServer, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tlsserver.AssertTLS(t, r, ptls.Default) badMuxInvalidJWKSURIScheme.ServeHTTP(w, r) }), tlsserver.RecordTLSHello) badMuxInvalidJWKSURIScheme.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - _, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, badOIDCIssuerServerInvalidJWKSURIScheme.URL, "http://.café.com/café/café/café/coffee/jwks.json") + _, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, badOIDCIssuerServerInvalidJWKSURISchemeServer.URL, "http://.café.com/café/café/café/coffee/jwks.json") require.NoError(t, err) })) @@ -204,9 +242,33 @@ func TestController(t *testing.T) { require.NoError(t, err) })) + // No real OIDC provider should ever be using our hardcoded private key as a signing key, so this should never happen + // in real life. Simulating this here just so we can have test coverage for the expected error that the production + // code should return in this case. + badMuxUsesOurHardcodedPrivateKey := http.NewServeMux() + badOIDCIssuerUsesOurHardcodedPrivateKeyServer, _ := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tlsserver.AssertTLS(t, r, ptls.Default) + badMuxUsesOurHardcodedPrivateKey.ServeHTTP(w, r) + }), tlsserver.RecordTLSHello) + badMuxUsesOurHardcodedPrivateKey.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, badOIDCIssuerUsesOurHardcodedPrivateKeyServer.URL, badOIDCIssuerUsesOurHardcodedPrivateKeyServer.URL+"/jwks.json") + require.NoError(t, err) + })) + badMuxUsesOurHardcodedPrivateKey.Handle("/jwks.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ecJWK := jose.JSONWebKey{ + Key: hardcodedPrivateKey, + KeyID: goodRSASigningKeyID, + Algorithm: string(goodRSASigningAlgo), + Use: "sig", + } + jwks := jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ecJWK.Public()}, + } + require.NoError(t, json.NewEncoder(w).Encode(jwks)) + })) + goodIssuer := goodOIDCIssuerServer.URL - badIssuerInvalidJWKSURI := badOIDCIssuerServerInvalidJWKSURI.URL - badIssuerInvalidJWKSURIScheme := badOIDCIssuerServerInvalidJWKSURIScheme.URL someOtherIssuer := "https://some-other-issuer.com" // placeholder only for tests that don't get far enough to make requests nowDoesntMatter := time.Date(1122, time.September, 33, 4, 55, 56, 778899, time.Local) @@ -254,7 +316,6 @@ func TestController(t *testing.T) { Audience: goodAudience, TLS: &authenticationv1alpha1.TLSSpec{CertificateAuthorityData: "invalid base64-encoded data"}, } - invalidIssuerJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: "https://.café .com/café/café/café/coffee", Audience: goodAudience, @@ -265,22 +326,25 @@ func TestController(t *testing.T) { Audience: goodAudience, TLS: conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS), } - validIssuerURLButDoesNotExistJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: goodIssuer + "/foo/bar/baz/shizzle", Audience: goodAudience, } badIssuerJWKSURIJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ - Issuer: badIssuerInvalidJWKSURI, + Issuer: badOIDCIssuerServerInvalidJWKSURIServer.URL, Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURI.TLS), + TLS: conciergetestutil.TLSSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURIServer.TLS), } badIssuerJWKSURISchemeJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ - Issuer: badIssuerInvalidJWKSURIScheme, + Issuer: badOIDCIssuerServerInvalidJWKSURISchemeServer.URL, Audience: goodAudience, - TLS: conciergetestutil.TLSSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURIScheme.TLS), + TLS: conciergetestutil.TLSSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURISchemeServer.TLS), + } + badIssuerUsesOurHardcodedPrivateKeyJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ + Issuer: badOIDCIssuerUsesOurHardcodedPrivateKeyServer.URL, + Audience: goodAudience, + TLS: conciergetestutil.TLSSpecFromTLSConfig(badOIDCIssuerUsesOurHardcodedPrivateKeyServer.TLS), } - jwksFetchShouldFailJWTAuthenticatorSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: jwksFetchShouldFailServer.URL, Audience: goodAudience, @@ -543,14 +607,14 @@ func TestController(t *testing.T) { Message: "unable to validate; see other conditions for details", } } - sadJWKSFetch := func(time metav1.Time, observedGeneration int64) metav1.Condition { + sadJWKSFetch := func(msg string, time metav1.Time, observedGeneration int64) metav1.Condition { return metav1.Condition{ Type: "JWKSFetchValid", Status: "False", ObservedGeneration: observedGeneration, LastTransitionTime: time, Reason: "InvalidCouldNotFetchJWKS", - Message: "could not fetch keys: fetching keys oidc: get keys failed: 404 Not Found 404 page not found\n", + Message: msg, } } @@ -645,7 +709,8 @@ func TestController(t *testing.T) { } }, wantCacheEntries: 1, - }, { + }, + { name: "Sync: changed JWTAuthenticator: loop will update timestamps only on relevant statuses", syncKey: controllerlib.Key{Name: "test-name"}, jwtAuthenticators: []runtime.Object{ @@ -953,22 +1018,25 @@ func TestController(t *testing.T) { Spec: *someJWTAuthenticatorSpec, }, }, - wantLogs: []map[string]any{{ - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "wrong JWT authenticator type in cache", - "actualType": "struct { authenticator.Token }", - }, { - "level": "info", - "timestamp": "2099-08-08T13:57:36.123456Z", - "logger": "jwtcachefiller-controller", - "message": "added new jwt authenticator", - "issuer": goodIssuer, - "jwtAuthenticator": map[string]any{ - "name": "test-name", + wantLogs: []map[string]any{ + { + "level": "info", + "timestamp": "2099-08-08T13:57:36.123456Z", + "logger": "jwtcachefiller-controller", + "message": "wrong JWT authenticator type in cache", + "actualType": "struct { authenticator.Token }", }, - }}, + { + "level": "info", + "timestamp": "2099-08-08T13:57:36.123456Z", + "logger": "jwtcachefiller-controller", + "message": "added new jwt authenticator", + "issuer": goodIssuer, + "jwtAuthenticator": map[string]any{ + "name": "test-name", + }, + }, + }, wantActions: func() []coretesting.Action { updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{ @@ -1074,7 +1142,8 @@ func TestController(t *testing.T) { } }, wantCacheEntries: 0, - }, { + }, + { name: "validateIssuer: parsing error (spec.issuer URL is invalid): loop will fail sync, will write failed and unknown status conditions, but will not enqueue a resync due to user config error", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ @@ -1113,7 +1182,8 @@ func TestController(t *testing.T) { updateStatusAction, } }, - }, { + }, + { name: "validateIssuer: parsing error (spec.issuer URL has invalid scheme, requires https): loop will fail sync, will write failed and unknown conditions, but will not enqueue a resync due to user config error", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ @@ -1152,7 +1222,8 @@ func TestController(t *testing.T) { updateStatusAction, } }, - }, { + }, + { name: "validateIssuer: issuer cannot include fragment: loop will fail sync, will write failed and unknown conditions, but will not enqueue a resync due to user config error", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ @@ -1199,7 +1270,8 @@ func TestController(t *testing.T) { updateStatusAction, } }, - }, { + }, + { name: "validateIssuer: issuer cannot include query params: loop will fail sync, will write failed and unknown conditions, but will not enqueue a resync due to user config error", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ @@ -1246,7 +1318,8 @@ func TestController(t *testing.T) { updateStatusAction, } }, - }, { + }, + { name: "validateIssuer: issuer cannot include .well-known in path: loop will fail sync, will write failed and unknown conditions, but will not enqueue a resync due to user config error", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ @@ -1293,7 +1366,8 @@ func TestController(t *testing.T) { updateStatusAction, } }, - }, { + }, + { name: "validateProviderDiscovery: could not perform oidc discovery on provider issuer: loop will fail sync, will write failed and unknown conditions, and will enqueue new sync", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ @@ -1334,7 +1408,8 @@ func TestController(t *testing.T) { } }, wantSyncLoopErr: testutil.WantExactErrorString(`could not perform oidc discovery on provider issuer: Get "` + goodIssuer + `/foo/bar/baz/shizzle/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`), - }, { + }, + { name: "validateProviderDiscovery: excessively long errors truncated: loop will fail sync, will write failed and unknown conditions, and will enqueue new sync", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ @@ -1428,7 +1503,8 @@ func TestController(t *testing.T) { } }, wantSyncLoopErr: testutil.WantExactErrorString(`could not parse provider jwks_uri: parse "https://.café .com/café/café/café/coffee/jwks.json": invalid character " " in host name`), - }, { + }, + { name: "validateProviderJWKSURL: invalid scheme, requires 'https': loop will fail sync, will write failed and unknown conditions, and will enqueue new sync", jwtAuthenticators: []runtime.Object{ &authenticationv1alpha1.JWTAuthenticator{ @@ -1468,8 +1544,47 @@ func TestController(t *testing.T) { }, wantSyncLoopErr: testutil.WantExactErrorString("jwks_uri http://.café.com/café/café/café/coffee/jwks.json has invalid scheme, require 'https'"), }, - // since this is a hard-coded token we can't do any meaningful testing for this case (and should also never have an error) - // {name: "validateJWKSFetch: could not sign tokens"}, + { + name: "validateProviderJWKSURL: remote jwks should not have been able to verify hardcoded test jwt token: loop will fail sync, will write failed and unknown conditions, and will enqueue new sync", + jwtAuthenticators: []runtime.Object{ + &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *badIssuerUsesOurHardcodedPrivateKeyJWTAuthenticatorSpec, + }, + }, + syncKey: controllerlib.Key{Name: "test-name"}, + wantActions: func() []coretesting.Action { + updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &authenticationv1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: *badIssuerUsesOurHardcodedPrivateKeyJWTAuthenticatorSpec, + Status: authenticationv1alpha1.JWTAuthenticatorStatus{ + Conditions: conditionstestutil.Replace( + allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0), + []metav1.Condition{ + happyIssuerURLValid(frozenMetav1Now, 0), + sadReadyCondition(frozenMetav1Now, 0), + unknownAuthenticatorValid(frozenMetav1Now, 0), + happyDiscoveryURLValid(frozenMetav1Now, 0), + sadJWKSFetch("remote jwks should not have been able to verify hardcoded test jwt token", + frozenMetav1Now, 0), + }, + ), + Phase: "Error", + }, + }) + updateStatusAction.Subresource = "status" + return []coretesting.Action{ + coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}), + coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}), + updateStatusAction, + } + }, + wantSyncLoopErr: testutil.WantExactErrorString("remote jwks should not have been able to verify hardcoded test jwt token"), + }, { name: "validateJWKSFetch: could not fetch keys: loop will fail sync, will write failed and unknown status conditions, and will enqueue a resync", jwtAuthenticators: []runtime.Object{ @@ -1494,7 +1609,8 @@ func TestController(t *testing.T) { happyIssuerURLValid(frozenMetav1Now, 0), sadReadyCondition(frozenMetav1Now, 0), unknownAuthenticatorValid(frozenMetav1Now, 0), - sadJWKSFetch(frozenMetav1Now, 0), + sadJWKSFetch("could not fetch keys: fetching keys oidc: get keys failed: 404 Not Found 404 page not found\n", + frozenMetav1Now, 0), }, ), Phase: "Error", @@ -1783,13 +1899,13 @@ func TestController(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - wellKnownClaims := jwt.Claims{ + wellKnownClaims := josejwt.Claims{ Issuer: goodIssuer, Subject: goodSubject, Audience: []string{goodAudience}, - Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)), - NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Hour)), - IssuedAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), + Expiry: josejwt.NewNumericDate(time.Now().Add(time.Hour)), + NotBefore: josejwt.NewNumericDate(time.Now().Add(-time.Hour)), + IssuedAt: josejwt.NewNumericDate(time.Now().Add(-time.Hour)), } var groups any username := goodUsername @@ -1860,7 +1976,7 @@ func testTableForAuthenticateTokenTests( issuer string, ) []struct { name string - jwtClaims func(wellKnownClaims *jwt.Claims, groups *any, username *string) + jwtClaims func(wellKnownClaims *josejwt.Claims, groups *any, username *string) jwtSignature func(key *any, algo *jose.SignatureAlgorithm, kid *string) wantResponse *authenticator.Response wantAuthenticated bool @@ -1869,7 +1985,7 @@ func testTableForAuthenticateTokenTests( } { tests := []struct { name string - jwtClaims func(wellKnownClaims *jwt.Claims, groups *any, username *string) + jwtClaims func(wellKnownClaims *josejwt.Claims, groups *any, username *string) jwtSignature func(key *any, algo *jose.SignatureAlgorithm, kid *string) wantResponse *authenticator.Response wantAuthenticated bool @@ -1901,7 +2017,7 @@ func testTableForAuthenticateTokenTests( }, { name: "good token with groups as array", - jwtClaims: func(_ *jwt.Claims, groups *any, username *string) { + jwtClaims: func(_ *josejwt.Claims, groups *any, username *string) { *groups = []string{group0, group1} }, wantResponse: &authenticator.Response{ @@ -1914,7 +2030,7 @@ func testTableForAuthenticateTokenTests( }, { name: "good token with good distributed groups", - jwtClaims: func(claims *jwt.Claims, groups *any, username *string) { + jwtClaims: func(claims *josejwt.Claims, groups *any, username *string) { }, distributedGroupsClaimURL: issuer + "/claim_source", wantResponse: &authenticator.Response{ @@ -1927,21 +2043,21 @@ func testTableForAuthenticateTokenTests( }, { name: "distributed groups returns a 404", - jwtClaims: func(claims *jwt.Claims, groups *any, username *string) { + jwtClaims: func(claims *josejwt.Claims, groups *any, username *string) { }, distributedGroupsClaimURL: issuer + "/not_found_claim_source", wantErr: testutil.WantMatchingErrorString(`oidc: could not expand distributed claims: while getting distributed claim "` + expectedGroupsClaim + `": error while getting distributed claim JWT: 404 Not Found`), }, { name: "distributed groups doesn't return the right claim", - jwtClaims: func(claims *jwt.Claims, groups *any, username *string) { + jwtClaims: func(claims *josejwt.Claims, groups *any, username *string) { }, distributedGroupsClaimURL: issuer + "/wrong_claim_source", wantErr: testutil.WantMatchingErrorString(`oidc: could not expand distributed claims: jwt returned by distributed claim endpoint "` + issuer + `/wrong_claim_source" did not contain claim: `), }, { name: "good token with groups as string", - jwtClaims: func(_ *jwt.Claims, groups *any, username *string) { + jwtClaims: func(_ *josejwt.Claims, groups *any, username *string) { *groups = group0 }, wantResponse: &authenticator.Response{ @@ -1954,7 +2070,7 @@ func testTableForAuthenticateTokenTests( }, { name: "good token with nbf unset", - jwtClaims: func(claims *jwt.Claims, _ *any, username *string) { + jwtClaims: func(claims *josejwt.Claims, _ *any, username *string) { claims.NotBefore = nil }, wantResponse: &authenticator.Response{ @@ -1966,14 +2082,14 @@ func testTableForAuthenticateTokenTests( }, { name: "bad token with groups as map", - jwtClaims: func(_ *jwt.Claims, groups *any, username *string) { + jwtClaims: func(_ *josejwt.Claims, groups *any, username *string) { *groups = map[string]string{"not an array": "or a string"} }, wantErr: testutil.WantMatchingErrorString("oidc: parse groups claim \"" + expectedGroupsClaim + "\": json: cannot unmarshal object into Go value of type string"), }, { name: "bad token with wrong issuer", - jwtClaims: func(claims *jwt.Claims, _ *any, username *string) { + jwtClaims: func(claims *josejwt.Claims, _ *any, username *string) { claims.Issuer = "wrong-issuer" }, wantResponse: nil, @@ -1981,42 +2097,42 @@ func testTableForAuthenticateTokenTests( }, { name: "bad token with no audience", - jwtClaims: func(claims *jwt.Claims, _ *any, username *string) { + jwtClaims: func(claims *josejwt.Claims, _ *any, username *string) { claims.Audience = nil }, wantErr: testutil.WantMatchingErrorString(`oidc: verify token: oidc: expected audience "some-audience" got \[\]`), }, { name: "bad token with wrong audience", - jwtClaims: func(claims *jwt.Claims, _ *any, username *string) { + jwtClaims: func(claims *josejwt.Claims, _ *any, username *string) { claims.Audience = []string{"wrong-audience"} }, wantErr: testutil.WantMatchingErrorString(`oidc: verify token: oidc: expected audience "some-audience" got \["wrong-audience"\]`), }, { name: "bad token with nbf in the future", - jwtClaims: func(claims *jwt.Claims, _ *any, username *string) { - claims.NotBefore = jwt.NewNumericDate(time.Date(3020, 2, 3, 4, 5, 6, 7, time.UTC)) + jwtClaims: func(claims *josejwt.Claims, _ *any, username *string) { + claims.NotBefore = josejwt.NewNumericDate(time.Date(3020, 2, 3, 4, 5, 6, 7, time.UTC)) }, wantErr: testutil.WantMatchingErrorString(`oidc: verify token: oidc: current time .* before the nbf \(not before\) time: 3020-.*`), }, { name: "bad token with exp in past", - jwtClaims: func(claims *jwt.Claims, _ *any, username *string) { - claims.Expiry = jwt.NewNumericDate(time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC)) + jwtClaims: func(claims *josejwt.Claims, _ *any, username *string) { + claims.Expiry = josejwt.NewNumericDate(time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC)) }, wantErr: testutil.WantMatchingErrorString(`oidc: verify token: oidc: token is expired \(Token Expiry: .+`), }, { name: "bad token without exp", - jwtClaims: func(claims *jwt.Claims, _ *any, username *string) { + jwtClaims: func(claims *josejwt.Claims, _ *any, username *string) { claims.Expiry = nil }, wantErr: testutil.WantMatchingErrorString(`oidc: verify token: oidc: token is expired \(Token Expiry: .+`), }, { name: "token does not have username claim", - jwtClaims: func(claims *jwt.Claims, _ *any, username *string) { + jwtClaims: func(claims *josejwt.Claims, _ *any, username *string) { *username = "" }, wantErr: testutil.WantMatchingErrorString(`oidc: parse username claims "` + expectedUsernameClaim + `": claim not present`), @@ -2051,7 +2167,7 @@ func createJWT( signingKey any, signingAlgo jose.SignatureAlgorithm, kid string, - claims *jwt.Claims, + claims *josejwt.Claims, groupsClaim string, groupsValue any, distributedGroupsClaimURL string, @@ -2066,7 +2182,7 @@ func createJWT( ) require.NoError(t, err) - builder := jwt.Signed(sig).Claims(claims) + builder := josejwt.Signed(sig).Claims(claims) if groupsValue != nil { builder = builder.Claims(map[string]any{groupsClaim: groupsValue}) } @@ -2077,7 +2193,7 @@ func createJWT( if usernameValue != "" { builder = builder.Claims(map[string]any{usernameClaim: usernameValue}) } - jwt, err := builder.CompactSerialize() + jwt, err := builder.Serialize() require.NoError(t, err) return jwt diff --git a/internal/controller/authenticator/jwtcachefiller/testdata/test.key b/internal/controller/authenticator/jwtcachefiller/testdata/test.key new file mode 100644 index 000000000..b13a09c67 --- /dev/null +++ b/internal/controller/authenticator/jwtcachefiller/testdata/test.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAtZBQ4XhDQDayJhzbl6Bimq4Ca5KpNHdhveYzbYNtRUzDDRZ7 +1GSbduxI2jHWVw7/F2lppypb3SxzJj1fEc5YnyFgso2VnihwVwR7qvuVDwO1ZIWI +9G6e0y8/oy+Z2f3zqTEwd5LIdo1PaWIJMKwNIvGZNtoqdvc0R2wm9f4ygdhxN4+n +0XRx6NzAVvIonYE4p+93MXPW8+gWfHVXmF6dGNDZCbWFayHP6ZLri1p9S+LzlwhV +av9jE30RdnjXo8yL5L4D31qTct+HXgXq+p/g2Tnu58hyNr1TEzEWAhVRB63KTk/4 +O+0LAxfxt28HGdrMmGP2Qid/PqGvL5JHj7tzTQIDAQABAoIBAHMtN4Gwbsj/aYev +6sWHIsYI+NQQ13HHAaQbsigYpWq+xUU6LBeSMuUAAz8XOmdDxiKt5i37OwdVOT7a +08JR6foYjGT4WB9ae9lXqLPQoMBDlABOjZCx0+MYKAB3I9wbs0RzRdG0taIvBl9N +p5LOsg6mwJEBWMrbCrj8LMMEHDotaBSqD+A2vAsP9Z6g1+Eh+T8eRRGKmNlPkfmW +V7WplhIL6JdWI/+LsjCsWkOjMI8hndGWuXKt3wBmAAf8W0l/k3jAdR6C4gX2z5rn +nLFYjlHD/mZ3ii3SG7iZOVRpLiwCSA/lIoUXl7we2uGKkbP7WKuZfSQkJ18A5X2r +yeLjPQECgYEA0f05bXYgHRcc5fdn5mCjQGgdTvP2MDkI3pMpWgi6AGhjP1LXF/I/ +MK4v061wWKqIlc6g/Hwi1yClap1c3qLCbvvonFTLwvRM23OTcHAAT3/iXZ4jhmFt +96KIeVlgEUFS6tkhxXi/H0lwpK18QDaxSwxJRM3A35jPUTyTU/ADKaECgYEA3Vik +5TTaNdjt5WRQysEGQENSTDPJ0c9M8/KnknEa7Xr6vkI81EVMRwCT59ll8tAD0fvB +am/YQ0xn8tJh9yuCjSyxHJ7qSjPDnZUbOgPo7r82iWDb7hpsFFnuUUddMMRxe8ag +iCTEDXicwLcB8RnioICZyI+GEVxlT6jsW69N4i0CgYEAoCOn9vU9ulGVBT9u6f7K +oOSFbV+ZYN8uB2ddAr9i8cqp3XHUfPuN+xbrfFdpNQUgUnaYyNP2Ue8glzTYzSR8 +eNz9YLM+DTf3oOf8CaQwaHBTdieSWfnVPiOiRkDFhYM2s8jQ+2KBBmAgWkW/Ws0a +2evNuH3c1+gWOpKinEGOd+ECgYEAlLYcuz3iKXFgi9D1EvPSflR8s2PMAWF0gyWR +firte4Y3dqJL+hXA5Kc3t/pwq00kc+zgCuGv+68W26aLWSPrZ2wSZndCU64pi/ME +wtqjodvoCS6BNJyd5qJxIjx/GOeykwVlD3McISzarAOIk3LftxQPvhbnbTyVeIq2 +mfbSrdUCgYEAmlxEU8VYH4J1HGROcpBGWLFzDjMFKX1LSUFMchJ+Fa1bup3w1upW +Zuwm9JtgxuaADyzYFESfzXOaxsW7Z320V/baAqigpoKC1Cyr1KXGKZfOcMLyAKK9 +q88zOKCZ083T4E/6h7TmSPck6SrqF9TyqlHsaSCbQxe6JhSdIOnGYZA= +-----END RSA PRIVATE KEY----- diff --git a/internal/controller/supervisorconfig/jwks_observer.go b/internal/controller/supervisorconfig/jwks_observer.go index 4eef5bf9f..154515570 100644 --- a/internal/controller/supervisorconfig/jwks_observer.go +++ b/internal/controller/supervisorconfig/jwks_observer.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package supervisorconfig @@ -7,7 +7,7 @@ import ( "encoding/json" "fmt" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "k8s.io/apimachinery/pkg/labels" corev1informers "k8s.io/client-go/informers/core/v1" diff --git a/internal/controller/supervisorconfig/jwks_observer_test.go b/internal/controller/supervisorconfig/jwks_observer_test.go index 7cfb04a72..1c79dac7c 100644 --- a/internal/controller/supervisorconfig/jwks_observer_test.go +++ b/internal/controller/supervisorconfig/jwks_observer_test.go @@ -8,7 +8,7 @@ import ( "encoding/json" "testing" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/sclevine/spec" "github.com/sclevine/spec/report" "github.com/stretchr/testify/require" diff --git a/internal/controller/supervisorconfig/jwks_writer.go b/internal/controller/supervisorconfig/jwks_writer.go index c15a7fb9b..040b70d18 100644 --- a/internal/controller/supervisorconfig/jwks_writer.go +++ b/internal/controller/supervisorconfig/jwks_writer.go @@ -12,7 +12,7 @@ import ( "fmt" "io" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/internal/federationdomain/downstreamsession/downstream_session.go b/internal/federationdomain/downstreamsession/downstream_session.go index f477b1d10..512557afd 100644 --- a/internal/federationdomain/downstreamsession/downstream_session.go +++ b/internal/federationdomain/downstreamsession/downstream_session.go @@ -12,7 +12,7 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" - "github.com/ory/fosite/token/jwt" + fositejwt "github.com/ory/fosite/token/jwt" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/constable" @@ -64,7 +64,7 @@ func NewPinnipedSession( pinnipedSession := &psession.PinnipedSession{ Fosite: &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ + Claims: &fositejwt.IDTokenClaims{ Subject: c.UpstreamIdentity.DownstreamSubject, RequestedAt: now, AuthTime: now, diff --git a/internal/federationdomain/endpoints/auth/auth_handler.go b/internal/federationdomain/endpoints/auth/auth_handler.go index 5afcab382..13f30df8b 100644 --- a/internal/federationdomain/endpoints/auth/auth_handler.go +++ b/internal/federationdomain/endpoints/auth/auth_handler.go @@ -12,7 +12,7 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" - "github.com/ory/fosite/token/jwt" + fositejwt "github.com/ory/fosite/token/jwt" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/federationdomain/csrftoken" @@ -360,7 +360,7 @@ func generateUpstreamAuthorizeRequestState( now := time.Now() _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{ Fosite: &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ + Claims: &fositejwt.IDTokenClaims{ // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. Subject: "none", AuthTime: now, diff --git a/internal/federationdomain/endpoints/jwks/dynamic_jwks_provider.go b/internal/federationdomain/endpoints/jwks/dynamic_jwks_provider.go index a2f08897f..28b3acb4b 100644 --- a/internal/federationdomain/endpoints/jwks/dynamic_jwks_provider.go +++ b/internal/federationdomain/endpoints/jwks/dynamic_jwks_provider.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package jwks @@ -6,7 +6,7 @@ package jwks import ( "sync" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" ) type DynamicJWKSProvider interface { diff --git a/internal/federationdomain/endpoints/jwks/jwks_handler_test.go b/internal/federationdomain/endpoints/jwks/jwks_handler_test.go index 12463b376..5e6abb1d3 100644 --- a/internal/federationdomain/endpoints/jwks/jwks_handler_test.go +++ b/internal/federationdomain/endpoints/jwks/jwks_handler_test.go @@ -9,7 +9,7 @@ import ( "net/http/httptest" "testing" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/require" "go.pinniped.dev/internal/here" diff --git a/internal/federationdomain/endpoints/token/token_handler_test.go b/internal/federationdomain/endpoints/token/token_handler_test.go index 1d07aded5..76cfe2683 100644 --- a/internal/federationdomain/endpoints/token/token_handler_test.go +++ b/internal/federationdomain/endpoints/token/token_handler_test.go @@ -22,13 +22,13 @@ import ( "testing" "time" - "github.com/go-jose/go-jose/v3" - josejwt "github.com/go-jose/go-jose/v3/jwt" + "github.com/go-jose/go-jose/v4" + josejwt "github.com/go-jose/go-jose/v4/jwt" "github.com/ory/fosite" fositeoauth2 "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" fositepkce "github.com/ory/fosite/handler/pkce" - "github.com/ory/fosite/token/jwt" + fositejwt "github.com/ory/fosite/token/jwt" "github.com/pkg/errors" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" @@ -1716,7 +1716,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn claimsOfFirstIDToken := map[string]any{} originalIDToken := parsedAuthcodeExchangeResponseBody["id_token"].(string) - firstIDTokenDecoded, _ := josejwt.ParseSigned(originalIDToken) + firstIDTokenDecoded, _ := josejwt.ParseSigned(originalIDToken, []jose.SignatureAlgorithm{jose.ES256}) err = firstIDTokenDecoded.UnsafeClaimsWithoutVerification(&claimsOfFirstIDToken) require.NoError(t, err) @@ -1725,7 +1725,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn require.Equal(t, "urn:ietf:params:oauth:token-type:jwt", parsedResponseBody["issued_token_type"]) // Parse the returned token. - parsedJWT, err := jose.ParseSigned(parsedResponseBody["access_token"].(string)) + parsedJWT, err := jose.ParseSigned(parsedResponseBody["access_token"].(string), []jose.SignatureAlgorithm{jose.ES256}) require.NoError(t, err) var tokenClaims map[string]any require.NoError(t, json.Unmarshal(parsedJWT.UnsafePayloadWithoutVerification(), &tokenClaims)) @@ -4831,12 +4831,12 @@ func TestRefreshGrant(t *testing.T) { if wantIDToken { var claimsOfFirstIDToken map[string]any - firstIDTokenDecoded, _ := josejwt.ParseSigned(parsedAuthcodeExchangeResponseBody["id_token"].(string)) + firstIDTokenDecoded, _ := josejwt.ParseSigned(parsedAuthcodeExchangeResponseBody["id_token"].(string), []jose.SignatureAlgorithm{jose.ES256}) err := firstIDTokenDecoded.UnsafeClaimsWithoutVerification(&claimsOfFirstIDToken) require.NoError(t, err) var claimsOfSecondIDToken map[string]any - secondIDTokenDecoded, _ := josejwt.ParseSigned(parsedRefreshResponseBody["id_token"].(string)) + secondIDTokenDecoded, _ := josejwt.ParseSigned(parsedRefreshResponseBody["id_token"].(string), []jose.SignatureAlgorithm{jose.ES256}) err = secondIDTokenDecoded.UnsafeClaimsWithoutVerification(&claimsOfSecondIDToken) require.NoError(t, err) @@ -5145,7 +5145,7 @@ func simulateAuthEndpointHavingAlreadyRun( ctx := context.Background() session := &psession.PinnipedSession{ Fosite: &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ + Claims: &fositejwt.IDTokenClaims{ Subject: goodSubject, RequestedAt: goodRequestedAtTime, AuthTime: goodAuthTime, diff --git a/internal/federationdomain/endpointsmanager/manager_test.go b/internal/federationdomain/endpointsmanager/manager_test.go index 6707a9d8c..dfde19130 100644 --- a/internal/federationdomain/endpointsmanager/manager_test.go +++ b/internal/federationdomain/endpointsmanager/manager_test.go @@ -14,7 +14,7 @@ import ( "strings" "testing" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/sclevine/spec" "github.com/stretchr/testify/require" "k8s.io/client-go/kubernetes/fake" diff --git a/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go b/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go index 0fe8ea733..67f5c0aa6 100644 --- a/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go +++ b/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go @@ -14,10 +14,10 @@ import ( "testing" "time" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" - "github.com/ory/fosite/token/jwt" + fositejwt "github.com/ory/fosite/token/jwt" "github.com/stretchr/testify/require" "go.pinniped.dev/internal/federationdomain/endpoints/jwks" @@ -103,7 +103,7 @@ func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) { ID: clientID, }, Session: &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ + Claims: &fositejwt.IDTokenClaims{ Subject: goodSubject, }, Subject: goodSubject, diff --git a/internal/fositestorage/accesstoken/accesstoken_test.go b/internal/fositestorage/accesstoken/accesstoken_test.go index f0026bb20..8a4cc4d60 100644 --- a/internal/fositestorage/accesstoken/accesstoken_test.go +++ b/internal/fositestorage/accesstoken/accesstoken_test.go @@ -11,7 +11,7 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" - "github.com/ory/fosite/token/jwt" + fositejwt "github.com/ory/fosite/token/jwt" "github.com/pkg/errors" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -334,8 +334,8 @@ func TestReadFromSecret(t *testing.T) { Fosite: &openid.DefaultSession{ Username: "snorlax", Subject: "panda", - Claims: &jwt.IDTokenClaims{JTI: "xyz"}, - Headers: &jwt.Headers{Extra: map[string]any{"myheader": "foo"}}, + Claims: &fositejwt.IDTokenClaims{JTI: "xyz"}, + Headers: &fositejwt.Headers{Extra: map[string]any{"myheader": "foo"}}, }, Custom: &psession.CustomSessionData{ Username: "fake-username", diff --git a/internal/fositestorage/authorizationcode/authorizationcode_test.go b/internal/fositestorage/authorizationcode/authorizationcode_test.go index 06085f7a0..3a0e5d00d 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode_test.go +++ b/internal/fositestorage/authorizationcode/authorizationcode_test.go @@ -15,12 +15,12 @@ import ( "testing" "time" - "github.com/go-jose/go-jose/v3" + oldjosev3 "github.com/go-jose/go-jose/v3" // we need to use the same version of jose that fosite uses when fuzzing fosite objects fuzz "github.com/google/gofuzz" "github.com/ory/fosite" fositeoauth2 "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" - "github.com/ory/fosite/token/jwt" + fositejwt "github.com/ory/fosite/token/jwt" "github.com/pkg/errors" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -343,7 +343,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) { }, // JWK contains an any Key that we need to handle // this is safe because JWK explicitly implements JSON marshalling and unmarshalling - func(jwk *jose.JSONWebKey, c fuzz.Continue) { + func(jwk *oldjosev3.JSONWebKey, c fuzz.Continue) { key, _, err := ed25519.GenerateKey(c) require.NoError(t, err) jwk.Key = key @@ -470,8 +470,8 @@ func TestReadFromSecret(t *testing.T) { Fosite: &openid.DefaultSession{ Username: "snorlax", Subject: "panda", - Claims: &jwt.IDTokenClaims{JTI: "xyz"}, - Headers: &jwt.Headers{Extra: map[string]any{"myheader": "foo"}}, + Claims: &fositejwt.IDTokenClaims{JTI: "xyz"}, + Headers: &fositejwt.Headers{Extra: map[string]any{"myheader": "foo"}}, }, Custom: &psession.CustomSessionData{ Username: "fake-username", diff --git a/internal/fositestorage/refreshtoken/refreshtoken_test.go b/internal/fositestorage/refreshtoken/refreshtoken_test.go index 2b96d8799..1ac19e836 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken_test.go +++ b/internal/fositestorage/refreshtoken/refreshtoken_test.go @@ -11,7 +11,7 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" - "github.com/ory/fosite/token/jwt" + fositejwt "github.com/ory/fosite/token/jwt" "github.com/pkg/errors" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -391,8 +391,8 @@ func TestReadFromSecret(t *testing.T) { Fosite: &openid.DefaultSession{ Username: "snorlax", Subject: "panda", - Claims: &jwt.IDTokenClaims{JTI: "xyz"}, - Headers: &jwt.Headers{Extra: map[string]any{"myheader": "foo"}}, + Claims: &fositejwt.IDTokenClaims{JTI: "xyz"}, + Headers: &fositejwt.Headers{Extra: map[string]any{"myheader": "foo"}}, }, Custom: &psession.CustomSessionData{ Username: "fake-username", diff --git a/internal/psession/pinniped_session.go b/internal/psession/pinniped_session.go index 41fd6d330..d82f89a0a 100644 --- a/internal/psession/pinniped_session.go +++ b/internal/psession/pinniped_session.go @@ -10,7 +10,7 @@ import ( "github.com/mohae/deepcopy" "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" - "github.com/ory/fosite/token/jwt" + fositejwt "github.com/ory/fosite/token/jwt" "k8s.io/apimachinery/pkg/types" ) @@ -157,8 +157,8 @@ func (s *GitHubSessionData) Clone() *GitHubSessionData { func NewPinnipedSession() *PinnipedSession { return &PinnipedSession{ Fosite: &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{}, - Headers: &jwt.Headers{}, + Claims: &fositejwt.IDTokenClaims{}, + Headers: &fositejwt.Headers{}, }, Custom: &CustomSessionData{}, } @@ -192,10 +192,10 @@ func (s *PinnipedSession) GetSubject() string { return s.Fosite.GetSubject() } -func (s *PinnipedSession) IDTokenHeaders() *jwt.Headers { +func (s *PinnipedSession) IDTokenHeaders() *fositejwt.Headers { return s.Fosite.IDTokenHeaders() } -func (s *PinnipedSession) IDTokenClaims() *jwt.IDTokenClaims { +func (s *PinnipedSession) IDTokenClaims() *fositejwt.IDTokenClaims { return s.Fosite.IDTokenClaims() } diff --git a/internal/testutil/oidctestutil/verify_id_token.go b/internal/testutil/oidctestutil/verify_id_token.go index 5eff6a8b2..050058567 100644 --- a/internal/testutil/oidctestutil/verify_id_token.go +++ b/internal/testutil/oidctestutil/verify_id_token.go @@ -11,7 +11,7 @@ import ( "testing" coreosoidc "github.com/coreos/go-oidc/v3/oidc" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/require" ) @@ -24,7 +24,7 @@ func newStaticKeySet(publicKey crypto.PublicKey) coreosoidc.KeySet { } func (s *staticKeySet) VerifySignature(_ context.Context, jwt string) ([]byte, error) { - jws, err := jose.ParseSigned(jwt) + jws, err := jose.ParseSigned(jwt, []jose.SignatureAlgorithm{jose.ES256}) if err != nil { return nil, fmt.Errorf("oidc: malformed jwt: %w", err) } diff --git a/internal/upstreamoidc/upstreamoidc_test.go b/internal/upstreamoidc/upstreamoidc_test.go index 2ce45cdbb..762e09de2 100644 --- a/internal/upstreamoidc/upstreamoidc_test.go +++ b/internal/upstreamoidc/upstreamoidc_test.go @@ -17,7 +17,7 @@ import ( "unsafe" coreosoidc "github.com/coreos/go-oidc/v3/oidc" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "golang.org/x/oauth2" @@ -1413,7 +1413,7 @@ func mockVerifier() *coreosoidc.IDTokenVerifier { mockKeySet.EXPECT().VerifySignature(gomock.Any(), gomock.Any()). AnyTimes(). DoAndReturn(func(ctx context.Context, jwt string) ([]byte, error) { - jws, err := jose.ParseSigned(jwt) + jws, err := jose.ParseSigned(jwt, []jose.SignatureAlgorithm{jose.ES256, jose.RS256}) if err != nil { return nil, err } diff --git a/test/integration/cli_test.go b/test/integration/cli_test.go index f5d8a239a..36269e339 100644 --- a/test/integration/cli_test.go +++ b/test/integration/cli_test.go @@ -19,7 +19,7 @@ import ( "testing" "time" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" @@ -193,7 +193,7 @@ func TestCLILoginOIDC_Browser(t *testing.T) { // Assert some properties about the token, which should be a valid JWT. require.NotEmpty(t, credOutput.Status.Token) - jws, err := jose.ParseSigned(credOutput.Status.Token) + jws, err := jose.ParseSigned(credOutput.Status.Token, []jose.SignatureAlgorithm{jose.ES256, jose.RS256}) require.NoError(t, err) claims := map[string]any{} require.NoError(t, json.Unmarshal(jws.UnsafePayloadWithoutVerification(), &claims)) diff --git a/test/integration/concierge_credentialrequest_test.go b/test/integration/concierge_credentialrequest_test.go index 491f745bc..01f5dd98c 100644 --- a/test/integration/concierge_credentialrequest_test.go +++ b/test/integration/concierge_credentialrequest_test.go @@ -10,7 +10,8 @@ import ( "testing" "time" - "github.com/go-jose/go-jose/v3/jwt" + "github.com/go-jose/go-jose/v4" + josejwt "github.com/go-jose/go-jose/v4/jwt" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -219,7 +220,7 @@ func safeDerefStringPtr(s *string) string { func getJWTSubAndGroupsClaims(t *testing.T, jwtToken string) (string, []string) { t.Helper() - token, err := jwt.ParseSigned(jwtToken) + token, err := josejwt.ParseSigned(jwtToken, []jose.SignatureAlgorithm{jose.ES256, jose.RS256}) require.NoError(t, err) var claims struct { diff --git a/test/integration/supervisor_secrets_test.go b/test/integration/supervisor_secrets_test.go index 65c782f13..7a1972f36 100644 --- a/test/integration/supervisor_secrets_test.go +++ b/test/integration/supervisor_secrets_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"