Skip to content

Commit

Permalink
Adds support for Google OAuth2 authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
claudioc committed May 28, 2014
1 parent 93b32a4 commit 58e30df
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 112 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/*
config.yaml
config.yml
data/*
12 changes: 11 additions & 1 deletion ChangeLog.md
Original file line number Diff line number Diff line change
@@ -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
=============================

Expand Down Expand Up @@ -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
=============================
Expand Down
117 changes: 61 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).
Expand All @@ -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
-------
Expand All @@ -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.

82 changes: 51 additions & 31 deletions jingo
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>', 'Specify the config file')
.option('-#, --hash-string <string>', 'Create an hash for a string')
.option('-l, --local', 'Listen on localhost only')
Expand Down Expand Up @@ -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;
}
Expand All @@ -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");
Expand Down Expand Up @@ -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);
Expand All @@ -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);
});

Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand Down
Loading

0 comments on commit 58e30df

Please sign in to comment.