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"
|
|
|
|
|
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
|
|
|
coreosoidc "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"
|
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
|
|
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
2021-02-16 19:00:08 +00:00
|
|
|
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-08-24 21:45:55 +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 {
|
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
|
|
|
provider *coreosoidc.Provider
|
2020-12-02 16:27:20 +00:00
|
|
|
client *http.Client
|
|
|
|
}
|
|
|
|
|
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
|
|
|
func (c *lruValidatorCache) getProvider(spec *v1alpha1.OIDCIdentityProviderSpec) (*coreosoidc.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
|
|
|
}
|
|
|
|
|
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
|
|
|
func (c *lruValidatorCache) putProvider(spec *v1alpha1.OIDCIdentityProviderSpec, provider *coreosoidc.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 {
|
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
|
|
|
getProvider(*v1alpha1.OIDCIdentityProviderSpec) (*coreosoidc.Provider, *http.Client)
|
|
|
|
putProvider(*v1alpha1.OIDCIdentityProviderSpec, *coreosoidc.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,
|
2022-09-20 21:54:10 +00:00
|
|
|
AdditionalClaimMappings: upstream.Spec.Claims.AdditionalClaimMappings,
|
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
|
|
|
|
}
|
|
|
|
|
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
|
|
|
discoveredProvider, err = coreosoidc.NewProvider(coreosoidc.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()
|
|
|
|
|
2022-06-17 16:56:53 +00:00
|
|
|
hadErrorCondition := conditionsutil.MergeIDPConditions(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 {
|
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
|
|
|
return []string{oidcapi.ScopeOpenID, oidcapi.ScopeOfflineAccess, oidcapi.ScopeEmail, oidcapi.ScopeProfile}
|
2021-10-18 23:41:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, first compute the unique set of scopes, including "openid" (de-duplicate).
|
2021-10-20 22:53:25 +00:00
|
|
|
set := sets.NewString()
|
Create username scope, required for clients to get username in ID token
- For backwards compatibility with older Pinniped CLIs, the pinniped-cli
client does not need to request the username or groups scopes for them
to be granted. For dynamic clients, the usual OAuth2 rules apply:
the client must be allowed to request the scopes according to its
configuration, and the client must actually request the scopes in the
authorization request.
- If the username scope was not granted, then there will be no username
in the ID token, and the cluster-scoped token exchange will fail since
there would be no username in the resulting cluster-scoped ID token.
- The OIDC well-known discovery endpoint lists the username and groups
scopes in the scopes_supported list, and lists the username and groups
claims in the claims_supported list.
- Add username and groups scopes to the default list of scopes
put into kubeconfig files by "pinniped get kubeconfig" CLI command,
and the default list of scopes used by "pinniped login oidc" when
no list of scopes is specified in the kubeconfig file
- The warning header about group memberships changing during upstream
refresh will only be sent to the pinniped-cli client, since it is
only intended for kubectl and it could leak the username to the
client (which may not have the username scope granted) through the
warning message text.
- Add the user's username to the session storage as a new field, so that
during upstream refresh we can compare the original username from the
initial authorization to the refreshed username, even in the case when
the username scope was not granted (and therefore the username is not
stored in the ID token claims of the session storage)
- Bump the Supervisor session storage format version from 2 to 3
due to the username field being added to the session struct
- Extract commonly used string constants related to OIDC flows to api
package.
- Change some import names to make them consistent:
- Always import github.com/coreos/go-oidc/v3/oidc as "coreosoidc"
- Always import go.pinniped.dev/generated/latest/apis/supervisor/oidc
as "oidcapi"
- Always import go.pinniped.dev/internal/oidc as "oidc"
2022-08-08 23:29:22 +00:00
|
|
|
set.Insert(oidcapi.ScopeOpenID)
|
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
|
|
|
|
}
|