If you're using xurl to authenticate with the X API and hitting this error:
OAuth2 authentication failed: Auth Error: TokenExchangeError
(cause: oauth2: "unauthorized_client" "Missing valid authorization header")
You're not alone — and the fix is a single setting in the X developer portal.
Why It Happens
xurl uses the OAuth 2.0 PKCE flow, which is designed for public clients (mobile apps, CLIs, SPAs). Public clients send credentials in the request body during token exchange.
However, X API apps are created as Confidential Clients by default. Confidential clients require credentials to be sent as an Authorization: Basic header — a different mechanism that xurl doesn't use. When xurl sends a token exchange request without that header, X rejects it with unauthorized_client.
You can confirm your app is a confidential client by base64-decoding your Client ID:
echo "YOUR_CLIENT_ID" | base64 -d
If the decoded value ends in :ci, it's a confidential client. If it ends in :na, it's a native (public) client.
The Fix
Change your X app type from Web App to Native App in the developer portal:
- Go to developer.x.com/en/portal/dashboard
- Select your app → User authentication settings → Edit
- Change App type to Native App
- Set the Callback URI to
http://localhost:8080/callback - Save
Then re-register fresh credentials and authenticate:
# Re-register with your new credentials after regenerating them in the portal
xurl auth apps add my-app --client-id YOUR_NEW_CLIENT_ID --client-secret YOUR_NEW_CLIENT_SECRET
# Store bearer token and OAuth 1.0a credentials while you're at it
xurl auth app --bearer-token YOUR_BEARER_TOKEN
xurl auth oauth1 \
--consumer-key YOUR_CONSUMER_KEY \
--consumer-secret YOUR_CONSUMER_SECRET \
--access-token YOUR_ACCESS_TOKEN \
--token-secret YOUR_ACCESS_TOKEN_SECRET
# Set as default app
xurl auth default my-app
# Now the OAuth 2.0 PKCE flow works
xurl --app my-app auth oauth2
After completing the browser-based consent flow, verify everything:
xurl auth status
xurl --auth oauth2 /2/users/me # user context
xurl --auth oauth1 /2/users/me # also works
xurl --auth app "/2/tweets/search/recent?query=hello&max_results=5" # app-only
Bonus: Why You Should Regenerate Credentials After Changing App Type
When you change the app type, X issues a new Client ID with the :na suffix. Your old Client ID (with :ci) becomes invalid for PKCE flows, so make sure to copy the new values from the Keys and Tokens tab before re-registering with xurl.
TL;DR
| App type | Client ID suffix | Works with xurl OAuth 2.0? |
|---|---|---|
| Web App / Bot |
:ci (confidential) |
❌ |
| Native App / SPA |
:na (public) |
✓ |
Change to Native App in the X developer portal, regenerate your credentials, and xurl's OAuth 2.0 flow will work as expected.
Do Web Apps support OAuth2.0?
Web App/Bot apps do support OAuth 2.0 — just not via xurl, because xurl only implements the public-client PKCE flow. Confidential clients require Authorization: Basic at the token exchange step, which no CLI tool currently handles for X.
Here's how to do it manually with curl:
Step 1 — Build the auth URL and open it in a browser:
# Generate a code verifier + challenge (PKCE is optional but recommended even for confidential clients)
VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)
CHALLENGE=$(echo -n "$VERIFIER" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=')
STATE=$(openssl rand -hex 16)
CLIENT_ID="YOUR_CLIENT_ID"
REDIRECT="http://localhost:8080/callback"
echo "https://x.com/i/oauth2/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT}&scope=tweet.read%20users.read%20offline.access&state=${STATE}&code_challenge=${CHALLENGE}&code_challenge_method=S256"
Open that URL in a browser, authorize, and grab the code=... value from the redirect URL.
Step 2 — Exchange the code using Basic Auth:
CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_SECRET="YOUR_CLIENT_SECRET"
CODE="CODE_FROM_REDIRECT"
VERIFIER="THE_VERIFIER_FROM_STEP_1"
curl -X POST https://api.x.com/2/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $(echo -n "${CLIENT_ID}:${CLIENT_SECRET}" | base64 -w 0)" \
-d "grant_type=authorization_code&code=${CODE}&redirect_uri=http://localhost:8080/callback&code_verifier=${VERIFIER}"
This returns an access_token you can then use directly with xurl:
xurl -H "Authorization: Bearer ACCESS_TOKEN" /2/users/me
When to use which:
| Use case | App type | Auth method |
|---|---|---|
| CLI / local dev | Native App | xurl oauth2 (PKCE, no secret needed) |
| Server-side app | Web App/Bot | OAuth 2.0 confidential (Basic Auth, secret stays on server) |
| Posting as yourself | Any | OAuth 1.0a (simplest for personal use) |
| Read-only public data | Any | Bearer token (app-only) |
Web App/Bot + OAuth 2.0 is designed for server-side apps where the client secret never leaves your server. For local CLI use, Native App is the correct choice. OAuth 1.0a works with either app type and is often the path of least resistance for personal/development use.
Top comments (0)