Authentication with OAuth 2.0 (PKCE)

Updated October 20, 2026

App-only vs user-context, and how the PKCE flow works without a client secret.

Two kinds of request

Type Credential Can do
App-only Bearer token Read public data
User-context OAuth 2.0 access token Post, like, read private

The PKCE flow

PKCE lets a public app authenticate a user without embedding a secret. Send the user to the authorize endpoint with a code challenge:

// 1. Make a code verifier + challenge
const verifier = randomString(64);
const challenge = base64url(await sha256(verifier));

// 2. Send the user to authorize
location = "https://x.com/i/oauth2/authorize?response_type=code" +
  `&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT}` +
  `&scope=tweet.read tweet.write users.read&state=xyz` +
  `&code_challenge=${challenge}&code_challenge_method=S256`;

After the user approves, X redirects back with a code. Exchange it (with the original verifier) for an access token at https://api.x.com/2/oauth2/token. The auth code expires in 30 seconds, so exchange it immediately.

Store refresh tokens server-side. Never ship your client secret in a browser bundle.

Frequently asked questions

What is PKCE?

Proof Key for Code Exchange. It lets a public app authenticate a user without embedding a client secret, using a code verifier and challenge.

How long does an access token last?

Access tokens are valid for two hours by default. Add the offline.access scope to receive a refresh token.