forked from flanchan/doushio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
259 lines
7.2 KiB
259 lines
7.2 KiB
var _ = require('../lib/underscore'),
|
|
common = require('../common'),
|
|
config = require('../config'),
|
|
crypto = require('crypto'),
|
|
formidable = require('formidable'),
|
|
querystring = require('querystring'),
|
|
RES = require('./state').resources,
|
|
request = require('request'),
|
|
winston = require('winston');
|
|
|
|
function connect() {
|
|
return global.redis;
|
|
}
|
|
|
|
exports.login = function (req, resp) {
|
|
var ip = req.ident.ip;
|
|
// if login cookie present, redirect to board (preferably, go back. but will that be easy?)
|
|
var r = connect();
|
|
function fail(error) {
|
|
respond_error(resp, error)
|
|
}
|
|
if (req.query.state) {
|
|
var state = req.query.state;
|
|
r.get('github:'+state, function (err, savedIP) {
|
|
if (err) {
|
|
winston.error("Couldn't read login: " + err);
|
|
fail("Couldn't read login attempt.");
|
|
return;
|
|
}
|
|
if (!savedIP) {
|
|
winston.info("Expired login attempt from " + ip);
|
|
fail("Login attempt expired. Please try again.");
|
|
return;
|
|
}
|
|
if (savedIP != ip) {
|
|
winston.warn("IP changed from " + savedIP + " to " + ip);
|
|
fail("Your IP changed during login. Please try again.");
|
|
return;
|
|
}
|
|
if (req.query.error == 'access_denied') {
|
|
fail("User did not approve GitHub app access.");
|
|
return;
|
|
}
|
|
if (req.query.error) {
|
|
// escaping out of paranoia (though respond_error emits JSON)
|
|
var err = common.escape_html(req.query.error);
|
|
winston.error("OAuth error: " + err);
|
|
if (req.query.error_description) {
|
|
err = common.escape_html(req.query.error_description);
|
|
winston.error("Desc: " + err);
|
|
}
|
|
fail("OAuth login failure: " + err);
|
|
return;
|
|
}
|
|
var code = req.query.code;
|
|
if (!code) {
|
|
fail("OAuth code missing!");
|
|
return;
|
|
}
|
|
request_access_token(req.query.code, state, function (err, token) {
|
|
if (err) {
|
|
winston.error("Github access token: " + err);
|
|
fail("Couldn't obtain token from GitHub. Try again.");
|
|
return;
|
|
}
|
|
request_username(token, function (err, username) {
|
|
if (err) {
|
|
winston.error("Username: " + err);
|
|
fail("Couldn't read username. Try again.");
|
|
return;
|
|
}
|
|
r.del('github:'+state, function (err) {});
|
|
if (/^popup:/.test(state))
|
|
req.popup_HACK = true;
|
|
verify_auth(req, resp, username);
|
|
});
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
// new login attempt; TODO rate limit
|
|
var nonce = random_str();
|
|
if (req.query.popup !== undefined)
|
|
nonce = 'popup:' + nonce;
|
|
r.setex('github:'+nonce, 60, ip, function (err) {
|
|
if (err) {
|
|
winston.error("Couldn't save login: " + err);
|
|
fail("Couldn't persist login attempt.");
|
|
return;
|
|
}
|
|
var params = {
|
|
client_id: config.GITHUB_CLIENT_ID,
|
|
state: nonce,
|
|
allow_signup: 'false',
|
|
};
|
|
var url = 'https://github.com/login/oauth/authorize?' +
|
|
querystring.stringify(params);
|
|
resp.writeHead(303, {Location: url});
|
|
resp.end('Redirect to GitHub Login');
|
|
});
|
|
}
|
|
|
|
function request_access_token(code, state, cb) {
|
|
var payload = {
|
|
client_id: config.GITHUB_CLIENT_ID,
|
|
client_secret: config.GITHUB_CLIENT_SECRET,
|
|
code: code,
|
|
state: state,
|
|
};
|
|
var opts = {
|
|
url: 'https://github.com/login/oauth/access_token',
|
|
body: payload,
|
|
json: true,
|
|
};
|
|
request.post(opts, function (err, tokenResp, packet) {
|
|
if (err || !packet || typeof packet.access_token != 'string') {
|
|
cb(err || "No access token in response");
|
|
}
|
|
else {
|
|
cb(null, packet.access_token);
|
|
}
|
|
});
|
|
}
|
|
|
|
function request_username(token, cb) {
|
|
var opts = {
|
|
url: 'https://api.github.com/user',
|
|
headers: {Authorization: 'token ' + token, 'User-Agent': 'Doushio-Auth'},
|
|
json: true,
|
|
};
|
|
request.get(opts, function (err, usernameResp, packet) {
|
|
if (err || !packet || typeof packet.login != 'string') {
|
|
cb(err || "Invalid username response");
|
|
}
|
|
else {
|
|
cb(null, packet.login);
|
|
}
|
|
});
|
|
}
|
|
|
|
function verify_auth(req, resp, user) {
|
|
if (!user)
|
|
return respond_error(resp, 'Invalid username.');
|
|
var ip = req.ident.ip;
|
|
var packet = {ip: ip, user: user, date: Date.now()};
|
|
if (config.ADMIN_GITHUBS.indexOf(user) >= 0) {
|
|
winston.info("@" + user + " logging in as admin from " + ip);
|
|
packet.auth = 'Admin';
|
|
exports.set_cookie(req, resp, packet);
|
|
}
|
|
else if (config.MODERATOR_GITHUBS.indexOf(user) >= 0) {
|
|
winston.info("@" + user + " logging in as moderator from " + ip);
|
|
packet.auth = 'Moderator';
|
|
exports.set_cookie(req, resp, packet);
|
|
}
|
|
else {
|
|
winston.error("Login attempt by @" + user + " from " + ip);
|
|
return respond_error(resp, 'Check your privilege.');
|
|
}
|
|
};
|
|
|
|
exports.set_cookie = function (req, resp, info) {
|
|
var pass = random_str();
|
|
info.csrf = random_str();
|
|
|
|
var m = connect().multi();
|
|
m.hmset('session:'+pass, info);
|
|
m.expire('session:'+pass, config.LOGIN_SESSION_TIME);
|
|
m.exec(function (err) {
|
|
if (err)
|
|
return oauth_error(resp, err);
|
|
respond_ok(req, resp, make_cookie('a', pass, info.expires));
|
|
});
|
|
};
|
|
|
|
function extract_login_cookie(chunks) {
|
|
if (!chunks || !chunks.a)
|
|
return false;
|
|
return /^[a-zA-Z0-9+\/]{20}$/.test(chunks.a) ? chunks.a : false;
|
|
}
|
|
exports.extract_login_cookie = extract_login_cookie;
|
|
|
|
exports.check_cookie = function (cookie, callback) {
|
|
var r = connect();
|
|
r.hgetall('session:' + cookie, function (err, session) {
|
|
if (err)
|
|
return callback(err);
|
|
else if (_.isEmpty(session))
|
|
return callback('Not logged in.');
|
|
callback(null, session);
|
|
});
|
|
};
|
|
|
|
exports.logout = function (req, resp) {
|
|
if (req.method != 'POST') {
|
|
resp.writeHead(200, {'Content-Type': 'text/html'});
|
|
resp.end('<!doctype html><title>Logout</title><form method=post>' +
|
|
'<input type=submit value=Logout></form>');
|
|
return;
|
|
}
|
|
var r = connect();
|
|
var cookie = extract_login_cookie(req.cookies);
|
|
if (!cookie) {
|
|
console.log('no cookie');
|
|
return respond_error(resp, "No login cookie for logout.");
|
|
}
|
|
r.hgetall('session:' + cookie, function (err, session) {
|
|
if (err)
|
|
return respond_error(resp, "Logout error.");
|
|
r.del('session:' + cookie);
|
|
respond_ok(req, resp, 'a=; expires=Thu, 01 Jan 1970 00:00:00 GMT');
|
|
});
|
|
};
|
|
|
|
function respond_error(resp, message) {
|
|
resp.writeHead(200, {'Content-Type': 'application/json'});
|
|
resp.end(JSON.stringify({status: 'error', message: message}));
|
|
}
|
|
|
|
function respond_ok(req, resp, cookie) {
|
|
var headers = {'Set-Cookie': cookie};
|
|
if (/json/.test(req.headers.accept)) {
|
|
headers['Content-Type'] = 'application/json';
|
|
resp.writeHead(200, headers);
|
|
resp.end(JSON.stringify({status: 'okay'}));
|
|
}
|
|
else if (req.popup_HACK) {
|
|
headers['Content-Type'] = 'text/html';
|
|
resp.writeHead(200, headers);
|
|
resp.end('<!doctype html><title>OK</title>Logged in!' +
|
|
'<script>window.opener.location.reload(); window.close();</script>');
|
|
}
|
|
else {
|
|
headers.Location = config.DEFAULT_BOARD + '/';
|
|
resp.writeHead(303, headers);
|
|
resp.end("OK! Redirecting.");
|
|
}
|
|
}
|
|
|
|
function make_expiry() {
|
|
var expiry = new Date(Date.now()
|
|
+ config.LOGIN_SESSION_TIME*1000).toUTCString();
|
|
/* Change it to the expected dash-separated format */
|
|
var m = expiry.match(/^(\w+,\s+\d+)\s+(\w+)\s+(\d+\s+[\d:]+\s+\w+)$/);
|
|
return m ? m[1] + '-' + m[2] + '-' + m[3] : expiry;
|
|
}
|
|
|
|
function make_cookie(key, val) {
|
|
var header = key + '=' + val + '; Expires=' + make_expiry();
|
|
var domain = config.LOGIN_COOKIE_DOMAIN;
|
|
if (domain)
|
|
header += '; Domain=' + domain;
|
|
return header;
|
|
}
|
|
|
|
function random_str() {
|
|
return crypto.randomBytes(15).toString('base64');
|
|
}
|