2021-01-07 22:58:09 +00:00
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
2020-12-08 01:39:51 +00:00
// SPDX-License-Identifier: Apache-2.0
// Package jwtcachefiller implements a controller for filling an authncache.Cache with each
// added/updated JWTAuthenticator.
package jwtcachefiller
import (
2021-10-20 11:59:24 +00:00
"context"
2020-12-08 01:39:51 +00:00
"fmt"
2021-10-20 11:59:24 +00:00
"net/url"
2020-12-08 20:14:05 +00:00
"reflect"
2021-10-20 11:59:24 +00:00
"time"
2020-12-08 01:39:51 +00:00
2021-10-20 11:59:24 +00:00
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
2020-12-08 01:39:51 +00:00
"github.com/go-logr/logr"
"gopkg.in/square/go-jose.v2"
"k8s.io/apimachinery/pkg/api/errors"
2020-12-08 20:14:05 +00:00
"k8s.io/apiserver/pkg/authentication/authenticator"
2021-08-09 23:16:25 +00:00
"k8s.io/apiserver/pkg/server/dynamiccertificates"
2020-12-08 01:39:51 +00:00
"k8s.io/apiserver/plugin/pkg/authenticator/token/oidc"
"k8s.io/klog/v2"
2021-02-16 19:00:08 +00:00
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
authinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions/authentication/v1alpha1"
2020-12-08 01:39:51 +00:00
pinnipedcontroller "go.pinniped.dev/internal/controller"
2020-12-08 20:14:05 +00:00
pinnipedauthenticator "go.pinniped.dev/internal/controller/authenticator"
2020-12-08 01:39:51 +00:00
"go.pinniped.dev/internal/controller/authenticator/authncache"
"go.pinniped.dev/internal/controllerlib"
2021-10-20 11:59:24 +00:00
"go.pinniped.dev/internal/net/phttp"
2020-12-08 01:39:51 +00:00
)
// These default values come from the way that the Supervisor issues and signs tokens. We make these
// the defaults for a JWTAuthenticator so that they can easily integrate with the Supervisor.
const (
2020-12-15 22:37:38 +00:00
defaultUsernameClaim = "username"
2020-12-08 01:39:51 +00:00
defaultGroupsClaim = "groups"
)
// defaultSupportedSigningAlgos returns the default signing algos that this JWTAuthenticator
// supports (i.e., if none are supplied by the user).
func defaultSupportedSigningAlgos ( ) [ ] string {
return [ ] string {
// RS256 is recommended by the OIDC spec and required, in some capacity. Since we want the
// JWTAuthenticator to be able to support many OIDC ID tokens out of the box, we include this
// algorithm by default.
string ( jose . RS256 ) ,
// ES256 is what the Supervisor does, by default. We want integration with the JWTAuthenticator
// to be as seamless as possible, so we include this algorithm by default.
string ( jose . ES256 ) ,
}
}
2020-12-08 20:14:05 +00:00
type tokenAuthenticatorCloser interface {
authenticator . Token
pinnipedauthenticator . Closer
}
type jwtAuthenticator struct {
tokenAuthenticatorCloser
spec * auth1alpha1 . JWTAuthenticatorSpec
}
2020-12-08 01:39:51 +00:00
// New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache.
func New (
cache * authncache . Cache ,
jwtAuthenticators authinformers . JWTAuthenticatorInformer ,
log logr . Logger ,
) controllerlib . Controller {
return controllerlib . New (
controllerlib . Config {
Name : "jwtcachefiller-controller" ,
Syncer : & controller {
cache : cache ,
jwtAuthenticators : jwtAuthenticators ,
log : log . WithName ( "jwtcachefiller-controller" ) ,
} ,
} ,
controllerlib . WithInformer (
jwtAuthenticators ,
pinnipedcontroller . MatchAnythingFilter ( nil ) , // nil parent func is fine because each event is distinct
controllerlib . InformerOption { } ,
) ,
)
}
type controller struct {
cache * authncache . Cache
jwtAuthenticators authinformers . JWTAuthenticatorInformer
log logr . Logger
}
// Sync implements controllerlib.Syncer.
func ( c * controller ) Sync ( ctx controllerlib . Context ) error {
2021-02-09 18:59:32 +00:00
obj , err := c . jwtAuthenticators . Lister ( ) . Get ( ctx . Key . Name )
2020-12-08 01:39:51 +00:00
if err != nil && errors . IsNotFound ( err ) {
c . log . Info ( "Sync() found that the JWTAuthenticator does not exist yet or was deleted" )
return nil
}
if err != nil {
return fmt . Errorf ( "failed to get JWTAuthenticator %s/%s: %w" , ctx . Key . Namespace , ctx . Key . Name , err )
}
2020-12-08 16:08:53 +00:00
cacheKey := authncache . Key {
2021-02-09 23:16:22 +00:00
APIGroup : auth1alpha1 . GroupName ,
Kind : "JWTAuthenticator" ,
Name : ctx . Key . Name ,
2020-12-08 16:08:53 +00:00
}
2020-12-08 20:14:05 +00:00
// If this authenticator already exists, then only recreate it if is different from the desired
// authenticator. We don't want to be creating a new authenticator for every resync period.
//
// If we do need to recreate the authenticator, then make sure we close the old one to avoid
// goroutine leaks.
2020-12-08 16:08:53 +00:00
if value := c . cache . Get ( cacheKey ) ; value != nil {
2020-12-08 20:14:05 +00:00
jwtAuthenticator := c . extractValueAsJWTAuthenticator ( value )
if jwtAuthenticator != nil {
if reflect . DeepEqual ( jwtAuthenticator . spec , & obj . Spec ) {
c . log . WithValues ( "jwtAuthenticator" , klog . KObj ( obj ) , "issuer" , obj . Spec . Issuer ) . Info ( "actual jwt authenticator and desired jwt authenticator are the same" )
return nil
}
jwtAuthenticator . Close ( )
2020-12-08 16:08:53 +00:00
}
}
2020-12-08 20:14:05 +00:00
// Make a deep copy of the spec so we aren't storing pointers to something that the informer cache
// may mutate!
jwtAuthenticator , err := newJWTAuthenticator ( obj . Spec . DeepCopy ( ) )
2020-12-08 01:39:51 +00:00
if err != nil {
return fmt . Errorf ( "failed to build jwt authenticator: %w" , err )
}
2020-12-08 16:08:53 +00:00
c . cache . Store ( cacheKey , jwtAuthenticator )
2020-12-08 01:39:51 +00:00
c . log . WithValues ( "jwtAuthenticator" , klog . KObj ( obj ) , "issuer" , obj . Spec . Issuer ) . Info ( "added new jwt authenticator" )
return nil
}
2020-12-08 20:14:05 +00:00
func ( c * controller ) extractValueAsJWTAuthenticator ( value authncache . Value ) * jwtAuthenticator {
jwtAuthenticator , ok := value . ( * jwtAuthenticator )
if ! ok {
actualType := "<nil>"
if t := reflect . TypeOf ( value ) ; t != nil {
actualType = t . String ( )
}
c . log . WithValues ( "actualType" , actualType ) . Info ( "wrong JWT authenticator type in cache" )
return nil
}
return jwtAuthenticator
}
2020-12-08 01:39:51 +00:00
// newJWTAuthenticator creates a jwt authenticator from the provided spec.
2020-12-08 20:14:05 +00:00
func newJWTAuthenticator ( spec * auth1alpha1 . JWTAuthenticatorSpec ) ( * jwtAuthenticator , error ) {
2021-10-20 11:59:24 +00:00
rootCAs , caBundle , err := pinnipedauthenticator . CABundle ( spec . TLS )
2020-12-08 01:39:51 +00:00
if err != nil {
return nil , fmt . Errorf ( "invalid TLS configuration: %w" , err )
}
2021-08-09 23:16:25 +00:00
var caContentProvider oidc . CAContentProvider
if len ( caBundle ) != 0 {
var caContentProviderErr error
caContentProvider , caContentProviderErr = dynamiccertificates . NewStaticCAContent ( "ignored" , caBundle )
if caContentProviderErr != nil {
return nil , caContentProviderErr // impossible since caBundle is validated already
2020-12-08 01:39:51 +00:00
}
}
2020-12-16 17:42:19 +00:00
usernameClaim := spec . Claims . Username
2020-12-16 00:11:53 +00:00
if usernameClaim == "" {
usernameClaim = defaultUsernameClaim
}
2020-12-16 17:42:19 +00:00
groupsClaim := spec . Claims . Groups
2020-12-16 00:11:53 +00:00
if groupsClaim == "" {
groupsClaim = defaultGroupsClaim
}
2020-12-08 01:39:51 +00:00
2021-10-20 11:59:24 +00:00
// copied from Kube OIDC code
issuerURL , err := url . Parse ( spec . Issuer )
if err != nil {
return nil , err
}
if issuerURL . Scheme != "https" {
return nil , fmt . Errorf ( "issuer (%q) has invalid scheme (%q), require 'https'" , spec . Issuer , issuerURL . Scheme )
}
client := phttp . Default ( rootCAs )
client . Timeout = 30 * time . Second // copied from Kube OIDC code
ctx := coreosoidc . ClientContext ( context . Background ( ) , client )
provider , err := coreosoidc . NewProvider ( ctx , spec . Issuer )
if err != nil {
return nil , fmt . Errorf ( "could not initialize provider: %w" , err )
}
providerJSON := & struct {
JWKSURL string ` json:"jwks_uri" `
} { }
if err := provider . Claims ( providerJSON ) ; err != nil {
return nil , fmt . Errorf ( "could not get provider jwks_uri: %w" , err ) // should be impossible because coreosoidc.NewProvider validates this
}
if len ( providerJSON . JWKSURL ) == 0 {
return nil , fmt . Errorf ( "issuer %q does not have jwks_uri set" , spec . Issuer )
}
oidcAuthenticator , err := oidc . New ( oidc . Options {
2020-12-08 01:39:51 +00:00
IssuerURL : spec . Issuer ,
2021-10-20 11:59:24 +00:00
KeySet : coreosoidc . NewRemoteKeySet ( ctx , providerJSON . JWKSURL ) ,
2020-12-08 01:39:51 +00:00
ClientID : spec . Audience ,
2020-12-16 00:11:53 +00:00
UsernameClaim : usernameClaim ,
GroupsClaim : groupsClaim ,
2020-12-08 01:39:51 +00:00
SupportedSigningAlgs : defaultSupportedSigningAlgos ( ) ,
2021-10-20 11:59:24 +00:00
// this is still needed for distributed claim resolution, meaning this uses a http client that does not honor our TLS config
// TODO fix when we pick up https://github.com/kubernetes/kubernetes/pull/106141
CAContentProvider : caContentProvider ,
2020-12-08 01:39:51 +00:00
} )
2020-12-08 20:14:05 +00:00
if err != nil {
return nil , fmt . Errorf ( "could not initialize authenticator: %w" , err )
}
return & jwtAuthenticator {
2021-10-20 11:59:24 +00:00
tokenAuthenticatorCloser : oidcAuthenticator ,
2020-12-08 20:14:05 +00:00
spec : spec ,
} , nil
2020-12-08 01:39:51 +00:00
}