2023-04-10 15:36:59 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2023-01-13 13:14:50 +00:00
|
|
|
package oidc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/hashicorp/cap/oidc"
|
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
|
|
)
|
|
|
|
|
|
|
|
// providerConfig returns the OIDC provider configuration for an OIDC
|
|
|
|
// auth-method.
|
|
|
|
func providerConfig(authMethod *structs.ACLAuthMethod) (*oidc.Config, error) {
|
|
|
|
var algs []oidc.Alg
|
|
|
|
if len(authMethod.Config.SigningAlgs) > 0 {
|
|
|
|
for _, alg := range authMethod.Config.SigningAlgs {
|
|
|
|
algs = append(algs, oidc.Alg(alg))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
algs = []oidc.Alg{oidc.RS256}
|
|
|
|
}
|
|
|
|
|
|
|
|
return oidc.NewConfig(
|
|
|
|
authMethod.Config.OIDCDiscoveryURL,
|
|
|
|
authMethod.Config.OIDCClientID,
|
|
|
|
oidc.ClientSecret(authMethod.Config.OIDCClientSecret),
|
|
|
|
algs,
|
|
|
|
authMethod.Config.AllowedRedirectURIs,
|
|
|
|
oidc.WithAudiences(authMethod.Config.BoundAudiences...),
|
|
|
|
oidc.WithProviderCA(strings.Join(authMethod.Config.DiscoveryCaPem, "\n")),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ProviderCache is a cache for OIDC providers. OIDC providers are something
|
|
|
|
// you don't want to recreate per-request since they make HTTP requests
|
2023-01-17 08:45:17 +00:00
|
|
|
// when they're constructed.
|
2023-01-13 13:14:50 +00:00
|
|
|
//
|
|
|
|
// The ProviderCache purges a provider under two scenarios: (1) the
|
|
|
|
// provider config is updated, and it is different and (2) after a set
|
|
|
|
// amount of time (see cacheExpiry for value) in case the remote provider
|
|
|
|
// configuration changed.
|
|
|
|
type ProviderCache struct {
|
|
|
|
providers map[string]*oidc.Provider
|
|
|
|
mu sync.RWMutex
|
|
|
|
|
|
|
|
// cancel is used to trigger cancellation of any routines when the cache
|
|
|
|
// has been informed its parent process is exiting.
|
|
|
|
cancel context.CancelFunc
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewProviderCache should be used to initialize a provider cache. This
|
|
|
|
// will start up background resources to manage the cache.
|
|
|
|
func NewProviderCache() *ProviderCache {
|
|
|
|
|
|
|
|
// Create a context, so a server that is shutting down can correctly
|
|
|
|
// shut down the cache loop and OIDC provider background processes.
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
|
|
|
result := &ProviderCache{
|
|
|
|
providers: map[string]*oidc.Provider{},
|
|
|
|
cancel: cancel,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start the cleanup timer
|
|
|
|
go result.runCleanupLoop(ctx)
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get returns the OIDC provider for the given auth method configuration.
|
|
|
|
// This will initialize the provider if it isn't already in the cache or
|
|
|
|
// if the configuration changed.
|
|
|
|
func (c *ProviderCache) Get(authMethod *structs.ACLAuthMethod) (*oidc.Provider, error) {
|
|
|
|
|
|
|
|
// No matter what we'll use the config of the arg method since we'll
|
|
|
|
// use it to compare to existing (if exists) or initialize a new provider.
|
|
|
|
oidcCfg, err := providerConfig(authMethod)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get any current provider for the named auth-method.
|
|
|
|
var (
|
|
|
|
current *oidc.Provider
|
|
|
|
ok bool
|
|
|
|
)
|
|
|
|
|
|
|
|
c.mu.RLock()
|
2023-01-17 08:45:17 +00:00
|
|
|
current, ok = c.providers[authMethod.Name]
|
2023-01-13 13:14:50 +00:00
|
|
|
c.mu.RUnlock()
|
|
|
|
|
|
|
|
// If we have a current value, we want to compare hashes to detect changes.
|
|
|
|
if ok {
|
|
|
|
currentHash, err := current.ConfigHash()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
newHash, err := oidcCfg.Hash()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the hashes match, this is can be classed as a cache hit.
|
|
|
|
if currentHash == newHash {
|
|
|
|
return current, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we made it here, the provider isn't in the cache OR the config
|
|
|
|
// changed. We therefore, need to initialize a new provider.
|
|
|
|
newProvider, err := oidc.NewProvider(oidcCfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
c.mu.Lock()
|
|
|
|
defer c.mu.Unlock()
|
|
|
|
|
|
|
|
// If we have an old provider, clean up resources.
|
|
|
|
if current != nil {
|
|
|
|
current.Done()
|
|
|
|
}
|
|
|
|
|
|
|
|
c.providers[authMethod.Name] = newProvider
|
|
|
|
|
|
|
|
return newProvider, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete force deletes a single auth method from the cache by name.
|
|
|
|
func (c *ProviderCache) Delete(name string) {
|
|
|
|
c.mu.Lock()
|
|
|
|
defer c.mu.Unlock()
|
|
|
|
|
|
|
|
p, ok := c.providers[name]
|
|
|
|
if ok {
|
|
|
|
p.Done()
|
|
|
|
delete(c.providers, name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shutdown stops any long-lived cache process and informs each OIDC provider
|
|
|
|
// that they are done. This should be called whenever the Nomad server is
|
|
|
|
// shutting down.
|
|
|
|
func (c *ProviderCache) Shutdown() {
|
|
|
|
c.cancel()
|
|
|
|
c.clear()
|
|
|
|
}
|
|
|
|
|
|
|
|
// runCleanupLoop runs an infinite loop that clears the cache every cacheExpiry
|
|
|
|
// duration. This ensures that we force refresh our provider info periodically
|
|
|
|
// in case anything changes.
|
|
|
|
func (c *ProviderCache) runCleanupLoop(ctx context.Context) {
|
|
|
|
|
|
|
|
ticker := time.NewTicker(cacheExpiry)
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
|
|
|
|
// We could be more clever and do a per-entry expiry but Nomad won't
|
|
|
|
// have more than one ot two auth methods configured, therefore it's
|
|
|
|
// not worth the added complexity.
|
|
|
|
case <-ticker.C:
|
|
|
|
c.clear()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// clear is called to delete all the providers in the cache.
|
|
|
|
func (c *ProviderCache) clear() {
|
|
|
|
c.mu.Lock()
|
|
|
|
defer c.mu.Unlock()
|
|
|
|
for _, p := range c.providers {
|
|
|
|
p.Done()
|
|
|
|
}
|
|
|
|
c.providers = map[string]*oidc.Provider{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// cacheExpiry is the duration after which the provider cache is reset.
|
|
|
|
const cacheExpiry = 6 * time.Hour
|