wire up discovery url in authenticator
Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
This commit is contained in:
		@@ -35,6 +35,7 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
@@ -66,6 +67,10 @@ var (
 | 
				
			|||||||
	synchronizeTokenIDVerifierForTest = false
 | 
						synchronizeTokenIDVerifierForTest = false
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						wellKnownEndpointPath = "/.well-known/openid-configuration"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Options struct {
 | 
					type Options struct {
 | 
				
			||||||
	// JWTAuthenticator is the authenticator that will be used to verify the JWT.
 | 
						// JWTAuthenticator is the authenticator that will be used to verify the JWT.
 | 
				
			||||||
	JWTAuthenticator apiserver.JWTAuthenticator
 | 
						JWTAuthenticator apiserver.JWTAuthenticator
 | 
				
			||||||
@@ -268,6 +273,28 @@ func New(opts Options) (authenticator.Token, error) {
 | 
				
			|||||||
		client = &http.Client{Transport: tr, Timeout: 30 * time.Second}
 | 
							client = &http.Client{Transport: tr, Timeout: 30 * time.Second}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If the discovery URL is set in authentication configuration, we set up a
 | 
				
			||||||
 | 
						// roundTripper to rewrite the {url}/.well-known/openid-configuration to
 | 
				
			||||||
 | 
						// the discovery URL. This is useful for self-hosted providers, for example,
 | 
				
			||||||
 | 
						// providers that run on top of Kubernetes itself.
 | 
				
			||||||
 | 
						if len(opts.JWTAuthenticator.Issuer.DiscoveryURL) > 0 {
 | 
				
			||||||
 | 
							discoveryURL, err := url.Parse(opts.JWTAuthenticator.Issuer.DiscoveryURL)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("oidc: invalid discovery URL: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							clientWithDiscoveryURL := *client
 | 
				
			||||||
 | 
							baseTransport := clientWithDiscoveryURL.Transport
 | 
				
			||||||
 | 
							if baseTransport == nil {
 | 
				
			||||||
 | 
								baseTransport = http.DefaultTransport
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// This matches the url construction in oidc.NewProvider as of go-oidc v2.2.1.
 | 
				
			||||||
 | 
							// xref: https://github.com/coreos/go-oidc/blob/40cd342c4a2076195294612a834d11df23c1b25a/oidc.go#L114
 | 
				
			||||||
 | 
							urlToRewrite := strings.TrimSuffix(opts.JWTAuthenticator.Issuer.URL, "/") + wellKnownEndpointPath
 | 
				
			||||||
 | 
							clientWithDiscoveryURL.Transport = &discoveryURLRoundTripper{baseTransport, discoveryURL, urlToRewrite}
 | 
				
			||||||
 | 
							client = &clientWithDiscoveryURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
						ctx, cancel := context.WithCancel(context.Background())
 | 
				
			||||||
	ctx = oidc.ClientContext(ctx, client)
 | 
						ctx = oidc.ClientContext(ctx, client)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -339,6 +366,26 @@ func New(opts Options) (authenticator.Token, error) {
 | 
				
			|||||||
	return newInstrumentedAuthenticator(issuerURL, authenticator), nil
 | 
						return newInstrumentedAuthenticator(issuerURL, authenticator), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// discoveryURLRoundTripper is a http.RoundTripper that rewrites the
 | 
				
			||||||
 | 
					// {url}/.well-known/openid-configuration to the discovery URL.
 | 
				
			||||||
 | 
					type discoveryURLRoundTripper struct {
 | 
				
			||||||
 | 
						base http.RoundTripper
 | 
				
			||||||
 | 
						// discoveryURL is the URL to use to fetch the openid configuration
 | 
				
			||||||
 | 
						discoveryURL *url.URL
 | 
				
			||||||
 | 
						// urlToRewrite is the URL to rewrite to the discovery URL
 | 
				
			||||||
 | 
						urlToRewrite string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *discoveryURLRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
 | 
				
			||||||
 | 
						if req.Method == http.MethodGet && req.URL.String() == t.urlToRewrite {
 | 
				
			||||||
 | 
							clone := req.Clone(req.Context())
 | 
				
			||||||
 | 
							clone.Host = ""
 | 
				
			||||||
 | 
							clone.URL = t.discoveryURL
 | 
				
			||||||
 | 
							return t.base.RoundTrip(clone)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return t.base.RoundTrip(req)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// untrustedIssuer extracts an untrusted "iss" claim from the given JWT token,
 | 
					// untrustedIssuer extracts an untrusted "iss" claim from the given JWT token,
 | 
				
			||||||
// or returns an error if the token can not be parsed.  Since the JWT is not
 | 
					// or returns an error if the token can not be parsed.  Since the JWT is not
 | 
				
			||||||
// verified, the returned issuer should not be trusted.
 | 
					// verified, the returned issuer should not be trusted.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,6 +36,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"gopkg.in/square/go-jose.v2"
 | 
						"gopkg.in/square/go-jose.v2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/util/wait"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/apis/apiserver"
 | 
						"k8s.io/apiserver/pkg/apis/apiserver"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/authentication/user"
 | 
						"k8s.io/apiserver/pkg/authentication/user"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/features"
 | 
						"k8s.io/apiserver/pkg/features"
 | 
				
			||||||
@@ -146,6 +147,7 @@ type claimsTest struct {
 | 
				
			|||||||
	wantInitErr         string
 | 
						wantInitErr         string
 | 
				
			||||||
	claimToResponseMap  map[string]string
 | 
						claimToResponseMap  map[string]string
 | 
				
			||||||
	openIDConfig        string
 | 
						openIDConfig        string
 | 
				
			||||||
 | 
						fetchKeysFromRemote bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Replace formats the contents of v into the provided template.
 | 
					// Replace formats the contents of v into the provided template.
 | 
				
			||||||
@@ -175,7 +177,8 @@ func newClaimServer(t *testing.T, keys jose.JSONWebKeySet, signer jose.Signer, c
 | 
				
			|||||||
			klog.V(5).Infof("%v: returning: %+v", r.URL, string(keyBytes))
 | 
								klog.V(5).Infof("%v: returning: %+v", r.URL, string(keyBytes))
 | 
				
			||||||
			w.Write(keyBytes)
 | 
								w.Write(keyBytes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		case "/.well-known/openid-configuration":
 | 
							// /c/d/bar/.well-known/openid-configuration is used to test issuer url and discovery url with a path
 | 
				
			||||||
 | 
							case "/.well-known/openid-configuration", "/c/d/bar/.well-known/openid-configuration":
 | 
				
			||||||
			w.Header().Set("Content-Type", "application/json")
 | 
								w.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
			klog.V(5).Infof("%v: returning: %+v", r.URL, *openIDConfig)
 | 
								klog.V(5).Infof("%v: returning: %+v", r.URL, *openIDConfig)
 | 
				
			||||||
			w.Write([]byte(*openIDConfig))
 | 
								w.Write([]byte(*openIDConfig))
 | 
				
			||||||
@@ -262,14 +265,17 @@ func (c *claimsTest) run(t *testing.T) {
 | 
				
			|||||||
	c.claims = replace(c.claims, &v)
 | 
						c.claims = replace(c.claims, &v)
 | 
				
			||||||
	c.openIDConfig = replace(c.openIDConfig, &v)
 | 
						c.openIDConfig = replace(c.openIDConfig, &v)
 | 
				
			||||||
	c.options.JWTAuthenticator.Issuer.URL = replace(c.options.JWTAuthenticator.Issuer.URL, &v)
 | 
						c.options.JWTAuthenticator.Issuer.URL = replace(c.options.JWTAuthenticator.Issuer.URL, &v)
 | 
				
			||||||
 | 
						c.options.JWTAuthenticator.Issuer.DiscoveryURL = replace(c.options.JWTAuthenticator.Issuer.DiscoveryURL, &v)
 | 
				
			||||||
	for claim, response := range c.claimToResponseMap {
 | 
						for claim, response := range c.claimToResponseMap {
 | 
				
			||||||
		c.claimToResponseMap[claim] = replace(response, &v)
 | 
							c.claimToResponseMap[claim] = replace(response, &v)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	c.wantErr = replace(c.wantErr, &v)
 | 
						c.wantErr = replace(c.wantErr, &v)
 | 
				
			||||||
	c.wantInitErr = replace(c.wantInitErr, &v)
 | 
						c.wantInitErr = replace(c.wantInitErr, &v)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !c.fetchKeysFromRemote {
 | 
				
			||||||
		// Set the verifier to use the public key set instead of reading from a remote.
 | 
							// Set the verifier to use the public key set instead of reading from a remote.
 | 
				
			||||||
		c.options.KeySet = &staticKeySet{keys: c.pubKeys}
 | 
							c.options.KeySet = &staticKeySet{keys: c.pubKeys}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if c.optsFunc != nil {
 | 
						if c.optsFunc != nil {
 | 
				
			||||||
		c.optsFunc(&c.options)
 | 
							c.optsFunc(&c.options)
 | 
				
			||||||
@@ -307,7 +313,27 @@ func (c *claimsTest) run(t *testing.T) {
 | 
				
			|||||||
		t.Fatalf("serialize token: %v", err)
 | 
							t.Fatalf("serialize token: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	got, ok, err := a.AuthenticateToken(testContext(t), token)
 | 
						ia, ok := a.(*instrumentedAuthenticator)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							t.Fatalf("expected authenticator to be instrumented")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						authenticator, ok := ia.delegate.(*Authenticator)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							t.Fatalf("expected delegate to be Authenticator")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx := testContext(t)
 | 
				
			||||||
 | 
						// wait for the authenticator to be initialized
 | 
				
			||||||
 | 
						err = wait.PollUntilContextCancel(ctx, time.Millisecond, true, func(context.Context) (bool, error) {
 | 
				
			||||||
 | 
							if v, _ := authenticator.idTokenVerifier(); v == nil {
 | 
				
			||||||
 | 
								return false, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return true, nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("failed to initialize the authenticator: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						got, ok, err := a.AuthenticateToken(ctx, token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	expectErr := len(c.wantErr) > 0
 | 
						expectErr := len(c.wantErr) > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2986,6 +3012,191 @@ func TestToken(t *testing.T) {
 | 
				
			|||||||
				Name: "jane",
 | 
									Name: "jane",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "discovery-url",
 | 
				
			||||||
 | 
								options: Options{
 | 
				
			||||||
 | 
									JWTAuthenticator: apiserver.JWTAuthenticator{
 | 
				
			||||||
 | 
										Issuer: apiserver.Issuer{
 | 
				
			||||||
 | 
											URL:          "https://auth.example.com",
 | 
				
			||||||
 | 
											DiscoveryURL: "{{.URL}}/.well-known/openid-configuration",
 | 
				
			||||||
 | 
											Audiences:    []string{"my-client"},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										ClaimMappings: apiserver.ClaimMappings{
 | 
				
			||||||
 | 
											Username: apiserver.PrefixedClaimOrExpression{
 | 
				
			||||||
 | 
												Claim:  "username",
 | 
				
			||||||
 | 
												Prefix: pointer.String(""),
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									now: func() time.Time { return now },
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
				
			||||||
 | 
								pubKeys: []*jose.JSONWebKey{
 | 
				
			||||||
 | 
									loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								claims: fmt.Sprintf(`{
 | 
				
			||||||
 | 
									"iss": "https://auth.example.com",
 | 
				
			||||||
 | 
									"aud": "my-client",
 | 
				
			||||||
 | 
									"username": "jane",
 | 
				
			||||||
 | 
									"exp": %d
 | 
				
			||||||
 | 
								}`, valid.Unix()),
 | 
				
			||||||
 | 
								openIDConfig: `{
 | 
				
			||||||
 | 
										"issuer": "https://auth.example.com",
 | 
				
			||||||
 | 
										"jwks_uri": "{{.URL}}/.testing/keys"
 | 
				
			||||||
 | 
								}`,
 | 
				
			||||||
 | 
								fetchKeysFromRemote: true,
 | 
				
			||||||
 | 
								want: &user.DefaultInfo{
 | 
				
			||||||
 | 
									Name: "jane",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "discovery url, issuer has a path",
 | 
				
			||||||
 | 
								options: Options{
 | 
				
			||||||
 | 
									JWTAuthenticator: apiserver.JWTAuthenticator{
 | 
				
			||||||
 | 
										Issuer: apiserver.Issuer{
 | 
				
			||||||
 | 
											URL:          "https://auth.example.com/a/b/foo",
 | 
				
			||||||
 | 
											DiscoveryURL: "{{.URL}}/.well-known/openid-configuration",
 | 
				
			||||||
 | 
											Audiences:    []string{"my-client"},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										ClaimMappings: apiserver.ClaimMappings{
 | 
				
			||||||
 | 
											Username: apiserver.PrefixedClaimOrExpression{
 | 
				
			||||||
 | 
												Claim:  "username",
 | 
				
			||||||
 | 
												Prefix: pointer.String(""),
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									now: func() time.Time { return now },
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
				
			||||||
 | 
								pubKeys: []*jose.JSONWebKey{
 | 
				
			||||||
 | 
									loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								claims: fmt.Sprintf(`{
 | 
				
			||||||
 | 
									"iss": "https://auth.example.com/a/b/foo",
 | 
				
			||||||
 | 
									"aud": "my-client",
 | 
				
			||||||
 | 
									"username": "jane",
 | 
				
			||||||
 | 
									"exp": %d
 | 
				
			||||||
 | 
								}`, valid.Unix()),
 | 
				
			||||||
 | 
								openIDConfig: `{
 | 
				
			||||||
 | 
										"issuer": "https://auth.example.com/a/b/foo",
 | 
				
			||||||
 | 
										"jwks_uri": "{{.URL}}/.testing/keys"
 | 
				
			||||||
 | 
								}`,
 | 
				
			||||||
 | 
								fetchKeysFromRemote: true,
 | 
				
			||||||
 | 
								want: &user.DefaultInfo{
 | 
				
			||||||
 | 
									Name: "jane",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "discovery url has a path, issuer url has no path",
 | 
				
			||||||
 | 
								options: Options{
 | 
				
			||||||
 | 
									JWTAuthenticator: apiserver.JWTAuthenticator{
 | 
				
			||||||
 | 
										Issuer: apiserver.Issuer{
 | 
				
			||||||
 | 
											URL:          "https://auth.example.com",
 | 
				
			||||||
 | 
											DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration",
 | 
				
			||||||
 | 
											Audiences:    []string{"my-client"},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										ClaimMappings: apiserver.ClaimMappings{
 | 
				
			||||||
 | 
											Username: apiserver.PrefixedClaimOrExpression{
 | 
				
			||||||
 | 
												Claim:  "username",
 | 
				
			||||||
 | 
												Prefix: pointer.String(""),
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									now: func() time.Time { return now },
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
				
			||||||
 | 
								pubKeys: []*jose.JSONWebKey{
 | 
				
			||||||
 | 
									loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								claims: fmt.Sprintf(`{
 | 
				
			||||||
 | 
									"iss": "https://auth.example.com",
 | 
				
			||||||
 | 
									"aud": "my-client",
 | 
				
			||||||
 | 
									"username": "jane",
 | 
				
			||||||
 | 
									"exp": %d
 | 
				
			||||||
 | 
								}`, valid.Unix()),
 | 
				
			||||||
 | 
								openIDConfig: `{
 | 
				
			||||||
 | 
										"issuer": "https://auth.example.com",
 | 
				
			||||||
 | 
										"jwks_uri": "{{.URL}}/.testing/keys"
 | 
				
			||||||
 | 
								}`,
 | 
				
			||||||
 | 
								fetchKeysFromRemote: true,
 | 
				
			||||||
 | 
								want: &user.DefaultInfo{
 | 
				
			||||||
 | 
									Name: "jane",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "discovery url and issuer url have paths",
 | 
				
			||||||
 | 
								options: Options{
 | 
				
			||||||
 | 
									JWTAuthenticator: apiserver.JWTAuthenticator{
 | 
				
			||||||
 | 
										Issuer: apiserver.Issuer{
 | 
				
			||||||
 | 
											URL:          "https://auth.example.com/a/b/foo",
 | 
				
			||||||
 | 
											DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration",
 | 
				
			||||||
 | 
											Audiences:    []string{"my-client"},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										ClaimMappings: apiserver.ClaimMappings{
 | 
				
			||||||
 | 
											Username: apiserver.PrefixedClaimOrExpression{
 | 
				
			||||||
 | 
												Claim:  "username",
 | 
				
			||||||
 | 
												Prefix: pointer.String(""),
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									now: func() time.Time { return now },
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
				
			||||||
 | 
								pubKeys: []*jose.JSONWebKey{
 | 
				
			||||||
 | 
									loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								claims: fmt.Sprintf(`{
 | 
				
			||||||
 | 
									"iss": "https://auth.example.com/a/b/foo",
 | 
				
			||||||
 | 
									"aud": "my-client",
 | 
				
			||||||
 | 
									"username": "jane",
 | 
				
			||||||
 | 
									"exp": %d
 | 
				
			||||||
 | 
								}`, valid.Unix()),
 | 
				
			||||||
 | 
								openIDConfig: `{
 | 
				
			||||||
 | 
										"issuer": "https://auth.example.com/a/b/foo",
 | 
				
			||||||
 | 
										"jwks_uri": "{{.URL}}/.testing/keys"
 | 
				
			||||||
 | 
								}`,
 | 
				
			||||||
 | 
								fetchKeysFromRemote: true,
 | 
				
			||||||
 | 
								want: &user.DefaultInfo{
 | 
				
			||||||
 | 
									Name: "jane",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "discovery url and issuer url have paths, issuer url has trailing slash",
 | 
				
			||||||
 | 
								options: Options{
 | 
				
			||||||
 | 
									JWTAuthenticator: apiserver.JWTAuthenticator{
 | 
				
			||||||
 | 
										Issuer: apiserver.Issuer{
 | 
				
			||||||
 | 
											URL:          "https://auth.example.com/a/b/foo/",
 | 
				
			||||||
 | 
											DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration",
 | 
				
			||||||
 | 
											Audiences:    []string{"my-client"},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										ClaimMappings: apiserver.ClaimMappings{
 | 
				
			||||||
 | 
											Username: apiserver.PrefixedClaimOrExpression{
 | 
				
			||||||
 | 
												Claim:  "username",
 | 
				
			||||||
 | 
												Prefix: pointer.String(""),
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									now: func() time.Time { return now },
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
				
			||||||
 | 
								pubKeys: []*jose.JSONWebKey{
 | 
				
			||||||
 | 
									loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								claims: fmt.Sprintf(`{
 | 
				
			||||||
 | 
									"iss": "https://auth.example.com/a/b/foo/",
 | 
				
			||||||
 | 
									"aud": "my-client",
 | 
				
			||||||
 | 
									"username": "jane",
 | 
				
			||||||
 | 
									"exp": %d
 | 
				
			||||||
 | 
								}`, valid.Unix()),
 | 
				
			||||||
 | 
								openIDConfig: `{
 | 
				
			||||||
 | 
										"issuer": "https://auth.example.com/a/b/foo/",
 | 
				
			||||||
 | 
										"jwks_uri": "{{.URL}}/.testing/keys"
 | 
				
			||||||
 | 
								}`,
 | 
				
			||||||
 | 
								fetchKeysFromRemote: true,
 | 
				
			||||||
 | 
								want: &user.DefaultInfo{
 | 
				
			||||||
 | 
									Name: "jane",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var successTestCount, failureTestCount int
 | 
						var successTestCount, failureTestCount int
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -141,7 +141,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
 | 
				
			|||||||
			) {
 | 
								) {
 | 
				
			||||||
				caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
 | 
									caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
 | 
				
			||||||
				signingPrivateKey, publicKey := keyFunc(t)
 | 
									signingPrivateKey, publicKey := keyFunc(t)
 | 
				
			||||||
				oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
 | 
									oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if useAuthenticationConfig {
 | 
									if useAuthenticationConfig {
 | 
				
			||||||
					authenticationConfig := fmt.Sprintf(`
 | 
										authenticationConfig := fmt.Sprintf(`
 | 
				
			||||||
@@ -274,7 +274,7 @@ jwt:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				signingPrivateKey, _ = keyFunc(t)
 | 
									signingPrivateKey, _ = keyFunc(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
 | 
									oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if useAuthenticationConfig {
 | 
									if useAuthenticationConfig {
 | 
				
			||||||
					authenticationConfig := fmt.Sprintf(`
 | 
										authenticationConfig := fmt.Sprintf(`
 | 
				
			||||||
@@ -888,6 +888,104 @@ jwt:
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestStructuredAuthenticationDiscoveryURL tests that the discovery URL configured in jwt.issuer.discoveryURL is used to
 | 
				
			||||||
 | 
					// fetch the discovery document and the issuer in jwt.issuer.url is used to validate the ID token.
 | 
				
			||||||
 | 
					func TestStructuredAuthenticationDiscoveryURL(t *testing.T) {
 | 
				
			||||||
 | 
						defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name         string
 | 
				
			||||||
 | 
							issuerURL    string
 | 
				
			||||||
 | 
							discoveryURL func(baseURL string) string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:         "discovery url and issuer url with no path",
 | 
				
			||||||
 | 
								issuerURL:    "https://example.com",
 | 
				
			||||||
 | 
								discoveryURL: func(baseURL string) string { return baseURL },
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:         "discovery url has path, issuer url has no path",
 | 
				
			||||||
 | 
								issuerURL:    "https://example.com",
 | 
				
			||||||
 | 
								discoveryURL: func(baseURL string) string { return fmt.Sprintf("%s/c/d/bar", baseURL) },
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:         "discovery url has no path, issuer url has path",
 | 
				
			||||||
 | 
								issuerURL:    "https://example.com/a/b/foo",
 | 
				
			||||||
 | 
								discoveryURL: func(baseURL string) string { return baseURL },
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:      "discovery url and issuer url have paths",
 | 
				
			||||||
 | 
								issuerURL: "https://example.com/a/b/foo",
 | 
				
			||||||
 | 
								discoveryURL: func(baseURL string) string {
 | 
				
			||||||
 | 
									return fmt.Sprintf("%s/c/d/bar", baseURL)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
 | 
				
			||||||
 | 
								signingPrivateKey, publicKey := rsaGenerateKey(t)
 | 
				
			||||||
 | 
								// set the issuer in the discovery document to issuer url (different from the discovery URL) to assert
 | 
				
			||||||
 | 
								// 1. discovery URL is used to fetch the discovery document and
 | 
				
			||||||
 | 
								// 2. issuer in the discovery document is used to validate the ID token
 | 
				
			||||||
 | 
								oidcServer := utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, tt.issuerURL)
 | 
				
			||||||
 | 
								discoveryURL := strings.TrimSuffix(tt.discoveryURL(oidcServer.URL()), "/") + "/.well-known/openid-configuration"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								authenticationConfig := fmt.Sprintf(`
 | 
				
			||||||
 | 
					apiVersion: apiserver.config.k8s.io/v1alpha1
 | 
				
			||||||
 | 
					kind: AuthenticationConfiguration
 | 
				
			||||||
 | 
					jwt:
 | 
				
			||||||
 | 
					- issuer:
 | 
				
			||||||
 | 
					    url: %s
 | 
				
			||||||
 | 
					    discoveryURL: %s
 | 
				
			||||||
 | 
					    audiences:
 | 
				
			||||||
 | 
					    - foo
 | 
				
			||||||
 | 
					    audienceMatchPolicy: MatchAny
 | 
				
			||||||
 | 
					    certificateAuthority: |
 | 
				
			||||||
 | 
					        %s
 | 
				
			||||||
 | 
					  claimMappings:
 | 
				
			||||||
 | 
					    username:
 | 
				
			||||||
 | 
					      expression: "'k8s-' + claims.sub"
 | 
				
			||||||
 | 
					  claimValidationRules:
 | 
				
			||||||
 | 
					  - expression: 'claims.hd == "example.com"'
 | 
				
			||||||
 | 
					    message: "the hd claim must be set to example.com"
 | 
				
			||||||
 | 
					`, tt.issuerURL, discoveryURL, indentCertificateAuthority(string(caCertContent)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, publicKey))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								apiServer := startTestAPIServerForOIDC(t, apiServerOIDCConfig{authenticationConfigYAML: authenticationConfig}, publicKey)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								idTokenLifetime := time.Second * 1200
 | 
				
			||||||
 | 
								oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
 | 
				
			||||||
 | 
									t,
 | 
				
			||||||
 | 
									signingPrivateKey,
 | 
				
			||||||
 | 
									map[string]interface{}{
 | 
				
			||||||
 | 
										"iss": tt.issuerURL, // issuer in the discovery document is used to validate the ID token
 | 
				
			||||||
 | 
										"sub": defaultOIDCClaimedUsername,
 | 
				
			||||||
 | 
										"aud": "foo",
 | 
				
			||||||
 | 
										"exp": time.Now().Add(idTokenLifetime).Unix(),
 | 
				
			||||||
 | 
										"hd":  "example.com",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									defaultStubAccessToken,
 | 
				
			||||||
 | 
									defaultStubRefreshToken,
 | 
				
			||||||
 | 
								))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								tokenURL, err := oidcServer.TokenURL()
 | 
				
			||||||
 | 
								require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								client := configureClientFetchingOIDCCredentials(t, apiServer.ClientConfig, caCertContent, caFilePath, oidcServer.URL(), tokenURL)
 | 
				
			||||||
 | 
								ctx := testContext(t)
 | 
				
			||||||
 | 
								res, err := client.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
 | 
				
			||||||
 | 
								require.NoError(t, err)
 | 
				
			||||||
 | 
								assert.Equal(t, authenticationv1.UserInfo{
 | 
				
			||||||
 | 
									Username: "k8s-john_doe",
 | 
				
			||||||
 | 
									Groups:   []string{"system:authenticated"},
 | 
				
			||||||
 | 
								}, res.Status.UserInfo)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func rsaGenerateKey(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) {
 | 
					func rsaGenerateKey(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) {
 | 
				
			||||||
	t.Helper()
 | 
						t.Helper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -919,7 +1017,7 @@ func configureTestInfrastructure[K utilsoidc.JosePrivateKey, L utilsoidc.JosePub
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	signingPrivateKey, publicKey := keyFunc(t)
 | 
						signingPrivateKey, publicKey := keyFunc(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
 | 
						oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	authenticationConfig := fn(t, oidcServer.URL(), string(caCertContent))
 | 
						authenticationConfig := fn(t, oidcServer.URL(), string(caCertContent))
 | 
				
			||||||
	if len(authenticationConfig) > 0 {
 | 
						if len(authenticationConfig) > 0 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -80,7 +80,7 @@ func (ts *TestServer) TokenURL() (string, error) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// BuildAndRunTestServer configures OIDC TLS server and its routing
 | 
					// BuildAndRunTestServer configures OIDC TLS server and its routing
 | 
				
			||||||
func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath string) *TestServer {
 | 
					func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath, issuerOverride string) *TestServer {
 | 
				
			||||||
	t.Helper()
 | 
						t.Helper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	certContent, err := os.ReadFile(caPath)
 | 
						certContent, err := os.ReadFile(caPath)
 | 
				
			||||||
@@ -111,33 +111,21 @@ func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath string) *TestServer {
 | 
				
			|||||||
		jwksHandler:  NewMockJWKsHandler(mockCtrl),
 | 
							jwksHandler:  NewMockJWKsHandler(mockCtrl),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						issuer := httpServer.URL
 | 
				
			||||||
 | 
						// issuerOverride is used to override the issuer URL in the well-known configuration.
 | 
				
			||||||
 | 
						// This is useful to validate scenarios where discovery url is different from the issuer url.
 | 
				
			||||||
 | 
						if len(issuerOverride) > 0 {
 | 
				
			||||||
 | 
							issuer = issuerOverride
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mux.HandleFunc(openIDWellKnownWebPath, func(writer http.ResponseWriter, request *http.Request) {
 | 
						mux.HandleFunc(openIDWellKnownWebPath, func(writer http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
		authURL, err := url.JoinPath(httpServer.URL + authWebPath)
 | 
							discoveryDocHandler(t, writer, httpServer.URL, issuer)
 | 
				
			||||||
		require.NoError(t, err)
 | 
					 | 
				
			||||||
		tokenURL, err := url.JoinPath(httpServer.URL + tokenWebPath)
 | 
					 | 
				
			||||||
		require.NoError(t, err)
 | 
					 | 
				
			||||||
		jwksURL, err := url.JoinPath(httpServer.URL + jwksWebPath)
 | 
					 | 
				
			||||||
		require.NoError(t, err)
 | 
					 | 
				
			||||||
		userInfoURL, err := url.JoinPath(httpServer.URL + authWebPath)
 | 
					 | 
				
			||||||
		require.NoError(t, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		err = json.NewEncoder(writer).Encode(struct {
 | 
					 | 
				
			||||||
			Issuer      string `json:"issuer"`
 | 
					 | 
				
			||||||
			AuthURL     string `json:"authorization_endpoint"`
 | 
					 | 
				
			||||||
			TokenURL    string `json:"token_endpoint"`
 | 
					 | 
				
			||||||
			JWKSURL     string `json:"jwks_uri"`
 | 
					 | 
				
			||||||
			UserInfoURL string `json:"userinfo_endpoint"`
 | 
					 | 
				
			||||||
		}{
 | 
					 | 
				
			||||||
			Issuer:      httpServer.URL,
 | 
					 | 
				
			||||||
			AuthURL:     authURL,
 | 
					 | 
				
			||||||
			TokenURL:    tokenURL,
 | 
					 | 
				
			||||||
			JWKSURL:     jwksURL,
 | 
					 | 
				
			||||||
			UserInfoURL: userInfoURL,
 | 
					 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
		require.NoError(t, err)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		writer.Header().Add("Content-Type", "application/json")
 | 
						// /c/d/bar/.well-known/openid-configuration is used to validate scenarios where discovery url is different from the issuer url
 | 
				
			||||||
		writer.WriteHeader(http.StatusOK)
 | 
						// and discovery url contains path.
 | 
				
			||||||
 | 
						mux.HandleFunc("/c/d/bar"+openIDWellKnownWebPath, func(writer http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
 | 
							discoveryDocHandler(t, writer, httpServer.URL, issuer)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mux.HandleFunc(tokenWebPath, func(writer http.ResponseWriter, request *http.Request) {
 | 
						mux.HandleFunc(tokenWebPath, func(writer http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
@@ -171,6 +159,34 @@ func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath string) *TestServer {
 | 
				
			|||||||
	return oidcServer
 | 
						return oidcServer
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func discoveryDocHandler(t *testing.T, writer http.ResponseWriter, httpServerURL, issuer string) {
 | 
				
			||||||
 | 
						authURL, err := url.JoinPath(httpServerURL + authWebPath)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						tokenURL, err := url.JoinPath(httpServerURL + tokenWebPath)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						jwksURL, err := url.JoinPath(httpServerURL + jwksWebPath)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						userInfoURL, err := url.JoinPath(httpServerURL + authWebPath)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						writer.Header().Add("Content-Type", "application/json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = json.NewEncoder(writer).Encode(struct {
 | 
				
			||||||
 | 
							Issuer      string `json:"issuer"`
 | 
				
			||||||
 | 
							AuthURL     string `json:"authorization_endpoint"`
 | 
				
			||||||
 | 
							TokenURL    string `json:"token_endpoint"`
 | 
				
			||||||
 | 
							JWKSURL     string `json:"jwks_uri"`
 | 
				
			||||||
 | 
							UserInfoURL string `json:"userinfo_endpoint"`
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							Issuer:      issuer,
 | 
				
			||||||
 | 
							AuthURL:     authURL,
 | 
				
			||||||
 | 
							TokenURL:    tokenURL,
 | 
				
			||||||
 | 
							JWKSURL:     jwksURL,
 | 
				
			||||||
 | 
							UserInfoURL: userInfoURL,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type JosePrivateKey interface {
 | 
					type JosePrivateKey interface {
 | 
				
			||||||
	*rsa.PrivateKey | *ecdsa.PrivateKey
 | 
						*rsa.PrivateKey | *ecdsa.PrivateKey
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user