Fix: `xurl` OAuth 2.0 Fails with 'unauthorized_client' on X API

Published: (February 23, 2026 at 07:22 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

If you’re using xurl to authenticate with the X API and see this error:

OAuth2 authentication failed: Auth Error: TokenExchangeError
(cause: oauth2: "unauthorized_client" "Missing valid authorization header")

the fix is a single setting in the X developer portal.

Why It Happens

xurl uses the OAuth 2.0 PKCE flow, which is intended for public clients (mobile apps, CLIs, SPAs). Public clients send credentials in the request body during token exchange.

X API apps are created as Confidential Clients by default. Confidential clients must send credentials via an Authorization: Basic header—a mechanism that xurl does not use. When xurl sends a token‑exchange request without that header, X rejects it with unauthorized_client.

You can confirm your app type by base64‑decoding the Client ID:

echo "YOUR_CLIENT_ID" | base64 -d

If the decoded value ends with :ci, it’s a confidential client; if it ends with :na, it’s a native (public) client.

The Fix

  1. Change your X app type from Web App to Native App in the developer portal:

  2. In the portal:
    Select your app → User authentication settingsEdit

    • Set App type to Native App
    • Set Callback URI to http://localhost:8080/callback
    • Save
  3. Re‑register fresh credentials and authenticate with xurl:

# Re‑register with 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 (optional)
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

# Run the PKCE flow
xurl --app my-app auth oauth2
  1. After completing the browser‑based consent flow, verify everything works:
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: Regenerate Credentials After Changing App Type

When you switch the app type, X issues a new Client ID with the :na suffix. The old :ci ID becomes invalid for PKCE flows, so copy the new values from the Keys and Tokens tab before re‑registering with xurl.

TL;DR

App typeClient ID suffixWorks 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 OAuth 2.0?

Web App/Bot apps do support OAuth 2.0, but not via xurl, because xurl only implements the public‑client PKCE flow. Confidential clients require an Authorization: Basic header at the token‑exchange step, which most CLI tools (including xurl) don’t handle for X.

Manual OAuth 2.0 with curl

Step 1 – Build the auth URL and open it in a browser:

# Generate a code verifier + challenge (PKCE is optional but recommended)
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 the printed URL in a browser, authorize, and copy 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=${REDIRECT}&code_verifier=${VERIFIER}"

The response contains an access_token you can use directly with xurl:

xurl -H "Authorization: Bearer ACCESS_TOKEN" /2/users/me

When to Use Which

Use caseApp typeAuth method
CLI / local devNative Appxurl oauth2 (PKCE, no secret)
Server‑side appWeb App / BotOAuth 2.0 confidential (Basic Auth, secret stays on server)
Posting as yourselfAnyOAuth 1.0a (simplest for personal use)
Read‑only public dataAnyBearer token (app‑only)

Web App/Bot + OAuth 2.0 is designed for server‑side applications where the client secret never leaves your server. For local CLI use, a Native App is the correct choice. OAuth 1.0a works with either app type and is often the path of least resistance for personal or development use.

0 views
Back to Blog

Related posts

Read more »