Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial Commit

  • Loading branch information...
commit 806c384d15b29d8ad559530fb26173538cee8794 1 parent 76fa5e4
Steve King authored
View
3  .gitignore
@@ -0,0 +1,3 @@
+.idea/*
+*.iml
+node_modules/*
View
4 .npmignore
@@ -0,0 +1,4 @@
+.idea/*
+*.iml
+.git*
+node_modules/*
View
23 package.json
@@ -0,0 +1,23 @@
+{
+ "name": "gauth",
+ "version": "0.0.1",
+ "description": "Middleware component to authenticate users through their Google account",
+ "author": "Steve King <steve@mydev.co>",
+ "contributors": [ { "name": "Steve King", "email": "steve@mydev.co" } ],
+ "keywords": ["authentication", "oauth", "google", "express", "connect", "middleware"],
+ "repository": "git://github.com/steveukx/gauth",
+ "main":"./src/index.js",
+ "engines": { "node": ">= 0.8.0" },
+ "dependencies":{
+ "promise-lite": "",
+ "subscribable": "",
+ "xhrequest": ""
+ },
+ "devDependencies": {
+ "express": "3.1.0",
+ "unit-test": ""
+ },
+ "scripts": {
+ "test": "node test/test.js"
+ }
+}
View
41 src/accesstokengenerator.js
@@ -0,0 +1,41 @@
+/**
+ * @exports AccessTokenGenerator
+ */
+(function () {
+
+ "use strict";
+
+ /**
+ * The AccessTokenGenerator requires an end point URL that requests can be sent to and returns a promise interface
+ * that will be resolved with the access token or rejected should any errors take place.
+ *
+ * @name AccessTokenGenerator
+ * @constructor
+ */
+ function AccessTokenGenerator(endPointUrl) {
+ var promise = new (require('promise-lite').Promise);
+ var query = '?' + require('querystring').stringify({
+ "openid.ns": 'http://specs.openid.net/auth/2.0',
+ "openid.mode": "associate",
+ "openid.assoc_type": "HMAC-SHA1",
+ "openid.session_type": "no-encryption"
+ });
+
+ require('xhrequest')(endPointUrl + query, {
+ success: function (data) {
+ promise.resolve(AccessTokenGenerator.getResultFromServerResponse(data.toString('utf8')));
+ },
+ error: function () {
+ promise.reject(new Error("Unable to fetch an access token"));
+ }
+ });
+ return promise;
+ }
+
+ AccessTokenGenerator.getResultFromServerResponse = function (data) {
+ return data.match(/assoc_handle\:(.+)/)[1];
+ };
+
+ module.exports = AccessTokenGenerator;
+
+}());
View
128 src/authenticator.js
@@ -0,0 +1,128 @@
+/**
+ * @exports Authenticator
+ */
+(function () {
+
+ "use strict";
+
+ /**
+ *
+ * @name Authenticator
+ * @constructor
+ */
+ function Authenticator(endPointUrl, assocHandle, baseUrl) {
+ this._assocHandle = assocHandle;
+ this._endPointUrl = endPointUrl;
+ this._baseUrl = baseUrl;
+ this._realm = (function(url) { return url.protocol + '//' + url.host + '/'; }(require('url').parse(baseUrl)));
+ }
+
+ /**
+ * @type {String} The base url is the absolute URL to the root of the authenticated content (eg: http://domain.com/usercontent/)
+ */
+ Authenticator.prototype._baseUrl = "";
+
+ /**
+ * @type {String} The realm is the trusted domain that the user must agree to authenticating with, derived from the host of the base url (eg: http://domain.com/)
+ */
+ Authenticator.prototype._realm = "";
+
+ /**
+ * Sets the base url to be used in this authenticator, and will as a result also set the realm to match the base url.
+ * @param {String} baseUrl
+ * @return {Authenticator}
+ */
+ Authenticator.prototype.setBaseUrl = function(baseUrl) {
+ if(baseUrl != this._baseUrl) {
+ this._baseUrl = baseUrl;
+ this._realm = (function(url) { return url.protocol + '//' + url.host + '/'; }(require('url').parse(baseUrl)));
+ }
+ return this;
+ };
+
+ /**
+ * Gets the URL that should be used to authenticate a user with a terminal URL of the supplied backTo path.
+ *
+ * @param {String} backTo
+ * @return {string}
+ */
+ Authenticator.prototype.getLogInUrl = function(backTo) {
+ var params = this._getLoginParameters();
+ params['openid.return_to'] += '?next=' + backTo;
+
+ return this._endPointUrl + '?' + require('querystring').stringify(params);
+ };
+
+ /**
+ * Applies properties to the supplied object that will include email request parameters
+ * @param {Object} params
+ */
+ Authenticator.prototype._mergeEmailRequest = function(params) {
+ params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0';
+ params['openid.ax.mode'] = 'fetch_request';
+ params['openid.ax.required'] = 'email,firstname,lastname';
+
+ params['openid.ax.type.email'] = 'http://schema.openid.net/contact/email';
+ params['openid.ax.type.firstname'] = 'http://axschema.org/namePerson/first';
+ params['openid.ax.type.lastname'] = 'http://axschema.org/namePerson/last';
+ };
+
+ /**
+ * Gets the URL parameters used for associating a user's google account with this application
+ * @return {Object}
+ */
+ Authenticator.prototype._getLoginParameters = function() {
+ var params = this._getParameters();
+
+ params['openid.claimed_id'] = 'http://specs.openid.net/auth/2.0/identifier_select';
+ params['openid.identity'] = 'http://specs.openid.net/auth/2.0/identifier_select';
+ params['openid.return_to'] = this._baseUrl + '/responder';
+ params['openid.realm'] = this._realm;
+ params['openid.assoc_handle'] = '';
+ params['openid.mode'] = 'checkid_setup';
+ this._mergeEmailRequest(params);
+
+ return params;
+ };
+
+ Authenticator.prototype._getIdentityResponseParameters = function() {
+ var params = this._getParameters();
+
+ params['openid.claimed_id'] = 'http://specs.openid.net/auth/2.0/identifier_select';
+ params['openid.identity'] = 'http://specs.openid.net/auth/2.0/identifier_select';
+ params['openid.return_to'] = this._baseUrl + '/responder';
+ params['openid.realm'] = this._realm;
+ params['openid.assoc_handle'] = '';
+ params['openid.mode'] = 'checkid_setup';
+ this._mergeEmailRequest(params);
+
+ return params;
+ };
+
+ /**
+ * Gets the URL parameters used for cancelling the association of the google account with this application
+ * @return {Object}
+ */
+ Authenticator.prototype._getCancelParameters = function() {
+ return {
+ 'openid.ns': 'http://specs.openid.net/auth/2.0',
+ 'openid.mode': 'cancel'
+ };
+ };
+
+ /**
+ * Gets default parameters for all requests
+ * @return {Object}
+ */
+ Authenticator.prototype._getParameters = function() {
+ return {
+ 'openid.ns': 'http://specs.openid.net/auth/2.0',
+ 'openid.assoc_handle': this._assocHandle
+ };
+ };
+
+
+
+ module.exports = Authenticator;
+
+}());
View
43 src/endpointresolver.js
@@ -0,0 +1,43 @@
+/**
+ * @exports EndPointResolver
+ */
+(function () {
+
+ "use strict";
+
+ /**
+ * Sends a request to the supplied discovery URL and parses the response for the URL that subsequent requests should
+ * be sent. The result of calling the EndPointResolver is a promise interface that will be resolved with the end point
+ * url or rejected if any errors took place.
+ *
+ * @param {String} discoveryUrl
+ *
+ * @name EndPointResolver
+ * @constructor
+ */
+ function EndPointResolver(discoveryUrl) {
+ var promise = new (require('promise-lite').Promise);
+ require('xhrequest')(discoveryUrl, {
+ success: function(data) {
+ promise.resolve(EndPointResolver.getResultFromServerResponse(data.toString('utf8')));
+ },
+ error: function() {
+ promise.reject(new Error("Unable to connect to end point discovery URL"));
+ }
+ });
+ return promise;
+ }
+
+ /**
+ * Given the response data from the discovery resource, retrieves the URI for making all further requests bound to.
+ *
+ * @param {String} data
+ * @return {String}
+ */
+ EndPointResolver.getResultFromServerResponse = function(data) {
+ return data.match(/<URI>(.+)<\/URI>/i)[1];
+ };
+
+ module.exports = EndPointResolver;
+
+}());
View
246 src/index.js
@@ -0,0 +1,246 @@
+/**
+ * @exports GAuth
+ */
+(function () {
+
+ "use strict";
+
+ var Subscribable = require('subscribable');
+ var Promise = require('promise-lite').Promise;
+
+ /**
+ * The overall authenticator module
+ *
+ * @name GAuth
+ * @constructor
+ */
+ function GAuth() {
+ this._defaultConfiguration();
+
+ for (var i = 0, l = arguments.length; i < l; i++) {
+ if ('function' === typeof arguments[i]) {
+ arguments[i](this);
+ }
+ else {
+ // what other kind of argument do we care about?
+ }
+ }
+
+ this._getEndPoint()
+ .then(this._getAccessToken, this)
+ .then(this._buildAuthenticator, this)
+ .then(this._ready, this)
+ }
+ GAuth.prototype = Object.create(Subscribable.prototype);
+
+ /**
+ * @type {Function} The end point resolver
+ */
+ GAuth.endPointResolver = require('./endpointresolver.js');
+
+ /**
+ * @type {Function} The access token generator
+ */
+ GAuth.accessTokenGenerator = require('./accesstokengenerator.js');
+
+ /**
+ * Gets the serialization data for this instance
+ * @return {String}
+ */
+ GAuth.prototype.serialize = function() {
+ return JSON.stringify(this._configuration);
+ };
+
+ /**
+ * Sets up the request for an end point
+ * @return {Promise}
+ */
+ GAuth.prototype._getEndPoint = function () {
+ if(this._configuration['endpoint.url']) {
+ return new Promise().resolve(this._configuration['endpoint.url']);
+ }
+ else {
+ return GAuth.endPointResolver(this._configuration['discover.url'])
+ .done(function (endPointUrl) { this._configure('endpoint.url', endPointUrl) }, this);
+ }
+ };
+
+ /**
+ * Sets up the request for an access token
+ * @return {Promise}
+ */
+ GAuth.prototype._getAccessToken = function () {
+ if(this._configuration['access.token']) {
+ return new Promise().resolve(this._configuration['access.token']);
+ }
+ else {
+ return GAuth.accessTokenGenerator(this._configuration['endpoint.url'])
+ .done(function (endPointUrl) { this._configure('access.token', endPointUrl) }, this);
+ }
+ };
+
+ /**
+ *
+ * @return {Authenticator}
+ */
+ GAuth.prototype._buildAuthenticator = function() {
+ var Authenticator = require('./authenticator.js');
+ return this._authenticator = new Authenticator(this._configuration['endpoint.url'],
+ this._configuration['access.token'],
+ this._configuration['base.url']);
+ };
+
+ /**
+ * Fires the ready event for any listener that cares.
+ */
+ GAuth.prototype._ready = function() {
+ this.fire('ready', this);
+ };
+
+ /**
+ *
+ * @param returnPath
+ * @return Promise
+ */
+ GAuth.prototype.logIn = function(returnPath) {
+ var promise = arguments[1] || new Promise;
+
+ if(!this._authenticator) {
+ this.on('ready', this.logIn.bind(this, returnPath, promise));
+ }
+ else {
+ promise.resolve(this._authenticator.getLogInUrl(returnPath));
+ }
+
+ return promise;
+ };
+
+ /**
+ * Set the configuration options that should be used unless otherwise overridden.
+ */
+ GAuth.prototype._defaultConfiguration = function () {
+ this._configuration = {};
+ this._configure('discover.url', 'https://www.google.com/accounts/o8/id');
+ this._configure('base.url', '');
+ };
+
+ /**
+ * Set the end point URL of the authenticator, by setting this in advance you will prevent the authenticator
+ * from attempting to discover the end point URL.
+ *
+ * @param {String} endPointUrl
+ */
+ GAuth.endPoint = function (endPointUrl) {
+ return function (auth) {
+ auth._configure('endpoint.url', endPointUrl);
+ }
+ };
+
+ /**
+ * Set the end point URL of the authenticator, by setting this in advance you will prevent the authenticator
+ * from attempting to discover the end point URL.
+ *
+ * @param {String} endPointUrl
+ */
+ GAuth.endPoint = function (endPointUrl) {
+ return GAuth.configure('endpoint.url', endPointUrl);
+ };
+
+ /**
+ * Sets a configuration option for use by the authenticator
+ *
+ * @param {String} key
+ * @param {String|Object} value
+ */
+ GAuth.configure = GAuth.prototype._configure = function (key, value) {
+ if (this instanceof GAuth) {
+ this._configuration[key] = value;
+ return this;
+ }
+ else {
+ return function (auth) {
+ auth._configure(key, value);
+ }
+ }
+ };
+
+ /**
+ * Given a JSON string, configures the new instance with the properties in the JSON.
+ * @param {String} json
+ */
+ GAuth.inflate = function(json) {
+ var options = JSON.parse(json);
+ return function(auth) {
+ for(var opt in options) {
+ auth._configure(opt, options[opt]);
+ }
+ }
+ };
+
+ /**
+ * Automatically persists to and inflates from a file at the named file path.
+ * @param filePath
+ */
+ GAuth.filePersistence = function(filePath) {
+
+ var FileSystem = require('fs');
+ var options;
+ if(FileSystem.existsSync(filePath)) {
+ options = FileSystem.readFileSync(filePath, 'utf8');
+ }
+
+ return function(auth) {
+ if(options) {
+ GAuth.inflate(options)(auth);
+ }
+
+ process.on('exit', function() {
+ FileSystem.writeFileSync(filePath, auth.serialize(), 'utf-8');
+ });
+ }
+ };
+
+ /**
+ *
+ *
+ * @param absoluteBaseUrl
+ * @param userDb
+ * @return {*}
+ */
+ GAuth.prototype.middleware = function(absoluteBaseUrl, userDb) {
+
+ return function(req, res, next) {
+
+ // when no user attached - straight through to the login url
+ if(!req.session.user) {
+ res.redirect(this.logIn(req.origialUrl));
+ return undefined;
+ }
+
+ // when the URL is the response URL,
+
+ if(controlledUrlPrefix.test(req.url)) {
+ return res.redirect(this.logIn(req.origialUrl));
+ }
+
+ // not one of my URLs and not logged in
+ if(!controlledUrlPrefix.test(req.url) && !req.session.user) {
+ res.redirect(this.logIn(req.originalUrl));
+ }
+
+ else if(req.session.user.authenticating) {
+
+ }
+
+ else {
+ next();
+ }
+
+ return null;
+ }.bind(this);
+ };
+
+
+ module.exports = GAuth;
+
+}());
View
24 src/user.js
@@ -0,0 +1,24 @@
+/**
+ * @exports User
+ */
+(function () {
+
+ "use strict";
+
+ /**
+ *
+ * @name User
+ * @constructor
+ */
+ function User(email) {
+ this.email = email;
+ }
+
+ /**
+ * @type {String} The email address of the user
+ */
+ User.prototype.email = null;
+
+ module.exports = User;
+
+}());
View
1  test/.googleauth
@@ -0,0 +1 @@
+{"discover.url":"https://www.google.com/accounts/o8/id","base.url":"http://localhost/openid/return.php","endpoint.url":"https://www.google.com/accounts/o8/ud","access.token":"AMlYA9V0swRdpLS-IfjhSyYECPzXnfk8mOAKGzAbkWq9IJCQMLWbHdmd"}
View
11 test/example.js
@@ -0,0 +1,11 @@
+
+var GoogleAuth = require('../src/index.js');
+
+var googleAuth = new GoogleAuth(
+ GoogleAuth.configure('base.url', 'http://localhost/openid/return.php'),
+ GoogleAuth.filePersistence(__dirname + '/.googleauth'));
+
+
+googleAuth.logIn('http://localhost/posts/NTE/dfgdfg-d-gdfgd')
+ .then(console.log.bind(console));
+
View
21 test/test-middleware.js
@@ -0,0 +1,21 @@
+
+var TestCase = require('unit-test').TestCase,
+ Assertions = require('unit-test').Assertions,
+ sinon = require('unit-test').Sinon,
+ FileSystem = require('fs');
+
+var persistenceFilePath = __dirname + '';
+
+module.exports = new TestCase('Middleware', {
+
+ setUp: function() {
+ try {
+ FileSystem.unlinkSync(persistenceFilePath);
+ }
+ catch (e) {}
+ },
+
+ tearDown: function() {
+
+ }
+});
View
10 test/test.js
@@ -0,0 +1,10 @@
+
+var tests = require('unit-test'),
+ Suite = tests.Suite,
+ testPath = (/test.js$/.test(process.argv[1])) ? String(process.argv[1]).replace(/test.js$/, '') : process.cwd();
+
+
+Suite.paths( testPath, ['test-*.js'] );
+
+
+
Please sign in to comment.
Something went wrong with that request. Please try again.