Skip to content

Commit 75cd83b

Browse files
authored
FastBoot Compatibility (mainmatter#1035)
* DRAFT - Fastboot compatibility * improve fastboot support * add FastBoot info to README * fix tests * better name for api host conf setting * WIP: fixing tests * fix tests * Use version of ember-cookies with fix for fasboot * ember-cookies 0.0.6 * fix config for fastboot * never abort current transition in FastBoot * don't abort transitions in FastBoot * Fastboot Feature Branch (mainmatter#1032) * ignore store events while the session is busy (mainmatter#965) * 1.1.0-beta.5 * 1.1.0 * Check for resourceName in response in Devise Authenticator * change cookie default key names to be rfc2616 compliant (mainmatter#978) fixes mainmatter#977 * Test for session service data being set with Ember.set (mainmatter#972) * code/docs cleanup * Add tokenRefreshOffset property to OAuth2PasswordGrantAuthenticator (mainmatter#840) tokenRefreshOffset determines the offset seconds before the token expiration to refresh the token. This is randomized so as to reduce race conditions between multiple tabs from refreshing at the same time. This is configurable because in some cases, the offset randomization needs to be increased to decrease the probability of the above mentioned race conditions. Once more case would be in slow internet connections, you make a call to refresh the token but the server doesn't process it in time (or receive it in time), the server will check and the token that you sent up is now expired so the refresh will fail. * cleanup transition usage in authenticated and unauthenticated route mixins (mainmatter#992) no issue - fixes potential test timing issue - removes unecessary abort call * [BUGFIX] Remove Ember.Logger (mainmatter#993) Ember.Logger is not substituted by noops in production. More info in emberjs/guides#1467 * [WIP] Validate server responses in authenticators (mainmatter#957) * Validate response data in devise authenticator * Validate response data in OAuth2 authenticator * Add tests for oauth2 data validations * Add tests for devise data validations * Remove unncessary validations * Refactor 'restore' in devise authenticator * Fix test timeout errors * Minor cleanup * Consider resource name when validating response * Refactor devise authenticator _validate method * update dependencies (mainmatter#1004) This updates Ember, Ember Data, Ember CLI etc. to the latest versions. This also fixes a lot of JSCS warnings that were introduced by the latest version of ember-suave. * Use the term "squash" when referring to collapsing commits into one (mainmatter#1011) That's consistent with the term used in git-rebase and with the general public. * Add rejectWithXhr to optionally reject with XHR vs response body (mainmatter#1012) Allows ember apps using ember-simple-auth to receive the whole XHR object if the backend fails, instead of the response body, if they so choose. In the case of OAuth 2.0 backends, it's been a pattern in the wild to use X- headers to send context as to why a grant has failed. Examples include API throttling, brute force lockouts, and OTP/two-factor authentication information. Selfishly, I require this change so my application can be notified when the API has locked out an account due to suspicious activity via an X- header. The decision to expose it as an option was chosen so backwards compatibility is maintained and keeps the addon simple for those who need not be concerned with complex backends. * Add fastboot-dist to npmignore (mainmatter#1015) * Optionally send custom headers in authentication call (mainmatter#1018) Complex systems that offer Two Factor Authentication with their OAuth 2.0 implementation need to send additional context via the HTTP headers. This pattern has been observed in the wild by such systems such as GitHub. Because of the restrictions of OAuth 2.0 RFC, only headers can be used for additional context, not request/response bodies. This could be seen as a counterpart to mainmatter#1012, where using both features allow bi-directional context enabling 2FA, brute force lockouts, etc. * [fastboot-compatibility] initial work * [fastboot-compatbility] improve support * [fastboot-compatibility] Use [email protected] ember-cookies 0.0.6 [email protected] * [fastboot-compatbility] fix ember-build-cli.js * [fastboot-compatibility] fix route mixin transitions * [fastboot-compatibility] Update `session-stores/cookie` with `typeof` guard (#1) * [fastboot-compatiblity] fix tests * Use apiHost config for dummy app. better name for api host conf setting fix dummy app API endpoints * Helpful instructions for `npm run fastboot` * Restore cookie session renewal * Fix various rebase issues * [email protected] * Fix tests for fastboot feature branch (mainmatter#1034) * fix my messed up rebase 😳 * fix cookie store * [Fastboot] Remove inject of cookies in initializer; Bump ember-cookies; Add fastboot host whitelist (mainmatter#1039) * Add fastboot host whitelist * Bump ember-cookies to make dummy app work * Remove inject of cookies in initializers * [Fastboot] Integration testing for fastboot (mainmatter#1045) * Add session register back * Create a fastboot test app and create basic tests against fastboot. * fix bad merge 😞 * I suck at merging 😱 * we might need to cleanup for the node tests… * run both node test and FastBoot node test * fix node tests * increase timeout for fastboot node tests * Update OAuth2PasswordGrant Authenticator to use Fetch (mainmatter#1066) * Using ember-network fetch for the oauth2 password grand authenticator * Fixing unit tests for adding fetch to ember simple auth * Changing naming of rejectWithRequest to rejectWithResponse * Making rejectWithXhr a deprecatingAlias and adding import-polyfill to the node-tests app * docs fixes * don't bind to localStorage events in FastBoot * fix test * don't use window.location in fastboot * update docs for fastboot * use ember-network in devise authenticator * fix tests * rename fastboot app fixture * rename fastboot test * WIP: FastBoot regression test * WIP: FastBoot regression test * fix adaptive store for fastboot * Merge branch 'master' into fastboot (mainmatter#1098) * Merge branch 'master' into fastboot * Remove unneeded test * Refactor to use ember-cookies clear() * Try fix for 1.12 * Cleanup * Cookie store `_secureCookies`: use correct protocol property in FastBoot mode (mainmatter#1104) * Merge latest master into fastboot (mainmatter#1110) * Update managing-current-user.md (mainmatter#1068) Avoid making a current user request if not authenticated * Add explanatory comment to application route (mainmatter#1069) The setup-session-restoration initializer needs the application route to be explicitly defined in the consuming app, so simple auth must ship with this file in case the consuming app hasn't defined their own. However, if the consuming app has another addon that also provides an application route via it's app folder, simple auth may override that addon's file with this one, depending on the load order. This is easily fixed by specifying an explicit load order (i.e. `after: 'ember-simple-auth'`). However, because the contents of this file are completely generic, and Ember's build process merges this file into the app folder without retaining source information, it's difficult to know from the consuming app's perspective that simple-auth is the addon providing this generic file (and therefore the `after` option needs to specify "ember-simple-auth"). This adds an explanatory comment, hopefully making it a bit clearer in case anyone is trying to figure out where this empty application route file is coming from. * Cookie store rewrite (mainmatter#1056) * Persist to cookie when relevant attributes change * Refactor cookie properties on adaptive store * Fix cookie store getters and setters * Fix adaptive store test * Remove unused code * Fix cookie properties on adaptive store * Change cookie properties to adaptive store * Fix cookie setters * Fix jshint errors * Remove unused code * Wrap `rewriteCookie` in Ember.run.scheduleOnce * Remove extra run.scheduleOnce * Remove unnecessary jshint ignores * Minor cleanup * Fix cookie domain test * Add tests for rewrite behavior * Save WIP on shared cookie behavior * fix cookie store rewrite behavior tests * Small fixes (mainmatter#1047) * Fix jshint error * Allow cookie to write blank values on clear * Restore previous cookieDomain test * Persist value in test so cookie will write * Fix cookie domain test * simplify AdaptiveStore code * simplify cooke store code * use CPs consistently internally * code cleanup * handle changes of cookie name correctly * no need to clear before rewrite * clear old expiration cookie as well * Include path when deleting cookie (mainmatter#1067) * Refactor cookie string (mainmatter#1074) * Include path when deleting cookie * Add a cookie string method * Should `resolve` after fetching user. (mainmatter#1077) * Should `resolve` after fetching user. Else the promise remains unresolved. * Remove return as suggested in the review * Remove yet another return as suggested * Update "ember-cli-mocha" to v0.12.0 (mainmatter#1105) * Update "ember-cli-mocha" to v0.12.0 * tests: Import it() from "mocha" * tests: Use new testing API in ember-mocha * Fix OAuth2 authenticator tokenRefreshOffset (mainmatter#1106) * Fix OAuth2 authenticator tokenRefreshOffset * Fix lint mistake * Ensure correct root url handling (mainmatter#1070) * set rootURL on router correctly * document readonly correctly * use rootURL if present * Fix typo (mainmatter#1109) * cleanup * remove unnecessary injection * remove unnecessary property * fix dependency versions * use cookie session store in dummy app
1 parent 227fa7f commit 75cd83b

File tree

83 files changed

+1158
-359
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+1158
-359
lines changed

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ install:
3535
script:
3636
# Usually, it's ok to finish the test scenario without reverting
3737
# to the addon's original dependency state, skipping "cleanup".
38-
- ember try:one $EMBER_TRY_SCENARIO test --skip-cleanup
38+
- ember try:one $EMBER_TRY_SCENARIO test && npm run nodetest && npm run fastboot-nodetest
3939

4040
notifications:
4141
email: false

README.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,9 @@ store if `localStorage` is available.
540540
541541
[The Cookie store](http://ember-simple-auth.com/api/classes/CookieStore.html)
542542
stores its data in a cookie. This is used by the adaptive store if
543-
`localStorage` is not available.
543+
`localStorage` is not available. __This store must be used when the
544+
application uses
545+
[FastBoot](https://github.com/ember-fastboot/ember-cli-fastboot).__
544546
545547
#### Ephemeral Store
546548
@@ -590,6 +592,19 @@ export default Base.extend({
590592
});
591593
```
592594
595+
## FastBoot
596+
597+
Ember Simple Auth works with FastBoot out of the box as long as the Cookie
598+
session store is being used. In order to enable the cookie store, define it as
599+
the application store:
600+
601+
```js
602+
// app/session-stores/application.js
603+
import CookieStore from 'ember-simple-auth/session-stores/cookie';
604+
605+
export default CookieStore.extend();
606+
```
607+
593608
## Testing
594609
595610
Ember Simple Auth comes with a __set of test helpers that can be used in

addon/authenticators/devise.js

+32-22
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import Ember from 'ember';
22
import BaseAuthenticator from './base';
3+
import fetch from 'ember-network/fetch';
34

4-
const { RSVP: { Promise }, isEmpty, run, $: jQuery, assign: emberAssign, merge } = Ember;
5+
const { RSVP: { Promise }, isEmpty, run, assign: emberAssign, merge } = Ember;
56
const assign = emberAssign || merge;
67

8+
const JSON_CONTENT_TYPE = 'application/json';
9+
710
/**
811
Authenticator that works with the Ruby gem
912
[devise](https://github.com/plataformatec/devise).
@@ -118,18 +121,25 @@ export default BaseAuthenticator.extend({
118121
data[resourceName] = { password };
119122
data[resourceName][identificationAttributeName] = identification;
120123

121-
return this.makeRequest(data).then(
122-
(response) => {
123-
if (this._validate(response)) {
124-
const resourceName = this.get('resourceName');
125-
const _response = response[resourceName] ? response[resourceName] : response;
126-
run(null, resolve, _response);
124+
this.makeRequest(data).then((response) => {
125+
if (response.ok) {
126+
response.json().then((json) => {
127+
if (this._validate(json)) {
128+
const resourceName = this.get('resourceName');
129+
const _json = json[resourceName] ? json[resourceName] : json;
130+
run(null, resolve, _json);
131+
} else {
132+
run(null, reject, `Check that server response includes ${tokenAttributeName} and ${identificationAttributeName}`);
133+
}
134+
});
135+
} else {
136+
if (useXhr) {
137+
run(null, reject, response);
127138
} else {
128-
run(null, reject, `Check that server response includes ${tokenAttributeName} and ${identificationAttributeName}`);
139+
response.json().then((json) => run(null, reject, json));
129140
}
130-
},
131-
(xhr) => run(null, reject, useXhr ? xhr : (xhr.responseJSON || xhr.responseText))
132-
);
141+
}
142+
}).catch((error) => run(null, reject, error));
133143
});
134144
},
135145

@@ -149,25 +159,25 @@ export default BaseAuthenticator.extend({
149159
150160
@method makeRequest
151161
@param {Object} data The request data
152-
@param {Object} options Ajax configuration object merged into argument of `$.ajax`
153-
@return {jQuery.Deferred} A promise like jQuery.Deferred as returned by `$.ajax`
162+
@param {Object} options request options that are passed to `fetch`
163+
@return {Promise} The promise returned by `fetch`
154164
@protected
155165
*/
156-
makeRequest(data, options) {
157-
const serverTokenEndpoint = this.get('serverTokenEndpoint');
166+
makeRequest(data, options = {}) {
167+
let url = options.url || this.get('serverTokenEndpoint');
158168
let requestOptions = {};
169+
let body = JSON.stringify(data);
159170
assign(requestOptions, {
160-
url: serverTokenEndpoint,
161-
type: 'POST',
162-
dataType: 'json',
163-
data,
164-
beforeSend(xhr, settings) {
165-
xhr.setRequestHeader('Accept', settings.accepts.json);
171+
body,
172+
method: 'POST',
173+
headers: {
174+
'accept': JSON_CONTENT_TYPE,
175+
'content-type': JSON_CONTENT_TYPE
166176
}
167177
});
168178
assign(requestOptions, options || {});
169179

170-
return jQuery.ajax(requestOptions);
180+
return fetch(url, requestOptions);
171181
},
172182

173183
_validate(data) {

addon/authenticators/oauth2-password-grant.js

+45-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* jscs:disable requireDotNotation */
22
import Ember from 'ember';
33
import BaseAuthenticator from './base';
4+
import fetch from 'ember-network/fetch';
45

56
const {
67
RSVP,
@@ -11,7 +12,6 @@ const {
1112
assign: emberAssign,
1213
merge,
1314
A,
14-
$: jQuery,
1515
testing,
1616
warn,
1717
keys: emberKeys
@@ -137,9 +137,24 @@ export default BaseAuthenticator.extend({
137137
@property rejectWithXhr
138138
@type Boolean
139139
@default false
140+
@deprecated OAuth2PasswordGrantAuthenticator/rejectWithResponse:property
140141
@public
141142
*/
142-
rejectWithXhr: false,
143+
rejectWithXhr: computed.deprecatingAlias('rejectWithResponse'),
144+
145+
/**
146+
When authentication fails, the rejection callback is provided with the whole
147+
fetch response object instead of it's response JSON or text.
148+
149+
This is useful for cases when the backend provides additional context not
150+
available in the response body.
151+
152+
@property rejectWithResponse
153+
@type Boolean
154+
@default false
155+
@public
156+
*/
157+
rejectWithResponse: false,
143158

144159
/**
145160
Restores the session from a session data object; __will return a resolving
@@ -209,7 +224,7 @@ export default BaseAuthenticator.extend({
209224
return new RSVP.Promise((resolve, reject) => {
210225
const data = { 'grant_type': 'password', username: identification, password };
211226
const serverTokenEndpoint = this.get('serverTokenEndpoint');
212-
const useXhr = this.get('rejectWithXhr');
227+
const useResponse = this.get('rejectWithResponse');
213228
const scopesString = makeArray(scope).join(' ');
214229
if (!isEmpty(scopesString)) {
215230
data.scope = scopesString;
@@ -228,8 +243,8 @@ export default BaseAuthenticator.extend({
228243

229244
resolve(response);
230245
});
231-
}, (xhr) => {
232-
run(null, reject, useXhr ? xhr : (xhr.responseJSON || xhr.responseText));
246+
}, (response) => {
247+
run(null, reject, useResponse ? response : response.responseJSON);
233248
});
234249
});
235250
},
@@ -283,29 +298,39 @@ export default BaseAuthenticator.extend({
283298
@param {String} url The request URL
284299
@param {Object} data The request data
285300
@param {Object} headers Additional headers to send in request
286-
@return {jQuery.Deferred} A promise like jQuery.Deferred as returned by `$.ajax`
301+
@return {Promise} A promise that resolves with the response object`
287302
@protected
288303
*/
289304
makeRequest(url, data, headers = {}) {
305+
headers['Content-Type'] = 'application/x-www-form-urlencoded';
306+
307+
const body = keys(data).map((key) => {
308+
return `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`;
309+
}).join('&');
310+
290311
const options = {
291-
url,
292-
data,
293-
type: 'POST',
294-
dataType: 'json',
295-
contentType: 'application/x-www-form-urlencoded',
296-
headers
312+
body,
313+
headers,
314+
method: 'POST'
297315
};
298316

299317
const clientIdHeader = this.get('_clientIdHeader');
300318
if (!isEmpty(clientIdHeader)) {
301319
merge(options.headers, clientIdHeader);
302320
}
303-
304-
if (isEmpty(keys(options.headers))) {
305-
delete options.headers;
306-
}
307-
308-
return jQuery.ajax(options);
321+
return new RSVP.Promise((resolve, reject) => {
322+
fetch(url, options).then((response) => {
323+
response.text().then((text) => {
324+
let json = text ? JSON.parse(text) : {};
325+
if (!response.ok) {
326+
response.responseJSON = json;
327+
reject(response);
328+
} else {
329+
resolve(json);
330+
}
331+
});
332+
}).catch(reject);
333+
});
309334
},
310335

311336
_scheduleAccessTokenRefresh(expiresIn, expiresAt, refreshToken) {
@@ -340,8 +365,8 @@ export default BaseAuthenticator.extend({
340365
this.trigger('sessionDataUpdated', data);
341366
resolve(data);
342367
});
343-
}, (xhr, status, error) => {
344-
warn(`Access token could not be refreshed - server responded with ${error}.`);
368+
}, (response) => {
369+
warn(`Access token could not be refreshed - server responded with ${response.responseJSON}.`);
345370
reject();
346371
});
347372
});

addon/initializers/setup-session.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export default function setupSession(registry) {
1313
store = 'session-store:test';
1414
registry.register(store, Ephemeral);
1515
}
16+
1617
inject(registry, 'session:main', 'store', store);
1718
}

