feat: prevent call bursts on oidc provider refresh
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good

This commit is contained in:
2025-03-18 15:51:25 +01:00
parent 59ecfa7b4e
commit 692523e54f
5 changed files with 205 additions and 115 deletions

View File

@ -13,10 +13,12 @@ import (
"time"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/cache"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"forge.cadoles.com/cadoles/bouncer/internal/syncx"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/sessions"
"github.com/pkg/errors"
@ -25,10 +27,10 @@ import (
)
type Authenticator struct {
store sessions.Store
httpTransport *http.Transport
httpClientTimeout time.Duration
oidcProviderCache cache.Cache[string, *oidc.Provider]
store sessions.Store
httpTransport *http.Transport
httpClientTimeout time.Duration
cachedOIDCProvider *syncx.CachedResource[string, *oidc.Provider]
}
func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request, layer *store.Layer) error {
@ -54,7 +56,7 @@ func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request
return errors.WithStack(err)
}
client, err := a.getClient(options, loginCallbackURL.String())
client, err := a.getClient(ctx, options, loginCallbackURL.String())
if err != nil {
return errors.WithStack(err)
}
@ -160,7 +162,7 @@ func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, lay
return nil, errors.WithStack(err)
}
client, err := a.getClient(options, loginCallbackURL.String())
client, err := a.getClient(ctx, options, loginCallbackURL.String())
if err != nil {
return nil, errors.WithStack(err)
}
@ -362,9 +364,7 @@ func (a *Authenticator) templatize(rawTemplate string, proxyName store.ProxyName
return raw.String(), nil
}
func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*Client, error) {
ctx := context.Background()
func (a *Authenticator) getClient(ctx context.Context, options *LayerOptions, redirectURL string) (*Client, error) {
transport := a.httpTransport.Clone()
if options.OIDC.TLSInsecureSkipVerify {
@ -375,28 +375,24 @@ func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*C
transport.TLSClientConfig.InsecureSkipVerify = true
}
if options.OIDC.SkipIssuerVerification {
ctx = oidc.InsecureIssuerURLContext(ctx, options.OIDC.IssuerURL)
}
httpClient := &http.Client{
Timeout: a.httpClientTimeout,
Transport: transport,
}
provider, exists := a.oidcProviderCache.Get(options.OIDC.IssuerURL)
if !exists {
var err error
ctx = oidc.ClientContext(ctx, httpClient)
ctx = oidc.ClientContext(ctx, httpClient)
if options.OIDC.SkipIssuerVerification {
ctx = oidc.InsecureIssuerURLContext(ctx, options.OIDC.IssuerURL)
}
if options.OIDC.SkipIssuerVerification {
ctx = oidc.InsecureIssuerURLContext(ctx, options.OIDC.IssuerURL)
}
logger.Debug(ctx, "refreshing oidc provider", logger.F("issuerURL", options.OIDC.IssuerURL))
provider, err = oidc.NewProvider(ctx, options.OIDC.IssuerURL)
if err != nil {
return nil, errors.Wrap(err, "could not create oidc provider")
}
a.oidcProviderCache.Set(options.OIDC.IssuerURL, provider)
provider, _, err := a.cachedOIDCProvider.Get(ctx, options.OIDC.IssuerURL)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve oidc provider")
}
client := NewClient(
@ -411,6 +407,17 @@ func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*C
return client, nil
}
func (a *Authenticator) getOIDCProvider(ctx context.Context, issuerURL string) (*oidc.Provider, error) {
logger.Debug(ctx, "refreshing oidc provider", logger.F("issuerURL", issuerURL))
provider, err := oidc.NewProvider(ctx, issuerURL)
if err != nil {
return nil, errors.Wrap(err, "could not create oidc provider")
}
return provider, nil
}
const defaultCookieNamePrefix = "_bouncer_authn_oidc"
func (a *Authenticator) getCookieName(cookieName string, proxyName store.ProxyName, layerName store.LayerName) string {
@ -421,6 +428,25 @@ func (a *Authenticator) getCookieName(cookieName string, proxyName store.ProxyNa
return strings.ToLower(fmt.Sprintf("%s_%s_%s", defaultCookieNamePrefix, proxyName, layerName))
}
func NewAuthenticator(httpTransport *http.Transport, clientTimeout time.Duration, store sessions.Store, oidcProviderCacheTimeout time.Duration) *Authenticator {
authenticator := &Authenticator{
httpTransport: httpTransport,
httpClientTimeout: clientTimeout,
store: store,
}
authenticator.cachedOIDCProvider = syncx.NewCachedResource(
ttl.NewCache(
memory.NewCache[string, *oidc.Provider](),
memory.NewCache[string, time.Time](),
oidcProviderCacheTimeout,
),
authenticator.getOIDCProvider,
)
return authenticator
}
var (
_ authn.PreAuthentication = &Authenticator{}
_ authn.Authenticator = &Authenticator{}

View File

@ -1,13 +1,8 @@
package oidc
import (
"time"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/sessions"
)
@ -15,14 +10,11 @@ const LayerType store.LayerType = "authn-oidc"
func NewLayer(store sessions.Store, funcs ...OptionFunc) *authn.Layer {
opts := NewOptions(funcs...)
return authn.NewLayer(LayerType, &Authenticator{
httpTransport: opts.HTTPTransport,
httpClientTimeout: opts.HTTPClientTimeout,
store: store,
oidcProviderCache: ttl.NewCache(
memory.NewCache[string, *oidc.Provider](),
memory.NewCache[string, time.Time](),
opts.OIDCProviderCacheTimeout,
),
}, opts.AuthnOptions...)
authenticator := NewAuthenticator(
opts.HTTPTransport,
opts.HTTPClientTimeout,
store,
opts.OIDCProviderCacheTimeout,
)
return authn.NewLayer(LayerType, authenticator, opts.AuthnOptions...)
}