diff --git a/lib/teach-api.js b/lib/teach-api.js index 453eb2a6c..7990f8de7 100644 --- a/lib/teach-api.js +++ b/lib/teach-api.js @@ -1,12 +1,20 @@ var EventEmitter = require('events').EventEmitter; var util = require('util'); -var urlResolve = require('url').resolve; +var url = require('url'); +var urlResolve = url.resolve; var _ = require('underscore'); var request = require('superagent'); var ga = require('react-ga'); var STORAGE_KEY = 'TEACH_API_LOGIN_INFO'; +function generateRandom(charnum) { + charnum = charnum || 12; + var character = String.fromCharCode(0x41 + Math.random() * 25); + var tail = (charnum === 1) ? '' : generateRandom(charnum - 1); + return (Math.random() > 0.5 ? character.toLowerCase() : character.toUpperCase()) + tail; +} + function autobind(obj) { var prototypes = [].slice.call(arguments, 1); prototypes.forEach(function(prototype) { @@ -53,37 +61,100 @@ _.extend(TeachAPI.prototype, { var info = this.getLoginInfo(); return info && info.username; }, + + // The first half of the oauth2 login work flow: + // + // Form an oauth2 URL that users can be redirected to, which will eventually + // lead to continueLogin being called when the oauth2 service is done handling + // the authentication and remote login of the user. startLogin: function() { - if (!(process.browser && window.navigator.id)) { - return this.emit('login:error', - new Error('navigator.id does not exist')); + var state = generateRandom(); + try { + window.sessionStorage.setItem('oauth2_token', state); + } catch (securityError) { + console.warn('window.sessionStorage is not available, attempting document.cookie fallback...'); + document.cookie = 'oauth2_token=' + state; } - window.navigator.id.get(function(assertion) { - if (!assertion) { - return this.emit('login:cancel'); + + if (document.cookie.indexOf('oauth2_token=' + state) === -1) { + console.error('document.cookie fallback failed. Login will be unable to complete successfully!'); + } + + // We redirect the user to the oauth2 login service on id.webmaker.org, which + // will take care off the magic for us. This will eventually call us back by + // redirecting the user back to teach.wmo/oauth2/callback, which is a page that + // can accept login credentials, compare the token to the one we saved to + // window.sessionStorage['oauth2_token'], and then call the teach-API for local + // log in to the actual teach-relevant bits of the site. + + window.location = url.format({ + protocol: 'https', + host: 'id.webmaker.org/login/oauth/authorize', + query: { + client_id: 'test', + response_type: 'code', + scopes: 'user', + state: state } + }); + }, + + // The second half of the oauth2 work flow: + // + // This function is used to process the oauth2 login callback, when the login + // server on id.wmo redirects to teach.wmo/oauth2/callback + continueLogin: function() { + // grab the url parameters sent by the oauth2 service + var parsed = url.parse(window.location.toString()); + var params = parsed.query; + + // for the oauth callback, there are three values we are interested in: + var clientId = params.client_id; + var code = params.code; + var state = params.state; + + var correctState = false; + try { + correctState = window.sessionStorage.setItem('oauth2_token', state); + } catch (securityError) { + console.warn('window.sessionStorage is not available, attempting to read from document.cookie...'); + correctState = document.cookie.toString().indexOf('oauth2_token=' + state) > -1; + } + + // foremost, the client_id and "state" value (which we invented during startLogin) + // needs to match. Otherwise, this is not a genuine callback. + if (clientId === 'test' && correctState) { + // genuine call: we now call the teach-api with this information so that + // it can do server <-> server communication with id.wmo to verify that + // the code that we got in the callback is indeed a real auth code. request - .post(this.baseURL + '/auth/persona') - .type('form') - .send({ assertion: assertion }) - .end(function(err, res) { - if (err) { - err.hasNoWebmakerAccount = ( - err.response && err.response.forbidden && - err.response.text == 'invalid assertion or email' - ); - return this.emit('login:error', err); - } - - // TODO: Handle a thrown exception here. - this.storage[STORAGE_KEY] = JSON.stringify(res.body); - - this.emit('username:change', res.body.username); - this.emit('login:success', res.body); - }.bind(this)); - }.bind(this)); + .post(this.baseURL + '/auth') + .type('form') + .send({ code: code }) + .end(function(err, res) { + if (err) { + err.hasNoWebmakerAccount = ( + err.response && err.response.forbidden && + err.response.text == 'invalid authorization code' + ); + return this.emit('login:error', err); + } + // TODO: Handle a thrown exception here. + this.storage[STORAGE_KEY] = JSON.stringify(res.body); + this.emit('username:change', res.body.username); + this.emit('login:success', res.body); + }.bind(this)); + } + + // cleanup after login, regardless of whether it succeeded or not + try { + window.sessionStorage.removeItem('oauth2_token'); + } catch (securityError) { + document.cookie = 'oauth2_token=false'; + } }, + request: function(method, path) { var info = this.getLoginInfo(); var url = urlResolve(this.baseURL, path); diff --git a/pages/callback.jsx b/pages/callback.jsx new file mode 100644 index 000000000..558663a1c --- /dev/null +++ b/pages/callback.jsx @@ -0,0 +1,23 @@ +var React = require('react'); + +var OAuth2Callback = React.createClass({ + + mixins: [ + require('../lib/teach-api') + ], + + // The only thing this page does is take in the callback arguments, + // and verify they are in fact valie. If so, the teach-api function + // for continued login should redirect the user once authenticated + // or rejected. + componentDidMount: function() { + this.getTeachAPI().continueLogin(); + ga.event({ category: 'Login', action: 'Continue Login' }); + }, + + render: function() { + return