Published on

User login with Twitter OAuth 2.0 and Passport.js

Authors
  • Jan Vlnas
    Name
    Jan Vlnas
    Title
    Superface Alumni
    Social media profiles
    Fediverse

tl;dr Use the Node.js package @superfaceai/passport-twitter-oauth2 to handle user authentication with Twitter's v2 API and Passport.js.

Read on for the background, how to obtain Twitter credentials, example Express app, and code explanation.

Why use OAuth 2.0 for Twitter API

When you want to interact with Twitter's API or just provide a “social login” through Twitter, until last year you had to use legacy OAuth 1.0a authentication. While it still has its uses, Twitter is transitioning to the new version of their API (v2) and OAuth 2.0 protocol, which is also used by other providers, like Facebook, Google, and LinkedIn.

Besides being more widespread, Twitter's OAuth 2.0 has other advantages compared to OAuth 1.0a:

  • It gives you full access to Twitter's v2 API (including the endpoints for Twitter Spaces and editing tweets)
  • It lets you specify exactly what your application needs from the API through scopes, so users have a clear idea what your app can and cannot do with their account; OAuth 1.0a has only three levels of access: “read”, “read + write”, and “read + write + access DMs”
  • It is future-proof, as the development effort is focused on v2 API, for which OAuth 2.0 is the default authentication protocol.

When we wanted to build social media integrations, we couldn't find any up-to-date Node.js library to handle the authentication flow. So, we built one as a strategy for the popular Passport.js authentication middleware.

The strategy is available in @superfaceai/passport-twitter-oauth2 package. Recently we released a new minor version 1.2, which conducts a full rewrite to TypeScript.

Getting OAuth 2.0 Client ID and Secret from Twitter

To start using Twitter API, you need to register for a developer account (including phone number verification). Once you register, you will be prompted to create the first application. You will immediately receive API Key and API Key Secret – but ignore them, since these are only for OAuth 1.0. Instead, go to your project's dashboard, there go to the App Settings of the only application you created. In application settings, find User authentication settings and click Set up.

Twitter developer portal with application settings. In the User authentication settings section, there is a text 'User authentication not set up' and a 'Set up' button.

In User authentication settings make sure to select Type of App as Web App, Automated App or Bot.

In App info enter the following URL under Callback URI / Redirect URL:

http://localhost:3000/auth/twitter/callback

This is a URL where the user can be redirected after approving the application. The callback will be handled by the example server we will build in the next section.

Fill in also the Website URL, since it is a required value. Feel free to ignore other fields.

A page with User authentication set up opened. 'Type of app' is either 'Native app' or 'Web app, automated app, or bot' with the second option selected. In 'App info' the 'Callback URI / Redirect URL' is set to the localhost address.

Once you save the form, you will get OAuth 2.0 Client ID and Client Secret. Make sure to write these values down, since you can't reveal the secret later, only regenerate it.

Prompt with OAuth 2.0 Client ID and Client Secret and a disclaimer that "this will be the last time we'll display the Secret".

But in case you forget to save the secret, you can always regenerate it under Keys and Tokens section of your app.

Detail of Keys and Tokens section in application settings, which includes OAuth 2.0 Clients and Secrets section. The Client Secret section allows displaying a hint or regenerate the secret.

Authenticating with Passport.js and Twitter in Express.js

Let's create a simple Express server with Passport.js to show the authentication flow in action. Passport requires some session handling mechanism, usually through express-session. I will also use the dotenv package to load the credentials from .env file to process.environment object.

First, let's create a project and install dependencies:

mkdir twitter-oauth2-passport-demo
cd twitter-oauth2-passport-demo
npm install express express-session passport @superfaceai/passport-twitter-oauth2 dotenv

Now put your Client ID and Client Secret into .env file in the root of your project. Use the following template and make sure to paste in the correct values from the developer portal:

BASE_URL=http://localhost:3000
TWITTER_CLIENT_ID="OAuth 2.0 Client ID from Twitter Developer portal"
TWITTER_CLIENT_SECRET="OAuth 2.0 Client Secret from Twitter Developer portal"

