From a370af2f5fa12b6395b9e1c1846769090e92cd04 Mon Sep 17 00:00:00 2001 From: Paul Rodwell Date: Fri, 29 Apr 2016 21:29:58 +0100 Subject: [PATCH] adding https and wikiDomains for login, plus GitHub and Google login, and switching to using winchan for communication between windows --- .gitignore | 3 + client/dialog.css | 38 +++++ client/relay.html | 10 ++ client/security.coffee | 47 +++++- client/winchan.js | 301 ++++++++++++++++++++++++++++++++++++++ package.json | 3 + server/social.coffee | 205 +++++++++++++++----------- views/done.html | 17 ++- views/securityDialog.html | 6 +- 9 files changed, 526 insertions(+), 104 deletions(-) create mode 100644 client/relay.html create mode 100644 client/winchan.js diff --git a/.gitignore b/.gitignore index 1f0a036..1c3de6e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules *.log /client/*.js /client/*.map + +# don't ignore winchan.js +!/client/winchan.js diff --git a/client/dialog.css b/client/dialog.css index fd36106..2e7eed2 100644 --- a/client/dialog.css +++ b/client/dialog.css @@ -819,6 +819,35 @@ section>.contents { z-index: 10; } +/* Icon (github) */ +.github-button span:after{ + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAABBNpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnhtcC5kaWQ6RERCMUIwOUY4NkNFMTFFM0FBNTJFRTMzNTJEMUJDNDY8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpEZXJpdmVkRnJvbSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgIDxzdFJlZjppbnN0YW5jZUlEPnhtcC5paWQ6RTUxNzhBMkE5OUEwMTFFMjlBMTVCQzEwNDZBODkwNEQ8L3N0UmVmOmluc3RhbmNlSUQ+CiAgICAgICAgICAgIDxzdFJlZjpkb2N1bWVudElEPnhtcC5kaWQ6RTUxNzhBMkI5OUEwMTFFMjlBMTVCQzEwNDZBODkwNEQ8L3N0UmVmOmRvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6RERCMUIwOUU4NkNFMTFFM0FBNTJFRTMzNTJEMUJDNDY8L3htcE1NOkluc3RhbmNlSUQ+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgqBOjEgAAADH0lEQVRIDZWVzUuVQRTGu6kFJlFRBmKYfRhu+7CVVtCiNi6CPiDalVGgG1v0QdEfEi3bhLgIiigiqo0WbYI+FokStVAoWoVW3n6/cY5Nt0vYA897zpx5zjkz7zt3bmVZDarVakOlUvlpGL8Tsw8ehjvhJrgSzsIP8CW8Bx+TM4k1ZzHf8V9AsNwgthVeg+/hUqBOfWvOT3XqNahkQS/i8aLyfOHXc8t583pznVRvsRETsYN+/Jmi0nf8n3lssVo65fycTob5/bnRwo4INORAH340mMX/BgPRKMZhy7h684R1+nLdVN9vsBG+gAG3fRQOw1cRxH6Bn7KNsPMXoPryNVtvo40afYDzcBecgyvgOKflNtYFjGL2whk4DT15rs6PvAGOoZ3AqvUk7oHWsd45eN2JrXASini3N/EbYP1TQmYJdVlvnog61t3iTvbDDliFZdF5VmiCq3buz9NCAKQ4Or9NOW8d5zrgAXcyYiUQH3EaP45h2RR9faBPOvOg+SLqjdhkIoWqVY+ruGMpbLmy+tWLaOjNhyLqTbiC9qyNVX+sGRel/ulG/qea/HYnmnLQdyhqxwvRpT9r85ts4mUnokm6fxZC//WMfI+1iPGsTaZS6Pfp6eJ9dnJi5rHxO8qS+kZd1nei2FFTb8omYzmob/cueNoYiT8o4G+g7iEw7rw69eAM3A7nofXEuKfoBBSuXApPxmW41J00ob0C474ra52wSRuM++kW/jD8DMUzeBEehM0uC5t2hV0FD8GrcAwGbBC/Eeu2mWfiYFZ8xR6HXvklnjJYm7Vxa68j9qgU4Vu8bDKUGuTEFibv5oQp7GZ4DD6Bz+GRrPMbpO+Qx5cYC694i4v4Ed7Hb0lNcGJl3fivobiRizTjr07C4kEscs4qBh4Qm2jFO9idayStryudBOxu+BYKX9EQ9L+iJyfE94gm55gT7iTwBsfrfrGufgIT0cirfzQysh1UhB/Fw56v0XnZbsvaOMKp/uIDQTRqxD8JH0AxkBOjeNiBhenqQ+wpmK4UbP0G0akU4K+BPXB9bhKvK+x65vbCdPKy5q8GvwD3BqLoPnaIbQAAAABJRU5ErkJggg==) 4px center no-repeat; + content: ''; + display: block; + width: 31px; + + position: absolute; + bottom: 0; + left: -3px; + top: 0; + z-index: 10; +} + +/* Icon (google) */ +.google-button span:after{ + background: url(data:image/png;base64,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) 4px center no-repeat; + background-size: 18px; + content: ""; + display: block; + width: 31px; + + position: absolute; + bottom: 0; + left: 0; + top: 0; + z-index: 10; +} + /* Icon background */ .scheme-button span:before{ content: ''; @@ -846,6 +875,10 @@ section>.contents { border-radius: 3px 0 0 3px; } +.google-button span:before{ + background: white; +} + /* Triangle */ .scheme-button:before{ background: #42a9dd; @@ -879,6 +912,11 @@ section>.contents { transform: rotate(45deg); } +.google-button:before{ + background: white; + +} + /* Inset shadow (required here because the icon background clips it when on the `a` element) */ .scheme-button:after{ content: ''; diff --git a/client/relay.html b/client/relay.html new file mode 100644 index 0000000..bf8c611 --- /dev/null +++ b/client/relay.html @@ -0,0 +1,10 @@ + + + + + + diff --git a/client/security.coffee b/client/security.coffee index 647f533..0e3dd0d 100644 --- a/client/security.coffee +++ b/client/security.coffee @@ -13,6 +13,8 @@ ### +settings = {} + claim_wiki = () -> # we want to initiate a claim on a wiki # @@ -31,7 +33,6 @@ claim_wiki = () -> response.json().then (json) -> ownerName = json.ownerName update_footer ownerName, true, true - console.log 'owner: ', json.ownerName, ' : ', ownerName else console.log 'Attempt to claim site failed', response @@ -72,21 +73,53 @@ update_footer = (ownerName, isAuthenticated, isOwner) -> $('footer > #security').append "" $('footer > #security > #show-security-dialog').click (e) -> e.preventDefault() - securityDialog = window.open( - "/auth/loginDialog", - "_blank", - "width=700, height=375, menubar=no, location=no, chrome=yes, centerscreen") - securityDialog.window.focus() + + w = WinChan.open({ + url: settings.dialogURL + relay_url: settings.relayURL + window_features: "menubar=0, location=0, resizable=0, scrollbars=0, status=0, dialog=1, width=700, height=375" + params: {} + }, (err, r) -> + if err + console.log err + else if !isClaimed + claim_wiki() + else + update_footer ownerName, true) setup = (user) -> + # we will replace font-awesome with a small number of svg icons at a later date... if (!$("link[href='https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css']").length) $('').appendTo("head") + wiki.getScript '/security/winchan.js' if (!$("link[href='/security/style.css']").length) $('').appendTo("head") + myInit = { + method: 'GET' + cache: 'no-cache' + mode: 'same-origin' + } + fetch '/auth/client-settings.json', myInit + .then (response) -> + if response.ok + response.json().then (json) -> + settings = json + if settings.useHttps + dialogProtocol = 'https:' + else + dialogProtocol = window.location.protocol + if settings.wikiHost + dialogHost = settings.wikiHost + else + dialogHost = window.location.host + settings.dialogURL = dialogProtocol + '//' + dialogHost + '/auth/loginDialog' + settings.relayURL = dialogProtocol + '//' + dialogHost + '/auth/relay.html' - update_footer ownerName, isAuthenticated, isOwner + update_footer ownerName, isAuthenticated, isOwner + else + console.log 'Unable to fetch client settings: ', response window.plugins.security = {setup, claim_wiki, update_footer} diff --git a/client/winchan.js b/client/winchan.js new file mode 100644 index 0000000..23f784c --- /dev/null +++ b/client/winchan.js @@ -0,0 +1,301 @@ +var WinChan = (function() { + var RELAY_FRAME_NAME = "__winchan_relay_frame"; + var CLOSE_CMD = "die"; + + // a portable addListener implementation + function addListener(w, event, cb) { + if(w.attachEvent) w.attachEvent('on' + event, cb); + else if (w.addEventListener) w.addEventListener(event, cb, false); + } + + // a portable removeListener implementation + function removeListener(w, event, cb) { + if(w.detachEvent) w.detachEvent('on' + event, cb); + else if (w.removeEventListener) w.removeEventListener(event, cb, false); + } + + + // checking for IE8 or above + function isInternetExplorer() { + var rv = -1; // Return value assumes failure. + var ua = navigator.userAgent; + if (navigator.appName === 'Microsoft Internet Explorer') { + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) + rv = parseFloat(RegExp.$1); + } + // IE > 11 + else if (ua.indexOf("Trident") > -1) { + var re = new RegExp("rv:([0-9]{2,2}[\.0-9]{0,})"); + if (re.exec(ua) !== null) { + rv = parseFloat(RegExp.$1); + } + } + + return rv >= 8; + } + + // checking Mobile Firefox (Fennec) + function isFennec() { + try { + // We must check for both XUL and Java versions of Fennec. Both have + // distinct UA strings. + var userAgent = navigator.userAgent; + return (userAgent.indexOf('Fennec/') != -1) || // XUL + (userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1); // Java + } catch(e) {} + return false; + } + + // feature checking to see if this platform is supported at all + function isSupported() { + return (window.JSON && window.JSON.stringify && + window.JSON.parse && window.postMessage); + } + + // given a URL, extract the origin. Taken from: https://github.com/firebase/firebase-simple-login/blob/d2cb95b9f812d8488bdbfba51c3a7c153ba1a074/js/src/simple-login/transports/WinChan.js#L25-L30 + function extractOrigin(url) { + if (!/^https?:\/\//.test(url)) url = window.location.href; + var m = /^(https?:\/\/[\-_a-zA-Z\.0-9:]+)/.exec(url); + if (m) return m[1]; + return url; + } + + // find the relay iframe in the opener + function findRelay() { + var loc = window.location; + var frames = window.opener.frames; + for (var i = frames.length - 1; i >= 0; i--) { + try { + if (frames[i].location.protocol === window.location.protocol && + frames[i].location.host === window.location.host && + frames[i].name === RELAY_FRAME_NAME) + { + return frames[i]; + } + } catch(e) { } + } + return; + } + + var isIE = isInternetExplorer(); + + if (isSupported()) { + /* General flow: + * 0. user clicks + * (IE SPECIFIC) 1. caller adds relay iframe (served from trusted domain) to DOM + * 2. caller opens window (with content from trusted domain) + * 3. window on opening adds a listener to 'message' + * (IE SPECIFIC) 4. window on opening finds iframe + * 5. window checks if iframe is "loaded" - has a 'doPost' function yet + * (IE SPECIFIC5) 5a. if iframe.doPost exists, window uses it to send ready event to caller + * (IE SPECIFIC5) 5b. if iframe.doPost doesn't exist, window waits for frame ready + * (IE SPECIFIC5) 5bi. once ready, window calls iframe.doPost to send ready event + * 6. caller upon reciept of 'ready', sends args + */ + return { + open: function(opts, cb) { + if (!cb) throw "missing required callback argument"; + + // test required options + var err; + if (!opts.url) err = "missing required 'url' parameter"; + if (!opts.relay_url) err = "missing required 'relay_url' parameter"; + if (err) setTimeout(function() { cb(err); }, 0); + + // supply default options + if (!opts.window_name) opts.window_name = null; + if (!opts.window_features || isFennec()) opts.window_features = undefined; + + // opts.params may be undefined + + var iframe; + + // sanity check, are url and relay_url the same origin? + var origin = extractOrigin(opts.url); + if (origin !== extractOrigin(opts.relay_url)) { + return setTimeout(function() { + cb('invalid arguments: origin of url and relay_url must match'); + }, 0); + } + + var messageTarget; + + if (isIE) { + // first we need to add a "relay" iframe to the document that's served + // from the target domain. We can postmessage into a iframe, but not a + // window + iframe = document.createElement("iframe"); + // iframe.setAttribute('name', framename); + iframe.setAttribute('src', opts.relay_url); + iframe.style.display = "none"; + iframe.setAttribute('name', RELAY_FRAME_NAME); + document.body.appendChild(iframe); + messageTarget = iframe.contentWindow; + } + + var w = opts.popup || window.open(opts.url, opts.window_name, opts.window_features); + if (opts.popup) { + w.location.href = opts.url; + } + + if (!messageTarget) messageTarget = w; + + // lets listen in case the window blows up before telling us + var closeInterval = setInterval(function() { + if (w && w.closed) { + cleanup(); + if (cb) { + cb('User closed the popup window'); + cb = null; + } + } + }, 500); + + var req = JSON.stringify({a: 'request', d: opts.params}); + + // cleanup on unload + function cleanup() { + if (iframe) document.body.removeChild(iframe); + iframe = undefined; + if (closeInterval) closeInterval = clearInterval(closeInterval); + removeListener(window, 'message', onMessage); + removeListener(window, 'unload', cleanup); + if (w) { + try { + w.close(); + } catch (securityViolation) { + // This happens in Opera 12 sometimes + // see https://github.com/mozilla/browserid/issues/1844 + messageTarget.postMessage(CLOSE_CMD, origin); + } + } + w = messageTarget = undefined; + } + + addListener(window, 'unload', cleanup); + + function onMessage(e) { + if (e.origin !== origin) { return; } + try { + var d = JSON.parse(e.data); + if (d.a === 'ready') messageTarget.postMessage(req, origin); + else if (d.a === 'error') { + cleanup(); + if (cb) { + cb(d.d); + cb = null; + } + } else if (d.a === 'response') { + cleanup(); + if (cb) { + cb(null, d.d); + cb = null; + } + } + } catch(err) { } + } + + addListener(window, 'message', onMessage); + + return { + close: cleanup, + focus: function() { + if (w) { + try { + w.focus(); + } catch (e) { + // IE7 blows up here, do nothing + } + } + } + }; + }, + onOpen: function(cb) { + var o = "*"; + var msgTarget = isIE ? findRelay() : window.opener; + if (!msgTarget) throw "can't find relay frame"; + function doPost(msg) { + msg = JSON.stringify(msg); + if (isIE) msgTarget.doPost(msg, o); + else msgTarget.postMessage(msg, o); + } + + function onMessage(e) { + // only one message gets through, but let's make sure it's actually + // the message we're looking for (other code may be using + // postmessage) - we do this by ensuring the payload can + // be parsed, and it's got an 'a' (action) value of 'request'. + var d; + try { + d = JSON.parse(e.data); + } catch(err) { } + if (!d || d.a !== 'request') return; + removeListener(window, 'message', onMessage); + o = e.origin; + if (cb) { + // this setTimeout is critically important for IE8 - + // in ie8 sometimes addListener for 'message' can synchronously + // cause your callback to be invoked. awesome. + setTimeout(function() { + cb(o, d.d, function(r) { + cb = undefined; + doPost({a: 'response', d: r}); + }); + }, 0); + } + } + + function onDie(e) { + if (e.data === CLOSE_CMD) { + try { window.close(); } catch (o_O) {} + } + } + addListener(isIE ? msgTarget : window, 'message', onMessage); + addListener(isIE ? msgTarget : window, 'message', onDie); + + // we cannot post to our parent that we're ready before the iframe + // is loaded. (IE specific possible failure) + try { + doPost({a: "ready"}); + } catch(e) { + // this code should never be exectued outside IE + addListener(msgTarget, 'load', function(e) { + doPost({a: "ready"}); + }); + } + + // if window is unloaded and the client hasn't called cb, it's an error + var onUnload = function() { + try { + // IE8 doesn't like this... + removeListener(isIE ? msgTarget : window, 'message', onDie); + } catch (ohWell) { } + if (cb) doPost({ a: 'error', d: 'client closed window' }); + cb = undefined; + // explicitly close the window, in case the client is trying to reload or nav + try { window.close(); } catch (e) { } + }; + addListener(window, 'unload', onUnload); + return { + detach: function() { + removeListener(window, 'unload', onUnload); + } + }; + } + }; + } else { + return { + open: function(url, winopts, arg, cb) { + setTimeout(function() { cb("unsupported browser"); }, 0); + }, + onOpen: function(cb) { + setTimeout(function() { cb("unsupported browser"); }, 0); + } + }; + } +})(); + +if (typeof module !== 'undefined' && module.exports) { + module.exports = WinChan; +} diff --git a/package.json b/package.json index 4c08293..0208490 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,11 @@ "license": "MIT", "dependencies": { "coffee-script": "1.10", + "lodash": "4", "passport": "^0.3.2", "passport-twitter": "*", + "passport-github": "*", + "passport-google-oauth20": "*", "qs": "6.1" }, "devDependencies": { diff --git a/server/social.coffee b/server/social.coffee index c157eec..5e5ee04 100644 --- a/server/social.coffee +++ b/server/social.coffee @@ -13,6 +13,10 @@ path = require 'path' https = require 'https' qs = require 'qs' +url = require 'url' + +_ = require('lodash') + passport = require 'passport' @@ -27,6 +31,7 @@ module.exports = exports = (log, loga, argv) -> ownerName = '' user = {} wikiName = argv.url + wikiHost = argv.wiki_domain admin = argv.admin @@ -39,9 +44,19 @@ module.exports = exports = (log, loga, argv) -> personaIDFile = argv.id usingPersona = false - ids = {} + if argv.security_useHttps + useHttps = true + callbackProtocol = "https:" + else + useHttps = false + callbackProtocol = url.parse(argv.url).protocol - schemes = {} + if wikiHost + callbackHost = wikiHost + else + callbackHost = url.parse(argv.url).host + + ids = [] # Mozilla Persona service closes on personaEnd = new Date('2016-11-30') @@ -114,13 +129,12 @@ module.exports = exports = (log, loga, argv) -> # site not claimed? return true else - authorized = false try - authorized = switch req.session.passport.user.provider - when "twitter" - if owner.twitter.id is req.session.passport.user.id - true - return authorized + if owner[req.session.passport.user.provider].id is req.session.passport.user.id + return true + else + return false + return false security.isAdmin = (req) -> @@ -128,13 +142,12 @@ module.exports = exports = (log, loga, argv) -> # not added legacy support yet, so... return false else - admin = false try - admin = switch req.session.passport.user.provider - when "twitter" - if admin is req.session.passport.user.id - true - return admin + if admin is req.session.passport.user.id + return true + else + return false + return false security.login = (updateOwner) -> console.log "Login...." @@ -151,111 +164,115 @@ module.exports = exports = (log, loga, argv) -> passport.deserializeUser = (obj, req, done) -> done(null, obj) - - ### + # Github Strategy if argv.github_clientID? and argv.github_clientSecret? - github = {} - github['clientID'] = argv.github_clientID - github['clientSecret'] = argv.github_clientSecret - ids['github'] = github - + ids.push('github') GithubStrategy = require('passport-github').Strategy passport.use(new GithubStrategy({ - clientID: ids['github'].clientID - clientSecret: ids['github'].clientSecret - # this is not going to work - callback must equal that specified won github - # when the application was setup - it can't be dynamic.... - callbackURL: 'http://localhost:3000/auth/github/callback' - }, (accessToken, refreshToken, profile, cb) -> - User.findOrCreate({githubID: profile.id}, (err, user) -> - return cb(err, user)))) - ### - - if argv.twitter_consumerKey? and argv.twitter_consumerSecret? - schemes['twitter'] = true - twitter = {} - twitter['consumerKey'] = argv.twitter_consumerKey - twitter['consumerSecret'] = argv.twitter_consumerSecret - ids['twitter'] = twitter - - TwitterStrategy = require('passport-twitter').Strategy - - passport.use(new TwitterStrategy({ - consumerKey: ids['twitter'].consumerKey - consumerSecret: ids['twitter'].consumerSecret - callbackURL: '/auth/twitter/callback' + clientID: argv.github_clientID + clientSecret: argv.github_clientSecret + scope: 'user:emails' + # callbackURL is optional, and if it exists must match that given in + # the OAuth application settings - so we don't specify it. }, (accessToken, refreshToken, profile, cb) -> user = { - "provider": 'twitter', - "id": profile.id, - "username": profile.username, - "displayName": profile.displayName + provider: 'github', + id: profile.id + username: profile.username + displayName: profile.displayName + email: profile.emails[0].value } cb(null, user))) - ### - if argv.google_clientID? and argv.google_clientSecret? - google = {} - google['clientID'] = argv.google_clientID - google['clientSecret'] = argv.google_clientSecret - ids['google'] = google + # Twitter Strategy + if argv.twitter_consumerKey? and argv.twitter_consumerSecret? + ids.push('twitter') + TwitterStrategy = require('passport-twitter').Strategy + passport.use(new TwitterStrategy({ + consumerKey: argv.twitter_consumerKey + consumerSecret: argv.twitter_consumerSecret + callbackURL: callbackProtocol + '//' + callbackHost + '/auth/twitter/callback' + }, (accessToken, refreshToken, profile, cb) -> + user = { + provider: 'twitter', + id: profile.id, + username: profile.username, + displayName: profile.displayName + } + cb(null, user))) + + # Google Strategy + if argv.google_clientID? and argv.google_clientSecret? + ids.push('google') GoogleStrategy = require('passport-google-oauth20').Strategy passport.use(new GoogleStrategy({ - clientID: ids['google'].clientID - clientSecret: ids['google'].clientSecret - callbackURL: 'http://localhost:3000/auth/google/callback' + clientID: argv.google_clientID + clientSecret: argv.google_clientSecret + callbackURL: callbackProtocol + '//' + callbackHost + '/auth/google/callback' }, (accessToken, refreshToken, profile, cb) -> + user = { + provider: "google" + id: profile.id + displayName: profile.displayName + emails: profile.emails + } cb(null, profile))) - ### + app.use(passport.initialize()) app.use(passport.session()) - ### Github - app.get('/auth/github', passport.authenticate('github'), (req, res) -> ) + # Github + app.get('/auth/github', passport.authenticate('github', {scope: 'user:email'}), (req, res) -> ) app.get('/auth/github/callback', - passport.authenticate('github', { failureRedirect: '/'}), (req, res) -> - # do what ever happens on login - ) - ### + passport.authenticate('github', { successRedirect: '/auth/loginDone', failureRedirect: '/auth/loginDialog'})) # Twitter app.get('/auth/twitter', passport.authenticate('twitter'), (req, res) -> ) app.get('/auth/twitter/callback', passport.authenticate('twitter', { successRedirect: '/auth/loginDone', failureRedirect: '/auth/loginDialog'})) - - - - ### Google + # Google app.get('/auth/google', passport.authenticate('google', { scope: [ 'https://www.googleapis.com/auth/plus.profile.emails.read' ]})) app.get('/auth/google/callback', - passport.authenticate('google', {failureRedirect: '/'}), (req, res) -> - console.log 'google logged in!!!!' - res.redirect('/view/welcome-visitors')) - ### + passport.authenticate('google', { successRedirect: '/auth/loginDone', failureRedirect: '/auth/loginDialog'})) + + + app.get '/auth/client-settings.json', (req, res) -> + # the client needs some information to configure itself + settings = { + useHttps: useHttps + } + if wikiHost + settings.wikiHost = wikiHost + res.json settings app.get '/auth/loginDialog', (req, res) -> + referer = req.headers.referer + console.log "logging into: ", url.parse(referer).hostname - console.log 'owner: ', owner + schemeButtons = [] + _(ids).forEach (scheme) -> + console.log "Scheme: ", scheme + switch scheme + when "twitter" then schemeButtons.push({button: "Twitter"}) + when "github" then schemeButtons.push({button: "Github"}) + when "google" then schemeButtons.push({button: "Google"}) info = { - wikiName: req.hostname - wikiHostName: "a federated wiki site" - title: if owner - "Federated Wiki: Site Owner Sign-on" + wikiName: url.parse(referer).hostname + wikiHostName: if wikiHost + "part of " + req.hostname + " wiki farm" else - "Federated Wiki: Claim Wiki Site" - loginText: if owner - "Sign in to" - else - "Claim wiki" - schemes: "Twitter" + "a federated wiki site" + title: "Federated Wiki: Site Owner Sign-on" + loginText: "Sign in to" + schemes: schemeButtons } res.render(path.join(__dirname, '..', 'views', 'securityDialog.html'), info) @@ -276,6 +293,7 @@ module.exports = exports = (log, loga, argv) -> res.sendStatus(403) else user = req.session.passport.user + console.log "Claim: user = ", user id = switch user.provider when "twitter" then { name: user.displayName @@ -284,10 +302,25 @@ module.exports = exports = (log, loga, argv) -> username: user.username } } + when "github" then { + name: user.displayName + github: { + id: user.id + username: user.username + email: user.email + } + } + when "google" then { + name: user.displayName + google: { + id: user.id + emails: user.emails + } + } setOwner id, (err) -> if err - console.log 'Failed to claim wiki ', req.hostname, ' for ', id + console.log 'Failed to claim wiki ', req.hostname, ' for ', JSON.stringify(id) res.sendStatus(500) updateOwner getOwner() res.json({ @@ -301,8 +334,4 @@ module.exports = exports = (log, loga, argv) -> req.logout() res.send("OK") - - - - security diff --git a/views/done.html b/views/done.html index b180d64..cc2f26e 100644 --- a/views/done.html +++ b/views/done.html @@ -4,13 +4,6 @@ Federated Wiki: Sign-on - @@ -22,4 +15,14 @@ {{{authMessage}}} + + diff --git a/views/securityDialog.html b/views/securityDialog.html index 93e2b91..359d9c3 100644 --- a/views/securityDialog.html +++ b/views/securityDialog.html @@ -12,7 +12,7 @@
- +

{{wikiName}}

{{wikiHostName}}

@@ -24,7 +24,9 @@

{{loginText}} {{wikiName}} with...

- {{{schemes}}} + {{#schemes}} +

{{{button}}}

+ {{/schemes}}