Skip to content

Commit

Permalink
wire up discovery url in authenticator
Browse files Browse the repository at this point in the history
Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
  • Loading branch information
aramase committed Mar 4, 2024
1 parent 84852ff commit 78fb0ba
Show file tree
Hide file tree
Showing 4 changed files with 417 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strings"
"sync"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -134,18 +135,19 @@ var (
)

type claimsTest struct {
name string
options Options
optsFunc func(*Options)
signingKey *jose.JSONWebKey
pubKeys []*jose.JSONWebKey
claims string
want *user.DefaultInfo
wantSkip bool
wantErr string
wantInitErr string
claimToResponseMap map[string]string
openIDConfig string
name string
options Options
optsFunc func(*Options)
signingKey *jose.JSONWebKey
pubKeys []*jose.JSONWebKey
claims string
want *user.DefaultInfo
wantSkip bool
wantErr string
wantInitErr string
claimToResponseMap map[string]string
openIDConfig string
fetchKeysFromRemote bool
}

// Replace formats the contents of v into the provided template.
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)

// Set the verifier to use the public key set instead of reading from a remote.
c.options.KeySet = &staticKeySet{keys: c.pubKeys}
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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 78fb0ba

Please sign in to comment.