- Published on
User login with Twitter OAuth 2.0 and Passport.js
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.
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.
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.
But in case you forget to save the secret, you can always regenerate it under Keys and Tokens section of your app.
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:
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:
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.
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 profilesoffline.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
- Announcement of OAuth 2.0 General Availability by Twitter
- Twitter's guide Getting Access to the Twitter API is useful for the general overview of required steps and use of various credentials.
- Twitter API v2 authentication mapping for overview of what scopes are needed where.
- OAuth 2.0 Authorization Code Flow with PKCE explains details of Twitter's OAuth 2.0 implementation, like lifetime of access and refresh tokens, glossary, and overview of all scopes.