mirror of https://github.com/k3s-io/k3s
make oidc authenticator (more?) audience aware
Part of https://github.com/kubernetes/kubernetes/issues/69893pull/58/head
parent
5656ac754d
commit
a714d9cd04
|
@ -159,7 +159,18 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
|
|||
// simply returns an error, the OpenID Connect plugin may query the provider to
|
||||
// update the keys, causing performance hits.
|
||||
if len(config.OIDCIssuerURL) > 0 && len(config.OIDCClientID) > 0 {
|
||||
oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(config.OIDCIssuerURL, config.OIDCClientID, config.OIDCCAFile, config.OIDCUsernameClaim, config.OIDCUsernamePrefix, config.OIDCGroupsClaim, config.OIDCGroupsPrefix, config.OIDCSigningAlgs, config.OIDCRequiredClaims)
|
||||
oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(oidc.Options{
|
||||
IssuerURL: config.OIDCIssuerURL,
|
||||
ClientID: config.OIDCClientID,
|
||||
APIAudiences: config.APIAudiences,
|
||||
CAFile: config.OIDCCAFile,
|
||||
UsernameClaim: config.OIDCUsernameClaim,
|
||||
UsernamePrefix: config.OIDCUsernamePrefix,
|
||||
GroupsClaim: config.OIDCGroupsClaim,
|
||||
GroupsPrefix: config.OIDCGroupsPrefix,
|
||||
SupportedSigningAlgs: config.OIDCSigningAlgs,
|
||||
RequiredClaims: config.OIDCRequiredClaims,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -238,33 +249,23 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Token, e
|
|||
}
|
||||
|
||||
// newAuthenticatorFromOIDCIssuerURL returns an authenticator.Token or an error.
|
||||
func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClaim, usernamePrefix, groupsClaim, groupsPrefix string, signingAlgs []string, requiredClaims map[string]string) (authenticator.Token, error) {
|
||||
func newAuthenticatorFromOIDCIssuerURL(opts oidc.Options) (authenticator.Token, error) {
|
||||
const noUsernamePrefix = "-"
|
||||
|
||||
if usernamePrefix == "" && usernameClaim != "email" {
|
||||
if opts.UsernamePrefix == "" && opts.UsernameClaim != "email" {
|
||||
// Old behavior. If a usernamePrefix isn't provided, prefix all claims other than "email"
|
||||
// with the issuerURL.
|
||||
//
|
||||
// See https://github.com/kubernetes/kubernetes/issues/31380
|
||||
usernamePrefix = issuerURL + "#"
|
||||
opts.UsernamePrefix = opts.IssuerURL + "#"
|
||||
}
|
||||
|
||||
if usernamePrefix == noUsernamePrefix {
|
||||
if opts.UsernamePrefix == noUsernamePrefix {
|
||||
// Special value indicating usernames shouldn't be prefixed.
|
||||
usernamePrefix = ""
|
||||
opts.UsernamePrefix = ""
|
||||
}
|
||||
|
||||
tokenAuthenticator, err := oidc.New(oidc.Options{
|
||||
IssuerURL: issuerURL,
|
||||
ClientID: clientID,
|
||||
CAFile: caFile,
|
||||
UsernameClaim: usernameClaim,
|
||||
UsernamePrefix: usernamePrefix,
|
||||
GroupsClaim: groupsClaim,
|
||||
GroupsPrefix: groupsPrefix,
|
||||
SupportedSigningAlgs: signingAlgs,
|
||||
RequiredClaims: requiredClaims,
|
||||
})
|
||||
tokenAuthenticator, err := oidc.New(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ go_test(
|
|||
data = glob(["testdata/**"]),
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//vendor/github.com/coreos/go-oidc:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
|
|
|
@ -78,6 +78,12 @@ type Options struct {
|
|||
// See: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||
ClientID string
|
||||
|
||||
// APIAudiences are the audiences that the API server identitifes as. The
|
||||
// (API audiences unioned with the ClientIDs) should have a non-empty
|
||||
// intersection with the request's target audience. This preserves the
|
||||
// behavior of the OIDC authenticator pre-introduction of API audiences.
|
||||
APIAudiences authenticator.Audiences
|
||||
|
||||
// Path to a PEM encoded root certificate of the provider.
|
||||
CAFile string
|
||||
|
||||
|
@ -188,6 +194,8 @@ type Authenticator struct {
|
|||
groupsClaim string
|
||||
groupsPrefix string
|
||||
requiredClaims map[string]string
|
||||
clientIDs authenticator.Audiences
|
||||
apiAudiences authenticator.Audiences
|
||||
|
||||
// Contains an *oidc.IDTokenVerifier. Do not access directly use the
|
||||
// idTokenVerifier method.
|
||||
|
@ -317,6 +325,8 @@ func newAuthenticator(opts Options, initVerifier func(ctx context.Context, a *Au
|
|||
groupsClaim: opts.GroupsClaim,
|
||||
groupsPrefix: opts.GroupsPrefix,
|
||||
requiredClaims: opts.RequiredClaims,
|
||||
clientIDs: authenticator.Audiences{opts.ClientID},
|
||||
apiAudiences: opts.APIAudiences,
|
||||
cancel: cancel,
|
||||
resolver: resolver,
|
||||
}
|
||||
|
@ -532,6 +542,11 @@ func (r *claimResolver) resolve(endpoint endpoint, allClaims claims) error {
|
|||
}
|
||||
|
||||
func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
||||
if reqAuds, ok := authenticator.AudiencesFrom(ctx); ok {
|
||||
if len(reqAuds.Intersect(a.clientIDs)) == 0 && len(reqAuds.Intersect(a.apiAudiences)) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
if !hasCorrectIssuer(a.issuerURL, token) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ import (
|
|||
oidc "github.com/coreos/go-oidc"
|
||||
"github.com/golang/glog"
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
|
@ -140,6 +142,7 @@ type claimsTest struct {
|
|||
wantInitErr bool
|
||||
claimToResponseMap map[string]string
|
||||
openIDConfig string
|
||||
reqAudiences authenticator.Audiences
|
||||
}
|
||||
|
||||
// Replace formats the contents of v into the provided template.
|
||||
|
@ -296,7 +299,12 @@ func (c *claimsTest) run(t *testing.T) {
|
|||
t.Fatalf("serialize token: %v", err)
|
||||
}
|
||||
|
||||
got, ok, err := a.AuthenticateToken(context.Background(), token)
|
||||
ctx := context.Background()
|
||||
if c.reqAudiences != nil {
|
||||
ctx = authenticator.WithAudiences(ctx, c.reqAudiences)
|
||||
}
|
||||
|
||||
got, ok, err := a.AuthenticateToken(ctx, token)
|
||||
|
||||
if err != nil {
|
||||
if !c.wantErr {
|
||||
|
@ -1388,6 +1396,148 @@ func TestToken(t *testing.T) {
|
|||
Name: "thomas.jefferson@gmail.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good token with api req audience",
|
||||
options: Options{
|
||||
IssuerURL: "https://auth.example.com",
|
||||
ClientID: "my-client",
|
||||
APIAudiences: authenticator.Audiences{"api"},
|
||||
UsernameClaim: "username",
|
||||
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()),
|
||||
reqAudiences: authenticator.Audiences{"api"},
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good token with multiple api req audience",
|
||||
options: Options{
|
||||
IssuerURL: "https://auth.example.com",
|
||||
ClientID: "my-client",
|
||||
APIAudiences: authenticator.Audiences{"api", "other"},
|
||||
UsernameClaim: "username",
|
||||
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()),
|
||||
reqAudiences: authenticator.Audiences{"api"},
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good token with client_id req audience",
|
||||
options: Options{
|
||||
IssuerURL: "https://auth.example.com",
|
||||
ClientID: "my-client",
|
||||
APIAudiences: authenticator.Audiences{"api"},
|
||||
UsernameClaim: "username",
|
||||
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()),
|
||||
reqAudiences: authenticator.Audiences{"my-client"},
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good token with client_id and api req audience",
|
||||
options: Options{
|
||||
IssuerURL: "https://auth.example.com",
|
||||
ClientID: "my-client",
|
||||
APIAudiences: authenticator.Audiences{"api"},
|
||||
UsernameClaim: "username",
|
||||
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()),
|
||||
reqAudiences: authenticator.Audiences{"my-client", "api"},
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good token with client_id and api req audience",
|
||||
options: Options{
|
||||
IssuerURL: "https://auth.example.com",
|
||||
ClientID: "my-client",
|
||||
APIAudiences: authenticator.Audiences{"api"},
|
||||
UsernameClaim: "username",
|
||||
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()),
|
||||
reqAudiences: authenticator.Audiences{"my-client", "api"},
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good token with client_id and bad req audience",
|
||||
options: Options{
|
||||
IssuerURL: "https://auth.example.com",
|
||||
ClientID: "my-client",
|
||||
APIAudiences: authenticator.Audiences{"api"},
|
||||
UsernameClaim: "username",
|
||||
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()),
|
||||
reqAudiences: authenticator.Audiences{"other"},
|
||||
wantSkip: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
|
|
Loading…
Reference in New Issue