├── .gitignore ├── README.md ├── lib ├── emptyLayout.html ├── requestAuth.js ├── routes.js ├── sharedAuthFrame.html └── sharedAuthFrame.js ├── package.js └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Meteor Shared Auth 2 | 3 | For situations where you have *multiple meteor applications* running on 4 | *separate domains* but which share the *same database*, this package allows you 5 | to share the logged-in state among the applications -- e.g. if I log in to one, 6 | I will be automatically logged in to the other. 7 | 8 | All of the meteor applications must use Meteor.settings, and define the public 9 | setting ``sharedAuthDomains``, e.g.: 10 | 11 | // settings.json 12 | { 13 | "public": { 14 | "sharedAuthDomains": ["http://example.com", "http://example2.com"] 15 | } 16 | } 17 | 18 | Each application will attempt to share its logged-in (or logged out) state with 19 | each of the listed domains. 20 | 21 | ### Install 22 | 23 | Install with: 24 | 25 | meteor add admithub:shared-auth 26 | -------------------------------------------------------------------------------- /lib/emptyLayout.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /lib/requestAuth.js: -------------------------------------------------------------------------------- 1 | // This code runs in the parent (the top-level site) which embeds iframes to 2 | // child sites with which it is sharing auth state. 3 | 4 | if (Meteor.isClient) { 5 | // Add iframes for shared auth and set up CDM. 6 | Meteor.startup(function() { 7 | if (!(Meteor.settings && 8 | Meteor.settings.public && 9 | Meteor.settings.public.sharedAuthDomains)) { 10 | throw new Error("shared-auth: ``Meteor.settings.public.sharedAuthDomains`` is not defined on " + window.location.host + "."); 11 | } 12 | var domains = Meteor.settings.public.sharedAuthDomains; 13 | if (!domains) { 14 | return; 15 | } 16 | // Don't recurse! 17 | if (window.location.pathname === "/shared-auth-frame") { 18 | return; 19 | } 20 | // Find out our origin. We can't use process.env.ROOT_URL on client. 21 | // Some day we'll get window.location.origin, but IE. 22 | var selfOrigin = window.location.protocol + "//" + window.location.host; 23 | domains = _.without(domains, selfOrigin); 24 | // Add auth frames. 25 | var pollIntervals = {}; 26 | var childFrames = []; 27 | domains.forEach(function(domain) { 28 | var body = document.body; 29 | var iframe = document.createElement("iframe"); 30 | iframe.src = domain + "/shared-auth-frame"; 31 | iframe.width = 0; 32 | iframe.height = 0; 33 | iframe.style.display = "none"; 34 | body.appendChild(iframe); 35 | pollIntervals[domain] = setInterval(function() { 36 | iframe.contentWindow.postMessage("requestAuth", '*'); 37 | }, 100); 38 | childFrames.push(iframe.contentWindow); 39 | }); 40 | 41 | window.addEventListener("message", function(event) { 42 | if (_.contains(domains, event.origin)) { 43 | if (event.data.hasOwnProperty("Meteor.loginToken")) { 44 | clearInterval(pollIntervals[event.origin]); 45 | // We don't accept logout here. Logout is "push" based, so that it's 46 | // always an active session that will request that we logout, rather 47 | // than an un-logged-in site we're not active on informing us that 48 | // we're not logged in there. 49 | if (event.data["Meteor.loginToken"]) { 50 | // Login 51 | localStorage.setItem("Meteor.loginTokenExpires", 52 | event.data["Meteor.loginTokenExpires"]); 53 | localStorage.setItem("Meteor.loginToken", 54 | event.data["Meteor.loginToken"]); 55 | localStorage.setItem("Meteor.userId", 56 | event.data["Meteor.userId"]); 57 | } 58 | } 59 | } 60 | }, false); 61 | 62 | // Log frames out if we change users or log out. We can't do this as a 63 | // localStorage listener because those only work on different pages. 64 | var curUserId = localStorage.getItem("Meteor.userId"); 65 | Deps.autorun(function() { 66 | var newUserId = Meteor.userId(); 67 | //console.log("parent auth changed", newUserId); 68 | if (curUserId && (newUserId !== curUserId)) { 69 | //console.log("parent requesting children logout"); 70 | _.each(childFrames, function(cw) { 71 | cw.postMessage("requestLogout", "*"); 72 | }); 73 | } 74 | curUserId = newUserId; 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /lib/routes.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function() { 2 | Router.route("/shared-auth-frame", { 3 | name: "shared-auth-frame", 4 | template: 'sharedAuthFrame', 5 | layoutTemplate: 'emptyLayout', 6 | waitOn: function() { 7 | return {ready: function() { return !Meteor.loggingIn(); }} 8 | } 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/sharedAuthFrame.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /lib/sharedAuthFrame.js: -------------------------------------------------------------------------------- 1 | // This code runs in child site which is embedded in an iframe to 2 | // share auth state with the parent. 3 | 4 | if (Meteor.isClient) { 5 | // Given a function, return a wrapped version of the function that when 6 | // executed will attempt to catch and log any errors using Airbrake. 7 | var catchWrap = function(callable, thisval) { 8 | return function() { 9 | try { 10 | callable.apply(thisval || this, arguments); 11 | } catch (theError) { 12 | if (typeof Airbrake !== "undefined") { 13 | Airbrake.push({error: theError}); 14 | } else { 15 | throw theError; 16 | } 17 | } 18 | } 19 | }; 20 | 21 | Meteor.startup(function() { 22 | Template.sharedAuthFrame.helpers({ 23 | "sharedAuthFrame": function() { 24 | var parentOrigin = null; 25 | var parentSource = null; 26 | 27 | (catchWrap(function() { 28 | if (!(Meteor.settings && 29 | Meteor.settings.public && 30 | Meteor.settings.public.sharedAuthDomains)) { 31 | // No domain settings => no worky 32 | throw new Error("shared-auth: Meteor.settings.public.sharedAuthDomains on " + window.location.host + " is not defined."); 33 | } 34 | }))(); 35 | 36 | // Tell the parent about our login state. 37 | var notifyParent = catchWrap(function notifyParent() { 38 | if (!parentOrigin || !parentSource) { 39 | return; 40 | } 41 | //console.log("child announcing auth", localStorage.getItem("Meteor.userId")); 42 | parentSource.postMessage({ 43 | "Meteor.loginTokenExpires": localStorage.getItem("Meteor.loginTokenExpires"), 44 | "Meteor.loginToken": localStorage.getItem("Meteor.loginToken"), 45 | "Meteor.userId": localStorage.getItem("Meteor.userId") 46 | }, parentOrigin); 47 | }); 48 | 49 | // The origin which this frame allows to request auth tokens from us. 50 | var onMessage = catchWrap(function onMessage(event) { 51 | // Ensure we are within allowed origins. 52 | var found = false; 53 | for (var i=0; i < Meteor.settings.public.sharedAuthDomains.length; i++) { 54 | if (Meteor.settings.public.sharedAuthDomains[i] === event.origin) { 55 | found = true; 56 | break; 57 | } 58 | } 59 | if (!found) { 60 | if (event.data === "requestAuth" || event.data === "requestLogout") { 61 | console.log("sharedAuthFrame refusing to share auth with " + event.origin + ", which is not in Meteor.settings.public.sharedAuthDomains:", Meteor.settings.public.sharedAuthDomains); 62 | } 63 | return; 64 | } 65 | 66 | // Respond to the message with auth tokens. 67 | if (event.data === "requestAuth") { 68 | // We only set these after we're sure event.origin is in sharedAuthDomains. 69 | //console.log("child received requestAuth"); 70 | parentOrigin = event.origin; 71 | parentSource = event.source; 72 | notifyParent(); 73 | } else if (event.data === "requestLogout") { 74 | //console.log("child received requestLogout"); 75 | Meteor.logout(); 76 | } 77 | }); 78 | 79 | // Update parent if storage changes. 80 | var onStorageChange = catchWrap(function onStorageChange(event) { 81 | if (event.key === "Meteor.loginToken" || 82 | event.key === "Meteor.loginTokenExpires" || 83 | event.key === "Meteor.userId") { 84 | //console.log("child login changed", localStorage.getItem("Meteor.userId")); 85 | notifyParent(); 86 | } 87 | }); 88 | window.addEventListener("storage", onStorageChange, false); 89 | window.addEventListener("message", onMessage, false); 90 | } 91 | }); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "Share login among separate domains.", 3 | version: "0.0.7", 4 | name: "admithub:shared-auth", 5 | git: "https://github.com/AdmitHub/meteor-shared-auth.git" 6 | }); 7 | 8 | Package.onUse(function (api) { 9 | api.versionsFrom('0.9.2'); 10 | api.use(['iron:router@1.0.3', 'templating'], 'client'); 11 | api.add_files([ 12 | 'lib/routes.js', 13 | 'lib/sharedAuthFrame.html', 14 | 'lib/sharedAuthFrame.js', 15 | 'lib/emptyLayout.html', 16 | 'lib/requestAuth.js' 17 | ], 'client'); 18 | }); 19 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "application-configuration", 5 | "1.0.3" 6 | ], 7 | [ 8 | "base64", 9 | "1.0.1" 10 | ], 11 | [ 12 | "binary-heap", 13 | "1.0.1" 14 | ], 15 | [ 16 | "blaze", 17 | "2.0.3" 18 | ], 19 | [ 20 | "blaze-tools", 21 | "1.0.1" 22 | ], 23 | [ 24 | "boilerplate-generator", 25 | "1.0.1" 26 | ], 27 | [ 28 | "callback-hook", 29 | "1.0.1" 30 | ], 31 | [ 32 | "check", 33 | "1.0.2" 34 | ], 35 | [ 36 | "ddp", 37 | "1.0.11" 38 | ], 39 | [ 40 | "deps", 41 | "1.0.5" 42 | ], 43 | [ 44 | "ejson", 45 | "1.0.4" 46 | ], 47 | [ 48 | "follower-livedata", 49 | "1.0.2" 50 | ], 51 | [ 52 | "geojson-utils", 53 | "1.0.1" 54 | ], 55 | [ 56 | "html-tools", 57 | "1.0.2" 58 | ], 59 | [ 60 | "htmljs", 61 | "1.0.2" 62 | ], 63 | [ 64 | "id-map", 65 | "1.0.1" 66 | ], 67 | [ 68 | "iron:controller", 69 | "1.0.3" 70 | ], 71 | [ 72 | "iron:core", 73 | "1.0.3" 74 | ], 75 | [ 76 | "iron:dynamic-template", 77 | "1.0.3" 78 | ], 79 | [ 80 | "iron:layout", 81 | "1.0.3" 82 | ], 83 | [ 84 | "iron:location", 85 | "1.0.3" 86 | ], 87 | [ 88 | "iron:middleware-stack", 89 | "1.0.3" 90 | ], 91 | [ 92 | "iron:router", 93 | "1.0.3" 94 | ], 95 | [ 96 | "iron:url", 97 | "1.0.3" 98 | ], 99 | [ 100 | "jquery", 101 | "1.0.1" 102 | ], 103 | [ 104 | "json", 105 | "1.0.1" 106 | ], 107 | [ 108 | "logging", 109 | "1.0.5" 110 | ], 111 | [ 112 | "meteor", 113 | "1.1.3" 114 | ], 115 | [ 116 | "minifiers", 117 | "1.1.2" 118 | ], 119 | [ 120 | "minimongo", 121 | "1.0.5" 122 | ], 123 | [ 124 | "mongo", 125 | "1.0.8" 126 | ], 127 | [ 128 | "observe-sequence", 129 | "1.0.3" 130 | ], 131 | [ 132 | "ordered-dict", 133 | "1.0.1" 134 | ], 135 | [ 136 | "random", 137 | "1.0.1" 138 | ], 139 | [ 140 | "reactive-dict", 141 | "1.0.4" 142 | ], 143 | [ 144 | "reactive-var", 145 | "1.0.3" 146 | ], 147 | [ 148 | "retry", 149 | "1.0.1" 150 | ], 151 | [ 152 | "routepolicy", 153 | "1.0.2" 154 | ], 155 | [ 156 | "spacebars", 157 | "1.0.3" 158 | ], 159 | [ 160 | "spacebars-compiler", 161 | "1.0.3" 162 | ], 163 | [ 164 | "templating", 165 | "1.0.9" 166 | ], 167 | [ 168 | "tracker", 169 | "1.0.3" 170 | ], 171 | [ 172 | "ui", 173 | "1.0.4" 174 | ], 175 | [ 176 | "underscore", 177 | "1.0.1" 178 | ], 179 | [ 180 | "webapp", 181 | "1.1.4" 182 | ], 183 | [ 184 | "webapp-hashing", 185 | "1.0.1" 186 | ] 187 | ], 188 | "pluginDependencies": [], 189 | "toolVersion": "meteor-tool@1.0.35", 190 | "format": "1.0" 191 | } --------------------------------------------------------------------------------