Documentation

SSO

Twitter (X)

Add X (formerly Twitter) sign-in to VitNode with OAuth 2.0 and PKCE. Set up user authentication, request the confirmed email and wire up the adapter.

Add a Continue with X button using X's OAuth 2.0 authorization code flow with PKCE. The provider id stays twitter, so the callback is /login/sso/twitter.

Before you begin

Set up the app

Open user authentication settings

In the X developer portal, open your App inside a Project and press Set up (or Edit) under User authentication settings.

Choose a confidential web app

Set:

  • App permissions: Read only. Turn on Request email from users to receive the confirmed email.
  • Type of App: Web App, Automated App or Bot (a confidential client, because VitNode keeps the secret on the server).
  • Website URL: your site.

Add the callback URI

Add each callback URI. X matches them exactly, including a trailing slash:

EnvironmentCallback URI
Developmenthttp://127.0.0.1:3000/login/sso/twitter
Productionhttps://your-domain.com/login/sso/twitter

X documents 127.0.0.1 rather than localhost for local work. Set VITNODE_WEB_URL=http://127.0.0.1:3000 and open the site at that address, because VitNode builds the URI as {VITNODE_WEB_URL}/login/sso/twitter and the sign-in cookie belongs to the host you started on.

Copy the OAuth 2.0 credentials

After saving, copy the OAuth 2.0 Client ID and Client Secret. The API key and secret belong to OAuth 1.0a and won't work here:

.env
TWITTER_CLIENT_ID=AbCdEfGhIjKlMnOpQrStUvWxYz
TWITTER_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Add the adapter

Add TwitterSSOApiPlugin to authorization.ssoAdapters in the API's vitnode.api.config.ts (where it lives), next to any adapters you already have. Keep the rest of the config as it is:

src/vitnode.api.config.ts (excerpt)
import { TwitterSSOApiPlugin } from '@vitnode/core/api/adapters/sso/twitter'
import { buildApiConfig } from '@vitnode/core/vitnode.config'

export const vitNodeApiConfig = buildApiConfig({
  // ...
  authorization: {
    ssoAdapters: [
      // ...
      TwitterSSOApiPlugin({
        clientId: process.env.TWITTER_CLIENT_ID,
        clientSecret: process.env.TWITTER_CLIENT_SECRET,
      }),
    ],
  },
})

No authorization block yet? Create one with just ssoAdapters, as in the SSO setup. Then restart the dev server, since .env and the API config are only read at start-up.

Check the result

Open /login, press the X button under Continue with and Authorize app. You return to /login/sso/twitter signed in, or are asked to confirm an email when X shares none.

Email access

With Request email from users on, the adapter requests the users.email scope and reads confirmed_email, which X only returns for a confirmed address. Some accounts (for example phone-only ones) have none.

If your app cannot request email, X rejects the users.email scope. Turn it off in the adapter, and new members confirm an address by email instead:

src/vitnode.api.config.ts
TwitterSSOApiPlugin({
  clientId: process.env.TWITTER_CLIENT_ID,
  clientSecret: process.env.TWITTER_CLIENT_SECRET,
  requestEmail: false, 
}),

Options

NameTypeRequiredDefaultDescription
clientIdstring | undefinedYes-OAuth 2.0 Client ID
clientSecretstring | undefinedYes-OAuth 2.0 Client Secret
requestEmailbooleanNotrueRequest users.email and the confirmed email

Scopes and profile fields

SettingValue
Scopestweet.read users.read users.email (users.email optional)
PKCES256 (X requires PKCE)
Token authHTTP Basic with the client ID and secret
Account IDdata.id
Email trustedconfirmed_email, when present
Profile fieldsavatar

tweet.read is required by X to read the signed-in account, even though VitNode reads no posts.

Troubleshooting

  • "Something went wrong - You weren't able to give access to the App". The callback URI differs from {VITNODE_WEB_URL}/login/sso/twitter, or users.email is requested while email access is off.
  • "unauthorized_client". The app type is a public client (Native App or Single page App), or you used the OAuth 1.0a keys.
  • A provider error after authorizing. GET /2/users/me was refused; check your API access and plan in the developer portal.