2022-01-07 23:04:58 +00:00
|
|
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
2020-11-11 23:10:06 +00:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2021-05-12 21:00:39 +00:00
|
|
|
// Package oidcupstreamwatcher implements a controller which watches OIDCIdentityProviders.
|
|
|
|
package oidcupstreamwatcher
|
2020-11-11 23:10:06 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-11-17 00:15:58 +00:00
|
|
|
"crypto/x509"
|
|
|
|
"encoding/base64"
|
2020-11-11 23:10:06 +00:00
|
|
|
"fmt"
|
2020-11-17 00:15:58 +00:00
|
|
|
"net/http"
|
2020-11-11 23:10:06 +00:00
|
|
|
"net/url"
|
2021-05-10 04:22:34 +00:00
|
|
|
"strings"
|
2020-11-11 23:10:06 +00:00
|
|
|
"time"
|
|
|
|
|
2021-01-20 17:54:44 +00:00
|
|
|
"github.com/coreos/go-oidc/v3/oidc"
|
2020-11-11 23:10:06 +00:00
|
|
|
"github.com/go-logr/logr"
|
2020-11-30 20:54:11 +00:00
|
|
|
"golang.org/x/oauth2"
|
2020-12-17 23:43:20 +00:00
|
|
|
corev1 "k8s.io/api/core/v1"
|
2020-11-11 23:10:06 +00:00
|
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
|
|
"k8s.io/apimachinery/pkg/util/cache"
|
2022-04-16 02:43:53 +00:00
|
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
2020-11-11 23:10:06 +00:00
|
|
|
corev1informers "k8s.io/client-go/informers/core/v1"
|
|
|
|
|
2021-02-16 19:00:08 +00:00
|
|
|
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
|
|
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
|
|
|
idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1"
|
2020-11-11 23:10:06 +00:00
|
|
|
"go.pinniped.dev/internal/constable"
|
|
|
|
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
2021-05-12 21:00:39 +00:00
|
|
|
"go.pinniped.dev/internal/controller/conditionsutil"
|
|
|
|
"go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers"
|
2020-11-11 23:10:06 +00:00
|
|
|
"go.pinniped.dev/internal/controllerlib"
|
2021-10-20 11:59:24 +00:00
|
|
|
"go.pinniped.dev/internal/net/phttp"
|
2020-11-11 23:10:06 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/provider"
|
2022-04-16 02:43:53 +00:00
|
|
|
"go.pinniped.dev/internal/plog"
|
2020-11-30 20:54:11 +00:00
|
|
|
"go.pinniped.dev/internal/upstreamoidc"
|
2020-11-11 23:10:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Setup for the name of our controller in logs.
|
2021-04-09 15:43:09 +00:00
|
|
|
oidcControllerName = "oidc-upstream-observer"
|
2020-11-11 23:10:06 +00:00
|
|
|
|
|
|
|
// Constants related to the client credentials Secret.
|
2020-12-17 23:43:20 +00:00
|
|
|
oidcClientSecretType corev1.SecretType = "secrets.pinniped.dev/oidc-client"
|
|
|
|
|
|
|
|
clientIDDataKey = "clientID"
|
|
|
|
clientSecretDataKey = "clientSecret"
|
2020-11-11 23:10:06 +00:00
|
|
|
|
|
|
|
// Constants related to the OIDC provider discovery cache. These do not affect the cache of JWKS.
|
2021-04-09 15:43:09 +00:00
|
|
|
oidcValidatorCacheTTL = 15 * time.Minute
|
2020-11-11 23:10:06 +00:00
|
|
|
|
|
|
|
// Constants related to conditions.
|
2022-03-08 20:28:09 +00:00
|
|
|
typeClientCredentialsValid = "ClientCredentialsValid" //nolint:gosec // this is not a credential
|
2021-10-14 22:49:44 +00:00
|
|
|
typeAdditionalAuthorizeParametersValid = "AdditionalAuthorizeParametersValid"
|
|
|
|
typeOIDCDiscoverySucceeded = "OIDCDiscoverySucceeded"
|
2021-05-12 21:00:39 +00:00
|
|
|
|
2021-10-14 22:49:44 +00:00
|
|
|
reasonUnreachable = "Unreachable"
|
|
|
|
reasonInvalidResponse = "InvalidResponse"
|
|
|
|
reasonDisallowedParameterName = "DisallowedParameterName"
|
2021-10-22 17:23:21 +00:00
|
|
|
allParamNamesAllowedMsg = "additionalAuthorizeParameters parameter names are allowed"
|
2020-11-11 23:10:06 +00:00
|
|
|
|
|
|
|
// Errors that are generated by our reconcile process.
|
2021-04-09 15:43:09 +00:00
|
|
|
errOIDCFailureStatus = constable.Error("OIDCIdentityProvider has a failing condition")
|
2020-11-11 23:10:06 +00:00
|
|
|
)
|
|
|
|
|
2021-10-14 22:49:44 +00:00
|
|
|
var (
|
2022-04-16 02:43:53 +00:00
|
|
|
disallowedAdditionalAuthorizeParameters = map[string]bool{ // nolint: gochecknoglobals
|
2021-10-18 23:41:31 +00:00
|
|
|
// Reject these AdditionalAuthorizeParameters to avoid allowing the user's config to overwrite the parameters
|
|
|
|
// that are always used by Pinniped in authcode authorization requests. The OIDC library used would otherwise
|
|
|
|
// happily treat the user's config as an override. Users can already set the "client_id" and "scope" params
|
|
|
|
// using other settings, and the others never make sense to override. This map should be treated as read-only
|
|
|
|
// since it is a global variable.
|
2021-10-14 22:49:44 +00:00
|
|
|
"response_type": true,
|
|
|
|
"scope": true,
|
|
|
|
"client_id": true,
|
|
|
|
"state": true,
|
|
|
|
"nonce": true,
|
|
|
|
"code_challenge": true,
|
|
|
|
"code_challenge_method": true,
|
|
|
|
"redirect_uri": true,
|
2021-10-18 23:41:31 +00:00
|
|
|
|
|
|
|
// Reject "hd" for now because it is not safe to use with Google's OIDC provider until Pinniped also
|
|
|
|
// performs the corresponding validation on the ID token.
|
|
|
|
"hd": true,
|
2021-10-14 22:49:44 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2021-04-09 15:43:09 +00:00
|
|
|
// UpstreamOIDCIdentityProviderICache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations.
|
|
|
|
type UpstreamOIDCIdentityProviderICache interface {
|
2021-04-07 23:12:13 +00:00
|
|
|
SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI)
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 00:15:58 +00:00
|
|
|
// lruValidatorCache caches the *oidc.Provider associated with a particular issuer/TLS configuration.
|
|
|
|
type lruValidatorCache struct{ cache *cache.Expiring }
|
|
|
|
|
2020-12-02 16:27:20 +00:00
|
|
|
type lruValidatorCacheEntry struct {
|
|
|
|
provider *oidc.Provider
|
|
|
|
client *http.Client
|
|
|
|
}
|
|
|
|
|
2020-12-16 22:27:09 +00:00
|
|
|
func (c *lruValidatorCache) getProvider(spec *v1alpha1.OIDCIdentityProviderSpec) (*oidc.Provider, *http.Client) {
|
2020-11-17 00:15:58 +00:00
|
|
|
if result, ok := c.cache.Get(c.cacheKey(spec)); ok {
|
2020-12-02 16:27:20 +00:00
|
|
|
entry := result.(*lruValidatorCacheEntry)
|
|
|
|
return entry.provider, entry.client
|
2020-11-17 00:15:58 +00:00
|
|
|
}
|
2020-12-02 16:27:20 +00:00
|
|
|
return nil, nil
|
2020-11-17 00:15:58 +00:00
|
|
|
}
|
|
|
|
|
2020-12-16 22:27:09 +00:00
|
|
|
func (c *lruValidatorCache) putProvider(spec *v1alpha1.OIDCIdentityProviderSpec, provider *oidc.Provider, client *http.Client) {
|
2021-04-09 15:43:09 +00:00
|
|
|
c.cache.Set(c.cacheKey(spec), &lruValidatorCacheEntry{provider: provider, client: client}, oidcValidatorCacheTTL)
|
2020-11-17 00:15:58 +00:00
|
|
|
}
|
|
|
|
|
2020-12-16 22:27:09 +00:00
|
|
|
func (c *lruValidatorCache) cacheKey(spec *v1alpha1.OIDCIdentityProviderSpec) interface{} {
|
2020-11-17 00:15:58 +00:00
|
|
|
var key struct{ issuer, caBundle string }
|
|
|
|
key.issuer = spec.Issuer
|
|
|
|
if spec.TLS != nil {
|
|
|
|
key.caBundle = spec.TLS.CertificateAuthorityData
|
|
|
|
}
|
|
|
|
return key
|
|
|
|
}
|
|
|
|
|
2021-04-09 15:43:09 +00:00
|
|
|
type oidcWatcherController struct {
|
|
|
|
cache UpstreamOIDCIdentityProviderICache
|
2020-12-17 21:49:53 +00:00
|
|
|
log logr.Logger
|
|
|
|
client pinnipedclientset.Interface
|
|
|
|
oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer
|
|
|
|
secretInformer corev1informers.SecretInformer
|
|
|
|
validatorCache interface {
|
2020-12-16 22:27:09 +00:00
|
|
|
getProvider(*v1alpha1.OIDCIdentityProviderSpec) (*oidc.Provider, *http.Client)
|
|
|
|
putProvider(*v1alpha1.OIDCIdentityProviderSpec, *oidc.Provider, *http.Client)
|
2020-11-17 00:15:58 +00:00
|
|
|
}
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
|
|
|
|
2021-05-12 21:00:39 +00:00
|
|
|
// New instantiates a new controllerlib.Controller which will populate the provided UpstreamOIDCIdentityProviderICache.
|
|
|
|
func New(
|
2021-04-09 15:43:09 +00:00
|
|
|
idpCache UpstreamOIDCIdentityProviderICache,
|
2020-11-11 23:10:06 +00:00
|
|
|
client pinnipedclientset.Interface,
|
2020-12-17 21:49:53 +00:00
|
|
|
oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer,
|
|
|
|
secretInformer corev1informers.SecretInformer,
|
2020-11-11 23:10:06 +00:00
|
|
|
log logr.Logger,
|
2020-12-18 23:41:07 +00:00
|
|
|
withInformer pinnipedcontroller.WithInformerOptionFunc,
|
2020-11-11 23:10:06 +00:00
|
|
|
) controllerlib.Controller {
|
2021-04-09 15:43:09 +00:00
|
|
|
c := oidcWatcherController{
|
2020-12-17 21:49:53 +00:00
|
|
|
cache: idpCache,
|
2021-04-09 15:43:09 +00:00
|
|
|
log: log.WithName(oidcControllerName),
|
2020-12-17 21:49:53 +00:00
|
|
|
client: client,
|
|
|
|
oidcIdentityProviderInformer: oidcIdentityProviderInformer,
|
|
|
|
secretInformer: secretInformer,
|
|
|
|
validatorCache: &lruValidatorCache{cache: cache.NewExpiring()},
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
|
|
|
return controllerlib.New(
|
2021-04-09 15:43:09 +00:00
|
|
|
controllerlib.Config{Name: oidcControllerName, Syncer: &c},
|
2020-12-18 23:41:07 +00:00
|
|
|
withInformer(
|
|
|
|
oidcIdentityProviderInformer,
|
|
|
|
pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()),
|
|
|
|
controllerlib.InformerOption{},
|
|
|
|
),
|
|
|
|
withInformer(
|
|
|
|
secretInformer,
|
|
|
|
pinnipedcontroller.MatchAnySecretOfTypeFilter(oidcClientSecretType, pinnipedcontroller.SingletonQueue()),
|
|
|
|
controllerlib.InformerOption{},
|
|
|
|
),
|
2020-11-11 23:10:06 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sync implements controllerlib.Syncer.
|
2021-04-09 15:43:09 +00:00
|
|
|
func (c *oidcWatcherController) Sync(ctx controllerlib.Context) error {
|
2020-12-17 21:49:53 +00:00
|
|
|
actualUpstreams, err := c.oidcIdentityProviderInformer.Lister().List(labels.Everything())
|
2020-11-11 23:10:06 +00:00
|
|
|
if err != nil {
|
2020-12-16 22:27:09 +00:00
|
|
|
return fmt.Errorf("failed to list OIDCIdentityProviders: %w", err)
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
requeue := false
|
2020-11-18 21:38:13 +00:00
|
|
|
validatedUpstreams := make([]provider.UpstreamOIDCIdentityProviderI, 0, len(actualUpstreams))
|
2020-11-11 23:10:06 +00:00
|
|
|
for _, upstream := range actualUpstreams {
|
|
|
|
valid := c.validateUpstream(ctx, upstream)
|
|
|
|
if valid == nil {
|
|
|
|
requeue = true
|
|
|
|
} else {
|
2020-11-18 21:38:13 +00:00
|
|
|
validatedUpstreams = append(validatedUpstreams, provider.UpstreamOIDCIdentityProviderI(valid))
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
|
|
|
}
|
2021-04-07 23:12:13 +00:00
|
|
|
c.cache.SetOIDCIdentityProviders(validatedUpstreams)
|
2020-11-11 23:10:06 +00:00
|
|
|
if requeue {
|
|
|
|
return controllerlib.ErrSyntheticRequeue
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-12-16 22:27:09 +00:00
|
|
|
// validateUpstream validates the provided v1alpha1.OIDCIdentityProvider and returns the validated configuration as a
|
|
|
|
// provider.UpstreamOIDCIdentityProvider. As a side effect, it also updates the status of the v1alpha1.OIDCIdentityProvider.
|
2021-04-09 15:43:09 +00:00
|
|
|
func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upstream *v1alpha1.OIDCIdentityProvider) *upstreamoidc.ProviderConfig {
|
2021-10-14 22:49:44 +00:00
|
|
|
authorizationConfig := upstream.Spec.AuthorizationConfig
|
|
|
|
|
|
|
|
additionalAuthcodeAuthorizeParameters := map[string]string{}
|
|
|
|
var rejectedAuthcodeAuthorizeParameters []string
|
|
|
|
for _, p := range authorizationConfig.AdditionalAuthorizeParameters {
|
|
|
|
if disallowedAdditionalAuthorizeParameters[p.Name] {
|
|
|
|
rejectedAuthcodeAuthorizeParameters = append(rejectedAuthcodeAuthorizeParameters, p.Name)
|
|
|
|
} else {
|
|
|
|
additionalAuthcodeAuthorizeParameters[p.Name] = p.Value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-30 20:54:11 +00:00
|
|
|
result := upstreamoidc.ProviderConfig{
|
|
|
|
Name: upstream.Name,
|
|
|
|
Config: &oauth2.Config{
|
2021-10-18 23:41:31 +00:00
|
|
|
Scopes: computeScopes(authorizationConfig.AdditionalScopes),
|
2020-11-30 20:54:11 +00:00
|
|
|
},
|
2021-10-08 22:48:21 +00:00
|
|
|
UsernameClaim: upstream.Spec.Claims.Username,
|
|
|
|
GroupsClaim: upstream.Spec.Claims.Groups,
|
2021-10-14 22:49:44 +00:00
|
|
|
AllowPasswordGrant: authorizationConfig.AllowPasswordGrant,
|
|
|
|
AdditionalAuthcodeParams: additionalAuthcodeAuthorizeParameters,
|
2021-10-08 22:48:21 +00:00
|
|
|
ResourceUID: upstream.UID,
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
2021-10-14 22:49:44 +00:00
|
|
|
|
2020-11-11 23:10:06 +00:00
|
|
|
conditions := []*v1alpha1.Condition{
|
|
|
|
c.validateSecret(upstream, &result),
|
|
|
|
c.validateIssuer(ctx.Context, upstream, &result),
|
|
|
|
}
|
2021-10-14 22:49:44 +00:00
|
|
|
if len(rejectedAuthcodeAuthorizeParameters) > 0 {
|
|
|
|
conditions = append(conditions, &v1alpha1.Condition{
|
|
|
|
Type: typeAdditionalAuthorizeParametersValid,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reasonDisallowedParameterName,
|
|
|
|
Message: fmt.Sprintf("the following additionalAuthorizeParameters are not allowed: %s",
|
|
|
|
strings.Join(rejectedAuthcodeAuthorizeParameters, ",")),
|
|
|
|
})
|
2021-10-22 17:23:21 +00:00
|
|
|
} else {
|
|
|
|
conditions = append(conditions, &v1alpha1.Condition{
|
|
|
|
Type: typeAdditionalAuthorizeParametersValid,
|
|
|
|
Status: v1alpha1.ConditionTrue,
|
|
|
|
Reason: upstreamwatchers.ReasonSuccess,
|
|
|
|
Message: allParamNamesAllowedMsg,
|
|
|
|
})
|
2021-10-14 22:49:44 +00:00
|
|
|
}
|
|
|
|
|
2020-11-11 23:10:06 +00:00
|
|
|
c.updateStatus(ctx.Context, upstream, conditions)
|
|
|
|
|
|
|
|
valid := true
|
|
|
|
log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name)
|
|
|
|
for _, condition := range conditions {
|
|
|
|
if condition.Status == v1alpha1.ConditionFalse {
|
|
|
|
valid = false
|
|
|
|
log.WithValues(
|
|
|
|
"type", condition.Type,
|
|
|
|
"reason", condition.Reason,
|
|
|
|
"message", condition.Message,
|
2021-04-09 15:43:09 +00:00
|
|
|
).Error(errOIDCFailureStatus, "found failing condition")
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if valid {
|
|
|
|
return &result
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// validateSecret validates the .spec.client.secretName field and returns the appropriate ClientCredentialsValid condition.
|
2021-04-09 15:43:09 +00:00
|
|
|
func (c *oidcWatcherController) validateSecret(upstream *v1alpha1.OIDCIdentityProvider, result *upstreamoidc.ProviderConfig) *v1alpha1.Condition {
|
2020-11-11 23:10:06 +00:00
|
|
|
secretName := upstream.Spec.Client.SecretName
|
|
|
|
|
|
|
|
// Fetch the Secret from informer cache.
|
2020-12-17 21:49:53 +00:00
|
|
|
secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName)
|
2020-11-11 23:10:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return &v1alpha1.Condition{
|
2021-04-09 15:43:09 +00:00
|
|
|
Type: typeClientCredentialsValid,
|
2020-11-11 23:10:06 +00:00
|
|
|
Status: v1alpha1.ConditionFalse,
|
2021-05-12 21:00:39 +00:00
|
|
|
Reason: upstreamwatchers.ReasonNotFound,
|
2020-11-11 23:10:06 +00:00
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the secret .type field.
|
|
|
|
if secret.Type != oidcClientSecretType {
|
|
|
|
return &v1alpha1.Condition{
|
2021-04-09 15:43:09 +00:00
|
|
|
Type: typeClientCredentialsValid,
|
2020-11-11 23:10:06 +00:00
|
|
|
Status: v1alpha1.ConditionFalse,
|
2021-05-12 21:00:39 +00:00
|
|
|
Reason: upstreamwatchers.ReasonWrongType,
|
2020-11-11 23:10:06 +00:00
|
|
|
Message: fmt.Sprintf("referenced Secret %q has wrong type %q (should be %q)", secretName, secret.Type, oidcClientSecretType),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the secret .data field.
|
|
|
|
clientID := secret.Data[clientIDDataKey]
|
|
|
|
clientSecret := secret.Data[clientSecretDataKey]
|
|
|
|
if len(clientID) == 0 || len(clientSecret) == 0 {
|
|
|
|
return &v1alpha1.Condition{
|
2021-04-09 15:43:09 +00:00
|
|
|
Type: typeClientCredentialsValid,
|
2020-11-11 23:10:06 +00:00
|
|
|
Status: v1alpha1.ConditionFalse,
|
2021-05-12 21:00:39 +00:00
|
|
|
Reason: upstreamwatchers.ReasonMissingKeys,
|
2020-11-11 23:10:06 +00:00
|
|
|
Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", secretName, []string{clientIDDataKey, clientSecretDataKey}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If everything is valid, update the result and set the condition to true.
|
2020-11-30 20:54:11 +00:00
|
|
|
result.Config.ClientID = string(clientID)
|
2020-12-02 16:27:20 +00:00
|
|
|
result.Config.ClientSecret = string(clientSecret)
|
2020-11-11 23:10:06 +00:00
|
|
|
return &v1alpha1.Condition{
|
2021-04-09 15:43:09 +00:00
|
|
|
Type: typeClientCredentialsValid,
|
2020-11-11 23:10:06 +00:00
|
|
|
Status: v1alpha1.ConditionTrue,
|
2021-05-12 21:00:39 +00:00
|
|
|
Reason: upstreamwatchers.ReasonSuccess,
|
2020-11-11 23:10:06 +00:00
|
|
|
Message: "loaded client credentials",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// validateIssuer validates the .spec.issuer field, performs OIDC discovery, and returns the appropriate OIDCDiscoverySucceeded condition.
|
2021-04-09 15:43:09 +00:00
|
|
|
func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1alpha1.OIDCIdentityProvider, result *upstreamoidc.ProviderConfig) *v1alpha1.Condition {
|
2020-12-02 16:27:20 +00:00
|
|
|
// Get the provider and HTTP Client from cache if possible.
|
|
|
|
discoveredProvider, httpClient := c.validatorCache.getProvider(&upstream.Spec)
|
2020-11-11 23:10:06 +00:00
|
|
|
|
|
|
|
// If the provider does not exist in the cache, do a fresh discovery lookup and save to the cache.
|
|
|
|
if discoveredProvider == nil {
|
2021-10-20 11:59:24 +00:00
|
|
|
var err error
|
|
|
|
httpClient, err = getClient(upstream)
|
2020-11-17 00:15:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return &v1alpha1.Condition{
|
|
|
|
Type: typeOIDCDiscoverySucceeded,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
2021-05-12 21:00:39 +00:00
|
|
|
Reason: upstreamwatchers.ReasonInvalidTLSConfig,
|
2020-11-17 00:15:58 +00:00
|
|
|
Message: err.Error(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-04 00:11:53 +00:00
|
|
|
_, issuerURLCondition := validateHTTPSURL(upstream.Spec.Issuer, "issuer", reasonUnreachable)
|
|
|
|
if issuerURLCondition != nil {
|
|
|
|
return issuerURLCondition
|
|
|
|
}
|
|
|
|
|
Fix broken upstream OIDC discovery timeout added in previous commit
After noticing that the upstream OIDC discovery calls can hang
indefinitely, I had tried to impose a one minute timeout on them
by giving them a timeout context. However, I hadn't noticed that the
context also gets passed into the JWKS fetching object, which gets
added to our cache and used later. Therefore the timeout context
was added to the cache and timed out while sitting in the cache,
causing later JWKS fetchers to fail.
This commit is trying again to impose a reasonable timeout on these
discovery and JWKS calls, but this time by using http.Client's Timeout
field, which is documented to be a timeout for *each* request/response
cycle, so hopefully this is a more appropriate way to impose a timeout
for this use case. The http.Client instance ends up in the cache on
the JWKS fetcher object, so the timeout should apply to each JWKS
request as well.
Requests that can hang forever are effectively a server-side resource
leak, which could theoretically be taken advantage of in a denial of
service attempt, so it would be nice to avoid having them.
2021-07-08 16:44:02 +00:00
|
|
|
discoveredProvider, err = oidc.NewProvider(oidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer)
|
2020-11-11 23:10:06 +00:00
|
|
|
if err != nil {
|
2022-04-16 02:43:53 +00:00
|
|
|
c.log.V(plog.KlogLevelTrace).WithValues(
|
2021-05-07 19:59:04 +00:00
|
|
|
"namespace", upstream.Namespace,
|
|
|
|
"name", upstream.Name,
|
|
|
|
"issuer", upstream.Spec.Issuer,
|
|
|
|
).Error(err, "failed to perform OIDC discovery")
|
2020-11-11 23:10:06 +00:00
|
|
|
return &v1alpha1.Condition{
|
|
|
|
Type: typeOIDCDiscoverySucceeded,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reasonUnreachable,
|
2021-09-29 13:26:29 +00:00
|
|
|
Message: fmt.Sprintf("failed to perform OIDC discovery against %q:\n%s", upstream.Spec.Issuer, truncateMostLongErr(err)),
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the cache with the newly discovered value.
|
2020-12-02 16:27:20 +00:00
|
|
|
c.validatorCache.putProvider(&upstream.Spec, discoveredProvider, httpClient)
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
|
|
|
|
2021-10-22 21:32:26 +00:00
|
|
|
// Get the revocation endpoint, if there is one. Many providers do not offer a revocation endpoint.
|
|
|
|
var additionalDiscoveryClaims struct {
|
|
|
|
// "revocation_endpoint" is specified by https://datatracker.ietf.org/doc/html/rfc8414#section-2
|
|
|
|
RevocationEndpoint string `json:"revocation_endpoint"`
|
|
|
|
}
|
|
|
|
if err := discoveredProvider.Claims(&additionalDiscoveryClaims); err != nil {
|
|
|
|
// This shouldn't actually happen because the above call to NewProvider() would have already returned this error.
|
|
|
|
return &v1alpha1.Condition{
|
|
|
|
Type: typeOIDCDiscoverySucceeded,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reasonInvalidResponse,
|
|
|
|
Message: fmt.Sprintf("failed to unmarshal OIDC discovery response from %q:\n%s", upstream.Spec.Issuer, truncateMostLongErr(err)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if additionalDiscoveryClaims.RevocationEndpoint != "" {
|
2021-12-04 00:11:53 +00:00
|
|
|
// Found a revocation URL. Validate it.
|
|
|
|
revocationURL, revocationURLCondition := validateHTTPSURL(
|
|
|
|
additionalDiscoveryClaims.RevocationEndpoint,
|
|
|
|
"revocation endpoint",
|
|
|
|
reasonInvalidResponse,
|
|
|
|
)
|
|
|
|
if revocationURLCondition != nil {
|
|
|
|
return revocationURLCondition
|
2021-10-22 21:32:26 +00:00
|
|
|
}
|
|
|
|
// Remember the URL for later use.
|
|
|
|
result.RevocationURL = revocationURL
|
|
|
|
}
|
|
|
|
|
2021-12-04 00:11:53 +00:00
|
|
|
_, authorizeURLCondition := validateHTTPSURL(
|
|
|
|
discoveredProvider.Endpoint().AuthURL,
|
|
|
|
"authorization endpoint",
|
|
|
|
reasonInvalidResponse,
|
|
|
|
)
|
|
|
|
if authorizeURLCondition != nil {
|
|
|
|
return authorizeURLCondition
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
2021-12-04 00:11:53 +00:00
|
|
|
|
|
|
|
_, tokenURLCondition := validateHTTPSURL(
|
|
|
|
discoveredProvider.Endpoint().TokenURL,
|
|
|
|
"token endpoint",
|
|
|
|
reasonInvalidResponse,
|
|
|
|
)
|
|
|
|
if tokenURLCondition != nil {
|
|
|
|
return tokenURLCondition
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If everything is valid, update the result and set the condition to true.
|
2020-11-30 20:54:11 +00:00
|
|
|
result.Config.Endpoint = discoveredProvider.Endpoint()
|
|
|
|
result.Provider = discoveredProvider
|
2020-12-02 16:27:20 +00:00
|
|
|
result.Client = httpClient
|
2020-11-11 23:10:06 +00:00
|
|
|
return &v1alpha1.Condition{
|
|
|
|
Type: typeOIDCDiscoverySucceeded,
|
|
|
|
Status: v1alpha1.ConditionTrue,
|
2021-05-12 21:00:39 +00:00
|
|
|
Reason: upstreamwatchers.ReasonSuccess,
|
2020-11-11 23:10:06 +00:00
|
|
|
Message: "discovered issuer configuration",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-09 15:43:09 +00:00
|
|
|
func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.OIDCIdentityProvider, conditions []*v1alpha1.Condition) {
|
2020-11-11 23:10:06 +00:00
|
|
|
log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name)
|
|
|
|
updated := upstream.DeepCopy()
|
|
|
|
|
2021-05-12 21:00:39 +00:00
|
|
|
hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log)
|
2020-11-11 23:10:06 +00:00
|
|
|
|
2021-04-12 20:53:21 +00:00
|
|
|
updated.Status.Phase = v1alpha1.PhaseReady
|
|
|
|
if hadErrorCondition {
|
|
|
|
updated.Status.Phase = v1alpha1.PhaseError
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if equality.Semantic.DeepEqual(upstream, updated) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := c.client.
|
|
|
|
IDPV1alpha1().
|
2020-12-16 22:27:09 +00:00
|
|
|
OIDCIdentityProviders(upstream.Namespace).
|
2020-11-11 23:10:06 +00:00
|
|
|
UpdateStatus(ctx, updated, metav1.UpdateOptions{})
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err, "failed to update status")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-20 11:59:24 +00:00
|
|
|
func getClient(upstream *v1alpha1.OIDCIdentityProvider) (*http.Client, error) {
|
2021-05-12 21:05:08 +00:00
|
|
|
if upstream.Spec.TLS == nil || upstream.Spec.TLS.CertificateAuthorityData == "" {
|
2021-10-20 11:59:24 +00:00
|
|
|
return defaultClientShortTimeout(nil), nil
|
2021-05-12 21:05:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bundle, err := base64.StdEncoding.DecodeString(upstream.Spec.TLS.CertificateAuthorityData)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", err)
|
|
|
|
}
|
|
|
|
|
2021-10-20 11:59:24 +00:00
|
|
|
rootCAs := x509.NewCertPool()
|
|
|
|
if !rootCAs.AppendCertsFromPEM(bundle) {
|
2021-05-12 21:05:08 +00:00
|
|
|
return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", upstreamwatchers.ErrNoCertificates)
|
|
|
|
}
|
|
|
|
|
2021-10-20 11:59:24 +00:00
|
|
|
return defaultClientShortTimeout(rootCAs), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func defaultClientShortTimeout(rootCAs *x509.CertPool) *http.Client {
|
|
|
|
c := phttp.Default(rootCAs)
|
|
|
|
c.Timeout = time.Minute
|
|
|
|
return c
|
2021-05-12 21:05:08 +00:00
|
|
|
}
|
|
|
|
|
2021-10-18 23:41:31 +00:00
|
|
|
func computeScopes(additionalScopes []string) []string {
|
|
|
|
// If none are set then provide a reasonable default which only tries to use scopes defined in the OIDC spec.
|
|
|
|
if len(additionalScopes) == 0 {
|
|
|
|
return []string{"openid", "offline_access", "email", "profile"}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, first compute the unique set of scopes, including "openid" (de-duplicate).
|
2021-10-20 22:53:25 +00:00
|
|
|
set := sets.NewString()
|
|
|
|
set.Insert("openid")
|
2020-11-11 23:10:06 +00:00
|
|
|
for _, s := range additionalScopes {
|
2021-10-20 22:53:25 +00:00
|
|
|
set.Insert(s)
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
2021-10-18 23:41:31 +00:00
|
|
|
|
2021-10-20 22:53:25 +00:00
|
|
|
// Return the set as a sorted list.
|
|
|
|
return set.List()
|
2020-11-11 23:10:06 +00:00
|
|
|
}
|
2021-05-07 19:59:04 +00:00
|
|
|
|
2021-09-29 13:26:29 +00:00
|
|
|
func truncateMostLongErr(err error) string {
|
|
|
|
const max = 300
|
2021-05-07 19:59:04 +00:00
|
|
|
msg := err.Error()
|
|
|
|
|
2021-09-29 13:26:29 +00:00
|
|
|
// always log oidc and x509 errors completely
|
|
|
|
if len(msg) <= max || strings.Contains(msg, "oidc:") || strings.Contains(msg, "x509:") {
|
2021-05-07 19:59:04 +00:00
|
|
|
return msg
|
|
|
|
}
|
|
|
|
|
|
|
|
return msg[:max] + fmt.Sprintf(" [truncated %d chars]", len(msg)-max)
|
|
|
|
}
|
2021-12-04 00:11:53 +00:00
|
|
|
|
|
|
|
func validateHTTPSURL(maybeHTTPSURL, endpointType, reason string) (*url.URL, *v1alpha1.Condition) {
|
|
|
|
parsedURL, err := url.Parse(maybeHTTPSURL)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &v1alpha1.Condition{
|
|
|
|
Type: typeOIDCDiscoverySucceeded,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reason,
|
|
|
|
Message: fmt.Sprintf("failed to parse %s URL: %v", endpointType, truncateMostLongErr(err)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if parsedURL.Scheme != "https" {
|
|
|
|
return nil, &v1alpha1.Condition{
|
|
|
|
Type: typeOIDCDiscoverySucceeded,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reason,
|
2022-01-07 23:04:58 +00:00
|
|
|
Message: fmt.Sprintf(`%s URL '%s' must have "https" scheme, not %q`, endpointType, maybeHTTPSURL, parsedURL.Scheme),
|
2021-12-04 00:11:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(parsedURL.Query()) != 0 || parsedURL.Fragment != "" {
|
|
|
|
return nil, &v1alpha1.Condition{
|
|
|
|
Type: typeOIDCDiscoverySucceeded,
|
|
|
|
Status: v1alpha1.ConditionFalse,
|
|
|
|
Reason: reason,
|
2022-01-07 23:04:58 +00:00
|
|
|
Message: fmt.Sprintf(`%s URL '%s' cannot contain query or fragment component`, endpointType, maybeHTTPSURL),
|
2021-12-04 00:11:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return parsedURL, nil
|
|
|
|
}
|