addon/mixins/application-route-mixin.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Ember from 'ember';
2+
import getOwner from 'ember-getowner-polyfill';
23
import Configuration from './../configuration';
34

45
const { inject, Mixin, A, run: { bind }, testing, computed } = Ember;
@@ -22,7 +23,7 @@ const { inject, Mixin, A, run: { bind }, testing, computed } = Ember;
2223
applicationRoute.transitionTo('index');
2324
});
2425
session.on('invalidationSucceeded', function() {
25-
window.location.reload();
26+
applicationRoute.transitionTo('bye');
2627
});
2728
};
2829
@@ -52,6 +53,12 @@ export default Mixin.create({
5253
*/
5354
session: inject.service('session'),
5455

56+
_isFastBoot: computed(function() {
57+
const fastboot = getOwner(this).lookup('service:fastboot');
58+
59+
return fastboot ? fastboot.get('isFastBoot') : false;
60+
}),
61+
5562
/**
5663
The route to transition to after successful authentication.
5764
@@ -120,7 +127,11 @@ export default Mixin.create({
120127
*/
121128
sessionInvalidated() {
122129
if (!testing) {
123-
window.location.replace(Configuration.baseURL);
130+
if (this.get('_isFastBoot')) {
131+
this.transitionTo(Configuration.baseURL);
132+
} else {
133+
window.location.replace(Configuration.baseURL);
134+
}
124135
}
125136
}
126137
});

