Skip to content

Commit 18b2baa

Browse files
committed
fix: avoid calling the issuer's well-known endpoint for every routes with
Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
1 parent 63882d7 commit 18b2baa

File tree

3 files changed

+144
-3
lines changed

3 files changed

+144
-3
lines changed

internal/gatewayapi/securitypolicy.go

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343
defaultForwardAccessToken = false
4444
defaultRefreshToken = true
4545
defaultPassThroughAuthHeader = false
46+
defaultOIDCHTTPTimeout = 5 * time.Second
4647

4748
// nolint: gosec
4849
oidcHMACSecretName = "envoy-oidc-hmac"
@@ -55,6 +56,10 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security
5556
resources *resource.Resources,
5657
xdsIR resource.XdsIRMap,
5758
) []*egv1a1.SecurityPolicy {
59+
// Cache is only reused during one translation, so no need to lock it.
60+
// The failed fetches will be retried in the next translation when the provider resources are reconciled again.
61+
t.oidcDiscoveryCache = newOIDCDiscoveryCache()
62+
5863
// SecurityPolicies are already sorted by the provider layer
5964

6065
// First build a map out of the routes and gateways for faster lookup since users might have thousands of routes or more.
@@ -1419,7 +1424,7 @@ func (t *Translator) buildOIDCProvider(policy *egv1a1.SecurityPolicy, resources
14191424
// EG assumes that the issuer url uses the same protocol and CA as the token endpoint.
14201425
// If we need to support different protocols or CAs, we need to add more fields to the OIDCProvider CRD.
14211426
if provider.TokenEndpoint == nil || provider.AuthorizationEndpoint == nil {
1422-
discoveredConfig, err := fetchEndpointsFromIssuer(provider.Issuer, providerTLS)
1427+
discoveredConfig, err := t.fetchEndpointsFromIssuer(provider.Issuer, providerTLS)
14231428
if err != nil {
14241429
return nil, err
14251430
}
@@ -1507,7 +1512,25 @@ func (o *OpenIDConfig) validate() error {
15071512
return nil
15081513
}
15091514

1510-
func fetchEndpointsFromIssuer(issuerURL string, providerTLS *ir.TLSUpstreamConfig) (*OpenIDConfig, error) {
1515+
func (t *Translator) fetchEndpointsFromIssuer(issuerURL string, providerTLS *ir.TLSUpstreamConfig) (*OpenIDConfig, error) {
1516+
if config, cachedErr, ok := t.oidcDiscoveryCache.Get(issuerURL); ok {
1517+
if cachedErr != nil {
1518+
return nil, cachedErr
1519+
}
1520+
return config, nil
1521+
}
1522+
1523+
config, err := discoverEndpointsFromIssuer(issuerURL, providerTLS)
1524+
if err != nil {
1525+
t.oidcDiscoveryCache.Set(issuerURL, nil, err)
1526+
return nil, err
1527+
}
1528+
1529+
t.oidcDiscoveryCache.Set(issuerURL, config, nil)
1530+
return config, nil
1531+
}
1532+
1533+
func discoverEndpointsFromIssuer(issuerURL string, providerTLS *ir.TLSUpstreamConfig) (*OpenIDConfig, error) {
15111534
var (
15121535
tlsConfig *tls.Config
15131536
err error
@@ -1519,7 +1542,7 @@ func fetchEndpointsFromIssuer(issuerURL string, providerTLS *ir.TLSUpstreamConfi
15191542
}
15201543
}
15211544

1522-
client := &http.Client{}
1545+
client := &http.Client{Timeout: defaultOIDCHTTPTimeout}
15231546
if tlsConfig != nil {
15241547
client.Transport = &http.Transport{
15251548
TLSClientConfig: tlsConfig,
@@ -1564,6 +1587,46 @@ func fetchEndpointsFromIssuer(issuerURL string, providerTLS *ir.TLSUpstreamConfi
15641587
return &config, nil
15651588
}
15661589

1590+
// oidcDiscoveryCache is a cache for auto-discovered OIDC configurations from the issuer's well-known URL.
1591+
type oidcDiscoveryCache struct {
1592+
entries map[string]cachedOIDCEntry
1593+
}
1594+
1595+
type cachedOIDCEntry struct {
1596+
config *OpenIDConfig
1597+
err error
1598+
}
1599+
1600+
func newOIDCDiscoveryCache() *oidcDiscoveryCache {
1601+
return &oidcDiscoveryCache{
1602+
entries: make(map[string]cachedOIDCEntry),
1603+
}
1604+
}
1605+
1606+
func (c *oidcDiscoveryCache) Get(issuer string) (*OpenIDConfig, error, bool) {
1607+
if c == nil {
1608+
return nil, nil, false
1609+
}
1610+
1611+
entry, ok := c.entries[issuer]
1612+
if !ok {
1613+
return nil, nil, false
1614+
}
1615+
1616+
return entry.config, entry.err, true
1617+
}
1618+
1619+
func (c *oidcDiscoveryCache) Set(issuer string, cfg *OpenIDConfig, err error) {
1620+
if c == nil {
1621+
return
1622+
}
1623+
1624+
c.entries[issuer] = cachedOIDCEntry{
1625+
config: cfg,
1626+
err: err,
1627+
}
1628+
}
1629+
15671630
func retryable(code int) bool {
15681631
return code >= 500 &&
15691632
(code != http.StatusNotImplemented &&

internal/gatewayapi/securitypolicy_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
package gatewayapi
77

88
import (
9+
"fmt"
10+
"net/http"
11+
"net/http/httptest"
912
"regexp"
13+
"sync/atomic"
1014
"testing"
1115

1216
"github.com/stretchr/testify/assert"
@@ -780,6 +784,77 @@ func TestValidateCIDRs_ErrorOnBadCIDR(t *testing.T) {
780784
}
781785
}
782786

787+
func TestTranslatorFetchEndpointsFromIssuerCache(t *testing.T) {
788+
var (
789+
callCount atomic.Int32
790+
server *httptest.Server
791+
)
792+
793+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
794+
if r.URL.Path != "/.well-known/openid-configuration" {
795+
http.NotFound(w, r)
796+
return
797+
}
798+
799+
callCount.Add(1)
800+
w.Header().Set("Content-Type", "application/json")
801+
_, _ = fmt.Fprintf(w, `{"token_endpoint":%q,"authorization_endpoint":%q}`, server.URL+"/token", server.URL+"/authorize")
802+
}))
803+
defer server.Close()
804+
805+
tr := &Translator{GatewayControllerName: "gateway.envoyproxy.io/gatewayclass-controller"}
806+
807+
cfg, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
808+
require.NoError(t, err)
809+
require.NotNil(t, cfg)
810+
require.Equal(t, int32(1), callCount.Load())
811+
812+
cfgCached, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
813+
require.NoError(t, err)
814+
require.NotNil(t, cfgCached)
815+
require.Equal(t, int32(1), callCount.Load(), "second fetch should use cache")
816+
817+
cfgAgain, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
818+
require.NoError(t, err)
819+
require.NotNil(t, cfgAgain)
820+
require.Equal(t, int32(1), callCount.Load(), "subsequent fetch should continue using cache")
821+
}
822+
823+
func TestTranslatorFetchEndpointsFromIssuerCacheError(t *testing.T) {
824+
var (
825+
callCount atomic.Int32
826+
server *httptest.Server
827+
)
828+
829+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
830+
if r.URL.Path != "/.well-known/openid-configuration" {
831+
http.NotFound(w, r)
832+
return
833+
}
834+
835+
callCount.Add(1)
836+
http.NotFound(w, r)
837+
}))
838+
defer server.Close()
839+
840+
tr := &Translator{GatewayControllerName: "gateway.envoyproxy.io/gatewayclass-controller"}
841+
842+
cfg, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
843+
require.Error(t, err)
844+
require.Nil(t, cfg)
845+
require.Equal(t, int32(1), callCount.Load())
846+
847+
cfgCached, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
848+
require.Error(t, err)
849+
require.Nil(t, cfgCached)
850+
require.Equal(t, int32(1), callCount.Load(), "second fetch should use cached error")
851+
852+
cfgAfter, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
853+
require.Error(t, err)
854+
require.Nil(t, cfgAfter)
855+
require.Equal(t, int32(1), callCount.Load(), "subsequent fetch should continue using cached error")
856+
}
857+
783858
// / tiny helper to build a minimal SecurityPolicy
784859
func sp(ns, name string) *egv1a1.SecurityPolicy {
785860
return &egv1a1.SecurityPolicy{

internal/gatewayapi/translator.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ type Translator struct {
106106
// and reuses the specified value.
107107
ListenerPortShiftDisabled bool
108108

109+
// oidcDiscoveryCache is the cache for OIDC configurations discovered from issuer's well-known URL.
110+
oidcDiscoveryCache *oidcDiscoveryCache
111+
109112
// Logger is the logger used by the translator.
110113
Logger logging.Logger
111114
}

0 commit comments

Comments
 (0)