wiring existing oidc flags with internal API struct

Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>

Kubernetes-commit: 1bad3cbbf59a61805a48f609b8cc0a2a40c168ef
This commit is contained in:
Anish Ramasekar 2023-06-28 06:04:45 +00:00 committed by Kubernetes Publisher
parent 496ba1943b
commit fdfc990c33
4 changed files with 1279 additions and 276 deletions

View File

@ -0,0 +1,204 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"fmt"
"net/url"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/client-go/util/cert"
)
const (
atLeastOneRequiredErrFmt = "at least one %s is required"
)
var (
root = field.NewPath("jwt")
)
// ValidateAuthenticationConfiguration validates a given AuthenticationConfiguration.
func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration) field.ErrorList {
var allErrs field.ErrorList
// This stricter validation is solely based on what the current implementation supports.
// TODO(aramase): when StructuredAuthenticationConfiguration feature gate is added and wired up,
// relax this check to allow 0 authenticators. This will allow us to support the case where
// API server is initially configured with no authenticators and then authenticators are added
// later via dynamic config.
if len(c.JWT) == 0 {
allErrs = append(allErrs, field.Required(root, fmt.Sprintf(atLeastOneRequiredErrFmt, root)))
return allErrs
}
// This stricter validation is because the --oidc-* flag option is singular.
// TODO(aramase): when StructuredAuthenticationConfiguration feature gate is added and wired up,
// remove the 1 authenticator limit check and add set the limit to 64.
if len(c.JWT) > 1 {
allErrs = append(allErrs, field.TooMany(root, len(c.JWT), 1))
return allErrs
}
// TODO(aramase): right now we only support a single JWT authenticator as
// this is wired to the --oidc-* flags. When StructuredAuthenticationConfiguration
// feature gate is added and wired up, we will remove the 1 authenticator limit
// check and add validation for duplicate issuers.
for i, a := range c.JWT {
fldPath := root.Index(i)
allErrs = append(allErrs, validateJWTAuthenticator(a, fldPath)...)
}
return allErrs
}
// ValidateJWTAuthenticator validates a given JWTAuthenticator.
// This is exported for use in oidc package.
func ValidateJWTAuthenticator(authenticator api.JWTAuthenticator) field.ErrorList {
return validateJWTAuthenticator(authenticator, nil)
}
func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateIssuer(authenticator.Issuer, fldPath.Child("issuer"))...)
allErrs = append(allErrs, validateClaimValidationRules(authenticator.ClaimValidationRules, fldPath.Child("claimValidationRules"))...)
allErrs = append(allErrs, validateClaimMappings(authenticator.ClaimMappings, fldPath.Child("claimMappings"))...)
return allErrs
}
func validateIssuer(issuer api.Issuer, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateURL(issuer.URL, fldPath.Child("url"))...)
allErrs = append(allErrs, validateAudiences(issuer.Audiences, fldPath.Child("audiences"))...)
allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...)
return allErrs
}
func validateURL(issuerURL string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if len(issuerURL) == 0 {
allErrs = append(allErrs, field.Required(fldPath, "URL is required"))
return allErrs
}
u, err := url.Parse(issuerURL)
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, err.Error()))
return allErrs
}
if u.Scheme != "https" {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL scheme must be https"))
}
if u.User != nil {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a username or password"))
}
if len(u.RawQuery) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a query"))
}
if len(u.Fragment) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a fragment"))
}
return allErrs
}
func validateAudiences(audiences []string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if len(audiences) == 0 {
allErrs = append(allErrs, field.Required(fldPath, fmt.Sprintf(atLeastOneRequiredErrFmt, fldPath)))
return allErrs
}
// This stricter validation is because the --oidc-client-id flag option is singular.
// This will be removed when we support multiple audiences with the StructuredAuthenticationConfiguration feature gate.
if len(audiences) > 1 {
allErrs = append(allErrs, field.TooMany(fldPath, len(audiences), 1))
return allErrs
}
for i, audience := range audiences {
fldPath := fldPath.Index(i)
if len(audience) == 0 {
allErrs = append(allErrs, field.Required(fldPath, "audience can't be empty"))
}
}
return allErrs
}
func validateCertificateAuthority(certificateAuthority string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if len(certificateAuthority) == 0 {
return allErrs
}
_, err := cert.NewPoolFromBytes([]byte(certificateAuthority))
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, "<omitted>", err.Error()))
}
return allErrs
}
func validateClaimValidationRules(rules []api.ClaimValidationRule, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
seenClaims := sets.NewString()
for i, rule := range rules {
fldPath := fldPath.Index(i)
if len(rule.Claim) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("claim"), "claim name is required"))
continue
}
if seenClaims.Has(rule.Claim) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("claim"), rule.Claim))
continue
}
seenClaims.Insert(rule.Claim)
}
return allErrs
}
func validateClaimMappings(m api.ClaimMappings, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if len(m.Username.Claim) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("username", "claim"), "claim name is required"))
}
// TODO(aramase): when Expression is added to PrefixedClaimOrExpression, check prefix and expression are not both set.
if m.Username.Prefix == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("username", "prefix"), "prefix is required"))
}
if len(m.Groups.Claim) > 0 && m.Groups.Prefix == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("groups", "prefix"), "prefix is required when claim is set"))
}
if m.Groups.Prefix != nil && len(m.Groups.Claim) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("groups", "claim"), "non-empty claim name is required when prefix is set"))
}
return allErrs
}

