├── .gitignore ├── LICENSE ├── README.md ├── lib ├── client │ └── accountsClient.js └── server │ ├── accountsCommon.js │ ├── accountsConsumer.js │ └── accountsProvider.js ├── package.js └── test ├── client └── tests.js └── server └── provider-tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Oz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cluster-accounts 2 | This package adds support for accounts/authentication in meteorhacks:cluster 3 | 4 | ## Getting Started 5 | 6 | `meteor add ozsay:cluster-accounts` 7 | 8 | > If meteorhacks:cluster is not installed in your project, It will install it has a dependency. 9 | 10 | ## Usage 11 | 12 | > Please read the instructions at [meteorhacks:cluster](https://github.com/meteorhacks/cluster) for how to setup the cluster. 13 | 14 | After you setup the cluster in your projects you need to define the meteor app that manages all the user accounts 15 | (could be the `web` service or a dedicated service). From now on we will refer to that app as the **provider** since it 16 | provides the logged in users collection. All the other apps in your cluster that depend on `cluster-accounts` are the **consumers**. 17 | 18 | ### Setup a provider 19 | 20 | First, you need to define a string that will be a secret. 21 | 22 | You can define it via environment variable: `export CLUSTER_ACCOUNTS_SECRET=this_is_a_secret` 23 | or programmatically `Cluster.setAccountsSecret('this_is_a_secret')`. 24 | 25 | > It is very important that you don't expose this string to your users, because we use this string to authenticate a 26 | node in the cluster when we subscribe a consumer to the logged in users collection. 27 | 28 | Then start the provider with `Cluster.startProvider()`. 29 | 30 | ### Setup a consumer 31 | 32 | First, define the secret as you did with the provider. 33 | 34 | > The secret must be identical in all of your consumers. 35 | 36 | Then set the connection to the provider with `Cluster.setAccountsConnection(connection)`. 37 | 38 | example: 39 | ``` 40 | var connection = Cluster.discoverConnection("web"); 41 | 42 | Cluster.setAccountsConnection(connection); 43 | ``` 44 | 45 | And finally start the consumer: `Cluster.startConsumer()`. 46 | 47 | ### Logging 48 | 49 | By default, the packages writes logs to the console. 50 | You can override this behavior by setting a different logger via `Cluster.setAccountLogger`. 51 | 52 | The package writes logs about the following events: 53 | 54 | **Provider:** 55 | 56 | 1. When the provider has been successfully created (info). 57 | 58 | **Consumer:** 59 | 60 | 1. When the consumer has been successfully created (info). 61 | 2. When a user has been logged in (debug). 62 | 3. When a user has been logged out (debug). 63 | 64 | ## How it works 65 | 66 | ![Drawing](http://i66.tinypic.com/i5w5ld.jpg) 67 | 68 | - When you start the provider it will create a collection in the server of the logged in users. 69 | - When you start a consumer it will subscribe to the logged in users collection that exists in the provider. 70 | - When a client performs a login to the web service, `cluster-account` automatically will perform a login to each of the 71 | connected cluster nodes. 72 | -------------------------------------------------------------------------------- /lib/client/accountsClient.js: -------------------------------------------------------------------------------- 1 | var discoverConnection = Cluster.discoverConnection; 2 | 3 | Cluster.discoverConnection = function(service) { 4 | var connection = discoverConnection(service); 5 | 6 | function login() { 7 | if (Match.test(Meteor.user(), Object) && 8 | Match.test(Accounts._lastLoginTokenWhenPolled, String)) { 9 | connection.call('cluster-accounts-login', Meteor.user()._id, Accounts._lastLoginTokenWhenPolled); 10 | } 11 | } 12 | 13 | Accounts.onLogin(function() { 14 | login(); 15 | }); 16 | 17 | connection.onReconnect = function() { 18 | login(); 19 | }; 20 | 21 | return connection; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/server/accountsCommon.js: -------------------------------------------------------------------------------- 1 | Cluster._accountsSecret = process.env['CLUSTER_ACCOUNTS_SECRET']; 2 | 3 | Cluster.setAccountsSecret = function(secret) { 4 | Cluster._accountsSecret = secret; 5 | }; 6 | 7 | Cluster._accountsLogger = console; 8 | 9 | Cluster.setAccountLogger = function(logger) { 10 | Cluster._accountsLogger = logger; 11 | }; 12 | -------------------------------------------------------------------------------- /lib/server/accountsConsumer.js: -------------------------------------------------------------------------------- 1 | var crypto = Npm.require('crypto'); 2 | 3 | Cluster._accountsConnection = null; 4 | 5 | Cluster.setAccountsConnection = function(connection) { 6 | Cluster._accountsConnection = connection; 7 | }; 8 | 9 | Cluster.startConsumer = function() { 10 | Cluster._accountsConnection.subscribe("loggedInUsers", Cluster._accountsSecret); 11 | Cluster.LoggedInUsers = new Mongo.Collection("loggedInUsers", Cluster._accountsConnection); 12 | 13 | Cluster.LoggedInUsers.find().observeChanges({ 14 | changed: function(id, fields) { 15 | if (fields.tokens !== undefined) { 16 | for (var prop in Meteor.server.sessions) { 17 | var session = Meteor.server.sessions[prop]; 18 | 19 | var found = false; 20 | 21 | for (var i = 0; i < fields.tokens.length; i++) { 22 | if (session.userId === id && session.userLoginHashedToken === fields.tokens[i].hashedToken) { 23 | found = true; 24 | } 25 | } 26 | 27 | if (session.userId === id && !found) { 28 | session.__proto__._setUserId.call(session, null); 29 | Cluster._accountsLogger.info('User with id ' + id + ' has logged out'); 30 | } 31 | } 32 | } 33 | }, 34 | removed: function(id) { 35 | for (var prop in Meteor.server.sessions) { 36 | var session = Meteor.server.sessions[prop]; 37 | 38 | if (session.userId === id) { 39 | session.__proto__._setUserId.call(session, null); 40 | Cluster._accountsLogger.info('User with id ' + id + ' has logged out'); 41 | } 42 | } 43 | } 44 | }); 45 | 46 | Meteor.methods({ 47 | 'cluster-accounts-login': function(userId, token) { 48 | var hash = crypto.createHash('sha256'); 49 | hash.update(token); 50 | token = hash.digest('base64'); 51 | 52 | Meteor.server.sessions[this.connection.id].userLoginHashedToken = token; 53 | 54 | if (Cluster.LoggedInUsers.findOne({_id: userId, 'tokens.hashedToken': token}) !== undefined && 55 | this.userId === null) { 56 | 57 | this._setUserId(userId); 58 | Cluster._accountsLogger.info('User with id ' + userId + ' has logged in'); 59 | } 60 | } 61 | }); 62 | 63 | Cluster._accountsLogger.info('Cluster accounts consumer has been successfully initialized'); 64 | }; 65 | -------------------------------------------------------------------------------- /lib/server/accountsProvider.js: -------------------------------------------------------------------------------- 1 | Cluster.startProvider = function() { 2 | Cluster.LoggedInUsers = new Mongo.Collection("loggedInUsers", {connection: null}); 3 | 4 | Meteor.users.find({"services.resume.loginTokens.hashedToken": {$exists: true}}, 5 | {fields: {"services.resume.loginTokens.hashedToken": true}}).observeChanges({ 6 | added: function(id, fields) { 7 | Cluster.LoggedInUsers.insert({_id: id, tokens: fields.services.resume.loginTokens}); 8 | }, 9 | changed: function(id, fields) { 10 | Cluster.LoggedInUsers.update(id, {$set: {tokens: fields.services.resume.loginTokens}}); 11 | }, 12 | removed: function(id) { 13 | Cluster.LoggedInUsers.remove(id); 14 | } 15 | }); 16 | 17 | Meteor.publish("loggedInUsers", function(secret) { 18 | if (secret === Cluster._accountsSecret) { 19 | return Cluster.LoggedInUsers.find(); 20 | } else { 21 | return null; 22 | } 23 | }); 24 | 25 | Cluster._accountsLogger.info('Cluster accounts provider has been successfully initialized'); 26 | }; 27 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "ozsay:cluster-accounts", 3 | summary: "A solution for accounts/authentication in meteorhacks:cluster", 4 | version: "0.2.0", 5 | git: "https://github.com/ozsay/cluster-accounts.git", 6 | documentation: "README.md" 7 | }); 8 | 9 | Package.onUse(function (api) { 10 | api.versionsFrom('METEOR@0.9.2'); 11 | 12 | api.use(['meteorhacks:cluster@1.6.9'], ['server', 'client']); 13 | 14 | api.imply('meteorhacks:cluster'); 15 | 16 | api.use(['accounts-base', 'check'], 'client'); 17 | 18 | api.addFiles([ 19 | 'lib/server/accountsCommon.js', 20 | 'lib/server/accountsConsumer.js', 21 | 'lib/server/accountsProvider.js' 22 | ], ['server']); 23 | 24 | api.addFiles([ 25 | 'lib/client/accountsClient.js' 26 | ], ['client']); 27 | }); 28 | 29 | Package.onTest(function (api) { 30 | api.use(['ozsay:cluster-accounts', 'velocity:html-reporter', 'sanjo:jasmine@0.20.3', 'ddp', 'accounts-base'], ['client', 'server']); 31 | 32 | api.addFiles('test/server/provider-tests.js', ['server']); 33 | 34 | api.addFiles('test/client/tests.js', ['client']); 35 | }); 36 | -------------------------------------------------------------------------------- /test/client/tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe('Logged in client tests', function () { 4 | it('Initializing connection', function() { 5 | spyOn(DDP, "connect").and.callThrough(); 6 | 7 | Cluster.discoverConnection('testService'); 8 | 9 | expect(DDP.connect).toHaveBeenCalled(); 10 | expect(DDP.connect).toHaveBeenCalledWith('/testService'); 11 | }); 12 | 13 | it('Connecting to a cluster node', function() { 14 | var connection = { 15 | call: function() {} 16 | }; 17 | 18 | var user = { 19 | _id: 'userId' 20 | }; 21 | Accounts._lastLoginTokenWhenPolled = 'token'; 22 | 23 | spyOn(DDP, "connect").and.returnValue(connection); 24 | 25 | spyOn(Meteor, 'user').and.returnValue(user); 26 | 27 | spyOn(connection, "call"); 28 | 29 | Cluster.discoverConnection('testService'); 30 | 31 | connection.onReconnect(); 32 | 33 | expect(connection.call).toHaveBeenCalled(); 34 | expect(connection.call).toHaveBeenCalledWith('cluster-accounts-login', user._id, Accounts._lastLoginTokenWhenPolled); 35 | }); 36 | 37 | it('Login after connection is established', function() { 38 | var connection = { 39 | call: function() {} 40 | }; 41 | 42 | var user = null; 43 | Accounts._lastLoginTokenWhenPolled = 'token'; 44 | 45 | var loginCb = null; 46 | 47 | spyOn(DDP, "connect").and.returnValue(connection); 48 | 49 | spyOn(Meteor, 'user').and.callFake(function() { 50 | return user; 51 | }); 52 | 53 | 54 | spyOn(Accounts, "onLogin").and.callFake(function(cb) { 55 | loginCb = cb; 56 | }); 57 | 58 | spyOn(connection, "call"); 59 | 60 | Cluster.discoverConnection('testService'); 61 | 62 | connection.onReconnect(); 63 | 64 | expect(connection.call).not.toHaveBeenCalled(); 65 | 66 | user = { 67 | _id: 'userId' 68 | }; 69 | 70 | loginCb(); 71 | 72 | expect(connection.call).toHaveBeenCalled(); 73 | expect(connection.call).toHaveBeenCalledWith('cluster-accounts-login', user._id, Accounts._lastLoginTokenWhenPolled); 74 | }); 75 | }); 76 | 77 | describe('Guest client tests', function () { 78 | it('Initializing connection', function() { 79 | spyOn(DDP, "connect").and.callThrough(); 80 | 81 | Cluster.discoverConnection('testService'); 82 | 83 | expect(DDP.connect).toHaveBeenCalled(); 84 | expect(DDP.connect).toHaveBeenCalledWith('/testService'); 85 | }); 86 | 87 | it('Connecting to a cluster node', function() { 88 | var connection = { 89 | call: function() {} 90 | }; 91 | 92 | var user = null; 93 | 94 | spyOn(DDP, "connect").and.returnValue(connection); 95 | 96 | spyOn(Meteor, 'user').and.returnValue(user); 97 | 98 | spyOn(connection, "call"); 99 | 100 | Cluster.discoverConnection('testService'); 101 | 102 | connection.onReconnect(); 103 | 104 | expect(connection.call).not.toHaveBeenCalled(); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/server/provider-tests.js: -------------------------------------------------------------------------------- 1 | describe('Provider tests', function () { 2 | beforeEach(function() { 3 | MeteorStubs.install(); 4 | }); 5 | 6 | afterEach(function() { 7 | MeteorStubs.uninstall(); 8 | }); 9 | 10 | it('Creating the logged in users collection as a local mongo collection', function() { 11 | Cluster.startProvider(); 12 | 13 | expect(Cluster.LoggedInUsers).toBeDefined(); 14 | }); 15 | }); 16 | --------------------------------------------------------------------------------