34509e7430
- Enhance the token exchange to check that the same client is used compared to the client used during the original authorization and token requests, and also check that the client has the token-exchange grant type allowed in its configuration. - Reduce the minimum required bcrypt cost for OIDCClient secrets because 15 is too slow for real-life use, especially considering that every login and every refresh flow will require two client auths. - In unit tests, use bcrypt hashes with a cost of 4, because bcrypt slows down by 13x when run with the race detector, and we run our tests with the race detector enabled, causing the tests to be unacceptably slow. The production code uses a higher minimum cost. - Centralize all pre-computed bcrypt hashes used by unit tests to a single place. Also extract some other useful test helpers for unit tests related to OIDCClients. - Add tons of unit tests for the token endpoint related to dynamic clients for authcode exchanges, token exchanges, and refreshes.
196 lines
7.0 KiB
Go
196 lines
7.0 KiB
Go
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package manager
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
|
|
"go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1"
|
|
"go.pinniped.dev/internal/oidc"
|
|
"go.pinniped.dev/internal/oidc/auth"
|
|
"go.pinniped.dev/internal/oidc/callback"
|
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
|
"go.pinniped.dev/internal/oidc/discovery"
|
|
"go.pinniped.dev/internal/oidc/dynamiccodec"
|
|
"go.pinniped.dev/internal/oidc/idpdiscovery"
|
|
"go.pinniped.dev/internal/oidc/jwks"
|
|
"go.pinniped.dev/internal/oidc/login"
|
|
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
|
"go.pinniped.dev/internal/oidc/provider"
|
|
"go.pinniped.dev/internal/oidc/token"
|
|
"go.pinniped.dev/internal/plog"
|
|
"go.pinniped.dev/internal/secret"
|
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
|
)
|
|
|
|
// Manager can manage multiple active OIDC providers. It acts as a request router for them.
|
|
//
|
|
// It is thread-safe.
|
|
type Manager struct {
|
|
mu sync.RWMutex
|
|
providers []*provider.FederationDomainIssuer
|
|
providerHandlers map[string]http.Handler // map of all routes for all providers
|
|
nextHandler http.Handler // the next handler in a chain, called when this manager didn't know how to handle a request
|
|
dynamicJWKSProvider jwks.DynamicJWKSProvider // in-memory cache of per-issuer JWKS data
|
|
upstreamIDPs oidc.UpstreamIdentityProvidersLister // in-memory cache of upstream IDPs
|
|
secretCache *secret.Cache // in-memory cache of cryptographic material
|
|
secretsClient corev1client.SecretInterface
|
|
oidcClientsClient v1alpha1.OIDCClientInterface
|
|
}
|
|
|
|
// NewManager returns an empty Manager.
|
|
// nextHandler will be invoked for any requests that could not be handled by this manager's providers.
|
|
// dynamicJWKSProvider will be used as an in-memory cache for per-issuer JWKS data.
|
|
// upstreamIDPs will be used as an in-memory cache of currently configured upstream IDPs.
|
|
func NewManager(
|
|
nextHandler http.Handler,
|
|
dynamicJWKSProvider jwks.DynamicJWKSProvider,
|
|
upstreamIDPs oidc.UpstreamIdentityProvidersLister,
|
|
secretCache *secret.Cache,
|
|
secretsClient corev1client.SecretInterface,
|
|
oidcClientsClient v1alpha1.OIDCClientInterface,
|
|
) *Manager {
|
|
return &Manager{
|
|
providerHandlers: make(map[string]http.Handler),
|
|
nextHandler: nextHandler,
|
|
dynamicJWKSProvider: dynamicJWKSProvider,
|
|
upstreamIDPs: upstreamIDPs,
|
|
secretCache: secretCache,
|
|
secretsClient: secretsClient,
|
|
oidcClientsClient: oidcClientsClient,
|
|
}
|
|
}
|
|
|
|
// SetProviders adds or updates all the given providerHandlers using each provider's issuer string
|
|
// as the name of the provider to decide if it is an add or update operation.
|
|
//
|
|
// It also removes any providerHandlers that were previously added but were not passed in to
|
|
// the current invocation.
|
|
//
|
|
// This method assumes that all of the FederationDomainIssuer arguments have already been validated
|
|
// by someone else before they are passed to this method.
|
|
func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIssuer) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
m.providers = federationDomains
|
|
m.providerHandlers = make(map[string]http.Handler)
|
|
|
|
var csrfCookieEncoder = dynamiccodec.New(
|
|
oidc.CSRFCookieLifespan,
|
|
m.secretCache.GetCSRFCookieEncoderHashKey,
|
|
func() []byte { return nil },
|
|
)
|
|
|
|
for _, incomingProvider := range federationDomains {
|
|
issuer := incomingProvider.Issuer()
|
|
issuerHostWithPath := strings.ToLower(incomingProvider.IssuerHost()) + "/" + incomingProvider.IssuerPath()
|
|
|
|
tokenHMACKeyGetter := wrapGetter(incomingProvider.Issuer(), m.secretCache.GetTokenHMACKey)
|
|
|
|
timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration()
|
|
|
|
// Use NullStorage for the authorize endpoint because we do not actually want to store anything until
|
|
// the upstream callback endpoint is called later.
|
|
oauthHelperWithNullStorage := oidc.FositeOauth2Helper(
|
|
oidc.NewNullStorage(m.secretsClient, m.oidcClientsClient, oidcclientvalidator.DefaultMinBcryptCost),
|
|
issuer,
|
|
tokenHMACKeyGetter,
|
|
nil,
|
|
timeoutsConfiguration,
|
|
)
|
|
|
|
// For all the other endpoints, make another oauth helper with exactly the same settings except use real storage.
|
|
oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(
|
|
oidc.NewKubeStorage(m.secretsClient, m.oidcClientsClient, timeoutsConfiguration, oidcclientvalidator.DefaultMinBcryptCost),
|
|
issuer,
|
|
tokenHMACKeyGetter,
|
|
m.dynamicJWKSProvider,
|
|
timeoutsConfiguration,
|
|
)
|
|
|
|
var upstreamStateEncoder = dynamiccodec.New(
|
|
timeoutsConfiguration.UpstreamStateParamLifespan,
|
|
wrapGetter(incomingProvider.Issuer(), m.secretCache.GetStateEncoderHashKey),
|
|
wrapGetter(incomingProvider.Issuer(), m.secretCache.GetStateEncoderBlockKey),
|
|
)
|
|
|
|
m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer)
|
|
|
|
m.providerHandlers[(issuerHostWithPath + oidc.JWKSEndpointPath)] = jwks.NewHandler(issuer, m.dynamicJWKSProvider)
|
|
|
|
m.providerHandlers[(issuerHostWithPath + oidc.PinnipedIDPsPathV1Alpha1)] = idpdiscovery.NewHandler(m.upstreamIDPs)
|
|
|
|
m.providerHandlers[(issuerHostWithPath + oidc.AuthorizationEndpointPath)] = auth.NewHandler(
|
|
issuer,
|
|
m.upstreamIDPs,
|
|
oauthHelperWithNullStorage,
|
|
oauthHelperWithKubeStorage,
|
|
csrftoken.Generate,
|
|
pkce.Generate,
|
|
nonce.Generate,
|
|
upstreamStateEncoder,
|
|
csrfCookieEncoder,
|
|
)
|
|
|
|
m.providerHandlers[(issuerHostWithPath + oidc.CallbackEndpointPath)] = callback.NewHandler(
|
|
m.upstreamIDPs,
|
|
oauthHelperWithKubeStorage,
|
|
upstreamStateEncoder,
|
|
csrfCookieEncoder,
|
|
issuer+oidc.CallbackEndpointPath,
|
|
)
|
|
|
|
m.providerHandlers[(issuerHostWithPath + oidc.TokenEndpointPath)] = token.NewHandler(
|
|
m.upstreamIDPs,
|
|
oauthHelperWithKubeStorage,
|
|
)
|
|
|
|
m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler(
|
|
upstreamStateEncoder,
|
|
csrfCookieEncoder,
|
|
login.NewGetHandler(incomingProvider.IssuerPath()+oidc.PinnipedLoginPath),
|
|
login.NewPostHandler(issuer, m.upstreamIDPs, oauthHelperWithKubeStorage),
|
|
)
|
|
|
|
plog.Debug("oidc provider manager added or updated issuer", "issuer", issuer)
|
|
}
|
|
}
|
|
|
|
// ServeHTTP implements the http.Handler interface.
|
|
func (m *Manager) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
|
requestHandler := m.findHandler(req)
|
|
|
|
plog.Debug(
|
|
"oidc provider manager examining request",
|
|
"method", req.Method,
|
|
"host", req.Host,
|
|
"path", req.URL.Path,
|
|
"foundMatchingIssuer", requestHandler != nil,
|
|
)
|
|
|
|
if requestHandler == nil {
|
|
requestHandler = m.nextHandler // couldn't find an issuer to handle the request
|
|
}
|
|
requestHandler.ServeHTTP(resp, req)
|
|
}
|
|
|
|
func (m *Manager) findHandler(req *http.Request) http.Handler {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
return m.providerHandlers[strings.ToLower(req.Host)+"/"+req.URL.Path]
|
|
}
|
|
|
|
func wrapGetter(issuer string, getter func(string) []byte) func() []byte {
|
|
return func() []byte {
|
|
return getter(issuer)
|
|
}
|
|
}
|