wire up discovery url in authenticator
Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com> Kubernetes-commit: 78fb0bae22f2106219d19fff060caa7866c27430
This commit is contained in:
parent
f2c6133c7f
commit
d456bc0c1b
|
|
@ -35,6 +35,7 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -66,6 +67,10 @@ var (
|
|||
synchronizeTokenIDVerifierForTest = false
|
||||
)
|
||||
|
||||
const (
|
||||
wellKnownEndpointPath = "/.well-known/openid-configuration"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// JWTAuthenticator is the authenticator that will be used to verify the JWT.
|
||||
JWTAuthenticator apiserver.JWTAuthenticator
|
||||
|
|
@ -268,6 +273,28 @@ func New(opts Options) (authenticator.Token, error) {
|
|||
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 = oidc.ClientContext(ctx, client)
|
||||
|
||||
|
|
@ -339,6 +366,26 @@ func New(opts Options) (authenticator.Token, error) {
|
|||
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,
|
||||
// or returns an error if the token can not be parsed. Since the JWT is not
|
||||
// verified, the returned issuer should not be trusted.
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
|
|
@ -146,6 +147,7 @@ type claimsTest struct {
|
|||
wantInitErr string
|
||||
claimToResponseMap map[string]string
|
||||
openIDConfig string
|
||||
fetchKeysFromRemote bool
|
||||
}
|
||||
|
||||
// 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))
|
||||
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")
|
||||
klog.V(5).Infof("%v: returning: %+v", r.URL, *openIDConfig)
|
||||
w.Write([]byte(*openIDConfig))
|
||||
|
|
@ -262,14 +265,17 @@ func (c *claimsTest) run(t *testing.T) {
|
|||
c.claims = replace(c.claims, &v)
|
||||
c.openIDConfig = replace(c.openIDConfig, &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 {
|
||||
c.claimToResponseMap[claim] = replace(response, &v)
|
||||
}
|
||||
c.wantErr = replace(c.wantErr, &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.
|
||||
c.options.KeySet = &staticKeySet{keys: c.pubKeys}
|
||||
}
|
||||
|
||||
if c.optsFunc != nil {
|
||||
c.optsFunc(&c.options)
|
||||
|
|
@ -307,7 +313,27 @@ func (c *claimsTest) run(t *testing.T) {
|
|||
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
|
||||
|
||||
|
|
@ -2986,6 +3012,191 @@ func TestToken(t *testing.T) {
|
|||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue