diff --git a/.gitignore b/.gitignore index 4428cfefb..a8c329571 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ public/stylesheets/userbar.css dist/ client/ mozilla.webmaker.org +package-lock.json diff --git a/env.dist b/env.dist index c2df11b7c..ddd7bf4f0 100644 --- a/env.dist +++ b/env.dist @@ -71,9 +71,9 @@ export AWS_SECRET_ACCESS_KEY="foo" # id.wm.o config # -export OAUTH_CLIENT_ID="test" -export OAUTH_CLIENT_SECRET="test" -export OAUTH_AUTHORIZATION_URL="http://localhost:1234" +export OAUTH_WEBMAKER_CLIENT_ID="test" +export OAUTH_WEBMAKER_CLIENT_SECRET="test" +export OAUTH_WEBMAKER_AUTH_URL="http://localhost:1234" # Default content title, which should match a folder inside the repo's /default folder export DEFAULT_PROJECT_TITLE="empty-project" diff --git a/package.json b/package.json index d196cbe0d..faa773a5b 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,8 @@ "node-uuid": "~1.4.7", "npm-run-all": "^4.0.2", "nunjucks": "^2.3.0", + "passport": "^0.3.2", + "passport-webmaker": "^1.1.1", "properties-parser": "0.3.1", "q-io": "1.13.2", "request": "2.80.0", diff --git a/server/index.js b/server/index.js index 4852d570a..0ae1211a1 100644 --- a/server/index.js +++ b/server/index.js @@ -7,6 +7,7 @@ let express = require("express"); let path = require("path"); let favicon = require("serve-favicon"); let url = require("url"); +let passport = require("passport"); let env = require("./lib/environment"); let templatize = require("./templatize"); @@ -16,6 +17,7 @@ let localize = require("./localize"); let HttpError = require("./lib/http-error.js"); let routes = require("./routes")(); let Utils = require("./lib/utils"); +let passportConfig = require("./passport"); let server = express(); let environment = env.get("NODE_ENV"); @@ -53,6 +55,7 @@ requests.disableHeaders([ "x-powered-by" ]) .url({ extended: true }) .lessOptimizations(path.join(root, "public"), cssAssets, !isDevelopment) .healthcheck() +.passport(passport) .sessions({ key: "mozillaThimble", secret: env.get("SESSION_SECRET"), @@ -114,11 +117,15 @@ localize(server, Object.assign(env.get("L10N"), { excludeLocaleInUrl: [ "/projects/remix-bar" ] })); +/** + * Passport + */ + passportConfig(passport); /** * API routes */ -routes.init(server); +routes.init(server, passport); /* diff --git a/server/lib/middleware.js b/server/lib/middleware.js index c7abeb767..7c2b81240 100755 --- a/server/lib/middleware.js +++ b/server/lib/middleware.js @@ -22,7 +22,7 @@ const ASSERT_TOKEN = env.get("ASSERT_TOKEN"); module.exports = function middlewareConstructor(config) { // Set up a token decryptor using the default cryptr 2.0.0 algorithm. let cryptr = new Cryptr(env.get("SESSION_SECRET")); - + // Set up a fallback decryptor that matches the cryptr 1.0.0 algorithm // https://github.com/MauriceButler/cryptr/compare/fabae97a61119d69f03fc189f7c95dda826c96b7...master#diff-168726dbe96b3ce427e7fedce31bb0bcR9 let cryptrFallback = new Cryptr(env.get("SESSION_SECRET"), "aes256"); @@ -63,7 +63,7 @@ module.exports = function middlewareConstructor(config) { * sign out and sign in again (to bust browser cache). */ checkForAuth(req, res, next) { - if(req.session.user) { + if(req.session.passport.user) { return next(); } @@ -86,7 +86,7 @@ module.exports = function middlewareConstructor(config) { } // Decrypt oauth token - req.user = req.session.user; + req.user = req.session.passport.user; let token = req.user.token = cryptr.decrypt(req.session.token); if(ASSERT_TOKEN) { @@ -102,23 +102,23 @@ module.exports = function middlewareConstructor(config) { } let tokenType = typeof token; - + if(tokenType !== "string") { console.log("ASSERT_TOKEN FAILED: Expected token type to be String, instead got: " + tokenType); return false; } - + if(!/^[a-z0-9]{64}$/.test(token)) { console.log("ASSERT_TOKEN FAILED: Expected token to only have chars a-z, 0-9. Also got: '" + token.replace(/[a-z0-9]/g, ' ') + "'"); return false; } - + return true; }; if (!assert(token)) { console.log("ASSERT_TOKEN FAILED: retrying decryption using aes-256 rather than aes-256-ctr"); - + token = req.user.token = cryptrFallback.decrypt(req.session.token); if (!assert(token)) { @@ -137,7 +137,7 @@ module.exports = function middlewareConstructor(config) { qs = `?${qs}`; } - if(req.session.user) { + if(req.session.passport.user) { next(); } else { res.redirect(307, `/${locale}/anonymous/${uuid.v4()}${qs}`); @@ -286,6 +286,13 @@ module.exports = function middlewareConstructor(config) { clearRedirects(req, res, next) { delete req.session.home; next(); + }, + + logout(req, res) { + let locale = (req.localeInfo && req.localeInfo.lang) ? req.localeInfo.lang : "en-US"; + req.logout(); + req.session = null; + res.redirect(307, `/${locale}`); } }; }; diff --git a/server/passport.js b/server/passport.js new file mode 100644 index 000000000..34043302c --- /dev/null +++ b/server/passport.js @@ -0,0 +1,31 @@ +let WebmakerStrategy = require("passport-webmaker").Strategy; +let url = require("url"); +let Cryptr = require("cryptr"); + +let env = require("./lib/environment"); +let oauth = env.get("OAUTH"); + +module.exports = function (passport) { + let cryptr = new Cryptr(env.get("SESSION_SECRET")); + + passport.use(new WebmakerStrategy({ + clientID: oauth.webmaker_client_id, + clientSecret: oauth.webmaker_client_secret, + authorizationURL: url.resolve(oauth.webmaker_auth_url, "/login/oauth/authorize"), + tokenURL: url.resolve(oauth.webmaker_auth_url, "/login/oauth/access_token"), + profileURL: url.resolve(oauth.webmaker_auth_url, "/user"), + state: true, + passReqToCallback: true + }, function (req, accessToken, refreshToken, profile, done) { + req.session.token = cryptr.encrypt(accessToken); + return done(null, profile); + })); + + passport.serializeUser(function(user, done) { + done(null, user); + }); + + passport.deserializeUser(function(user, done) { + done(null, user); + }); +}; diff --git a/server/request.js b/server/request.js index 6f1b6bde9..c0b8511c5 100644 --- a/server/request.js +++ b/server/request.js @@ -111,6 +111,12 @@ Request.prototype = { res.json({ http: 'okay', version: version }); }); + return this; + }, + passport(passport) { + this.server.use(passport.initialize()); + this.server.use(passport.session()); + return this; } }; diff --git a/server/routes/auth/callback.js b/server/routes/auth/callback.js new file mode 100644 index 000000000..88a1d6dec --- /dev/null +++ b/server/routes/auth/callback.js @@ -0,0 +1,42 @@ +var HttpError = require("../../lib/http-error"); + +module.exports = function(config, passport, req, res, next) { + var locale = req.session.locale; + + if(!locale) { + // This can happen when we try to logout again when we are already + // logged out (i.e. the session doesn't exist and hence req.session.locale + // is undefined) + locale = (req.localeInfo && req.localeInfo.lang) ? req.localeInfo.lang : "en-US"; + } + + //var strategy = req.params.strategy.toLowerCase(); + var editorURL = `/${locale}/editor`; + + // TODO: When we implement multiple strategies, we need to incorporate this into an if/else or switch block. + // Right now we ignore the "strategy" variable, because we already know the only valid response is "webmaker". + passport.authenticate("webmaker", function(err, user) { + if (err) { + res.status(500); + return next(HttpError.format({ + message: `(Passport) Failed to authenticate user.`, + context: err + }, req)); + } + + if (!user) { + return res.redirect(`/${locale}/login/webmaker`); + } + + req.logIn(user, function(err) { + if (err) { + res.status(500); + return next(HttpError.format({ + message: `(Passport) Failed to serialize user session cookie.`, + context: err + }, req)); + } + return res.redirect(editorURL); + }); + })(req, res, next); +}; diff --git a/server/routes/auth/index.js b/server/routes/auth/index.js index c178f2fa9..99c1862c4 100644 --- a/server/routes/auth/index.js +++ b/server/routes/auth/index.js @@ -1,10 +1,13 @@ module.exports = { - init: function(app, middleware, config) { - app.get("/login", - require("./login").bind(app, config)); + init: function(app, middleware, config, passport) { - app.get("/callback", + app.get("/login/:strategy", + require("./login").bind(app, config, passport)); + + app.get("/login/:strategy/callback", middleware.setErrorMessage("errorAuthenticating"), - require("./oauth2-callback").bind(app, config)); + require("./callback").bind(app, config, passport)); + + app.get("/logout", middleware.logout); } }; diff --git a/server/routes/auth/login.js b/server/routes/auth/login.js index 7896bf5e4..0e8e5170e 100644 --- a/server/routes/auth/login.js +++ b/server/routes/auth/login.js @@ -1,4 +1,4 @@ -module.exports = function(config, req, res) { +module.exports = function(config, passport, req, res, next) { if (req.query.anonymousId) { req.session.project = { anonymousId: req.query.anonymousId, @@ -10,12 +10,10 @@ module.exports = function(config, req, res) { req.session.locale = (req.localeInfo && req.localeInfo.lang) ? req.localeInfo.lang : "en-US"; - var loginType = "&action=" + (req.query.signup ? "signup" : "signin"); - var state = "&state=" + req.cookies.state; + //var strategy = req.params.strategy.toLowerCase(); + var action = req.query.signup ? "signup" : "signin"; - res.set({ - "Cache-Control": "no-cache" - }); - - res.redirect(307, config.loginURL + state + loginType); + // TODO: When we implement multiple strategies, we need to incorporate this into an if/else or switch block. + // Right now we ignore the "strategy" variable, because we already know the only valid response is "webmaker". + passport.authenticate("webmaker", { scopes: ["user", "email"], action: action })(req, res, next); }; diff --git a/server/routes/auth/oauth2-callback.js b/server/routes/auth/oauth2-callback.js deleted file mode 100644 index aac16d937..000000000 --- a/server/routes/auth/oauth2-callback.js +++ /dev/null @@ -1,155 +0,0 @@ -"use strict"; - -var request = require("request"); -var HttpError = require("../../lib/http-error"); - -module.exports = function(config, req, res, next) { - var oauth = config.oauth; - var cryptr = config.cryptr; - var authURL = `${oauth.authorization_url}/login/oauth/access_token`; - var locale = req.session.locale; - if(!locale) { - // This can happen when we try to logout again when we are already - // logged out (i.e. the session doesn't exist and hence req.session.locale - // is undefined) - locale = (req.localeInfo && req.localeInfo.lang) ? req.localeInfo.lang : "en-US"; - } - - res.set("Cache-Control", "no-cache"); - - if (req.query.logout) { - req.session = null; - return res.redirect(307, "/" + locale); - } - - if (!req.query.code) { - res.status(401); - return next( - HttpError.format({ - message: "OAuth code was not set by the authentication server", - context: req.query - }, req) - ); - } - - if (!req.cookies.state || !req.query.state) { - res.status(401); - return next( - HttpError.format({ - message: "No state information was passed back by the authentication server", - context: req.query - }, req) - ); - } - - if (req.cookies.state !== req.query.state) { - res.status(401); - return next( - HttpError.format({ - message: "The initial state during login does not match the state returned by the authentication server.", - context: req.query - }, req) - ); - } - - if (req.query.client_id !== oauth.client_id) { - res.status(401); - return next( - HttpError.format({ - message: "The client id returned by the authentication server does not match Thimble's client id.", - context: req.query - }, req) - ); - } - - // First, fetch the token - request.post({ - url: authURL, - form: { - client_id: oauth.client_id, - client_secret: oauth.client_secret, - grant_type: "authorization_code", - code: req.query.code - } - }, function(err, response, body) { - if (err) { - res.status(500); - return next( - HttpError.format({ - message: `Failed to send request to ${authURL}. Verify that the authentication server is up and running.`, - context: err - }, req) - ); - } - - if (response.statusCode !== 200) { - res.status(response.statusCode); - return next( - HttpError.format({ - message: `Request to ${authURL} returned a status of ${response.statusCode}`, - context: response.body - }, req) - ); - } - - try { - body = JSON.parse(body); - } catch(e) { - res.status(500); - return next( - HttpError.format({ - message: "Data (access token) sent by the authentication server was in an invalid format. Failed to run `JSON.parse`", - context: e.message, - stack: e.stack - }, req) - ); - } - - req.session.token = cryptr.encrypt(body.access_token); - - var userURL = `${oauth.authorization_url}/user`; - - // Next, fetch user data - request.get({ - url: userURL, - headers: { - "Authorization": "token " + body.access_token - } - }, function(err, response, body) { - if (err) { - res.status(500); - return next( - HttpError.format({ - message: `Failed to send request to ${userURL}. Verify that the authentication server is up and running.`, - context: err - }, req) - ); - } - - if (response.statusCode !== 200) { - res.status(response.statusCode); - return next( - HttpError.format({ - message: `Request to ${userURL} returned a status of ${response.statusCode}`, - context: response.body - }, req) - ); - } - - try { - req.session.user = JSON.parse(body); - } catch(e) { - res.status(500); - return next( - HttpError.format({ - message: "User data sent by the authentication server was in an invalid format. Failed to run `JSON.parse`", - context: e.message, - stack: e.stack - }, req) - ); - } - - res.redirect(307, "/" + locale + "/editor"); - }); - }); -}; diff --git a/server/routes/config.js b/server/routes/config.js index f0a3e2d83..4a852478e 100644 --- a/server/routes/config.js +++ b/server/routes/config.js @@ -3,12 +3,9 @@ var Cryptr = require("cryptr"); var env = require("../lib/environment"); var oauth = env.get("OAUTH"); -var loginURL = oauth.authorization_url + "/login/oauth/authorize?" + [ - "client_id=" + oauth.client_id, - "response_type=code", - "scopes=user email" -].join("&"); -var logoutURL = oauth.authorization_url + "/logout?client_id=" + oauth.client_id; + +var loginURL = url.resolve(env.get("APP_HOSTNAME"), "/login/webmaker"); +var logoutURL = url.resolve(env.get("APP_HOSTNAME"), "/logout"); // We make sure to grab just the protocol and hostname for // postmessage security. diff --git a/server/routes/index.js b/server/routes/index.js index a1ef2db4c..5fb86881b 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -16,9 +16,8 @@ function getPageData(req) { module.exports = function() { return { - init: function(app) { + init: function(app, passport) { [ - require("./auth"), require("./main"), require("./projects"), require("./files"), @@ -26,6 +25,7 @@ module.exports = function() { ].forEach(function(module) { module.init(app, middleware, config); }); + require("./auth").init(app, middleware, config, passport); }, rawData: function(req, res) { diff --git a/server/routes/main/editor.js b/server/routes/main/editor.js index 9a4068135..3d7017d5c 100644 --- a/server/routes/main/editor.js +++ b/server/routes/main/editor.js @@ -67,7 +67,7 @@ module.exports = function(config, req, res, next) { languages: req.app.locals.languages, csrf: req.csrfToken(), editorHOST: config.editorHOST, - loginURL: config.appURL + "/" + locale + "/login", + loginURL: config.appURL + "/" + locale + "/login/webmaker", logoutURL: config.logoutURL, queryString: qs }; @@ -85,7 +85,7 @@ module.exports = function(config, req, res, next) { if (req.user) { options.username = req.user.username; - options.avatar = req.user.avatar; + options.avatar = req.user.photos[0].value; } getProjectMetadata(config, req, function(err, status, projectMetadata) { diff --git a/server/routes/main/features.js b/server/routes/main/features.js index 04b615788..60f3cffaf 100644 --- a/server/routes/main/features.js +++ b/server/routes/main/features.js @@ -8,7 +8,7 @@ module.exports = function(config, req, res) { } var options = { - loginURL: config.appURL + "/" + locale + "/login", + loginURL: config.appURL + "/" + locale + "/login/webmaker", editorHOST: config.editorHOST, editorURL: config.editorURL, URL_PATHNAME: "/" + qs, @@ -18,7 +18,7 @@ module.exports = function(config, req, res) { if (req.user) { options.username = req.user.username; - options.avatar = req.user.avatar; + options.avatar = req.user.photos[0].value; options.logoutURL = config.logoutURL; } diff --git a/server/routes/main/get-involved.js b/server/routes/main/get-involved.js index 027385bd6..22cf73015 100644 --- a/server/routes/main/get-involved.js +++ b/server/routes/main/get-involved.js @@ -8,7 +8,7 @@ module.exports = function(config, req, res) { } var options = { - loginURL: config.appURL + "/" + locale + "/login", + loginURL: config.appURL + "/" + locale + "/login/webmaker", editorHOST: config.editorHOST, editorURL: config.editorURL, URL_PATHNAME: "/" + qs, @@ -18,7 +18,7 @@ module.exports = function(config, req, res) { if (req.user) { options.username = req.user.username; - options.avatar = req.user.avatar; + options.avatar = req.user.photos[0].value; options.logoutURL = config.logoutURL; } diff --git a/server/routes/main/homepage.js b/server/routes/main/homepage.js index c73619ca3..2c56995ef 100644 --- a/server/routes/main/homepage.js +++ b/server/routes/main/homepage.js @@ -8,7 +8,7 @@ module.exports = function(config, req, res) { } var options = { - loginURL: config.appURL + "/" + locale + "/login", + loginURL: config.appURL + "/" + locale + "/login/webmaker", editorHOST: config.editorHOST, editorURL: config.editorURL, URL_PATHNAME: "/" + qs, @@ -18,7 +18,7 @@ module.exports = function(config, req, res) { if (req.user) { options.username = req.user.username; - options.avatar = req.user.avatar; + options.avatar = req.user.photos[0].value; options.logoutURL = config.logoutURL; } diff --git a/server/routes/projects/read.js b/server/routes/projects/read.js index af0eb1f5f..bf592a3eb 100644 --- a/server/routes/projects/read.js +++ b/server/routes/projects/read.js @@ -79,7 +79,7 @@ module.exports = function(config, req, res, next) { csrf: req.csrfToken ? req.csrfToken() : null, HTTP_STATIC_URL: "/" + locale, username: user.username, - avatar: user.avatar, + avatar: user.photos[0].value, projects: projects, queryString: qs, editorHOST: config.editorHOST,