Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 82 additions & 3 deletions packages/ember-simple-auth/src/internal-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,24 @@ export default ObjectProxy.extend({
return Promise.resolve();
}

let authenticator = this._lookupAuthenticator(this.authenticator);
if (!this.authenticator) {
this._busy = false;
return this._clear(true);
}

let authenticator;
try {
authenticator = this._lookupAuthenticator(this.authenticator);
} catch (e) {
this._busy = false;
return this._clear(true);
}

if (isNone(authenticator)) {
this._busy = false;
return this._clear(true);
}

return authenticator.invalidate(this.content.authenticated, ...arguments).then(
() => {
authenticator.off('sessionDataUpdated', this._onSessionDataUpdated);
Expand All @@ -120,7 +137,26 @@ export default ObjectProxy.extend({
let { authenticator: authenticatorFactory } = restoredContent.authenticated || {};
if (authenticatorFactory) {
delete restoredContent.authenticated.authenticator;
const authenticator = this._lookupAuthenticator(authenticatorFactory);

let authenticator;
try {
authenticator = this._lookupAuthenticator(authenticatorFactory);
} catch (e) {
debug(
`The authenticator "${authenticatorFactory}" could not be found - invalidating…`
);
this._busy = false;
return this._clearWithContent(restoredContent).then(reject, reject);
}

if (isNone(authenticator)) {
debug(
`The authenticator "${authenticatorFactory}" could not be found - invalidating…`
);
this._busy = false;
return this._clearWithContent(restoredContent).then(reject, reject);
}

return authenticator.restore(restoredContent.authenticated).then(
content => {
this.set('content', restoredContent);
Expand Down Expand Up @@ -178,6 +214,20 @@ export default ObjectProxy.extend({

_clear(trigger) {
trigger = Boolean(trigger) && this.get('isAuthenticated');

if (this.authenticator) {
let authenticator;
try {
authenticator = this._lookupAuthenticator(this.authenticator);
} catch (_) {
// authenticator could not be found; nothing to unbind
}
if (authenticator) {
try { authenticator.off('sessionDataUpdated', this._onSessionDataUpdated); } catch (e) { debug(`Failed to unbind sessionDataUpdated: ${e}`); }
try { authenticator.off('sessionDataInvalidated', this._onSessionDataInvalidated); } catch (e) { debug(`Failed to unbind sessionDataInvalidated: ${e}`); }
}
}

this.setProperties({
isAuthenticated: false,
authenticator: null,
Expand Down Expand Up @@ -207,6 +257,10 @@ export default ObjectProxy.extend({

_updateStore() {
let data = this.content;
if (this.isAuthenticated && isEmpty(this.authenticator)) {
debug('_updateStore: skipping persist — session is authenticated but authenticator is empty');
return Promise.resolve();
}
if (!isEmpty(this.authenticator)) {
set(
data,
Expand All @@ -219,11 +273,15 @@ export default ObjectProxy.extend({

_bindToAuthenticatorEvents() {
const authenticator = this._lookupAuthenticator(this.authenticator);
if (!authenticator) return;
try { authenticator.off('sessionDataUpdated', this._onSessionDataUpdated); } catch (e) { debug(`Failed to unbind sessionDataUpdated: ${e}`); }
try { authenticator.off('sessionDataInvalidated', this._onSessionDataInvalidated); } catch (e) { debug(`Failed to unbind sessionDataInvalidated: ${e}`); }
authenticator.on('sessionDataUpdated', this._onSessionDataUpdated);
authenticator.on('sessionDataInvalidated', this._onSessionDataInvalidated);
},

_onSessionDataUpdated: action(function ({ detail: content }) {
if (!this.authenticator) return;
this._setup(this.authenticator, content);
}),

Expand All @@ -238,7 +296,25 @@ export default ObjectProxy.extend({
let { authenticator: authenticatorFactory } = content.authenticated || {};
if (authenticatorFactory) {
delete content.authenticated.authenticator;
const authenticator = this._lookupAuthenticator(authenticatorFactory);

let authenticator;
try {
authenticator = this._lookupAuthenticator(authenticatorFactory);
} catch (e) {
debug(
`The authenticator "${authenticatorFactory}" could not be found - invalidating…`
);
this._busy = false;
this._clearWithContent(content, true);
return;
}

if (isNone(authenticator)) {
this._busy = false;
this._clearWithContent(content, true);
return;
}

authenticator.restore(content.authenticated).then(
authenticatedContent => {
this.set('content', content);
Expand All @@ -265,6 +341,9 @@ export default ObjectProxy.extend({
},

_lookupAuthenticator(authenticatorName) {
if (!authenticatorName) {
return null;
}
let owner = getOwner(this);
let authenticator = owner.lookup(authenticatorName);
assert(
Expand Down
92 changes: 92 additions & 0 deletions packages/test-esa/tests/unit/internal-session-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -974,4 +974,96 @@ module('InternalSession', function (hooks) {
});
});
});

module('race condition guards', function () {
module('_lookupAuthenticator', function () {
test('returns null when authenticator name is falsy', function (assert) {
assert.strictEqual(session._lookupAuthenticator(null), null);
assert.strictEqual(session._lookupAuthenticator(undefined), null);
assert.strictEqual(session._lookupAuthenticator(''), null);
});
});

module('invalidate', function () {
test('calls _clear(true) directly when this.authenticator is null', async function (assert) {
session.set('isAuthenticated', true);
session.set('authenticator', null);
assert.true(session.get('isAuthenticated'), 'precondition: session is authenticated');

assert.step('invalidating');
await session.invalidate();
assert.step('invalidated');

assert.notOk(session.get('isAuthenticated'), 'session should no longer be authenticated');
assert.verifySteps(['invalidating', 'invalidated'], 'invalidate resolved without error');
});

test('calls _clear(true) when authenticator lookup returns null', async function (assert) {
session.set('isAuthenticated', true);
session.set('authenticator', 'authenticator:nonexistent');
assert.true(session.get('isAuthenticated'), 'precondition: session is authenticated');

assert.step('invalidating');
// The lookup will throw an assertion in dev; we verify it handles the error gracefully
try {
await session.invalidate();
assert.step('invalidated');
} catch (_) {
assert.step('invalidate threw');
}

assert.notOk(session.get('isAuthenticated'), 'session should no longer be authenticated');
assert.verifySteps(['invalidating', 'invalidated'], 'invalidate resolved cleanly');
});
});

module('_onSessionDataUpdated', function () {
test('does nothing when this.authenticator is null', function (assert) {
session.set('authenticator', null);
sinon.spy(session, '_setup');

session._onSessionDataUpdated({ detail: { some: 'data' } });

assert.notOk(session._setup.called, '_setup should not be called when authenticator is null');
});
});

module('_updateStore', function () {
test('returns a resolved promise when isAuthenticated but authenticator is empty', async function (assert) {
session.set('isAuthenticated', true);
session.set('authenticator', null);
sinon.spy(store, 'persist');

await session._updateStore();

assert.notOk(store.persist.called, 'store.persist should not be called when authenticator is empty');
});
});

module('_bindToAuthenticatorEvents', function () {
test('does not throw and does not bind listeners when authenticator is null', function (assert) {
session.set('authenticator', null);
sinon.spy(authenticator, 'on');

session._bindToAuthenticatorEvents();

assert.ok(true, '_bindToAuthenticatorEvents did not throw with null authenticator');
assert.notOk(authenticator.on.called, 'authenticator.on should not be called when authenticator is null');
});
});

module('_clear', function () {
test('unbinds authenticator events before clearing', async function (assert) {
sinon.stub(authenticator, 'authenticate').resolves({ some: 'property' });
await session.authenticate('authenticator:test');

sinon.spy(authenticator, 'off');

await session._clear(true);

assert.ok(authenticator.off.called, 'authenticator.off should be called during _clear');
assert.notOk(session.get('isAuthenticated'));
});
});
});
});