Authentication with OAuth 2.0 (PKCE)
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.