├── .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 |
2 |
3 | {{> yield }}
4 |
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 |
2 |
6 | {{ sharedAuthFrame }}
7 |
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 | }
--------------------------------------------------------------------------------