2020-10-08 02:18:34 +00:00
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
2020-10-08 18:28:21 +00:00
package manager
2020-10-08 02:18:34 +00:00
import (
"net/http"
2020-10-23 23:25:44 +00:00
"strings"
2020-10-08 02:18:34 +00:00
"sync"
2020-11-11 01:58:00 +00:00
"github.com/gorilla/securecookie"
2020-12-03 01:39:45 +00:00
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
2020-10-08 02:18:34 +00:00
"go.pinniped.dev/internal/oidc"
2020-11-05 01:06:47 +00:00
"go.pinniped.dev/internal/oidc/auth"
2020-11-20 15:42:43 +00:00
"go.pinniped.dev/internal/oidc/callback"
2020-11-11 20:29:14 +00:00
"go.pinniped.dev/internal/oidc/csrftoken"
2020-10-08 02:18:34 +00:00
"go.pinniped.dev/internal/oidc/discovery"
2020-10-17 00:51:40 +00:00
"go.pinniped.dev/internal/oidc/jwks"
2020-10-08 18:28:21 +00:00
"go.pinniped.dev/internal/oidc/provider"
2020-11-10 15:22:16 +00:00
"go.pinniped.dev/internal/plog"
2020-11-17 18:46:54 +00:00
"go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/pkce"
2020-10-08 02:18:34 +00:00
)
// Manager can manage multiple active OIDC providers. It acts as a request router for them.
//
// It is thread-safe.
type Manager struct {
2020-10-17 00:51:40 +00:00
mu sync . RWMutex
providers [ ] * provider . OIDCProvider
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
2020-11-13 23:59:51 +00:00
idpListGetter oidc . IDPListGetter // in-memory cache of upstream IDPs
2020-12-03 01:39:45 +00:00
secretsClient corev1client . SecretInterface
2020-10-08 02:18:34 +00:00
}
2020-10-08 18:28:21 +00:00
// NewManager returns an empty Manager.
2020-10-08 02:18:34 +00:00
// nextHandler will be invoked for any requests that could not be handled by this manager's providers.
2020-10-17 00:51:40 +00:00
// dynamicJWKSProvider will be used as an in-memory cache for per-issuer JWKS data.
2020-11-05 01:06:47 +00:00
// idpListGetter will be used as an in-memory cache of currently configured upstream IDPs.
2020-12-03 01:39:45 +00:00
func NewManager (
nextHandler http . Handler ,
dynamicJWKSProvider jwks . DynamicJWKSProvider ,
idpListGetter oidc . IDPListGetter ,
secretsClient corev1client . SecretInterface ,
) * Manager {
2020-10-17 00:51:40 +00:00
return & Manager {
providerHandlers : make ( map [ string ] http . Handler ) ,
nextHandler : nextHandler ,
dynamicJWKSProvider : dynamicJWKSProvider ,
2020-11-05 01:06:47 +00:00
idpListGetter : idpListGetter ,
2020-12-03 01:39:45 +00:00
secretsClient : secretsClient ,
2020-10-17 00:51:40 +00:00
}
2020-10-08 02:18:34 +00:00
}
// 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 OIDCProvider arguments have already been validated
// by someone else before they are passed to this method.
2020-10-08 21:40:56 +00:00
func ( m * Manager ) SetProviders ( oidcProviders ... * provider . OIDCProvider ) {
m . mu . Lock ( )
defer m . mu . Unlock ( )
m . providers = oidcProviders
m . providerHandlers = make ( map [ string ] http . Handler )
2020-10-08 02:18:34 +00:00
for _ , incomingProvider := range oidcProviders {
2020-12-03 01:39:45 +00:00
issuer := incomingProvider . Issuer ( )
issuerHostWithPath := strings . ToLower ( incomingProvider . IssuerHost ( ) ) + "/" + incomingProvider . IssuerPath ( )
2020-10-17 00:51:40 +00:00
2020-12-03 01:39:45 +00:00
fositeHMACSecretForThisProvider := [ ] byte ( "some secret - must have at least 32 bytes" ) // TODO replace this secret
2020-10-17 00:51:40 +00:00
2020-11-11 22:49:24 +00:00
// Use NullStorage for the authorize endpoint because we do not actually want to store anything until
// the upstream callback endpoint is called later.
2020-12-03 01:39:45 +00:00
oauthHelperWithNullStorage := oidc . FositeOauth2Helper ( oidc . NullStorage { } , issuer , fositeHMACSecretForThisProvider )
// 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 ) , issuer , fositeHMACSecretForThisProvider )
2020-11-05 01:06:47 +00:00
2020-11-12 23:36:59 +00:00
// TODO use different codecs for the state and the cookie, because:
// 1. we would like to state to have an embedded expiration date while the cookie does not need that
// 2. we would like each downstream provider to use different secrets for signing/encrypting the upstream state, not share secrets
// 3. we would like *all* downstream providers to use the *same* signing key for the CSRF cookie (which doesn't need to be encrypted) because cookies are sent per-domain and our issuers can share a domain name (but have different paths)
2020-11-11 22:49:24 +00:00
var encoderHashKey = [ ] byte ( "fake-hash-secret" ) // TODO replace this secret
var encoderBlockKey = [ ] byte ( "16-bytes-aaaaaaa" ) // TODO replace this secret
2020-11-11 01:58:00 +00:00
var encoder = securecookie . New ( encoderHashKey , encoderBlockKey )
encoder . SetSerializer ( securecookie . JSONEncoder { } )
2020-12-03 01:39:45 +00:00
m . providerHandlers [ ( issuerHostWithPath + oidc . WellKnownEndpointPath ) ] = discovery . NewHandler ( issuer )
m . providerHandlers [ ( issuerHostWithPath + oidc . JWKSEndpointPath ) ] = jwks . NewHandler ( issuer , m . dynamicJWKSProvider )
m . providerHandlers [ ( issuerHostWithPath + oidc . AuthorizationEndpointPath ) ] = auth . NewHandler (
issuer ,
2020-12-02 16:36:07 +00:00
m . idpListGetter ,
2020-12-03 01:39:45 +00:00
oauthHelperWithNullStorage ,
2020-12-02 16:36:07 +00:00
csrftoken . Generate ,
pkce . Generate ,
nonce . Generate ,
encoder ,
encoder ,
)
2020-11-05 01:06:47 +00:00
2020-12-03 01:39:45 +00:00
m . providerHandlers [ ( issuerHostWithPath + oidc . CallbackEndpointPath ) ] = callback . NewHandler (
2020-12-02 16:36:07 +00:00
m . idpListGetter ,
2020-12-03 01:39:45 +00:00
oauthHelperWithKubeStorage ,
2020-12-02 16:36:07 +00:00
encoder ,
encoder ,
2020-12-03 01:39:45 +00:00
issuer + oidc . CallbackEndpointPath ,
2020-12-02 16:36:07 +00:00
)
2020-11-20 15:42:43 +00:00
2020-12-03 01:39:45 +00:00
plog . Debug ( "oidc provider manager added or updated issuer" , "issuer" , issuer )
2020-10-08 02:18:34 +00:00
}
}
// ServeHTTP implements the http.Handler interface.
2020-10-08 21:40:56 +00:00
func ( m * Manager ) ServeHTTP ( resp http . ResponseWriter , req * http . Request ) {
requestHandler := m . findHandler ( req )
2020-11-10 15:22:16 +00:00
plog . Debug (
2020-10-08 21:40:56 +00:00
"oidc provider manager examining request" ,
"method" , req . Method ,
"host" , req . Host ,
"path" , req . URL . Path ,
"foundMatchingIssuer" , requestHandler != nil ,
)
2020-10-08 02:18:34 +00:00
2020-10-08 21:40:56 +00:00
if requestHandler == nil {
requestHandler = m . nextHandler // couldn't find an issuer to handle the request
2020-10-08 02:18:34 +00:00
}
2020-10-08 21:40:56 +00:00
requestHandler . ServeHTTP ( resp , req )
2020-10-08 02:18:34 +00:00
}
2020-10-08 21:40:56 +00:00
func ( m * Manager ) findHandler ( req * http . Request ) http . Handler {
m . mu . RLock ( )
defer m . mu . RUnlock ( )
2020-10-23 23:25:44 +00:00
return m . providerHandlers [ strings . ToLower ( req . Host ) + "/" + req . URL . Path ]
2020-10-08 02:18:34 +00:00
}