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-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-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-05 01:06:47 +00:00
idpListGetter auth . IDPListGetter // in-memory cache of upstream IDPs
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.
func NewManager ( nextHandler http . Handler , dynamicJWKSProvider jwks . DynamicJWKSProvider , idpListGetter auth . IDPListGetter ) * 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-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-10-23 23:25:44 +00:00
wellKnownURL := strings . ToLower ( incomingProvider . IssuerHost ( ) ) + "/" + incomingProvider . IssuerPath ( ) + oidc . WellKnownEndpointPath
2020-10-17 00:51:40 +00:00
m . providerHandlers [ wellKnownURL ] = discovery . NewHandler ( incomingProvider . Issuer ( ) )
2020-10-23 23:25:44 +00:00
jwksURL := strings . ToLower ( incomingProvider . IssuerHost ( ) ) + "/" + incomingProvider . IssuerPath ( ) + oidc . JWKSEndpointPath
2020-10-17 00:51:40 +00:00
m . providerHandlers [ jwksURL ] = jwks . NewHandler ( incomingProvider . Issuer ( ) , m . dynamicJWKSProvider )
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-02 16:16:02 +00:00
oauthHelper := oidc . FositeOauth2Helper (
incomingProvider . Issuer ( ) ,
oidc . NullStorage { } ,
[ ] byte ( "some secret - must have at least 32 bytes" ) , // TODO replace this secret
nil , // TODO: inject me properly
)
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-11-05 01:06:47 +00:00
authURL := strings . ToLower ( incomingProvider . IssuerHost ( ) ) + "/" + incomingProvider . IssuerPath ( ) + oidc . AuthorizationEndpointPath
2020-11-12 23:36:59 +00:00
m . providerHandlers [ authURL ] = auth . NewHandler ( incomingProvider . Issuer ( ) , m . idpListGetter , oauthHelper , csrftoken . Generate , pkce . Generate , nonce . Generate , encoder , encoder )
2020-11-05 01:06:47 +00:00
2020-11-10 15:22:16 +00:00
plog . Debug ( "oidc provider manager added or updated issuer" , "issuer" , incomingProvider . 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
}