@@ -269,10 +269,10 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) {
269269 },
270270 },
271271 {
272- d : "fails and returns 503 Service Unavailable error due to the unavailability of the upstream service " ,
272+ d : "fails and returns not available after the resilient client retries a persistent 503 " ,
273273 r : upstreamFailure ,
274274 expectErr : helper .ErrUpstreamServiceNotAvailable (),
275- config : json .RawMessage (`{}` ),
275+ config : json .RawMessage (`{"retry":{"give_up_after":"10ms"} }` ),
276276 token_url : "" ,
277277 setup : func (t * testing.T , h * http.ServeMux , _ json.RawMessage ) {
278278 h .HandleFunc ("POST /oauth2/token" , func (w http.ResponseWriter , r * http.Request ) {
@@ -282,10 +282,13 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) {
282282 },
283283 },
284284 {
285- d : "fails and returns 504 Gateway Timeout error due to upstream service timeout" ,
285+ // The resilient client retries the 504 and, once retries are
286+ // exhausted, surfaces the upstream as not available (the specific
287+ // status is no longer available after give-up).
288+ d : "fails and returns not available after the resilient client retries a persistent 504" ,
286289 r : upstreamFailure ,
287- expectErr : helper .ErrUpstreamServiceTimeout (),
288- config : json .RawMessage (`{}` ),
290+ expectErr : helper .ErrUpstreamServiceNotAvailable (),
291+ config : json .RawMessage (`{"retry":{"give_up_after":"10ms"} }` ),
289292 token_url : "" ,
290293 setup : func (t * testing.T , h * http.ServeMux , _ json.RawMessage ) {
291294 h .HandleFunc ("POST /oauth2/token" , func (w http.ResponseWriter , r * http.Request ) {
@@ -295,10 +298,12 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) {
295298 },
296299 },
297300 {
298- d : "fails and returns 500 Internal Server Error error due to an unexpected error in the upstream service" ,
301+ // The resilient client retries the 500 and, once retries are
302+ // exhausted, surfaces the upstream as not available.
303+ d : "fails and returns not available after the resilient client retries a persistent 500" ,
299304 r : upstreamFailure ,
300- expectErr : helper .ErrUpstreamServiceInternalServerError (),
301- config : json .RawMessage (`{}` ),
305+ expectErr : helper .ErrUpstreamServiceNotAvailable (),
306+ config : json .RawMessage (`{"retry":{"give_up_after":"10ms"} }` ),
302307 token_url : "" ,
303308 setup : func (t * testing.T , h * http.ServeMux , _ json.RawMessage ) {
304309 h .HandleFunc ("POST /oauth2/token" , func (w http.ResponseWriter , r * http.Request ) {
@@ -398,3 +403,45 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) {
398403 require .NoError (t , a .Validate (json .RawMessage (`{"token_url":"` + ts .URL + "/oauth2/token" + `","retry":{"give_up_after":"3s", "max_delay":"100ms"}}` )))
399404 })
400405}
406+
407+ // TestAuthenticatorOAuth2ClientCredentialsHonorsMaxDelayTimeout is a regression
408+ // test for two coupled bugs that stopped retry.max_delay from being enforced as
409+ // the outbound HTTP timeout for the token request:
410+ //
411+ // - The parsed max_delay duration was multiplied by an extra factor of
412+ // time.Millisecond, inflating the timeout by 1e6 (a 50ms setting became
413+ // ~13.9 hours).
414+ // - The resilient client that carries the timeout (a.client) was never passed
415+ // to the OAuth2 token exchange; a method value (c.Client) was passed
416+ // instead, which the OAuth2 library ignores, so the default client with no
417+ // timeout was used.
418+ //
419+ // With either bug present, a call to a slow token endpoint blocks until the
420+ // server responds; with both fixed, it times out promptly.
421+ func TestAuthenticatorOAuth2ClientCredentialsHonorsMaxDelayTimeout (t * testing.T ) {
422+ t .Parallel ()
423+ reg := internal .NewRegistry (t , configx .SkipValidation ())
424+ a , err := reg .PipelineAuthenticator ("oauth2_client_credentials" )
425+ require .NoError (t , err )
426+
427+ slow := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
428+ time .Sleep (2 * time .Second )
429+ w .Header ().Set ("Content-Type" , "application/json" )
430+ _ , _ = w .Write ([]byte (`{"access_token":"foo","token_type":"bearer"}` ))
431+ }))
432+ t .Cleanup (slow .Close )
433+
434+ r := & http.Request {Header : http.Header {}}
435+ r .SetBasicAuth ("client" , "secret" )
436+ config , err := sjson .SetBytes (
437+ []byte (`{"retry":{"max_delay":"50ms","give_up_after":"10ms"}}` ), "token_url" , slow .URL + "/oauth2/token" )
438+ require .NoError (t , err )
439+
440+ start := time .Now ()
441+ err = a .Authenticate (r , new (authn.AuthenticationSession ), config , nil )
442+ elapsed := time .Since (start )
443+
444+ require .Error (t , err )
445+ assert .Less (t , elapsed , time .Second ,
446+ "the configured 50ms max_delay must time the token request out; the inflated value would block ~2s on the slow server" )
447+ }
0 commit comments