I have written a Proof of Concept here using Node and Express.
It all seems to be working fine, with one exception: users are always asked to authorize the app when they want to authenticate.
However other that use Twitter for authentication only ask for authorization once, the first time the user wants to authenticate.
Any ideas what am I missing here?
Here’s the code for completeness:
Backend
'use strict';
const oauth = require('oauth');
const express = require('express');
const session = require('express-session');
const LokiStore = require('connect-loki')(session);
const Twitter = require('twitter');
const app = express();
const CONSUMER_KEY = process.env.CONSUMER_KEY;
const CONSUMER_SECRET = process.env.CONSUMER_SECRET;
const OA = new oauth.OAuth(
'https://api.twitter.com/oauth/request_token',
'https://api.twitter.com/oauth/access_token',
CONSUMER_KEY,
CONSUMER_SECRET,
'1.0A',
null,
'HMAC-SHA1'
);
app.use(express.static('public'));
app.use(session({
store: new LokiStore({}),
secret: 'cookie secret',
cookie: {maxAge: 24 * 60 * 60 * 1000} // 24 hours
}));
function handleError(error, statusCode, res) {
console.log({
error: error
});
res.status(statusCode).send({
error: error
});
}
app.get('/auth/twitter', function (req, res) {
OA.getOAuthRequestToken(
function (error, oAuthToken, oAuthTokenSecret, results) {
if (error) {
handleError(error, 401, res);
return;
}
console.log(oAuthToken, oAuthTokenSecret, results);
req.session.requestToken = {
oauthToken: oAuthToken,
oauthTokenSecret: oAuthTokenSecret,
results: results
};
req.session.save();
res.redirect(`https://api.twitter.com/oauth/authenticate?oauth_token=${oAuthToken}`);
}
);
});
app.get('/auth/twitter/callback', function (req, res) {
let oauthToken = req.session.requestToken.oauthToken;
let oauthTokenSecret = req.session.requestToken.oauthTokenSecret;
let oauthVerifier = req.query.oauth_verifier;
OA.getOAuthAccessToken(oauthToken, oauthTokenSecret, oauthVerifier, function (error, accessToken, accessTokenSecret, results) {
if (error) {
handleError(error, 401, res);
return;
}
OA.get('https://api.twitter.com/1.1/account/verify_credentials.json', accessToken, accessTokenSecret, function (error, twitterResponseData, result) {
if (error) {
handleError(error, 401, res);
return;
}
req.session.twitterSession = {
accessToken: accessToken,
accessTokenSecret: accessTokenSecret,
profile: twitterResponseData
};
req.session.save();
res.redirect('http://localhost:3000');
});
}
);
});
app.get('/auth/me', function (req, res) {
let twitterSession = req.session.twitterSession;
if (twitterSession && twitterSession.profile) {
res.send(twitterSession.profile);
} else {
res.status(401).send({});
}
});
app.get('/auth/logout', function (req, res) {
req.session.destroy();
res.redirect('http://localhost:3000');
});
app.post('/tweet', function (req, res) {
let secret = {
consumer_key: CONSUMER_KEY,
consumer_secret: CONSUMER_SECRET,
access_token_key: req.session.twitterSession.accessToken,
access_token_secret: req.session.twitterSession.accessTokenSecret
};
let tw = new Twitter(secret);
tw.post('statuses/update', {status: 'Test'}, function (error, tweet, response) {
if (error) {
console.log(error);
}
console.log(tweet); // Tweet body.
console.log(response); // Raw response object.
res.send({
tweet: tweet,
response: response
});
});
});
app.listen(3000, function () {
console.log('Started Twitter Express');
});
Frontend
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Twitter Express</title>
<style>
.hidden {
display: none;
}
</style>
</head>
<body>
<h1>A Twitter authentication and authorization proof-of-concept</h1>
<h2>Features</h2>
<ul>
<li>Authenticate using Twitter to obtain <code>access token</code> and <code>token secret</code>.</li>
<li>Remember authentication state</li>
<li>Post tweets to Twitter</li>
</ul>
<div class="hidden" id="login-panel">
<a href="/auth/twitter">Login with your Twitter account</a>
</div>
<div class="hidden" id="logout-panel">
<a href="/auth/logout">Logout</a>
</div>
<div class="hidden" id="profile-panel">
<p>
Your profile information:
</p>
<pre id="profile-information">
</pre>
</div>
<div class="hidden" id="compose-panel">
<form>
<p>Post a tweet:</p>
<textarea id="compose-area" title="Tweet text"></textarea>
<div>
<input id="publish-tweet" type="submit">
</div>
</form>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script>
function getProfileInformation() {
return $.ajax('/auth/me', {
type: 'GET'
});
}
$(document).ready(function () {
getProfileInformation().done(function (userProfile) {
$("#profile-information").text(JSON.stringify(JSON.parse(userProfile), null, 2));
$("#login-panel").addClass('hidden');
$("#logout-panel, #profile-panel, #compose-panel").removeClass('hidden');
}).fail(function (xhr, textStatus, error) {
$("#logout-panel, #profile-panel, #compose-panel").addClass('hidden');
$("#login-panel").removeClass('hidden');
console.log(textStatus, error);
});
$('#publish-tweet').on('click', function () {
$.ajax({
type: "POST",
url: '/tweet',
data: $("#compose-area").val()
}).done(function (response) {
// TODO
}).fail(function (xhr, textStatus, error) {
// TODO
});
return false;
});
});
</script>
</body>
</html>
package.json
{
"name": "twitter-express",
"version": "0.0.1",
"description": "A simple application demonstrating authentication and authorization using Twitter",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"connect-loki": "^1.1.0",
"express": "^4.16.1",
"express-session": "^1.15.6",
"oauth": "^0.9.15",
"twitter": "^1.7.1"
}
}