@molecule/api-oauth-twitter
Provider bond · auth · API (Node) · v1.2.0 · Apache-2.0
Twitter OAuth provider for molecule.dev.
npm install @molecule/api-oauth-twitternpm · Source on GitHub · Implements @molecule/api-oauth
How it works
@molecule/api-oauth-twitter is a provider bond on the API (Node) side: it implements the auth core interface (@molecule/api-oauth) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
// npm install @molecule/api-oauth-twitter --workspace=api
// Env: OAUTH_TWITTER_CLIENT_ID, OAUTH_TWITTER_CLIENT_SECRET (server-only).
import { bond } from '@molecule/api-bond'
import { getAuthorizeUrl, serverName, verify } from '@molecule/api-oauth-twitter'
// api/src/bonds/oauth-twitter.ts, called from setupBonds() — the `oauth`
// category is NAMED: providers coexist, and the bond name IS the
// serverName ('twitter').
bond('oauth', serverName, { serverName, verify, getAuthorizeUrl })
// Redirect: X matches callback URIs exactly — register the app origin +
// each OAuth-starting page path (e.g. {origin} and {origin}/login) under
// "User authentication settings"; the login button just links to
// GET /users/oauth/twitter.Works with: @molecule/api-bond, @molecule/api-http, @molecule/api-oauth, @molecule/api-secrets
Secrets: OAUTH_TWITTER_CLIENT_ID, OAUTH_TWITTER_CLIENT_SECRET
Reference
Twitter OAuth provider for molecule.dev.
Setup
-
Log into Twitter (or sign up).
-
Open the Twitter Developer Portal.
-
Create a new project/app and follow Twitter's steps. You may need to verify your account with a phone number.
-
Fill out your app's information and you should eventually arrive at a screen with an ID, secret, and bearer token. These are not the OAuth client ID and secret, but you should still store them somewhere safe.
-
Upload your app's logo and description if necessary.
5.1. Return to Twitter's Developer Portal Dashboard.
5.2. Open your app under "Projects & Apps".
5.3. Click the "Edit" button.
5.4. Upload an image.
5.5. Update your app's description.
5.6. Click the "Save" button.
-
Under "User authentication settings", click the "Set up" button to begin enabling OAuth.
-
Enable "OAuth 2.0".
-
Choose "Web App" for "Type of App".
-
Under "Callback URI / Redirect URL", add entries for BOTH your app origin and each page your app starts OAuth from (X matches these exactly, and the API sends
redirect_uri = APP_ORIGIN + the initiating page's path):-
For development:
http://localhost:3000andhttp://localhost:3000/login -
For production: your app's origin (typically your API's
APP_ORIGINenvironment variable) and the same origin +/login(plus any other OAuth-initiating page paths).
-
-
Fill out the remaining information as necessary and click the "Save" button.
-
You should be taken to a screen containing your OAuth client ID and secret.
11.1. Set the client ID to your API's
OAUTH_TWITTER_CLIENT_IDenvironment variable.11.2. Set the client secret to your API's
OAUTH_TWITTER_CLIENT_SECRETenvironment variable. -
Restart your API and/or rebuild your app so that they have the environment variables.
Your users should now be able to log in via Twitter!
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
// npm install @molecule/api-oauth-twitter --workspace=api
// Env: OAUTH_TWITTER_CLIENT_ID, OAUTH_TWITTER_CLIENT_SECRET (server-only).
import { bond } from '@molecule/api-bond'
import { getAuthorizeUrl, serverName, verify } from '@molecule/api-oauth-twitter'
// api/src/bonds/oauth-twitter.ts, called from setupBonds() — the `oauth`
// category is NAMED: providers coexist, and the bond name IS the
// serverName ('twitter').
bond('oauth', serverName, { serverName, verify, getAuthorizeUrl })
// Redirect: X matches callback URIs exactly — register the app origin +
// each OAuth-starting page path (e.g. {origin} and {origin}/login) under
// "User authentication settings"; the login button just links to
// GET /users/oauth/twitter.
Type
provider
Installation
npm install @molecule/api-oauth-twitter @molecule/api-bond @molecule/api-http @molecule/api-oauth @molecule/api-secrets
API
Interfaces
OAuthUserProps
The properties returned when verifying an OAuth code.
interface OAuthUserProps {
/**
* An alphanumeric username derived from the OAuth provider.
*
* Format: `{provider_username}@{provider_name}`
*/
username: string
/**
* The user's display name from the OAuth provider.
*/
name?: string
/**
* The user's short biography / description from the OAuth provider
* (e.g. GitHub's `bio`, GitLab's `bio`, X's `description`). Omitted when
* the provider exposes no such field.
*/
bio?: string
/**
* URL of the user's profile image from the OAuth provider (e.g. Google's
* `picture`, GitHub/GitLab's `avatar_url`, X's `profile_image_url`).
* Omitted when the provider exposes none (Apple never does; Microsoft
* Graph only serves photos as a binary endpoint behind an extra scope).
*/
avatar?: string
/**
* The user's email address from the OAuth provider.
*/
email?: string
/**
* Whether the OAuth provider has affirmatively verified that the user
* controls this `email` mailbox.
*
* `true` MUST mean the provider proved mailbox ownership (e.g. Google's
* `email_verified`, Apple's `email_verified` ID-token claim). When the
* provider exposes no trustworthy verification signal in the profile data
* the verifier fetched, this MUST be `false`/`undefined` (never optimistically
* `true`) — consumers treat only an explicit `true` as verified.
*
* Consumers (e.g. the user resource's `logInOAuth` handler) use this to
* decide whether a provider-supplied email may be trusted over an existing,
* unverified local account — preventing an unverified squatter from blocking
* the verified mailbox owner.
*/
emailVerified?: boolean
/**
* The OAuth server identifier (e.g., 'github', 'google', 'twitter').
*/
oauthServer: string
/**
* Unique identifier for the user from the OAuth provider.
*/
oauthId: string
/**
* Raw user data from the OAuth provider.
*/
oauthData: Record<string, unknown>
}
OAuthAuthorizeUrlParams
Parameters for building a provider authorization (initiation) URL — the URL the user's browser is redirected to so the provider can authenticate them and send back an authorization code.
interface OAuthAuthorizeUrlParams {
/**
* Absolute URI the provider should redirect the user back to after
* authorization (the app origin, optionally with a path). When omitted,
* the builder leaves `redirect_uri` off the URL so the provider falls
* back to its registered callback URL.
*/
redirectUri?: string
/**
* The CSRF `state` parameter bound to the initiating session (stored in
* an httpOnly cookie by the initiation endpoint and validated by the
* login handler on callback).
*/
state: string
/**
* PKCE code challenge derived (S256) from the per-session code verifier.
* Omit only for providers that do not support PKCE.
*/
codeChallenge?: string
/**
* PKCE challenge method. Always prefer `'S256'`; `'plain'` exists only
* for providers that cannot hash.
*/
codeChallengeMethod?: 'S256' | 'plain'
}
Types
OAuthVerifier
Exchanges an OAuth authorization code for user profile information.
Implementations call the provider's token and user-info endpoints,
then return normalized OAuthUserProps for account creation or login.
Returning null means the provider AFFIRMATIVELY rejected the code
(e.g. GitHub's bad_verification_code, an expired/forged code) — the
consumer (logInOAuth) surfaces that as a clean 403 "verification
failed". A thrown error means an infrastructure failure (network,
provider outage) and surfaces as a 500. Implementations MUST NOT throw
for a rejected code — that would misreport a client mistake (or an
attack) as a server fault.
type OAuthVerifier = (
code: string,
codeVerifier?: string,
redirectUri?: string,
) => Promise<OAuthUserProps | null>
code— The authorization code received from the OAuth redirect.codeVerifier— The PKCE code verifier, if the flow uses proof-key exchange.redirectUri— The redirect URI that was used in the authorization request.
OAuthAuthorizeUrlBuilder
Builds the provider's authorization URL for OAuth initiation
(GET /users/oauth/:provider → 302 to this URL). Implementations embed
their own client id, scopes, and authorize endpoint so no consumer ever
hardcodes provider knowledge.
type OAuthAuthorizeUrlBuilder = (params: OAuthAuthorizeUrlParams) => string | null
params— State, PKCE challenge, and optional redirect URI.
Returns: The absolute authorization URL, or null when the provider is not configured (e.g. its client id env var is unset).
Functions
getAuthorizeUrl(params)
Builds the X (Twitter) authorization URL for OAuth initiation
(GET /users/oauth/:provider 302s the browser here). Embeds this bond's
client id and scopes (users.read tweet.read — exactly what verify's
GET /2/users/me requires) plus the caller's CSRF state and PKCE S256
challenge. The authorize endpoint defaults to https://x.com/i/oauth2/authorize
but can be overridden via OAUTH_TWITTER_AUTHORIZE_URL for E2E mocks.
X mandates PKCE on every authorization request — a URL built without a
code_challenge will be rejected by X. The shared initiation endpoint
always supplies an S256 challenge.
Deliberately does NOT request the offline.access scope: verify never
refreshes tokens — the two-hour access token is used exactly once to fetch
the user's profile.
function getAuthorizeUrl({
redirectUri,
state,
codeChallenge,
codeChallengeMethod,
}: OAuthAuthorizeUrlParams): string | null
params— State, PKCE challenge, and optional redirect URI.
Returns: The X authorize URL, or null when OAUTH_TWITTER_CLIENT_ID is unset.
verify(code, codeVerifier, redirectUri)
Verifies a Twitter OAuth code and responds with OAuth-related user props.
The token and user-info URLs default to api.twitter.com, but can be
overridden via OAUTH_TWITTER_TOKEN_URL and OAUTH_TWITTER_USER_URL
(pairing with OAUTH_TWITTER_AUTHORIZE_URL for E2E mock OAuth servers,
consistent with the github/gitlab/google bonds).
function verify(
code: string,
codeVerifier?: string,
redirectUri?: string,
): Promise<{
username: string
name: string | undefined
bio: string | undefined
avatar: string | undefined
email: string | undefined
emailVerified: false
oauthServer: 'twitter'
oauthId: string
oauthData: Record<string, unknown>
} | null>
code— The authorization code from the OAuth callback.codeVerifier— The PKCE code verifier (if PKCE was used).redirectUri— The redirect URI used in the authorization request.
Returns: An OAuthUserInfo with the user's Twitter username, email, and OAuth ID — or null when X affirmatively rejected the code (expired/reused/forged), which the consumer surfaces as a 403.
Constants
oauthTwitterSecretDefinitions
Secret definitions required by the X (Twitter) OAuth bond.
const oauthTwitterSecretDefinitions: SecretDefinition[]
serverName
The OAuth server identifier for Twitter.
const serverName: 'twitter'
Core Interface
Implements @molecule/api-oauth interface.
Bond Wiring
Setup function to register this provider with the bond system:
import { bond } from '@molecule/api-bond'
import { serverName, verify, getAuthorizeUrl } from '@molecule/api-oauth-twitter'
export function setupOauthTwitter(): void {
bond('oauth', serverName, { serverName, verify, getAuthorizeUrl })
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-http^1.0.1@molecule/api-oauth^1.0.1@molecule/api-secrets^1.0.1
Environment Variables
OAUTH_TWITTER_CLIENT_ID(required) — X (Twitter) OAuth client ID- Setup: Create a project + app in the X developer portal, enable OAuth 2.0, and copy the client ID.
- Get it here: https://developer.x.com/en/portal/dashboard
OAUTH_TWITTER_CLIENT_SECRET(required) — X (Twitter) OAuth client secret- Setup: Shown when enabling OAuth 2.0 for your app in the X developer portal.
- Get it here: https://developer.x.com/en/portal/dashboard
Runtime Dependencies
@molecule/api-bond@molecule/api-http@molecule/api-oauth@molecule/api-secrets
The token exchange (verify's call to X's token endpoint) is
application/x-www-form-urlencoded, per RFC 6749 §4.1.3 and X's own docs —
matching every other molecule.dev OAuth bond (google, gitlab, github,
apple, microsoft).
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- Clicking the app's "Sign in with {provider}" button (Google, GitHub, …) redirects to the provider's authorize URL carrying the correct client_id, the app's requested scopes, AND the app's registered redirect_uri — inspect the actual outbound URL (the 302 Location, or the address the popup/tab navigates to) and confirm each value; a missing or wrong one is the bug.
- The callback route exchanges the returned code SERVER-SIDE for a token, fetches the profile, and creates-or-links the app user + establishes a session — after the round-trip the app shows that user logged in. CAVEAT: the provider's own consent screen runs on ITS domain and CANNOT be driven in the sandbox, so verify the two boundaries you DO own — the authorize URL going out (above) and the callback coming back — not the provider's page. Complete the round-trip with a test/stub provider bond if one is wired; otherwise assert the callback handler's own behavior (state check → code exchange → user create-or-link → session). Never mock the flow or edit production code to bypass the provider.
- A returning OAuth user logs into the SAME account — sign in twice and confirm one user row linked by provider id (oauthServer + oauthId), not a fresh duplicate created each time.
- SECURITY — the
stateparameter is generated on initiation and verified on callback (CSRF protection): a mismatched or absentstateis rejected (403); theredirect_uriis validated against an allowlist so an attacker cannot redirect the code elsewhere; and the client secret + tokens stay server-side — grep the browser bundle and network tab to confirm the secret never reaches the client (only the authorize URL and returned code cross the boundary).