View File

@ -0,0 +1,414 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/pem"
"testing"
"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/apiserver/pkg/apis/apiserver"
certutil "k8s.io/client-go/util/cert"
"k8s.io/utils/pointer"
)
func TestValidateAuthenticationConfiguration(t *testing.T) {
testCases := []struct {
name string
in *api.AuthenticationConfiguration
want string
}{
{
name: "jwt authenticator is empty",
in: &api.AuthenticationConfiguration{},
want: "jwt: Required value: at least one jwt is required",
},
{
name: ">1 jwt authenticator",
in: &api.AuthenticationConfiguration{
JWT: []api.JWTAuthenticator{
{Issuer: api.Issuer{URL: "https://issuer-url", Audiences: []string{"audience"}}},
{Issuer: api.Issuer{URL: "https://issuer-url", Audiences: []string{"audience"}}},
},
},
want: "jwt: Too many: 2: must have at most 1 items",
},
{
name: "failed issuer validation",
in: &api.AuthenticationConfiguration{
JWT: []api.JWTAuthenticator{
{
Issuer: api.Issuer{
URL: "invalid-url",
Audiences: []string{"audience"},
},
ClaimMappings: api.ClaimMappings{
Username: api.PrefixedClaimOrExpression{
Claim: "claim",
Prefix: pointer.String("prefix"),
},
},
},
},
},
want: `jwt[0].issuer.url: Invalid value: "invalid-url": URL scheme must be https`,
},
{
name: "failed claimValidationRule validation",
in: &api.AuthenticationConfiguration{
JWT: []api.JWTAuthenticator{
{
Issuer: api.Issuer{
URL: "https://issuer-url",
Audiences: []string{"audience"},
},
ClaimValidationRules: []api.ClaimValidationRule{
{
Claim: "foo",
RequiredValue: "bar",
},
{
Claim: "foo",
RequiredValue: "baz",
},
},
ClaimMappings: api.ClaimMappings{
Username: api.PrefixedClaimOrExpression{
Claim: "claim",
Prefix: pointer.String("prefix"),
},
},
},
},
},
want: `jwt[0].claimValidationRules[1].claim: Duplicate value: "foo"`,
},
{
name: "failed claimMapping validation",
in: &api.AuthenticationConfiguration{
JWT: []api.JWTAuthenticator{
{
Issuer: api.Issuer{
URL: "https://issuer-url",
Audiences: []string{"audience"},
},
ClaimValidationRules: []api.ClaimValidationRule{
{
Claim: "foo",
RequiredValue: "bar",
},
},
ClaimMappings: api.ClaimMappings{
Username: api.PrefixedClaimOrExpression{
Prefix: pointer.String("prefix"),
},
},
},
},
},
want: "jwt[0].claimMappings.username.claim: Required value: claim name is required",
},
{
name: "valid authentication configuration",
in: &api.AuthenticationConfiguration{
JWT: []api.JWTAuthenticator{
{
Issuer: api.Issuer{
URL: "https://issuer-url",
Audiences: []string{"audience"},
},
ClaimValidationRules: []api.ClaimValidationRule{
{
Claim: "foo",
RequiredValue: "bar",
},
},
ClaimMappings: api.ClaimMappings{
Username: api.PrefixedClaimOrExpression{
Claim: "sub",
Prefix: pointer.String("prefix"),
},
},
},
},
},
want: "",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got := ValidateAuthenticationConfiguration(tt.in).ToAggregate()
if d := cmp.Diff(tt.want, errString(got)); d != "" {
t.Fatalf("AuthenticationConfiguration validation mismatch (-want +got):\n%s", d)
}
})
}
}
func TestValidateURL(t *testing.T) {
fldPath := field.NewPath("issuer", "url")
testCases := []struct {
name string
in string
want string
}{
{
name: "url is empty",
in: "",
want: "issuer.url: Required value: URL is required",
},
{
name: "url parse error",
in: "https://issuer-url:invalid-port",
want: `issuer.url: Invalid value: "https://issuer-url:invalid-port": parse "https://issuer-url:invalid-port": invalid port ":invalid-port" after host`,
},
{
name: "url is not https",
in: "http://issuer-url",
want: `issuer.url: Invalid value: "http://issuer-url": URL scheme must be https`,
},
{
name: "url user info is not allowed",
in: "https://user:pass@issuer-url",
want: `issuer.url: Invalid value: "https://user:pass@issuer-url": URL must not contain a username or password`,
},
{
name: "url raw query is not allowed",
in: "https://issuer-url?query",
want: `issuer.url: Invalid value: "https://issuer-url?query": URL must not contain a query`,
},
{
name: "url fragment is not allowed",
in: "https://issuer-url#fragment",
want: `issuer.url: Invalid value: "https://issuer-url#fragment": URL must not contain a fragment`,
},
{
name: "valid url",
in: "https://issuer-url",
want: "",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got := validateURL(tt.in, fldPath).ToAggregate()
if d := cmp.Diff(tt.want, errString(got)); d != "" {
t.Fatalf("URL validation mismatch (-want +got):\n%s", d)
}
})
}
}
func TestValidateAudiences(t *testing.T) {
fldPath := field.NewPath("issuer", "audiences")
testCases := []struct {
name string
in []string
want string
}{
{
name: "audiences is empty",
in: []string{},
want: "issuer.audiences: Required value: at least one issuer.audiences is required",
},
{
name: "at most one audiences is allowed",
in: []string{"audience1", "audience2"},
want: "issuer.audiences: Too many: 2: must have at most 1 items",
},
{
name: "audience is empty",
in: []string{""},
want: "issuer.audiences[0]: Required value: audience can't be empty",
},
{
name: "valid audience",
in: []string{"audience"},
want: "",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got := validateAudiences(tt.in, fldPath).ToAggregate()
if d := cmp.Diff(tt.want, errString(got)); d != "" {
t.Fatalf("Audiences validation mismatch (-want +got):\n%s", d)
}
})
}
}
func TestValidateCertificateAuthority(t *testing.T) {
fldPath := field.NewPath("issuer", "certificateAuthority")
testCases := []struct {
name string
in func() string
want string
}{
{
name: "invalid certificate authority",
in: func() string { return "invalid" },
want: `issuer.certificateAuthority: Invalid value: "<omitted>": data does not contain any valid RSA or ECDSA certificates`,
},
{
name: "certificate authority is empty",
in: func() string { return "" },
want: "",
},
{
name: "valid certificate authority",
in: func() string {
caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey)
if err != nil {
t.Fatal(err)
}
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}))
},
want: "",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got := validateCertificateAuthority(tt.in(), fldPath).ToAggregate()
if d := cmp.Diff(tt.want, errString(got)); d != "" {
t.Fatalf("CertificateAuthority validation mismatch (-want +got):\n%s", d)
}
})
}
}
func TestClaimValidationRules(t *testing.T) {
fldPath := field.NewPath("issuer", "claimValidationRules")
testCases := []struct {
name string
in []api.ClaimValidationRule
want string
}{
{
name: "claim validation rule claim is empty",
in: []api.ClaimValidationRule{{Claim: ""}},
want: "issuer.claimValidationRules[0].claim: Required value: claim name is required",
},
{
name: "duplicate claim",
in: []api.ClaimValidationRule{{
Claim: "claim", RequiredValue: "value1"},
{Claim: "claim", RequiredValue: "value2"},
},
want: `issuer.claimValidationRules[1].claim: Duplicate value: "claim"`,
},
{
name: "valid claim validation rule",
in: []api.ClaimValidationRule{{Claim: "claim", RequiredValue: "value"}},
want: "",
},
{
name: "valid claim validation rule with multiple rules",
in: []api.ClaimValidationRule{
{Claim: "claim1", RequiredValue: "value1"},
{Claim: "claim2", RequiredValue: "value2"},
},
want: "",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got := validateClaimValidationRules(tt.in, fldPath).ToAggregate()
if d := cmp.Diff(tt.want, errString(got)); d != "" {
t.Fatalf("ClaimValidationRules validation mismatch (-want +got):\n%s", d)
}
})
}
}
func TestValidateClaimMappings(t *testing.T) {
fldPath := field.NewPath("issuer", "claimMappings")
testCases := []struct {
name string
in api.ClaimMappings
want string
}{
{
name: "username claim is empty",
in: api.ClaimMappings{Username: api.PrefixedClaimOrExpression{Claim: "", Prefix: pointer.String("prefix")}},
want: "issuer.claimMappings.username.claim: Required value: claim name is required",
},
{
name: "username prefix is empty",
in: api.ClaimMappings{Username: api.PrefixedClaimOrExpression{Claim: "claim"}},
want: "issuer.claimMappings.username.prefix: Required value: prefix is required",
},
{
name: "groups prefix is empty",
in: api.ClaimMappings{
Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
Groups: api.PrefixedClaimOrExpression{Claim: "claim"},
},
want: "issuer.claimMappings.groups.prefix: Required value: prefix is required when claim is set",
},
{
name: "groups prefix set but claim is empty",
in: api.ClaimMappings{
Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
Groups: api.PrefixedClaimOrExpression{Prefix: pointer.String("prefix")},
},
want: "issuer.claimMappings.groups.claim: Required value: non-empty claim name is required when prefix is set",
},
{
name: "valid claim mappings",
in: api.ClaimMappings{
Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
Groups: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
},
want: "",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got := validateClaimMappings(tt.in, fldPath).ToAggregate()
if d := cmp.Diff(tt.want, errString(got)); d != "" {
t.Fatalf("ClaimMappings validation mismatch (-want +got):\n%s", d)
}
})
}
}
func errString(errs errors.Aggregate) string {
if errs != nil {
return errs.Error()
}
return ""
}