addon/mixins/authenticated-route-mixin.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Ember from 'ember';
2+
import getOwner from 'ember-getowner-polyfill';
23
import Configuration from './../configuration';
34

45
const { inject: { service }, Mixin, assert, computed } = Ember;
@@ -33,6 +34,12 @@ export default Mixin.create({
3334
*/
3435
session: service('session'),
3536

37+
_isFastBoot: computed(function() {
38+
const fastboot = getOwner(this).lookup('service:fastboot');
39+
40+
return fastboot ? fastboot.get('isFastBoot') : false;
41+
}),
42+
3643
/**
3744
The route to transition to for authentication. The
3845
{{#crossLink "AuthenticatedRouteMixin"}}{{/crossLink}} will transition to
@@ -72,7 +79,11 @@ export default Mixin.create({
7279
let authenticationRoute = this.get('authenticationRoute');
7380
assert('The route configured as Configuration.authenticationRoute cannot implement the AuthenticatedRouteMixin mixin as that leads to an infinite transitioning loop!', this.get('routeName') !== authenticationRoute);
7481

75-
this.set('session.attemptedTransition', transition);
82+
if (!this.get('_isFastBoot')) {
83+
transition.abort();
84+
this.set('session.attemptedTransition', transition);
85+
}
86+
7687
return this.transitionTo(authenticationRoute);
7788
} else {
7889
return this._super(...arguments);

addon/mixins/unauthenticated-route-mixin.js

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Ember from 'ember';
2+
import getOwner from 'ember-getowner-polyfill';
23
import Configuration from './../configuration';
34

45
const { inject: { service }, Mixin, assert, computed } = Ember;
@@ -34,6 +35,12 @@ export default Mixin.create({
3435
*/
3536
session: service('session'),
3637

38+
_isFastBoot: computed(function() {
39+
const fastboot = getOwner(this).lookup('service:fastboot');
40+
41+
return fastboot ? fastboot.get('isFastBoot') : false;
42+
}),
43+
3744
/**
3845
The route to transition to if a route that implements the
3946
{{#crossLink "UnauthenticatedRouteMixin"}}{{/crossLink}} is accessed when
@@ -61,10 +68,16 @@ export default Mixin.create({
6168
@param {Transition} transition The transition that lead to this route
6269
@public
6370
*/
64-
beforeModel() {
71+
beforeModel(transition) {
6572
if (this.get('session').get('isAuthenticated')) {
66-
assert('The route configured as Configuration.routeIfAlreadyAuthenticated cannot implement the UnauthenticatedRouteMixin mixin as that leads to an infinite transitioning loop!', this.get('routeName') !== this.get('routeIfAlreadyAuthenticated'));
67-
this.transitionTo(this.get('routeIfAlreadyAuthenticated'));
73+
let routeIfAlreadyAuthenticated = this.get('routeIfAlreadyAuthenticated');
74+
assert('The route configured as Configuration.routeIfAlreadyAuthenticated cannot implement the UnauthenticatedRouteMixin mixin as that leads to an infinite transitioning loop!', this.get('routeName') !== routeIfAlreadyAuthenticated);
75+
76+
if (!this.get('_isFastBoot')) {
77+
transition.abort();
78+
}
79+
80+
return this.transitionTo(routeIfAlreadyAuthenticated);
6881
} else {
6982
return this._super(...arguments);
7083
}

0 commit comments

Comments
 (0)