I have also prepared the BASE_URL variable, since we will be passing the callback URL during the authentication, and keeping this value configurable makes it easier to take your app to production. And now, let's create server.js file, and put in the following contents:

server.js
const express = require('express');
const passport = require('passport');
const { Strategy } = require('@superfaceai/passport-twitter-oauth2');
const session = require('express-session');
require('dotenv').config();

// <1> Serialization and deserialization
passport.serializeUser(function (user, done) {
  done(null, user);
});
passport.deserializeUser(function (obj, done) {
  done(null, obj);
});

// Use the Twitter OAuth2 strategy within Passport
passport.use(
  // <2> Strategy initialization
  new Strategy(
    {
      clientID: process.env.TWITTER_CLIENT_ID,
      clientSecret: process.env.TWITTER_CLIENT_SECRET,
      clientType: 'confidential',
      callbackURL: `${process.env.BASE_URL}/auth/twitter/callback`,
    },
    // <3> Verify callback
    (accessToken, refreshToken, profile, done) => {
      console.log('Success!', { accessToken, refreshToken });
      return done(null, profile);
    }
  )
);

const app = express();

// <4> Passport and session middleware initialization
app.use(passport.initialize());
app.use(
  session({ secret: 'keyboard cat', resave: false, saveUninitialized: true })
);

// <5> Start authentication flow
app.get(
  '/auth/twitter',
  passport.authenticate('twitter', {
    // <6> Scopes
    scope: ['tweet.read', 'users.read', 'offline.access'],
  })
);

// <7> Callback handler
app.get(
  '/auth/twitter/callback',
  passport.authenticate('twitter'),
  function (req, res) {
    const userData = JSON.stringify(req.user, undefined, 2);
    res.end(
      `<h1>Authentication succeeded</h1> User data: <pre>${userData}</pre>`
    );
  }
);

app.listen(3000, () => {
  console.log(`Listening on ${process.env.BASE_URL}`);
});

Now run your server with npm start and visit http://localhost:3000/auth/twitter. You will be redirected to Twitter authorization page:

Twitter prompt: "Passport OAuth 2.0 Tutorial wants to access your Twitter account." with buttons to "Authorize app" and "Cancel". Below, the following permissions are listed: App can view "All the Tweets you can view, including Tweets from protected accounts" and "Any account you can view, including protected accounts". The app can also "Stay connected to your account until you revoke access".

And once you authorize the app, you should see the user data from your profile and, in addition, accessToken and refreshToken will be logged into console.

Page from the application with the title "Authentication succeeded" and a dump of user data loaded from authorized user's profile.

Breaking down the example

Let's break down the example code above a bit.

User serialization and deserialization

// <1> Serialization and deserialization
passport.serializeUser(function (user, done) {
  done(null, user);
});
passport.deserializeUser(function (obj, done) {
  done(null, obj);
});

These functions serialize and deserialize user to and from session. In our example application, we keep all sessions in the memory with no permanent storage, so we just pass the whole user object.

Typically, you will persist data in a database. In that case, you will store the user ID in the session, and upon deserialization find the user in your database using the serialized ID, for example:

passport.serializeUser(function (user, done) {
  done(null, user.id);
});
passport.deserializeUser(function (id, done) {
  User.findOrCreate(id).then((user) => done(null, user));
});

The deserialized user object is then accessible through req.user property in middleware functions.

Strategy initialization

// Use the Twitter OAuth2 strategy within Passport
passport.use(
  // <2> Strategy initialization
  new Strategy(
    {
      clientID: process.env.TWITTER_CLIENT_ID,
      clientSecret: process.env.TWITTER_CLIENT_SECRET,
      clientType: 'confidential',
      callbackURL: `${process.env.BASE_URL}/auth/twitter/callback`,
    }
    // ...
  )
);

To use an authentication strategy, it must be registered with Passport through passport.use. Here, the Twitter OAuth 2.0 strategy is initialized with credentials from Twitter developer portal. The callback URL must be absolute and registered with Twitter – it's where the user is redirected after authorizing the application.

