Skip to content

Reproduction and documentation for oauth2 AuthStyleUnknown probe error swallowing

License

Notifications You must be signed in to change notification settings

masonelmore/authstyle-unknown

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

authstyle-unknown

Reproduction of a bug in Go's golang.org/x/oauth2 library. When AuthStyleUnknown triggers the auth style probe, the first token request's error is silently discarded.

Table of Contents

The Problem

When Endpoint.AuthStyle is AuthStyleUnknown (the default), RetrieveToken probes the token endpoint by trying two credential delivery methods:

  1. Client credentials in the Authorization header (AuthStyleInHeader)
  2. If that fails for any reason, retry with credentials in form params (AuthStyleInParams)

The second attempt overwrites the error from the first:

token, err := doTokenRoundTrip(ctx, req)
if err != nil && needsAuthStyleProbe {
    // If we get an error, assume the server wants the
    // clientID & clientSecret in a different form.
    authStyle = AuthStyleInParams // the second way we'll try
    req, _ = newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
    token, err = doTokenRoundTrip(ctx, req)
}

The probe works correctly when the provider rejects the wrong auth style without consuming the authorization code: the first attempt fails harmlessly, the second succeeds, and the style is cached for future requests.

The bug surfaces when:

  1. The provider accepts header auth (the first method the probe tries)
  2. The request fails for a reason unrelated to auth style (misconfiguration, expired key, invalid grant, etc.)
  3. The provider consumes the authorization code during the failed request
  4. The probe retries with params auth, which also fails
  5. Only the second error is returned; the first error is silently discarded

The caller sees a misleading error (e.g., "code already redeemed") instead of the actual problem (e.g., "missing signing key").

Configuration errors are most likely during initial setup, which is when users need clear error messages the most, and when this bug is most likely to bite.

Reproduction

Two self-contained reproductions, each in their own directory:

Quick demo with the mock:

cd mock-test && go run .
-> Request 1: credentials in Authorization header
-> Request 2: credentials in form params (probe fallback)
Exchange error: oauth2: "invalid_grant" "Authorization code has already been redeemed"

The mock returns "signing key not configured" on the first request (header auth) and "code already redeemed" on the second (params fallback). Only the second error reaches the caller.

Potential Fixes

Approaches for RetrieveToken when both probe attempts fail:

1. Join both errors

Use errors.Join so the caller sees both. This is what #786 suggests.

 token, err := doTokenRoundTrip(ctx, req)
 if err != nil && needsAuthStyleProbe {
+    headerErr := err
     authStyle = AuthStyleInParams
     req, _ = newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
     token, err = doTokenRoundTrip(ctx, req)
+    if err != nil {
+        err = errors.Join(headerErr, err)
+    }
 }

2. Wrap the first error with fallback context

Return both errors wrapped with context about the probe sequence.

 token, err := doTokenRoundTrip(ctx, req)
 if err != nil && needsAuthStyleProbe {
+    headerErr := err
     authStyle = AuthStyleInParams
     req, _ = newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
     token, err = doTokenRoundTrip(ctx, req)
+    if err != nil {
+        err = fmt.Errorf("auth style probe failed: header: %w; params: %w", headerErr, err)
+    }
 }

Downstream impact

Both approaches change the error that flows through oauth2.Exchange() to callers.

  • err.Error() string: Changes with both approaches.
  • errors.As(err, &oauth2.RetrieveError{}): Currently unwraps to the second error's RetrieveError. With either fix, it unwraps to the first (real) error instead. This is the point, but it's a behavior change for anyone keying on the fallback response.

3. Remove auto-detection entirely

Require callers to set AuthStyle explicitly. This would be a breaking change, but #718 and #535 show other problems caused by the probe.

Background

This was discovered while configuring SSO for Portainer with Microsoft Entra ID. The token request failed due to a missing signing key in the app registration (AADSTS50146), but the only error surfaced in Portainer's logs was that the authorization code had already been redeemed (AADSTS54005).

The Portainer team shipped an AuthStyle configuration option as a workaround, allowing users to skip the probe entirely.

A request to set AuthStyle explicitly on the library's Azure endpoint was closed as not planned, since Entra ID supports both header and params auth per RFC 6749. That's correct for style detection, but it doesn't address the error swallowing when the correctly-styled request fails for other reasons.

See journal.md for a first-person account of debugging this.

Links

Upstream:

Portainer:

About

Reproduction and documentation for oauth2 AuthStyleUnknown probe error swallowing

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors