mirror of
https://github.com/fatedier/frp.git
synced 2026-04-21 08:29:10 +08:00
auth/oidc: fix eager token fetch at startup, add validation and e2e tests (#5234)
This commit is contained in:
@@ -91,8 +91,10 @@ func TestOidcAuthProviderFallsBackWhenNoExpiry(t *testing.T) {
|
||||
)
|
||||
r.NoError(err)
|
||||
|
||||
// Constructor fetches the initial token (1 request).
|
||||
// Each subsequent call should also fetch a fresh token since there is no expiry.
|
||||
// Constructor no longer fetches a token eagerly.
|
||||
// The first SetLogin triggers the adaptive probe.
|
||||
r.Equal(int32(0), requestCount.Load())
|
||||
|
||||
loginMsg := &msg.Login{}
|
||||
err = provider.SetLogin(loginMsg)
|
||||
r.NoError(err)
|
||||
@@ -105,8 +107,8 @@ func TestOidcAuthProviderFallsBackWhenNoExpiry(t *testing.T) {
|
||||
r.Equal("fresh-test-token", pingMsg.PrivilegeKey)
|
||||
}
|
||||
|
||||
// 1 initial (constructor) + 1 login + 3 pings = 5 requests
|
||||
r.Equal(int32(5), requestCount.Load(), "each call should fetch a fresh token when expires_in is missing")
|
||||
// 1 probe (login) + 3 pings = 4 requests (probe doubles as the login token fetch)
|
||||
r.Equal(int32(4), requestCount.Load(), "each call should fetch a fresh token when expires_in is missing")
|
||||
}
|
||||
|
||||
func TestOidcAuthProviderCachesToken(t *testing.T) {
|
||||
@@ -134,10 +136,10 @@ func TestOidcAuthProviderCachesToken(t *testing.T) {
|
||||
)
|
||||
r.NoError(err)
|
||||
|
||||
// Constructor eagerly fetches the initial token (1 request).
|
||||
r.Equal(int32(1), requestCount.Load())
|
||||
// Constructor no longer fetches eagerly; first SetLogin triggers the probe.
|
||||
r.Equal(int32(0), requestCount.Load())
|
||||
|
||||
// SetLogin should reuse the cached token
|
||||
// SetLogin triggers the adaptive probe and caches the token.
|
||||
loginMsg := &msg.Login{}
|
||||
err = provider.SetLogin(loginMsg)
|
||||
r.NoError(err)
|
||||
@@ -153,3 +155,99 @@ func TestOidcAuthProviderCachesToken(t *testing.T) {
|
||||
}
|
||||
r.Equal(int32(1), requestCount.Load(), "token endpoint should only be called once; cached token should be reused")
|
||||
}
|
||||
|
||||
func TestOidcAuthProviderRetriesOnInitialFailure(t *testing.T) {
|
||||
r := require.New(t)
|
||||
|
||||
var requestCount atomic.Int32
|
||||
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
n := requestCount.Add(1)
|
||||
// The oauth2 library retries once internally, so we need two
|
||||
// consecutive failures to surface an error to the caller.
|
||||
if n <= 2 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"error": "temporarily_unavailable",
|
||||
"error_description": "service is starting up",
|
||||
})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
||||
"access_token": "retry-test-token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
})
|
||||
}))
|
||||
defer tokenServer.Close()
|
||||
|
||||
// Constructor succeeds even though the IdP is "down".
|
||||
provider, err := auth.NewOidcAuthSetter(
|
||||
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
||||
v1.AuthOIDCClientConfig{
|
||||
ClientID: "test-client",
|
||||
ClientSecret: "test-secret",
|
||||
TokenEndpointURL: tokenServer.URL,
|
||||
},
|
||||
)
|
||||
r.NoError(err)
|
||||
r.Equal(int32(0), requestCount.Load())
|
||||
|
||||
// First SetLogin hits the IdP, which returns an error (after internal retry).
|
||||
loginMsg := &msg.Login{}
|
||||
err = provider.SetLogin(loginMsg)
|
||||
r.Error(err)
|
||||
r.Equal(int32(2), requestCount.Load())
|
||||
|
||||
// Second SetLogin retries and succeeds.
|
||||
err = provider.SetLogin(loginMsg)
|
||||
r.NoError(err)
|
||||
r.Equal("retry-test-token", loginMsg.PrivilegeKey)
|
||||
r.Equal(int32(3), requestCount.Load())
|
||||
|
||||
// Subsequent calls use cached token.
|
||||
pingMsg := &msg.Ping{}
|
||||
err = provider.SetPing(pingMsg)
|
||||
r.NoError(err)
|
||||
r.Equal("retry-test-token", pingMsg.PrivilegeKey)
|
||||
r.Equal(int32(3), requestCount.Load())
|
||||
}
|
||||
|
||||
func TestNewOidcAuthSetterRejectsInvalidStaticConfig(t *testing.T) {
|
||||
r := require.New(t)
|
||||
tokenServer := httptest.NewServer(http.NotFoundHandler())
|
||||
defer tokenServer.Close()
|
||||
|
||||
_, err := auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||
ClientID: "test-client",
|
||||
TokenEndpointURL: "://bad",
|
||||
})
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "auth.oidc.tokenEndpointURL")
|
||||
|
||||
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||
TokenEndpointURL: tokenServer.URL,
|
||||
})
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "auth.oidc.clientID is required")
|
||||
|
||||
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||
ClientID: "test-client",
|
||||
TokenEndpointURL: tokenServer.URL,
|
||||
AdditionalEndpointParams: map[string]string{
|
||||
"scope": "profile",
|
||||
},
|
||||
})
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead")
|
||||
|
||||
_, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{
|
||||
ClientID: "test-client",
|
||||
TokenEndpointURL: tokenServer.URL,
|
||||
Audience: "api",
|
||||
AdditionalEndpointParams: map[string]string{"audience": "override"},
|
||||
})
|
||||
r.Error(err)
|
||||
r.Contains(err.Error(), "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user