Lately I found myself dealing with Twitter and Google OAuth (1.0a and 2.0) to authorize a user to act on their behalf, namely reading profile information, reading and posting content on their behalf or other common permissions.
Since my process of trying to understand and make sense of OAuth has been both fun and rough, I wanted to summarize a full example in a single JavaScript file and document other findings along the way.
Interested in the fastify version?
Documentation, documentation, documentation
If you want to read extensively the documentation and deeply understand the flow, these are some links that helped me out greatly:
- Authentication Methods
- Implementing Log in with Twitter
- Obtaining user access tokens using 3-legged OAuth
- /oauth/authenticate
- /oauth/authorize
Requirements
To have a fully working HTTP server working together with Twitter OAuth in Node.js you need
- an application created on developer.twitter.com to be used to sign in with Twitter
- a Consumer API key and secret for the OAuth part
- an HTTP server
- some lightweight Node.js modules
Read below for a step by step guide!
Twitter application for OAuth
- Visit Twitter Developer Portal, login and select Apps from the menu:
- Create a new app
- Give it a name and description
- Important: Select Enable Sign in with Twitter and add the following url to the list of Callback URLs:
http://127.0.0.1:3000/twitter/callback
- In your app settings, head over to Keys and Tokens
- Get your Consumer API key and Consumer API secret key and copy them to your clipboard, you’ll need them in the code
Dependencies
Using express as an example here, though you can use any other framework, the concept is the same.
We need 4 useful modules from npm:
oauth
for generating the request and access tokens for the OAuth flow, and for authenticating to any OAuth enable HTTP API (like Twitter)express
as the web serverexpress-session
,cookie-parser
for handling the user sessions and cookies
The HTTP server and endpoints
The local HTTP server will listen on the port 3000 and serves the following routes:
- GET / as the simplest possible authentication HTML page
- GET /twitter/authenticate and /twitter/authorize for the OAuth flow initiation
- GET /twitter/callback to get the authorized user access token and secret
- GET /twitter/logout to enable the user to log out from the application
To handle sessions and parse HTTP cookies express-session
and cookie-parser
are used.
This is how it looks like
An example OAuth 1.0a flow
The code
The full source code can be found on GitHub.
Learn how to make authenticated API calls with OAuth 1.0a and 2.0.
Step-by-step
Let’s import the relevant modules:
const oauth = require('oauth')
const express = require('express')
const session = require('express-session')
const cookieParser = require('cookie-parser')
const path = require('path')
const fs = require('fs')
const { promisify } = require('util')
Set the environment variables, either loaded via Environment variables set during the execution, or dynamically loaded by npm via .npmrc
const COOKIE_SECRET = process.env.npm_config_cookie_secret || process.env.COOKIE_SECRET
const TWITTER_CONSUMER_API_KEY = process.env.npm_config_twitter_consumer_api_key || process.env.TWITTER_CONSUMER_API_KEY
const TWITTER_CONSUMER_API_SECRET_KEY = process.env.npm_config_twitter_consumer_api_secret_key || process.env.TWITTER_CONSUMER_API_SECRET_KEY
The very simple HTTP server “dynamically“ renders an HTML page based on the signed in Twitter user, greeting them with their username:
const TEMPLATE = fs.readFileSync(path.resolve(__dirname, 'client', 'template.html'), { encoding: 'utf8' })
We need to create an OAuth Consumer to generate the request, access token and make authorised requests to the Twitter API:
const oauthConsumer = new oauth.OAuth(
'https://twitter.com/oauth/request_token', 'https://twitter.com/oauth/access_token',
TWITTER_CONSUMER_API_KEY,
TWITTER_CONSUMER_API_SECRET_KEY,
'1.0A', 'http://127.0.0.1:3000/twitter/callback', 'HMAC-SHA1')
Let’s run the main program!
main()
.catch(err => console.error(err.message, err))
The initial part of the main function, setting up the HTTP server:
async function main () {
const app = express()
app.use(cookieParser())
app.use(session({ secret: COOKIE_SECRET || 'secret' }))
app.listen(3000, () => console.log('listening on http://127.0.0.1:3000'))
…
Add a route to handle the “dynamic” page contains the logged in user’s twitter handle:
…
app.get('/', async (req, res, next) => {
console.log('/ req.cookies', req.cookies)
if (req.cookies && req.cookies.twitter_screen_name) {
console.log('/ authorized', req.cookies.twitter_screen_name)
return res.send(TEMPLATE.replace('CONTENT', `
<h1>Hello ${req.cookies.twitter_screen_name}</h1>
<br>
<a href=“/twitter/logout”>logout</a>
`))
}
return next()
})
…
Fallback to a static HTML page index.html
that has the login buttons, both for authorisation
and authentication
.
You can read more about authorization and authentication on the official documentation.
…
app.use(express.static(path.resolve(__dirname, 'client')))
…
Set up routes for /twitter/logout
, /twitter/authorize
and /twitter/authenticate
:
…
app.get('/twitter/logout', logout)
function logout (req, res, next) {
res.clearCookie('twitter_screen_name')
req.session.destroy(() => res.redirect('/'))
}
app.get('/twitter/authenticate', twitter('authenticate'))
app.get('/twitter/authorize', twitter('authorize'))
function twitter (method = 'authorize') {
return async (req, res) => {
console.log(`/twitter/${method}`)
const { oauthRequestToken, oauthRequestTokenSecret } = await getOAuthRequestToken()
console.log(`/twitter/${method} ->`, { oauthRequestToken, oauthRequestTokenSecret })
req.session = req.session || {}
req.session.oauthRequestToken = oauthRequestToken
req.session.oauthRequestTokenSecret = oauthRequestTokenSecret
const authorizationUrl = `https://api.twitter.com/oauth/${method}?oauth_token=${oauthRequestToken}`
console.log('redirecting user to ', authorizationUrl)
res.redirect(authorizationUrl)
}
}
…
Finally the last route, namely /twitter/callback
, which completes the OAuth flow!:
…
app.get('/twitter/callback', async (req, res) => {
const { oauthRequestToken, oauthRequestTokenSecret } = req.session
const { oauth_verifier: oauthVerifier } = req.query
console.log('/twitter/callback', { oauthRequestToken, oauthRequestTokenSecret, oauthVerifier })
const { oauthAccessToken, oauthAccessTokenSecret, results } = await getOAuthAccessTokenWith({ oauthRequestToken, oauthRequestTokenSecret, oauthVerifier })
req.session.oauthAccessToken = oauthAccessToken
const { user_id: userId /*, screen_name */ } = results
const user = await oauthGetUserById(userId, { oauthAccessToken, oauthAccessTokenSecret })
req.session.twitter_screen_name = user.screen_name
res.cookie('twitter_screen_name', user.screen_name, { maxAge: 900000, httpOnly: true })
console.log('user succesfully logged in with twitter', user.screen_name)
req.session.save(() => res.redirect('/'))
})
…
The remaining methods are used to interact with the Twitter OAuth API:
…
async function oauthGetUserById (userId, { oauthAccessToken, oauthAccessTokenSecret } = {}) {
return promisify(oauthConsumer.get.bind(oauthConsumer))(`https://api.twitter.com/1.1/users/show.json?user_id=${userId}`, oauthAccessToken, oauthAccessTokenSecret)
.then(body => JSON.parse(body))
}
async function getOAuthAccessTokenWith ({ oauthRequestToken, oauthRequestTokenSecret, oauthVerifier } = {}) {
return new Promise((resolve, reject) => {
oauthConsumer.getOAuthAccessToken(oauthRequestToken, oauthRequestTokenSecret, oauthVerifier, function (error, oauthAccessToken, oauthAccessTokenSecret, results) {
return error
? reject(new Error('Error getting OAuth access token'))
: resolve({ oauthAccessToken, oauthAccessTokenSecret, results })
})
})
}
async function getOAuthRequestToken () {
return new Promise((resolve, reject) => {
oauthConsumer.getOAuthRequestToken(function (error, oauthRequestToken, oauthRequestTokenSecret, results) {
return error
? reject(new Error('Error getting OAuth request token'))
: resolve({ oauthRequestToken, oauthRequestTokenSecret, results })
})
})
}
…
Running the code
As simple as this, you just need to have your TWITTER_CONSUMER_API_KEY
and TWITTER_CONSUMER_API_SECRET_KEY
ready:
TWITTER_CONSUMER_API_KEY=YOUR_KEY
TWITTER_CONSUMER_API_SECRET_KEY=YOUR_SECRET_KEY
node index.js