hashicorp/nomad · error

failed to exchange token with provider: %v

Error message

failed to exchange token with provider: %v

What it means

Thrown by OIDCCompleteAuth when the cap/go-oidc provider's Exchange() call fails to swap the OIDC authorization code (plus signed state) for a provider token. The wrapped provider error is embedded via %v, so the underlying cause (network, TLS, invalid code, client auth) is in the message. This happens server-side in Nomad during the callback step of the OIDC login flow.

Source

Thrown at nomad/acl_endpoint.go:2800

	}

	// Retrieve the request generated in OIDCAuthURL()
	oidcReq := a.oidcRequestCache.LoadAndDelete(args.ClientNonce) // I am so done with this NONCENSE
	if oidcReq == nil {
		// note: this may happen if there is a leader election between getting
		// the auth url and completing the login flow here.
		return errors.New("no OIDC request found for client nonce")
	}

	// Generate a context with a deadline. This is passed to the OIDC provider
	// and used when making remote HTTP requests.
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(aclOIDCCallbackRequestExpiryTime))
	defer cancel()

	// Exchange the state and code for an OIDC provider token.
	oidcToken, err := oidcProvider.Exchange(ctx, oidcReq, args.State, args.Code)
	if err != nil {
		return fmt.Errorf("failed to exchange token with provider: %v", err)
	}
	if !oidcToken.Valid() {
		return errors.New("exchanged token is not valid; potentially expired or empty")
	}

	var idTokenClaims map[string]any
	if err := oidcToken.IDToken().Claims(&idTokenClaims); err != nil {
		return fmt.Errorf("failed to retrieve the ID token claims: %v", err)
	}

	var userClaims map[string]any
	if !authMethod.Config.OIDCDisableUserInfo {
		if userTokenSource := oidcToken.StaticTokenSource(); userTokenSource != nil {
			if err := oidcProvider.UserInfo(ctx, userTokenSource, idTokenClaims["sub"].(string), &userClaims); err != nil {
				return fmt.Errorf("failed to retrieve the user info claims: %v", err)
			}
		}
	}

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Check the wrapped cause in the error message; fix the specific provider complaint (invalid code, invalid client, network).
  2. Ensure the callback is hit exactly once — auth codes are single-use; restart the login flow if the browser reloaded the callback.
  3. Verify OIDCClientID, OIDCClientSecret / OIDCClientAssertion (client-id, key-source) in the ACL auth method config match the IdP registration.
  4. From a Nomad server, verify network/DNS/TLS reachability of the provider's discovery URL.
  5. Check clock skew on Nomad servers and the IdP (NTP).

Example fix

// before: reusing a stale callback URL
curl 'https://nomad/ui/oidc/callback?code=OLD_CODE&state=OLD_STATE'
// after: start a fresh login
# navigate to https://nomad/ui/settings/tokens and click 'Sign in with SSO' again
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling Login/complete auth, verify config + reachability
for _, u := range []string{method.Config.DiscoveryURL} {
  resp, err := http.Get(u + "/.well-known/openid-configuration")
  if err != nil { return fmt.Errorf("provider unreachable: %w", err) }
  resp.Body.Close()
}

Try / catch

token, err := oidcProvider.Exchange(ctx, req, state, code)
if err != nil {
    return fmt.Errorf("failed to exchange token with provider: %v", err)
}
// inspect err string for 'invalid_grant' → restart flow; 'connection refused' → check network

Prevention

When it happens

Trigger: ACL.oidcClient.Exchange(ctx, oidcReq, args.State, args.Code) returns an error: the auth code was already used or expired, the state does not match, the provider is unreachable, or the client id/secret (or client assertion) is wrong.

Common situations: User reloads or double-submits the callback URL (code replay), clock skew between Nomad and the IdP, wrong OIDCClientID/OIDCClientSecret in the auth method config, IdP behind a firewall/DNS failure from the Nomad servers, provider token endpoint rejecting private_key_jwt client assertion.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/4d5e48c2477c5b21. Report an issue: GitHub.