From 58e30dff8ebcf529d469ad5b27c869d76595ef1a Mon Sep 17 00:00:00 2001 From: Claudio Cicali Date: Wed, 28 May 2014 15:28:50 +0200 Subject: [PATCH] Adds support for Google OAuth2 authentication --- .gitignore | 1 + ChangeLog.md | 12 ++++- README.md | 117 +++++++++++++++++++++++++----------------------- jingo | 82 ++++++++++++++++++++------------- lib/config.js | 2 + package.json | 44 +++++++++--------- routes/index.js | 7 ++- 7 files changed, 153 insertions(+), 112 deletions(-) diff --git a/.gitignore b/.gitignore index 50c659d7..2c05944a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/* +config.yaml config.yml data/* diff --git a/ChangeLog.md b/ChangeLog.md index a27446fb..90130d9b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,13 @@ +Version 0.6.0, May 28th, 20114 +============================= + +- Uses the OAuth 2 authentication instead of the OpenID 2.0 + (see also https://developers.google.com/accounts/docs/OpenID) + This will require to edit the config file and request Google for + a client id and client secret (see the README on how to do that) + + The update requires to issue a `npm install` + Version 0.5.2, May 26th, 20114 ============================= @@ -30,7 +40,7 @@ Version 0.4.3, July 10th, 2013 - Closes #19 - Better line height for LI -- Refines PR #20 +- Refines PR #20 Version 0.4.2, June 29th, 2013 ============================= diff --git a/README.md b/README.md index bece759e..f827f09a 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ A simple git based _wiki engine_ written for Node.js. The aim of this wiki engine is to provide a very easy way to create a centralized documentation area for people used to working with git and markdown. It should fit well -into a development team without the need to learn or install ad-hoc servers or applications. -Jingo is very much inspired by the github wiki system (gollum), but tries to be more +into a development team without the need to learn or install ad-hoc servers or applications. +Jingo is very much inspired by the github wiki system (gollum), but tries to be more a stand-alone and complete system than gollum is. -Think of jingo as "the github wiki, without github but with more features". "Jingo" +Think of jingo as "the github wiki, without github but with more features". "Jingo" means "Jingo is not Gollum" for a reason. There is a demo server running at http://jingo.cica.li:6067/wiki/home @@ -36,33 +36,18 @@ Features For code syntax highlighting, Jingo uses the `node-syntaxhighlighter` module. For the list of supported languages, please refer to [this page](https://github.com/thlorenz/node-syntaxhighlighter/tree/master/lib/scripts). -Known limitations ------------------ - -- The authentication is mandatory (no anonymous writing allowed). See also issue #4 -- The repository is "flat" (no directories or namespaces) -- Authorization is only based on a regexp'ed white list with matches on the user email address -- There is one authorization level only (no "administrators" and "editors") -- At the moment there is no "restore previous revision", just a revision browser -- No scheduled pull or fetch from the remote is provided (because handling conflicts would be - a bit too... _interesting_) - -Please note that at the moment it is quite "risky" to have someone else, other than jingo itself, -have write access to the remote jingo is pushing to. The push operation is supposed to always be -successfull and there is no pull or fetch. You can of course manage to handle pull requests yourself. - Installation ------------ `npm install jingo` or download the whole thing and run "npm install" -Jingo needs a config file and to create a sample config file, just run `jingo -s`, redirect the -output on a file and then edit it. The config file contains all the available configuration keys. -Be sure to provide a valid server hostname (like wiki.mycompany.com) for Google Auth to be able +Jingo needs a config file and to create a sample config file, just run `jingo -s`, redirect the +output on a file and then edit it. The config file contains all the available configuration keys. +Be sure to provide a valid server hostname (like wiki.mycompany.com) for Google Auth to be able to get back to you. -If you define a `remote` to push to, then jingo will automatically issue a push to that remote every -`pushInterval` seconds. You can also specify a branch using the syntax "remotename branchnama". If you +If you define a `remote` to push to, then jingo will automatically issue a push to that remote every +`pushInterval` seconds. You can also specify a branch using the syntax "remotename branchnama". If you don't specify a branch, that will be `master`. Please note that before the `push`, a `pull` will also be issued (at the moment Jingo will not try to resolve conflicts, though). @@ -71,71 +56,92 @@ The basic command to run the wiki will then be `jingo -c /path/to/config.yaml` (using `forever -w` is highly recommended, though) -Before running jingo you need to initialize its git repository somewhere (`git init` is enough). +Before running jingo you need to initialise its git repository somewhere (`git init` is enough). If you define a remote to push to, be sure that the user who'll push has the right to do so. -If your documents reside in subdirectory of your repository, you need to specify its name using the -`docSubdir` configuration option. The `repository` path _must_ be an absolute path pointing to the +If your documents reside in subdirectory of your repository, you need to specify its name using the +`docSubdir` configuration option. The `repository` path _must_ be an absolute path pointing to the root of the repository. If you want your wiki server to only listen to your `localhost`, set the configuration key `localOnly` to true. -Common problems ---------------- - -Sometimes upgrading your version of node.js could break the `iconv` module. Try updating it with `npm install iconv`. - Authentication and Authorization -------------------------------- -The _authorization_ section of the config file has two keys: anonRead and validMatches. If the +You can enable two authentication methodologies: _Google logins (OAuth2)_ or a simple, locally verified +username/password credentials match (called "alone"). If you use the _alone_ method, you can have _only one user_ +accessing the wiki (thus the name). + +The _Google Login_ uses OAuth 2 and that means that on a fresh installation you need to get a `client id` +and a `client secret` from Google and put those informations in the configuration file. Follow these instructions: + - Open the [Google developer console](https://code.google.com/apis/console/) + - Create a new project (you can leave the _Project id_ as it is). This will take a little while + - Open the _Consent screen_ page and fill in the details (particularly, the _product name_) + - Now open _APIs & auth_ => _Credentials_ and click on _Create new client id_ + - Here you need to specify the base URL of your jingo installation. Google will fill in automatically the other field + with a `/oauth2callback` URL, which is fine + - Now you need to copy the `Client ID` and `Client secret` in your jingo config file in the proper places + +The _alone_ method uses a `username`, a `passwordHash` and optionally an `email`. The password is hashed +using a _non salted_ SHA-1 algorithm, which makes this method not the safest in the world but at least you don't have +a clear text password in the config file. To generate the hash, use the `--hash-string` program option: once +you get the hash, copy it in the config file. + +You can enable both authentication options at the same time. The `alone` is disabled by default. + +The _authorization_ section of the config file has two keys: anonRead and validMatches. If the anonRead is true, then anyone can read anything. -If anonRead is false you need to authenticate also for reading and then the email of the user _must_ -match at least one of the regular expressions provided via validMatches, which is a comma separated + +If anonRead is false you need to authenticate also for reading and then the email of the user _must_ +match at least one of the regular expressions provided via validMatches, which is a comma separated list. There is no "anonWrite", though. To edit a page the user must be authenticated. The authentication is mandatory to edit pages from the web interface, but jingo works on a git repository; -that means that you could skip the authentication altogether and edit pages with your editor and push +that means that you could skip the authentication altogether and edit pages with your editor and push to the remote that jingo is serving. -You can enable two authentication methodologies: _Google logins_ or a simple, locally verified -username/password credentials match (called "alone"). +Common problems +--------------- -If you use the _alone_ method, you can have _only one user_ accessing the wiki (thus the name). +Sometimes upgrading your version of node.js could break the `iconv` module. Try updating it with `npm install iconv`. -The _Google Login_ doesn't need any configuration option: just enable it in the config file (it's enabled by default), -but assure yourself that the `baseUrl` configuration variable points to something that Google can reach ("http://localhost:6067" -is fine but you could have Jingo proxied via another web server which listens on the :80, for example). +Known limitations +----------------- -The _alone_ method uses a `username`, a `passwordHash` and optionally an `email`. The password is hashed -using a _non salted_ SHA-1 algorithm, which makes this method not the safest in the world but at least you don't have -a clear text password in the config file. To generate the hash, use the `--hash-string` program option: once -you get the hash, copy it in the config file. +- The authentication is mandatory (no anonymous writing allowed). See also issue #4 +- The repository is "flat" (no directories or namespaces) +- Authorization is only based on a regexp'ed white list with matches on the user email address +- There is one authorization level only (no "administrators" and "editors") +- At the moment there is no "restore previous revision", just a revision browser +- No scheduled pull or fetch from the remote is provided (because handling conflicts would be + a bit too... _interesting_) -You can enable both authentication options at the same time. The `alone` is disabled by default. +Please note that at the moment it is quite "risky" to have someone else, other than jingo itself, +have write access to the remote jingo is pushing to. The push operation is supposed to always be +successfull and there is no pull or fetch. You can of course manage to handle pull requests yourself. Customization ------------- You can customize jingo in four different ways: -- add a left sidebar to every page: just add a file named `_sidebar.md` containing the markdown you - want to display to the repository. You can edit or create the sidebar from jingo itself, visiting +- add a left sidebar to every page: just add a file named `_sidebar.md` containing the markdown you + want to display to the repository. You can edit or create the sidebar from jingo itself, visiting `/wiki/_sidebar` (note that the title of the page in this case is useless) -- add a footer to every page: the page you need to create is "_footer.md" and the same rules for the +- add a footer to every page: the page you need to create is "_footer.md" and the same rules for the sidebar apply -- add a custom CSS file, included in every page as the last file. The name of the file must be `_style.css` +- add a custom CSS file, included in every page as the last file. The name of the file must be `_style.css` and must reside in the repository. It is not possible to edit the file from jingo itself -- add a custom JavaScript file, included in every page as the last JavaScript file. The name of the file must +- add a custom JavaScript file, included in every page as the last JavaScript file. The name of the file must be `_script.js` and must reside in the repository. It is not possible to edit the file from jingo itself -All those files are cached (thus, not re-read for every page load, but kept in memory). This means that for -every modification in _style.css and _script.js you need to restart the server (sorry, working on that). -This is not true for the footer and the sidebar but ONLY IF you edit those pages from jingo (which in that +All those files are cached (thus, not re-read for every page load, but kept in memory). This means that for +every modification in _style.css and _script.js you need to restart the server (sorry, working on that). +This is not true for the footer and the sidebar but ONLY IF you edit those pages from jingo (which in that case will clear the cache by itself). -jingo uses twitter Bootstrap and jQuery as its front-end components. +jingo uses twitter Bootstrap and jQuery as its front-end components. Editing ------- @@ -156,4 +162,3 @@ page name, you can specify the actual page name after a pipe: [[How Jingo works|Jingo Works]] The above tag will link to `jingo-works.md` using "How Jingo Works" as the link text. - diff --git a/jingo b/jingo index caca244e..c5373272 100755 --- a/jingo +++ b/jingo @@ -20,12 +20,12 @@ var express = require('express') , expValidator = require('express-validator') , gravatar = require('gravatar') , Url = require('url') - , GoogleStrategy = require('passport-google').Strategy + , GoogleStrategy = require('passport-google-oauth').OAuth2Strategy , LocalStrategy = require('passport-local').Strategy , Flash = require('connect-flash') , program = require('commander'); -program.version('0.5.2') +program.version('0.6.0') .option('-c, --config ', 'Specify the config file') .option('-#, --hash-string ', 'Create an hash for a string') .option('-l, --local', 'Listen on localhost only') @@ -74,7 +74,7 @@ if (typeof app.locals.features.codemirror == 'undefined' && } // This should never happen, of course -if (app.locals.features.markitup && +if (app.locals.features.markitup && app.locals.features.codemirror) { app.locals.features.markitup = false; } @@ -101,12 +101,19 @@ app.locals.pretty = true; // Pretty HTML output from Jade var auth = app.locals.authentication = Config.get("authentication", { google: { enabled: true }, alone: { enabled: false } }); -if ( (!auth.google || !auth.google.enabled) && - (!auth.alone || !auth.alone.enabled) ) { +auth.google = auth.google || {enabled: false}; +auth.alone = auth.alone || {enabled: false}; + +if ( !auth.google.enabled && !auth.alone.enabled ) { console.log("Error: no authentication method provided. Cannot continue."); process.exit(-1); } +if (auth.google.enabled && (!auth.google.clientId || !auth.google.clientSecret)) { + console.log("Error: invalid or missing authentication credentials for Google (clientId and/or clientSecret)."); + process.exit(-1); +} + Components.init(Git); var routes = require("./routes"); @@ -180,39 +187,48 @@ function requireAuthentication(req, res, next) { } } -/* +/* * Passport configuration */ -passport.use(new GoogleStrategy({ - returnURL: app.locals.baseUrl + '/auth/google/return', - realm: app.locals.baseUrl - }, +if (auth.google.enabled) { - function(identifier, profile, done) { - usedAuthentication("google"); - done(undefined, profile); - } -)); + passport.use(new GoogleStrategy({ + clientID: auth.google.clientId, + clientSecret: auth.google.clientSecret, + // I will leave the horrible name as the default to make the painful creation + // of the client id/secret simpler + callbackURL: app.locals.baseUrl + '/oauth2callback' + }, -passport.use(new LocalStrategy( + function(accessToken, refreshToken, profile, done) { + usedAuthentication("google"); + done(null, profile); + } + )); +} - function(username, password, done) { +if (auth.alone.enabled) { - var user = { - displayName: auth.alone.username, - email: auth.alone.email || "" - }; + passport.use(new LocalStrategy( - if (username.toLowerCase() != auth.alone.username.toLowerCase() || Tools.hashify(password) != auth.alone.passwordHash) { - return done(null, false, { message: 'Incorrect username or password' }); - } + function(username, password, done) { - usedAuthentication("alone"); + var user = { + displayName: auth.alone.username, + email: auth.alone.email || "" + }; - return done(null, user); - } -)); + if (username.toLowerCase() != auth.alone.username.toLowerCase() || Tools.hashify(password) != auth.alone.passwordHash) { + return done(null, false, { message: 'Incorrect username or password' }); + } + + usedAuthentication("alone"); + + return done(null, user); + } + )); +} passport.serializeUser(function(user, done) { done(null, user); @@ -223,7 +239,7 @@ passport.deserializeUser(function(user, done) { user.email = user.emails[0].value; delete user.emails; } - user.asGitAuthor = user.displayName + " <" + user.email + ">"; + user.asGitAuthor = user.displayName + " <" + user.email + ">"; done(undefined, user); }); @@ -265,8 +281,12 @@ app.post ("/login", passport.authenticate('local', { successRe app.get ("/login", routes.login); app.get ("/logout", routes.logout); -app.get ("/auth/google", passport.authenticate('google')); -app.get ("/auth/google/return", passport.authenticate('google', { successRedirect: '/auth/done', failureRedirect: '/login' })); +app.get ("/auth/google", passport.authenticate('google', { + scope: ['https://www.googleapis.com/auth/userinfo.email'] } +)); + +app.get ("/oauth2callback", passport.authenticate('google', { successRedirect: '/auth/done', failureRedirect: '/login' })); + app.get ("/auth/done", routes.authDone); app.all('*', routes.error404); diff --git a/lib/config.js b/lib/config.js index 03a39309..4f822308 100644 --- a/lib/config.js +++ b/lib/config.js @@ -63,6 +63,8 @@ module.exports = Config = (function() { authentication:\n\ google:\n\ enabled: true\n\ + clientId: \"replace me with the real value\"\n\ + clientSecret: \"replace me with the real value\"\n\ alone:\n\ enabled: false\n\ username: \"\"\n\ diff --git a/package.json b/package.json index 7c683fe1..f6b70948 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,33 @@ { - "name": "jingo", - "version": "0.5.2", + "name": "jingo", + "version": "0.6.0", "description": "A nodejs based wiki engine (sort of Gollum port)", - "author": "Claudio Cicali ", - "main": "jingo", + "author": "Claudio Cicali ", + "main": "jingo", "bin": { "jingo": "./jingo" }, - "repository" : - { "type" : "git", - "url" : "https://github.com/claudioc/jingo" - }, - "directories" : { "lib" : "./lib/" }, + "repository": { + "type": "git", + "url": "https://github.com/claudioc/jingo" + }, + "directories": { + "lib": "./lib/" + }, "dependencies": { + "commander": "*", + "connect-flash": "*", "express": "3.x", + "express-validator": ">= 0.3.0", + "gravatar": ">= 1.0.6", + "iconv": "*", "jade": "*", + "marked": ">= 0.2.x", + "node-syntaxhighlighter": "*", "passport": "*", "passport-google": "*", + "passport-google-oauth": "^0.1.5", "passport-local": "*", - "iconv": "*", - "marked": ">= 0.2.x", - "node-syntaxhighlighter": "*", - "gravatar": ">= 1.0.6", - "express-validator": ">= 0.3.0", - "commander": "*", - "connect-flash": "*", "yaml-js": "*" }, "devDependencies": { @@ -38,9 +41,10 @@ "node": "0.8.x", "npm": "1.1.x" }, - "licenses": - [ { "type" : "MIT" - , "url" : "http://github.com/claudioc/jingo/raw/master/LICENSE" + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/claudioc/jingo/raw/master/LICENSE" } - ] + ] } diff --git a/routes/index.js b/routes/index.js index 7c3b8abc..23f17d1e 100644 --- a/routes/index.js +++ b/routes/index.js @@ -138,7 +138,7 @@ exports.pageShow = function(req, res) { res.render('show', { title: app.locals.appTitle + " – " + Tools.getPageTitle(content, pageName), content: Tools.hasTitle(content) ? Renderer.render(content) : Renderer.render("# " + pageName + "\n" + content), - pageName: pageName, + pageName: pageName, metadata: metadata }); @@ -327,7 +327,7 @@ exports.pageCompare = function(req, res) { var pageName = req.params.page , revisions = req.params.revisions; - + res.locals.revisions = revisions.split(".."); res.locals.lines = []; @@ -357,7 +357,7 @@ exports.pageCompare = function(req, res) { function leftDiffLineNumber(id, line) { var li; - + switch(true) { case line.slice(0,2) == '@@': @@ -475,7 +475,6 @@ exports.authDone = function(req, res) { return; } -console.log(req.session.destination); if (!app.locals.authentication.alone.used && !Tools.isAuthorized(res.locals.user.email, app.locals.authorization.validMatches)) { req.logout(); req.session = null;