clientType can be either confidential or public, but in case of server-side applications you will usually use confidential. As explained in OAuth specification, confidential clients can keep the secret safe, while public clients, like mobile applications, can't.

Success callback

passport.use(
  new Strategy(
    //...
    ,
    // <3> Verify callback
    (accessToken, refreshToken, profile, done) => {
      console.log('Success!', { accessToken, refreshToken });
      return done(null, profile);
    }
  )
);

The second argument to the strategy constructor is a verify function. In case of OAuth-based strategies, it is called at the end of successful authorization flow. The user has authorized your application, and you will receive their access token and (optionally) refresh token and user's profile (username, display name, profile image etc.).

Now it's your turn: typically you will want to update or create the user in your database and store the tokens, so you can call the API with them. The done callback should receive a user object, which is passed in req.user property.

Passport and Session middlewares initialization

// <4> Passport and session middleware initialization
app.use(passport.initialize());
app.use(
  session({ secret: 'keyboard cat', resave: false, saveUninitialized: true })
);

Passport needs to be initialized as middleware as well. And it requires a session middleware for storing state and user data. The most common session middleware is express-session.

By default, express-session stores all data in memory, which is good for testing, but not intended for production: if your server gets restarted, all users will be logged out. There is a wide selection of compatible session stores – pick one which fits with the rest of your stack.

Start the authentication flow

Now we get to the juicy part, routes where the authentication happens. The first route is /auth/twitter:

// <5> Start authentication flow
app.get(
  '/auth/twitter',
  passport.authenticate('twitter', {
    //...
  })
);

passport.authenticate creates a middleware for the given strategy. It redirects the user to Twitter with URL parameters, so Twitter knows what application the user is authorizing and where the user should be then redirected back. authenticate function accepts a second parameter with additional options, where the most important is scopes.

Authorization scopes

passport.authenticate('twitter', {
  // <6> Scopes
  scope: ['tweet.read', 'users.read', 'offline.access'],
});

OAuth scopes define what the application is allowed to do on behalf of the user. The user can then review and approve these permissions.

In this case, the following scopes are requested:

  • tweet.read – allows reading of user's and others' tweets (including tweets from private accounts the user follows)
  • users.read – allows reading information about users profiles
  • offline.access – allows access even when the user isn't signed in; in practice, the application receives the refresh token

Both tweet.read and users.read are necessary for accessing information about the signed-in user. offline.access is useful when you want to do something when the user isn't directly interacting with your application, for example if you post scheduled tweets, build a bot, or monitor mentions on Twitter.

For a detailed overview of what scopes are used in Twitter's API, check Twitter's authentication mapping.

Callback handler

// <7> Callback handler
app.get(
  '/auth/twitter/callback',
  passport.authenticate('twitter'),
  function (req, res) {
    const userData = JSON.stringify(req.user, undefined, 2);
    res.end(
      `<h1>Authentication succeeded</h1> User data: <pre>${userData}</pre>`
    );
  }
);

This is the final step in the authentication flow. After the user authorized your application, they are redirected to /auth/twitter/callback route. The passport.authenticate middleware is here again, but this time it checks query parameters Twitter provided on redirect, checks if everything is correct, and obtains access and refresh tokens.

If the authentication succeeds, the next middleware function is called – typically you will display some success message to the user or redirect them back to your application. Since the authentication passed, you can now find the user data in req.user property.

Next steps

Passport.js isn't limited to just Express, you can use our strategy with other frameworks with Passport compatibility like NestJS, Fastify, or Koa.

With the obtained access token, you can start integrating Twitter's API. Check out our twitter-demo repository with CLI scripts showing how to list followers, lookup posts by hashtag, or publish a tweet.

We also have a bit more complex social-media-demo which demonstrates authentication and integrations with Facebook, Instagram, LinkedIn, and Twitter.

I will be diving into more hands-on examples with Twitter, Instagram, and other social media in the future posts. Subscribe to our blog or sign up for our monthly newsletter, so you don't miss it.

Try our strategy and let me know what you are building with it!

Resources

Automate the impossible.
Superface. The LLM-powered automation agent that connects to all your systems.

Try it now