Skip to content

Commit 2ec695d

Browse files
authored
fix: avoid calling the issuer's well-known endpoint for every routes (#7394)
* fix: avoid calling the issuer's well-known endpoint for every routes with Signed-off-by: Huabing Zhao <[email protected]>
1 parent 04ee6c0 commit 2ec695d

File tree

3 files changed

+147
-3
lines changed

3 files changed

+147
-3
lines changed

internal/gatewayapi/securitypolicy.go

Lines changed: 67 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 across multiple routes and gateways.
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,47 @@ 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+
// The cache is only used within the current translation, so no need to lock it or expire entries.
1592+
type oidcDiscoveryCache struct {
1593+
entries map[string]cachedOIDCEntry
1594+
}
1595+
1596+
type cachedOIDCEntry struct {
1597+
config *OpenIDConfig
1598+
err error
1599+
}
1600+
1601+
func newOIDCDiscoveryCache() *oidcDiscoveryCache {
1602+
return &oidcDiscoveryCache{
1603+
entries: make(map[string]cachedOIDCEntry),
1604+
}
1605+
}
1606+
1607+
func (c *oidcDiscoveryCache) Get(issuer string) (*OpenIDConfig, error, bool) {
1608+
if c == nil {
1609+
return nil, nil, false
1610+
}
1611+
1612+
entry, ok := c.entries[issuer]
1613+
if !ok {
1614+
return nil, nil, false
1615+
}
1616+
1617+
return entry.config, entry.err, true
1618+
}
1619+
1620+
func (c *oidcDiscoveryCache) Set(issuer string, cfg *OpenIDConfig, err error) {
1621+
if c == nil {
1622+
return
1623+
}
1624+
1625+
c.entries[issuer] = cachedOIDCEntry{
1626+
config: cfg,
1627+
err: err,
1628+
}
1629+
}
1630+
15671631
func retryable(code int) bool {
15681632
return code >= 500 &&
15691633
(code != http.StatusNotImplemented &&

internal/gatewayapi/securitypolicy_test.go

Lines changed: 77 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,79 @@ 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+
tr.oidcDiscoveryCache = newOIDCDiscoveryCache()
807+
808+
cfg, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
809+
require.NoError(t, err)
810+
require.NotNil(t, cfg)
811+
require.Equal(t, int32(1), callCount.Load())
812+
813+
cfgCached, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
814+
require.NoError(t, err)
815+
require.NotNil(t, cfgCached)
816+
require.Equal(t, int32(1), callCount.Load(), "second fetch should use cache")
817+
818+
cfgAgain, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
819+
require.NoError(t, err)
820+
require.NotNil(t, cfgAgain)
821+
require.Equal(t, int32(1), callCount.Load(), "subsequent fetch should continue using cache")
822+
}
823+
824+
func TestTranslatorFetchEndpointsFromIssuerCacheError(t *testing.T) {
825+
var (
826+
callCount atomic.Int32
827+
server *httptest.Server
828+
)
829+
830+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
831+
if r.URL.Path != "/.well-known/openid-configuration" {
832+
http.NotFound(w, r)
833+
return
834+
}
835+
836+
callCount.Add(1)
837+
http.NotFound(w, r)
838+
}))
839+
defer server.Close()
840+
841+
tr := &Translator{GatewayControllerName: "gateway.envoyproxy.io/gatewayclass-controller"}
842+
tr.oidcDiscoveryCache = newOIDCDiscoveryCache()
843+
844+
cfg, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
845+
require.Error(t, err)
846+
require.Nil(t, cfg)
847+
require.Equal(t, int32(1), callCount.Load())
848+
849+
cfgCached, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
850+
require.Error(t, err)
851+
require.Nil(t, cfgCached)
852+
require.Equal(t, int32(1), callCount.Load(), "second fetch should use cached error")
853+
854+
cfgAfter, err := tr.fetchEndpointsFromIssuer(server.URL, nil)
855+
require.Error(t, err)
856+
require.Nil(t, cfgAfter)
857+
require.Equal(t, int32(1), callCount.Load(), "subsequent fetch should continue using cached error")
858+
}
859+
783860
// / tiny helper to build a minimal SecurityPolicy
784861
func sp(ns, name string) *egv1a1.SecurityPolicy {
785862
return &egv1a1.SecurityPolicy{

internal/gatewayapi/translator.go

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

113+
// oidcDiscoveryCache is the cache for OIDC configurations discovered from issuer's well-known URL.
114+
oidcDiscoveryCache *oidcDiscoveryCache
115+
113116
// Logger is the logger used by the translator.
114117
Logger logging.Logger
115118
}

0 commit comments

Comments
 (0)