mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
oidc authentication: switch to v2 of coreos/go-oidc
This commit is contained in:
parent
9586cd06c2
commit
48c6d1abf5
@ -210,6 +210,7 @@ func TestAddFlags(t *testing.T) {
|
|||||||
BootstrapToken: &kubeoptions.BootstrapTokenAuthenticationOptions{},
|
BootstrapToken: &kubeoptions.BootstrapTokenAuthenticationOptions{},
|
||||||
OIDC: &kubeoptions.OIDCAuthenticationOptions{
|
OIDC: &kubeoptions.OIDCAuthenticationOptions{
|
||||||
UsernameClaim: "sub",
|
UsernameClaim: "sub",
|
||||||
|
SigningAlgs: []string{"RS256"},
|
||||||
},
|
},
|
||||||
PasswordFile: &kubeoptions.PasswordFileAuthenticationOptions{},
|
PasswordFile: &kubeoptions.PasswordFileAuthenticationOptions{},
|
||||||
RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{},
|
RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{},
|
||||||
|
@ -58,6 +58,7 @@ type AuthenticatorConfig struct {
|
|||||||
OIDCUsernamePrefix string
|
OIDCUsernamePrefix string
|
||||||
OIDCGroupsClaim string
|
OIDCGroupsClaim string
|
||||||
OIDCGroupsPrefix string
|
OIDCGroupsPrefix string
|
||||||
|
OIDCSigningAlgs []string
|
||||||
ServiceAccountKeyFiles []string
|
ServiceAccountKeyFiles []string
|
||||||
ServiceAccountLookup bool
|
ServiceAccountLookup bool
|
||||||
WebhookTokenAuthnConfigFile string
|
WebhookTokenAuthnConfigFile string
|
||||||
@ -143,7 +144,7 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
|
|||||||
// simply returns an error, the OpenID Connect plugin may query the provider to
|
// simply returns an error, the OpenID Connect plugin may query the provider to
|
||||||
// update the keys, causing performance hits.
|
// update the keys, causing performance hits.
|
||||||
if len(config.OIDCIssuerURL) > 0 && len(config.OIDCClientID) > 0 {
|
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)
|
oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(config.OIDCIssuerURL, config.OIDCClientID, config.OIDCCAFile, config.OIDCUsernameClaim, config.OIDCUsernamePrefix, config.OIDCGroupsClaim, config.OIDCGroupsPrefix, config.OIDCSigningAlgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -235,7 +236,7 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Token, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newAuthenticatorFromOIDCIssuerURL returns an authenticator.Token or an error.
|
// newAuthenticatorFromOIDCIssuerURL returns an authenticator.Token or an error.
|
||||||
func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClaim, usernamePrefix, groupsClaim, groupsPrefix string) (authenticator.Token, error) {
|
func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClaim, usernamePrefix, groupsClaim, groupsPrefix string, signingAlgs []string) (authenticator.Token, error) {
|
||||||
const noUsernamePrefix = "-"
|
const noUsernamePrefix = "-"
|
||||||
|
|
||||||
if usernamePrefix == "" && usernameClaim != "email" {
|
if usernamePrefix == "" && usernameClaim != "email" {
|
||||||
@ -251,14 +252,15 @@ func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClai
|
|||||||
usernamePrefix = ""
|
usernamePrefix = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenAuthenticator, err := oidc.New(oidc.OIDCOptions{
|
tokenAuthenticator, err := oidc.New(oidc.Options{
|
||||||
IssuerURL: issuerURL,
|
IssuerURL: issuerURL,
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
CAFile: caFile,
|
CAFile: caFile,
|
||||||
UsernameClaim: usernameClaim,
|
UsernameClaim: usernameClaim,
|
||||||
UsernamePrefix: usernamePrefix,
|
UsernamePrefix: usernamePrefix,
|
||||||
GroupsClaim: groupsClaim,
|
GroupsClaim: groupsClaim,
|
||||||
GroupsPrefix: groupsPrefix,
|
GroupsPrefix: groupsPrefix,
|
||||||
|
SupportedSigningAlgs: signingAlgs,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -61,6 +61,7 @@ type OIDCAuthenticationOptions struct {
|
|||||||
UsernamePrefix string
|
UsernamePrefix string
|
||||||
GroupsClaim string
|
GroupsClaim string
|
||||||
GroupsPrefix string
|
GroupsPrefix string
|
||||||
|
SigningAlgs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PasswordFileAuthenticationOptions struct {
|
type PasswordFileAuthenticationOptions struct {
|
||||||
@ -208,6 +209,10 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
|
|||||||
"If provided, all groups will be prefixed with this value to prevent conflicts with "+
|
"If provided, all groups will be prefixed with this value to prevent conflicts with "+
|
||||||
"other authentication strategies.")
|
"other authentication strategies.")
|
||||||
|
|
||||||
|
fs.StringSliceVar(&s.OIDC.SigningAlgs, "oidc-signing-algs", []string{"RS256"}, ""+
|
||||||
|
"Comma-separated list of allowed JOSE asymmetric signing algorithms. JWTs with a "+
|
||||||
|
"'alg' header value not in this list will be rejected. "+
|
||||||
|
"Values are defined by RFC 7518 https://tools.ietf.org/html/rfc7518#section-3.1.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.PasswordFile != nil {
|
if s.PasswordFile != nil {
|
||||||
@ -272,6 +277,7 @@ func (s *BuiltInAuthenticationOptions) ToAuthenticationConfig() authenticator.Au
|
|||||||
ret.OIDCIssuerURL = s.OIDC.IssuerURL
|
ret.OIDCIssuerURL = s.OIDC.IssuerURL
|
||||||
ret.OIDCUsernameClaim = s.OIDC.UsernameClaim
|
ret.OIDCUsernameClaim = s.OIDC.UsernameClaim
|
||||||
ret.OIDCUsernamePrefix = s.OIDC.UsernamePrefix
|
ret.OIDCUsernamePrefix = s.OIDC.UsernamePrefix
|
||||||
|
ret.OIDCSigningAlgs = s.OIDC.SigningAlgs
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.PasswordFile != nil {
|
if s.PasswordFile != nil {
|
||||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
/*
|
/*
|
||||||
oidc implements the authenticator.Token interface using the OpenID Connect protocol.
|
oidc implements the authenticator.Token interface using the OpenID Connect protocol.
|
||||||
|
|
||||||
config := oidc.OIDCOptions{
|
config := oidc.Options{
|
||||||
IssuerURL: "https://accounts.google.com",
|
IssuerURL: "https://accounts.google.com",
|
||||||
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
|
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
|
||||||
UsernameClaim: "email",
|
UsernameClaim: "email",
|
||||||
@ -27,25 +27,28 @@ oidc implements the authenticator.Token interface using the OpenID Connect proto
|
|||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/jose"
|
oidc "github.com/coreos/go-oidc"
|
||||||
"github.com/coreos/go-oidc/oidc"
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
"k8s.io/apimachinery/pkg/util/net"
|
"k8s.io/apimachinery/pkg/util/net"
|
||||||
"k8s.io/apimachinery/pkg/util/runtime"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
certutil "k8s.io/client-go/util/cert"
|
certutil "k8s.io/client-go/util/cert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OIDCOptions struct {
|
type Options struct {
|
||||||
// IssuerURL is the URL the provider signs ID Tokens as. This will be the "iss"
|
// 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
|
// field of all tokens produced by the provider and is used for configuration
|
||||||
// discovery.
|
// discovery.
|
||||||
@ -83,30 +86,85 @@ type OIDCOptions struct {
|
|||||||
// GroupsPrefix, if specified, causes claims mapping to group names to be prefixed with the
|
// 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".
|
// value. A value "oidc:" would result in groups like "oidc:engineering" and "oidc:marketing".
|
||||||
GroupsPrefix string
|
GroupsPrefix string
|
||||||
|
|
||||||
|
// SupportedSigningAlgs sets the accepted set of JOSE signing algorithms that
|
||||||
|
// can be used by the provider to sign tokens.
|
||||||
|
//
|
||||||
|
// https://tools.ietf.org/html/rfc7518#section-3.1
|
||||||
|
//
|
||||||
|
// This value defaults to RS256, the value recommended by the OpenID Connect
|
||||||
|
// spec:
|
||||||
|
//
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||||
|
SupportedSigningAlgs []string
|
||||||
|
|
||||||
|
// now is used for testing. It defaults to time.Now.
|
||||||
|
now func() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type OIDCAuthenticator struct {
|
type Authenticator struct {
|
||||||
issuerURL string
|
issuerURL string
|
||||||
|
|
||||||
trustedClientID string
|
|
||||||
|
|
||||||
usernameClaim string
|
usernameClaim string
|
||||||
usernamePrefix string
|
usernamePrefix string
|
||||||
groupsClaim string
|
groupsClaim string
|
||||||
groupsPrefix string
|
groupsPrefix string
|
||||||
|
|
||||||
httpClient *http.Client
|
// Contains an *oidc.IDTokenVerifier. Do not access directly use the
|
||||||
|
// idTokenVerifier method.
|
||||||
|
verifier atomic.Value
|
||||||
|
|
||||||
// Contains an *oidc.Client. Do not access directly. Use client() method.
|
cancel context.CancelFunc
|
||||||
oidcClient atomic.Value
|
|
||||||
|
|
||||||
// Guards the close method and is used to lock during initialization and closing.
|
|
||||||
mu sync.Mutex
|
|
||||||
close func() // May be nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a token authenticator which validates OpenID Connect ID Tokens.
|
func (a *Authenticator) setVerifier(v *oidc.IDTokenVerifier) {
|
||||||
func New(opts OIDCOptions) (*OIDCAuthenticator, error) {
|
a.verifier.Store(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) idTokenVerifier() (*oidc.IDTokenVerifier, bool) {
|
||||||
|
if v := a.verifier.Load(); v != nil {
|
||||||
|
return v.(*oidc.IDTokenVerifier), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) Close() {
|
||||||
|
a.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts Options) (*Authenticator, error) {
|
||||||
|
return newAuthenticator(opts, func(ctx context.Context, a *Authenticator, config *oidc.Config) {
|
||||||
|
// Asynchronously attempt to initialize the authenticator. This enables
|
||||||
|
// self-hosted providers, providers that run on top of Kubernetes itself.
|
||||||
|
go wait.PollUntil(time.Second*10, func() (done bool, err error) {
|
||||||
|
provider, err := oidc.NewProvider(ctx, a.issuerURL)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("oidc authenticator: initializing plugin: %v", err)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := provider.Verifier(config)
|
||||||
|
a.setVerifier(verifier)
|
||||||
|
return true, nil
|
||||||
|
}, ctx.Done())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// whitelist of signing algorithms to ensure users don't mistakenly pass something
|
||||||
|
// goofy.
|
||||||
|
var allowedSigningAlgs = map[string]bool{
|
||||||
|
oidc.RS256: true,
|
||||||
|
oidc.RS384: true,
|
||||||
|
oidc.RS512: true,
|
||||||
|
oidc.ES256: true,
|
||||||
|
oidc.ES384: true,
|
||||||
|
oidc.ES512: true,
|
||||||
|
oidc.PS256: true,
|
||||||
|
oidc.PS384: true,
|
||||||
|
oidc.PS512: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthenticator(opts Options, initVerifier func(ctx context.Context, a *Authenticator, config *oidc.Config)) (*Authenticator, error) {
|
||||||
url, err := url.Parse(opts.IssuerURL)
|
url, err := url.Parse(opts.IssuerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -120,6 +178,18 @@ func New(opts OIDCOptions) (*OIDCAuthenticator, error) {
|
|||||||
return nil, errors.New("no username claim provided")
|
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
|
||||||
|
// providers are required to implement.
|
||||||
|
supportedSigningAlgs = []string{oidc.RS256}
|
||||||
|
}
|
||||||
|
for _, alg := range supportedSigningAlgs {
|
||||||
|
if !allowedSigningAlgs[alg] {
|
||||||
|
return nil, fmt.Errorf("oidc: unsupported signing alg: %q", alg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var roots *x509.CertPool
|
var roots *x509.CertPool
|
||||||
if opts.CAFile != "" {
|
if opts.CAFile != "" {
|
||||||
roots, err = certutil.NewPool(opts.CAFile)
|
roots, err = certutil.NewPool(opts.CAFile)
|
||||||
@ -137,137 +207,91 @@ func New(opts OIDCOptions) (*OIDCAuthenticator, error) {
|
|||||||
TLSClientConfig: &tls.Config{RootCAs: roots},
|
TLSClientConfig: &tls.Config{RootCAs: roots},
|
||||||
})
|
})
|
||||||
|
|
||||||
authenticator := &OIDCAuthenticator{
|
client := &http.Client{Transport: tr, Timeout: 30 * time.Second}
|
||||||
issuerURL: opts.IssuerURL,
|
|
||||||
trustedClientID: opts.ClientID,
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
usernameClaim: opts.UsernameClaim,
|
ctx = oidc.ClientContext(ctx, client)
|
||||||
usernamePrefix: opts.UsernamePrefix,
|
|
||||||
groupsClaim: opts.GroupsClaim,
|
authenticator := &Authenticator{
|
||||||
groupsPrefix: opts.GroupsPrefix,
|
issuerURL: opts.IssuerURL,
|
||||||
httpClient: &http.Client{Transport: tr},
|
usernameClaim: opts.UsernameClaim,
|
||||||
|
usernamePrefix: opts.UsernamePrefix,
|
||||||
|
groupsClaim: opts.GroupsClaim,
|
||||||
|
groupsPrefix: opts.GroupsPrefix,
|
||||||
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to initialize the authenticator asynchronously.
|
now := opts.now
|
||||||
//
|
if now == nil {
|
||||||
// Ignore errors instead of returning it since the OpenID Connect provider might not be
|
now = time.Now
|
||||||
// available yet, for instance if it's running on the cluster and needs the API server
|
}
|
||||||
// to come up first. Errors will be logged within the client() method.
|
|
||||||
go func() {
|
|
||||||
defer runtime.HandleCrash()
|
|
||||||
authenticator.client()
|
|
||||||
}()
|
|
||||||
|
|
||||||
|
verifierConfig := &oidc.Config{
|
||||||
|
ClientID: opts.ClientID,
|
||||||
|
SupportedSigningAlgs: supportedSigningAlgs,
|
||||||
|
Now: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
initVerifier(ctx, authenticator, verifierConfig)
|
||||||
return authenticator, nil
|
return authenticator, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close stops all goroutines used by the authenticator.
|
func hasCorrectIssuer(iss, tokenData string) bool {
|
||||||
func (a *OIDCAuthenticator) Close() {
|
parts := strings.Split(tokenData, ".")
|
||||||
a.mu.Lock()
|
if len(parts) != 3 {
|
||||||
defer a.mu.Unlock()
|
return false
|
||||||
|
|
||||||
if a.close != nil {
|
|
||||||
a.close()
|
|
||||||
}
|
}
|
||||||
return
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
claims := struct {
|
||||||
|
// WARNING: this JWT is not verified. Do not trust these claims.
|
||||||
|
Issuer string `json:"iss"`
|
||||||
|
}{}
|
||||||
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if claims.Issuer != iss {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *OIDCAuthenticator) client() (*oidc.Client, error) {
|
func (a *Authenticator) AuthenticateToken(token string) (user.Info, bool, error) {
|
||||||
// Fast check to see if client has already been initialized.
|
if !hasCorrectIssuer(a.issuerURL, token) {
|
||||||
if client := a.oidcClient.Load(); client != nil {
|
return nil, false, nil
|
||||||
return client.(*oidc.Client), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire lock, then recheck initialization.
|
ctx := context.Background()
|
||||||
a.mu.Lock()
|
verifier, ok := a.idTokenVerifier()
|
||||||
defer a.mu.Unlock()
|
|
||||||
if client := a.oidcClient.Load(); client != nil {
|
|
||||||
return client.(*oidc.Client), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to initialize client.
|
|
||||||
providerConfig, err := oidc.FetchProviderConfig(a.httpClient, a.issuerURL)
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("oidc authenticator: failed to fetch provider discovery data: %v", err)
|
|
||||||
return nil, fmt.Errorf("fetch provider config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientConfig := oidc.ClientConfig{
|
|
||||||
HTTPClient: a.httpClient,
|
|
||||||
Credentials: oidc.ClientCredentials{ID: a.trustedClientID},
|
|
||||||
ProviderConfig: providerConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := oidc.NewClient(clientConfig)
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf("oidc authenticator: failed to create client: %v", err)
|
|
||||||
return nil, fmt.Errorf("create client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncProviderConfig will start a goroutine to periodically synchronize the provider config.
|
|
||||||
// The synchronization interval is set by the expiration length of the config, and has a minimum
|
|
||||||
// and maximum threshold.
|
|
||||||
stop := client.SyncProviderConfig(a.issuerURL)
|
|
||||||
a.oidcClient.Store(client)
|
|
||||||
a.close = func() {
|
|
||||||
// This assumes the stop is an unbuffered channel.
|
|
||||||
// So instead of closing the channel, we send am empty struct here.
|
|
||||||
// This guarantees that when this function returns, there is no flying requests,
|
|
||||||
// because a send to an unbuffered channel happens after the receive from the channel.
|
|
||||||
stop <- struct{}{}
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticateToken decodes and verifies an ID Token using the OIDC client, if the verification succeeds,
|
|
||||||
// then it will extract the user info from the JWT claims.
|
|
||||||
func (a *OIDCAuthenticator) AuthenticateToken(value string) (user.Info, bool, error) {
|
|
||||||
jwt, err := jose.ParseJWT(value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := a.client()
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
if err := client.VerifyJWT(jwt); err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
claims, err := jwt.Claims()
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
return a.parseTokenClaims(claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseTokenClaims maps a set of claims to a user. It performs basic validation such as
|
|
||||||
// ensuring the email is verified.
|
|
||||||
func (a *OIDCAuthenticator) parseTokenClaims(claims jose.Claims) (user.Info, bool, error) {
|
|
||||||
username, ok, err := claims.StringClaim(a.usernameClaim)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false, fmt.Errorf("cannot find %q in JWT claims", a.usernameClaim)
|
return nil, false, fmt.Errorf("oidc: authenticator not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := verifier.Verify(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("oidc: verify token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var c claims
|
||||||
|
if err := idToken.Claims(&c); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("oidc: parse claims: %v", err)
|
||||||
|
}
|
||||||
|
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 a.usernameClaim == "email" {
|
if a.usernameClaim == "email" {
|
||||||
verified, ok := claims["email_verified"]
|
// Check the email_verified claim to ensure the email is valid.
|
||||||
if !ok {
|
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||||
return nil, false, errors.New("'email_verified' claim not present")
|
var emailVerified bool
|
||||||
|
if err := c.unmarshalClaim("email_verified", &emailVerified); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("oidc: parse 'email_verified' claim: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
emailVerified, ok := verified.(bool)
|
|
||||||
if !ok {
|
|
||||||
// OpenID Connect spec defines 'email_verified' as a boolean. For now, be a pain and error if
|
|
||||||
// it's a different type. If there are enough misbehaving providers we can relax this latter.
|
|
||||||
//
|
|
||||||
// See: https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
|
||||||
return nil, false, fmt.Errorf("malformed claim 'email_verified', expected boolean got %T", verified)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !emailVerified {
|
if !emailVerified {
|
||||||
return nil, false, errors.New("email not verified")
|
return nil, false, fmt.Errorf("oidc: email not verified")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,21 +299,18 @@ func (a *OIDCAuthenticator) parseTokenClaims(claims jose.Claims) (user.Info, boo
|
|||||||
username = a.usernamePrefix + username
|
username = a.usernamePrefix + username
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(yifan): Add UID, also populate the issuer to upper layer.
|
|
||||||
info := &user.DefaultInfo{Name: username}
|
info := &user.DefaultInfo{Name: username}
|
||||||
|
|
||||||
if a.groupsClaim != "" {
|
if a.groupsClaim != "" {
|
||||||
groups, found, err := claims.StringsClaim(a.groupsClaim)
|
if _, ok := c[a.groupsClaim]; ok {
|
||||||
if err != nil {
|
// Some admins want to use string claims like "role" as the group value.
|
||||||
// Groups type is present but is not an array of strings, try to decode as a string.
|
// Allow the group claim to be a single string instead of an array.
|
||||||
group, _, err := claims.StringClaim(a.groupsClaim)
|
//
|
||||||
if err != nil {
|
// See: https://github.com/kubernetes/kubernetes/issues/33290
|
||||||
// Custom claim is present, but isn't an array of strings or a string.
|
var groups stringOrArray
|
||||||
return nil, false, fmt.Errorf("custom group claim contains invalid type: %T", claims[a.groupsClaim])
|
if err := c.unmarshalClaim(a.groupsClaim, &groups); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", a.groupsClaim, err)
|
||||||
}
|
}
|
||||||
info.Groups = []string{group}
|
info.Groups = []string(groups)
|
||||||
} else if found {
|
|
||||||
info.Groups = groups
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,6 +319,31 @@ func (a *OIDCAuthenticator) parseTokenClaims(claims jose.Claims) (user.Info, boo
|
|||||||
info.Groups[i] = a.groupsPrefix + group
|
info.Groups[i] = a.groupsPrefix + group
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return info, true, nil
|
return info, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type stringOrArray []string
|
||||||
|
|
||||||
|
func (s *stringOrArray) UnmarshalJSON(b []byte) error {
|
||||||
|
var a []string
|
||||||
|
if err := json.Unmarshal(b, &a); err == nil {
|
||||||
|
*s = a
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var str string
|
||||||
|
if err := json.Unmarshal(b, &str); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*s = []string{str}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type claims map[string]json.RawMessage
|
||||||
|
|
||||||
|
func (c claims) unmarshalClaim(name string, v interface{}) error {
|
||||||
|
val, ok := c[name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("claim not present")
|
||||||
|
}
|
||||||
|
return json.Unmarshal([]byte(val), v)
|
||||||
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
27
staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/testdata/gen.sh
vendored
Executable file
27
staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/testdata/gen.sh
vendored
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
# Copyright 2018 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.
|
||||||
|
|
||||||
|
rm *.pem
|
||||||
|
|
||||||
|
for N in `seq 1 3`; do
|
||||||
|
ssh-keygen -t rsa -b 2048 -f rsa_$N.pem -N ''
|
||||||
|
done
|
||||||
|
|
||||||
|
for N in `seq 1 3`; do
|
||||||
|
ssh-keygen -t ecdsa -b 521 -f ecdsa_$N.pem -N ''
|
||||||
|
done
|
||||||
|
|
||||||
|
rm *.pub
|
Loading…
Reference in New Issue
Block a user