ContainerImage.Pinniped/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go

780 lines
28 KiB
Go
Raw Normal View History

// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package jwtcachefiller
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
pinnipedfake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
pinnipedinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions"
"go.pinniped.dev/internal/controller/authenticator/authncache"
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/crypto/ptls"
"go.pinniped.dev/internal/mocks/mocktokenauthenticatorcloser"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger"
"go.pinniped.dev/internal/testutil/tlsserver"
)
func TestController(t *testing.T) {
t.Parallel()
const (
goodECSigningKeyID = "some-ec-key-id"
goodRSASigningKeyID = "some-rsa-key-id"
goodAudience = "some-audience"
)
goodECSigningKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
goodECSigningAlgo := jose.ES256
require.NoError(t, err)
goodRSASigningKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
goodRSASigningAlgo := jose.RS256
customGroupsClaim := "my-custom-groups-claim"
distributedGroups := []string{"some-distributed-group-1", "some-distributed-group-2"}
mux := http.NewServeMux()
server := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tlsserver.AssertTLS(t, r, ptls.Default)
mux.ServeHTTP(w, r)
}), tlsserver.RecordTLSHello)
mux.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"}`, server.URL, server.URL+"/jwks.json")
require.NoError(t, err)
}))
mux.Handle("/jwks.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ecJWK := jose.JSONWebKey{
Key: goodECSigningKey,
KeyID: goodECSigningKeyID,
Algorithm: string(goodECSigningAlgo),
Use: "sig",
}
rsaJWK := jose.JSONWebKey{
Key: goodRSASigningKey,
KeyID: goodRSASigningKeyID,
Algorithm: string(goodRSASigningAlgo),
Use: "sig",
}
jwks := jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{ecJWK.Public(), rsaJWK.Public()},
}
require.NoError(t, json.NewEncoder(w).Encode(jwks))
}))
// Claims without the subject, to be used distributed claims tests.
// OIDC 1.0 section 5.6.2:
// 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{
Issuer: server.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)),
}
mux.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
// mux.Handle. This means that we can't return a different groups claim per test; we have to
// return both and predecide which groups are returned.
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: goodECSigningAlgo, Key: goodECSigningKey},
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", goodECSigningKeyID),
)
require.NoError(t, err)
builder := jwt.Signed(sig).Claims(claimsWithoutSubject)
builder = builder.Claims(map[string]interface{}{customGroupsClaim: distributedGroups})
builder = builder.Claims(map[string]interface{}{"groups": distributedGroups})
distributedClaimsJwt, err := builder.CompactSerialize()
require.NoError(t, err)
_, err = w.Write([]byte(distributedClaimsJwt))
require.NoError(t, err)
}))
mux.Handle("/wrong_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
// mux.Handle. This means that we can't return a different groups claim per test; we have to
// return both and predecide which groups are returned.
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: goodECSigningAlgo, Key: goodECSigningKey},
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", goodECSigningKeyID),
)
require.NoError(t, err)
builder := jwt.Signed(sig).Claims(claimsWithoutSubject)
builder = builder.Claims(map[string]interface{}{"some-other-claim": distributedGroups})
distributedClaimsJwt, err := builder.CompactSerialize()
require.NoError(t, err)
_, err = w.Write([]byte(distributedClaimsJwt))
require.NoError(t, err)
}))
goodIssuer := server.URL
someJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: goodIssuer,
Audience: goodAudience,
TLS: tlsSpecFromTLSConfig(server.TLS),
}
someJWTAuthenticatorSpecWithUsernameClaim := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: goodIssuer,
Audience: goodAudience,
TLS: tlsSpecFromTLSConfig(server.TLS),
Claims: auth1alpha1.JWTTokenClaims{
Username: "my-custom-username-claim",
},
}
someJWTAuthenticatorSpecWithGroupsClaim := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: goodIssuer,
Audience: goodAudience,
TLS: tlsSpecFromTLSConfig(server.TLS),
Claims: auth1alpha1.JWTTokenClaims{
Groups: customGroupsClaim,
},
}
otherJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: "https://some-other-issuer.com",
Audience: goodAudience,
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"},
}
missingTLSJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: goodIssuer,
Audience: goodAudience,
}
invalidTLSJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: "https://some-other-issuer.com",
Audience: goodAudience,
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "invalid base64-encoded data"},
}
tests := []struct {
name string
cache func(*testing.T, *authncache.Cache, bool)
syncKey controllerlib.Key
jwtAuthenticators []runtime.Object
wantClose bool
wantErr testutil.RequireErrorStringFunc
wantLogs []string
wantCacheEntries int
wantUsernameClaim string
wantGroupsClaim string
runTestsOnResultingAuthenticator bool
}{
{
name: "not found",
syncKey: controllerlib.Key{Name: "test-name"},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="Sync() found that the JWTAuthenticator does not exist yet or was deleted"`,
},
},
{
name: "valid jwt authenticator with CA",
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *someJWTAuthenticatorSpec,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantCacheEntries: 1,
runTestsOnResultingAuthenticator: true,
},
{
name: "valid jwt authenticator with custom username claim",
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *someJWTAuthenticatorSpecWithUsernameClaim,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantCacheEntries: 1,
wantUsernameClaim: someJWTAuthenticatorSpecWithUsernameClaim.Claims.Username,
runTestsOnResultingAuthenticator: true,
},
{
name: "valid jwt authenticator with custom groups claim",
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *someJWTAuthenticatorSpecWithGroupsClaim,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantCacheEntries: 1,
wantGroupsClaim: someJWTAuthenticatorSpecWithGroupsClaim.Claims.Groups,
runTestsOnResultingAuthenticator: true,
},
{
name: "updating jwt authenticator with new fields closes previous instance",
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
cache.Store(
authncache.Key{
Name: "test-name",
Kind: "JWTAuthenticator",
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
},
newCacheValue(t, *otherJWTAuthenticatorSpec, wantClose),
)
},
wantClose: true,
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *someJWTAuthenticatorSpec,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantCacheEntries: 1,
runTestsOnResultingAuthenticator: true,
},
{
name: "updating jwt authenticator with the same value does nothing",
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
cache.Store(
authncache.Key{
Name: "test-name",
Kind: "JWTAuthenticator",
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
},
newCacheValue(t, *someJWTAuthenticatorSpec, wantClose),
)
},
wantClose: false,
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *someJWTAuthenticatorSpec,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="actual jwt authenticator and desired jwt authenticator are the same" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantCacheEntries: 1,
runTestsOnResultingAuthenticator: false, // skip the tests because the authenticator left in the cache is the mock version that was added above
},
{
name: "updating jwt authenticator when cache value is wrong type",
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
cache.Store(
authncache.Key{
Name: "test-name",
Kind: "JWTAuthenticator",
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
},
struct{ authenticator.Token }{},
)
},
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *someJWTAuthenticatorSpec,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="wrong JWT authenticator type in cache" "actualType"="struct { authenticator.Token }"`,
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantCacheEntries: 1,
runTestsOnResultingAuthenticator: true,
},
{
name: "valid jwt authenticator without CA",
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *missingTLSJWTAuthenticatorSpec,
},
},
wantErr: testutil.WantX509UntrustedCertErrorString(`failed to build jwt authenticator: could not initialize provider: Get "`+goodIssuer+`/.well-known/openid-configuration": %s`, "Acme Co"),
},
{
name: "invalid jwt authenticator CA",
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *invalidTLSJWTAuthenticatorSpec,
},
},
wantErr: testutil.WantExactErrorString("failed to build jwt authenticator: invalid TLS configuration: illegal base64 data at input byte 7"),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
fakeClient := pinnipedfake.NewSimpleClientset(tt.jwtAuthenticators...)
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
cache := authncache.New()
testLog := testlogger.NewLegacy(t) //nolint:staticcheck // old test with lots of log statements
if tt.cache != nil {
tt.cache(t, cache, tt.wantClose)
}
Update all deps to latest where possible, bump Kube deps to v0.23.1 Highlights from this dep bump: 1. Made a copy of the v0.4.0 github.com/go-logr/stdr implementation for use in tests. We must bump this dep as Kube code uses a newer version now. We would have to rewrite hundreds of test log assertions without this copy. 2. Use github.com/felixge/httpsnoop to undo the changes made by ory/fosite#636 for CLI based login flows. This is required for backwards compatibility with older versions of our CLI. A separate change after this will update the CLI to be more flexible (it is purposefully not part of this change to confirm that we did not break anything). For all browser login flows, we now redirect using http.StatusSeeOther instead of http.StatusFound. 3. Drop plog.RemoveKlogGlobalFlags as klog no longer mutates global process flags 4. Only bump github.com/ory/x to v0.0.297 instead of the latest v0.0.321 because v0.0.298+ pulls in a newer version of go.opentelemetry.io/otel/semconv which breaks k8s.io/apiserver. We should update k8s.io/apiserver to use the newer code. 5. Migrate all code from k8s.io/apimachinery/pkg/util/clock to k8s.io/utils/clock and k8s.io/utils/clock/testing 6. Delete testutil.NewDeleteOptionsRecorder and migrate to the new kubetesting.NewDeleteActionWithOptions 7. Updated ExpectedAuthorizeCodeSessionJSONFromFuzzing caused by fosite's new rotated_secrets OAuth client field. This new field is currently not relevant to us as we have no private clients. Signed-off-by: Monis Khan <mok@vmware.com>
2021-12-10 22:22:36 +00:00
controller := New(cache, informers.Authentication().V1alpha1().JWTAuthenticators(), testLog.Logger)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
informers.Start(ctx.Done())
controllerlib.TestRunSynchronously(t, controller)
syncCtx := controllerlib.Context{Context: ctx, Key: tt.syncKey}
if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != nil {
testutil.RequireErrorStringFromErr(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.wantLogs, testLog.Lines())
require.Equal(t, tt.wantCacheEntries, len(cache.Keys()))
if !tt.runTestsOnResultingAuthenticator {
return // end of test unless we wanted to run tests on the resulting authenticator from the cache
}
// We expected the cache to have an entry, so pull that entry from the cache and test it.
expectedCacheKey := authncache.Key{
APIGroup: auth1alpha1.GroupName,
Kind: "JWTAuthenticator",
Name: syncCtx.Key.Name,
}
cachedAuthenticator := cache.Get(expectedCacheKey)
require.NotNil(t, cachedAuthenticator)
// Schedule it to be closed at the end of the test.
t.Cleanup(cachedAuthenticator.(*jwtAuthenticator).Close)
const (
goodSubject = "some-subject"
group0 = "some-group-0"
group1 = "some-group-1"
goodUsername = "pinny123"
)
if tt.wantUsernameClaim == "" {
tt.wantUsernameClaim = "username"
}
if tt.wantGroupsClaim == "" {
tt.wantGroupsClaim = "groups"
}
for _, test := range testTableForAuthenticateTokenTests(
t,
goodRSASigningKey,
goodRSASigningAlgo,
goodRSASigningKeyID,
group0,
group1,
goodUsername,
tt.wantUsernameClaim,
tt.wantGroupsClaim,
goodIssuer,
) {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
wellKnownClaims := jwt.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)),
}
var groups interface{}
username := goodUsername
if test.jwtClaims != nil {
test.jwtClaims(&wellKnownClaims, &groups, &username)
}
var signingKey interface{} = goodECSigningKey
signingAlgo := goodECSigningAlgo
signingKID := goodECSigningKeyID
if test.jwtSignature != nil {
test.jwtSignature(&signingKey, &signingAlgo, &signingKID)
}
jwt := createJWT(
t,
signingKey,
signingAlgo,
signingKID,
&wellKnownClaims,
tt.wantGroupsClaim,
groups,
test.distributedGroupsClaimURL,
tt.wantUsernameClaim,
username,
)
// Loop for a while here to allow the underlying OIDC authenticator to initialize itself asynchronously.
var (
rsp *authenticator.Response
authenticated bool
err error
)
_ = wait.PollImmediate(10*time.Millisecond, 5*time.Second, func() (bool, error) {
rsp, authenticated, err = cachedAuthenticator.AuthenticateToken(context.Background(), jwt)
return !isNotInitialized(err), nil
})
if test.wantErr != nil {
testutil.RequireErrorStringFromErr(t, err, test.wantErr)
} else {
require.NoError(t, err)
require.Equal(t, test.wantResponse, rsp)
require.Equal(t, test.wantAuthenticated, authenticated)
}
})
}
})
}
}
// isNotInitialized checks if the error is the internally-defined "oidc: authenticator not initialized" error from
// the underlying OIDC authenticator or "verifier is not initialized" from verifying distributed claims,
// both of which are initialized asynchronously.
func isNotInitialized(err error) bool {
return err != nil && (strings.Contains(err.Error(), "authenticator not initialized") || strings.Contains(err.Error(), "verifier not initialized"))
}
func testTableForAuthenticateTokenTests(
t *testing.T,
goodRSASigningKey *rsa.PrivateKey,
goodRSASigningAlgo jose.SignatureAlgorithm,
goodRSASigningKeyID string,
group0 string,
group1 string,
goodUsername string,
expectedUsernameClaim string,
expectedGroupsClaim string,
issuer string,
) []struct {
name string
jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string)
jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string)
wantResponse *authenticator.Response
wantAuthenticated bool
wantErr testutil.RequireErrorStringFunc
distributedGroupsClaimURL string
} {
tests := []struct {
name string
jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string)
jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string)
wantResponse *authenticator.Response
wantAuthenticated bool
wantErr testutil.RequireErrorStringFunc
distributedGroupsClaimURL string
}{
{
name: "good token without groups and with EC signature",
wantResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: goodUsername,
},
},
wantAuthenticated: true,
},
{
name: "good token without groups and with RSA signature",
jwtSignature: func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) {
*key = goodRSASigningKey
*algo = goodRSASigningAlgo
*kid = goodRSASigningKeyID
},
wantResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: goodUsername,
},
},
wantAuthenticated: true,
},
{
name: "good token with groups as array",
jwtClaims: func(_ *jwt.Claims, groups *interface{}, username *string) {
*groups = []string{group0, group1}
},
wantResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: goodUsername,
Groups: []string{group0, group1},
},
},
wantAuthenticated: true,
},
{
name: "good token with good distributed groups",
jwtClaims: func(claims *jwt.Claims, groups *interface{}, username *string) {
},
distributedGroupsClaimURL: issuer + "/claim_source",
wantResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: goodUsername,
Groups: []string{"some-distributed-group-1", "some-distributed-group-2"},
},
},
wantAuthenticated: true,
},
{
name: "distributed groups returns a 404",
jwtClaims: func(claims *jwt.Claims, groups *interface{}, 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 *interface{}, 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 *interface{}, username *string) {
*groups = group0
},
wantResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: goodUsername,
Groups: []string{group0},
},
},
wantAuthenticated: true,
},
{
name: "good token with nbf unset",
jwtClaims: func(claims *jwt.Claims, _ *interface{}, username *string) {
claims.NotBefore = nil
},
wantResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: goodUsername,
},
},
wantAuthenticated: true,
},
{
name: "bad token with groups as map",
jwtClaims: func(_ *jwt.Claims, groups *interface{}, 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, _ *interface{}, username *string) {
claims.Issuer = "wrong-issuer"
},
wantResponse: nil,
wantAuthenticated: false,
},
{
name: "bad token with no audience",
jwtClaims: func(claims *jwt.Claims, _ *interface{}, 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, _ *interface{}, 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, _ *interface{}, username *string) {
claims.NotBefore = jwt.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, _ *interface{}, username *string) {
claims.Expiry = jwt.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, _ *interface{}, 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, _ *interface{}, username *string) {
*username = ""
},
wantErr: testutil.WantMatchingErrorString(`oidc: parse username claims "` + expectedUsernameClaim + `": claim not present`),
},
{
name: "signing key is wrong",
jwtSignature: func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) {
var err error
*key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
*algo = jose.ES256
},
wantErr: testutil.WantMatchingErrorString(`oidc: verify token: failed to verify signature: failed to verify id token signature`),
},
{
name: "signing algo is unsupported",
jwtSignature: func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) {
var err error
*key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
require.NoError(t, err)
*algo = jose.ES384
},
wantErr: testutil.WantMatchingErrorString(`oidc: verify token: oidc: id token signed with unsupported algorithm, expected \["RS256" "ES256"\] got "ES384"`),
},
}
return tests
}
func tlsSpecFromTLSConfig(tls *tls.Config) *auth1alpha1.TLSSpec {
pemData := make([]byte, 0)
for _, certificate := range tls.Certificates {
for _, reallyCertificate := range certificate.Certificate {
pemData = append(pemData, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: reallyCertificate,
})...)
}
}
return &auth1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString(pemData),
}
}
func createJWT(
t *testing.T,
signingKey interface{},
signingAlgo jose.SignatureAlgorithm,
kid string,
claims *jwt.Claims,
groupsClaim string,
groupsValue interface{},
distributedGroupsClaimURL string,
usernameClaim string,
usernameValue string,
) string {
t.Helper()
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: signingAlgo, Key: signingKey},
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", kid),
)
require.NoError(t, err)
builder := jwt.Signed(sig).Claims(claims)
if groupsValue != nil {
builder = builder.Claims(map[string]interface{}{groupsClaim: groupsValue})
}
if distributedGroupsClaimURL != "" {
builder = builder.Claims(map[string]interface{}{"_claim_names": map[string]string{groupsClaim: "src1"}})
builder = builder.Claims(map[string]interface{}{"_claim_sources": map[string]interface{}{"src1": map[string]string{"endpoint": distributedGroupsClaimURL}}})
}
if usernameValue != "" {
builder = builder.Claims(map[string]interface{}{usernameClaim: usernameValue})
}
jwt, err := builder.CompactSerialize()
require.NoError(t, err)
return jwt
}
func newCacheValue(t *testing.T, spec auth1alpha1.JWTAuthenticatorSpec, wantClose bool) authncache.Value {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
tokenAuthenticatorCloser := mocktokenauthenticatorcloser.NewMockTokenAuthenticatorCloser(ctrl)
wantCloses := 0
if wantClose {
wantCloses++
}
tokenAuthenticatorCloser.EXPECT().Close().Times(wantCloses)
return &jwtAuthenticator{
tokenAuthenticatorCloser: tokenAuthenticatorCloser,
spec: &spec,
}
}