View File

@ -32,11 +32,9 @@ import (
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
"sync/atomic"
@ -46,6 +44,8 @@ import (
"k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/apis/apiserver"
apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
certutil "k8s.io/client-go/util/cert"
@ -59,50 +59,17 @@ var (
)
type Options struct {
// IssuerURL is the URL the provider signs ID Tokens as. This will be the "iss"
// field of all tokens produced by the provider and is used for configuration
// discovery.
//
// The URL is usually the provider's URL without a path, for example
// "https://accounts.google.com" or "https://login.salesforce.com".
//
// The provider must implement configuration discovery.
// See: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
IssuerURL string
// JWTAuthenticator is the authenticator that will be used to verify the JWT.
JWTAuthenticator apiserver.JWTAuthenticator
// Optional KeySet to allow for synchronous initialization instead of fetching from the remote issuer.
KeySet oidc.KeySet
// ClientID the JWT must be issued for, the "sub" field. This plugin only trusts a single
// client to ensure the plugin can be used with public providers.
//
// The plugin supports the "authorized party" OpenID Connect claim, which allows
// specialized providers to issue tokens to a client for a different client.
// See: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
ClientID string
// PEM encoded root certificate contents of the provider. Mutually exclusive with Client.
CAContentProvider CAContentProvider
// Optional http.Client used to make all requests to the remote issuer. Mutually exclusive with CAContentProvider.
Client *http.Client
// UsernameClaim is the JWT field to use as the user's username.
UsernameClaim string
// UsernamePrefix, if specified, causes claims mapping to username to be prefix with
// the provided value. A value "oidc:" would result in usernames like "oidc:john".
UsernamePrefix string
// GroupsClaim, if specified, causes the OIDCAuthenticator to try to populate the user's
// groups with an ID Token field. If the GroupsClaim field is present in an ID Token the value
// must be a string or list of strings.
GroupsClaim string
// GroupsPrefix, if specified, causes claims mapping to group names to be prefixed with the
// value. A value "oidc:" would result in groups like "oidc:engineering" and "oidc:marketing".
GroupsPrefix string
// SupportedSigningAlgs sets the accepted set of JOSE signing algorithms that
// can be used by the provider to sign tokens.
//
@ -114,10 +81,6 @@ type Options struct {
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
SupportedSigningAlgs []string
// RequiredClaims, if specified, causes the OIDCAuthenticator to verify that all the
// required claims key value pairs are present in the ID Token.
RequiredClaims map[string]string
// now is used for testing. It defaults to time.Now.
now func() time.Time
}
@ -192,13 +155,7 @@ func (a *asyncIDTokenVerifier) verifier() *oidc.IDTokenVerifier {
}
type Authenticator struct {
issuerURL string
usernameClaim string
usernamePrefix string
groupsClaim string
groupsPrefix string
requiredClaims map[string]string
jwtAuthenticator apiserver.JWTAuthenticator
// Contains an *oidc.IDTokenVerifier. Do not access directly use the
// idTokenVerifier method.
@ -240,19 +197,10 @@ var allowedSigningAlgs = map[string]bool{
}
func New(opts Options) (*Authenticator, error) {
url, err := url.Parse(opts.IssuerURL)
if err != nil {
if err := apiservervalidation.ValidateJWTAuthenticator(opts.JWTAuthenticator).ToAggregate(); err != nil {
return nil, err
}
if url.Scheme != "https" {
return nil, fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", opts.IssuerURL, url.Scheme)
}
if opts.UsernameClaim == "" {
return nil, errors.New("no username claim provided")
}
supportedSigningAlgs := opts.SupportedSigningAlgs
if len(supportedSigningAlgs) == 0 {
// RS256 is the default recommended by OpenID Connect and an 'alg' value
@ -273,6 +221,7 @@ func New(opts Options) (*Authenticator, error) {
if client == nil {
var roots *x509.CertPool
var err error
if opts.CAContentProvider != nil {
// TODO(enj): make this reload CA data dynamically
roots, err = certutil.NewPoolFromBytes(opts.CAContentProvider.CurrentCABundleContent())
@ -302,35 +251,30 @@ func New(opts Options) (*Authenticator, error) {
}
verifierConfig := &oidc.Config{
ClientID: opts.ClientID,
ClientID: opts.JWTAuthenticator.Issuer.Audiences[0],
SupportedSigningAlgs: supportedSigningAlgs,
Now: now,
}
var resolver *claimResolver
if opts.GroupsClaim != "" {
resolver = newClaimResolver(opts.GroupsClaim, client, verifierConfig)
if opts.JWTAuthenticator.ClaimMappings.Groups.Claim != "" {
resolver = newClaimResolver(opts.JWTAuthenticator.ClaimMappings.Groups.Claim, client, verifierConfig)
}
authenticator := &Authenticator{
issuerURL: opts.IssuerURL,
usernameClaim: opts.UsernameClaim,
usernamePrefix: opts.UsernamePrefix,
groupsClaim: opts.GroupsClaim,
groupsPrefix: opts.GroupsPrefix,
requiredClaims: opts.RequiredClaims,
cancel: cancel,
resolver: resolver,
jwtAuthenticator: opts.JWTAuthenticator,
cancel: cancel,
resolver: resolver,
}
if opts.KeySet != nil {
// We already have a key set, synchronously initialize the verifier.
authenticator.setVerifier(oidc.NewVerifier(opts.IssuerURL, opts.KeySet, verifierConfig))
authenticator.setVerifier(oidc.NewVerifier(opts.JWTAuthenticator.Issuer.URL, opts.KeySet, verifierConfig))
} else {
// Asynchronously attempt to initialize the authenticator. This enables
// self-hosted providers, providers that run on top of Kubernetes itself.
go wait.PollImmediateUntil(10*time.Second, func() (done bool, err error) {
provider, err := oidc.NewProvider(ctx, opts.IssuerURL)
provider, err := oidc.NewProvider(ctx, opts.JWTAuthenticator.Issuer.URL)
if err != nil {
klog.Errorf("oidc authenticator: initializing plugin: %v", err)
return false, nil
@ -552,7 +496,7 @@ func (r *claimResolver) resolve(ctx context.Context, endpoint endpoint, allClaim
}
func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
if !hasCorrectIssuer(a.issuerURL, token) {
if !hasCorrectIssuer(a.jwtAuthenticator.Issuer.URL, token) {
return nil, false, nil
}
@ -577,11 +521,11 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a
}
var username string
if err := c.unmarshalClaim(a.usernameClaim, &username); err != nil {
return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", a.usernameClaim, err)
if err := c.unmarshalClaim(a.jwtAuthenticator.ClaimMappings.Username.Claim, &username); err != nil {
return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", a.jwtAuthenticator.ClaimMappings.Username.Claim, err)
}
if a.usernameClaim == "email" {
if a.jwtAuthenticator.ClaimMappings.Username.Claim == "email" {
// If the email_verified claim is present, ensure the email is valid.
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
if hasEmailVerified := c.hasClaim("email_verified"); hasEmailVerified {
@ -597,33 +541,36 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a
}
}
if a.usernamePrefix != "" {
username = a.usernamePrefix + username
if a.jwtAuthenticator.ClaimMappings.Username.Prefix != nil && *a.jwtAuthenticator.ClaimMappings.Username.Prefix != "" {
username = *a.jwtAuthenticator.ClaimMappings.Username.Prefix + username
}
info := &user.DefaultInfo{Name: username}
if a.groupsClaim != "" {
if _, ok := c[a.groupsClaim]; ok {
if a.jwtAuthenticator.ClaimMappings.Groups.Claim != "" {
if _, ok := c[a.jwtAuthenticator.ClaimMappings.Groups.Claim]; ok {
// Some admins want to use string claims like "role" as the group value.
// Allow the group claim to be a single string instead of an array.
//
// See: https://github.com/kubernetes/kubernetes/issues/33290
var groups stringOrArray
if err := c.unmarshalClaim(a.groupsClaim, &groups); err != nil {
return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", a.groupsClaim, err)
if err := c.unmarshalClaim(a.jwtAuthenticator.ClaimMappings.Groups.Claim, &groups); err != nil {
return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", a.jwtAuthenticator.ClaimMappings.Groups.Claim, err)
}
info.Groups = []string(groups)
}
}
if a.groupsPrefix != "" {
if a.jwtAuthenticator.ClaimMappings.Groups.Prefix != nil && *a.jwtAuthenticator.ClaimMappings.Groups.Prefix != "" {
for i, group := range info.Groups {
info.Groups[i] = a.groupsPrefix + group
info.Groups[i] = *a.jwtAuthenticator.ClaimMappings.Groups.Prefix + group
}
}
// check to ensure all required claims are present in the ID token and have matching values.
for claim, value := range a.requiredClaims {
for _, claimValidationRule := range a.jwtAuthenticator.ClaimValidationRules {
claim := claimValidationRule.Claim
value := claimValidationRule.RequiredValue
if !c.hasClaim(claim) {
return nil, false, fmt.Errorf("oidc: required claim %s not present in ID token", claim)
}

File diff suppressed because it is too large Load Diff