diff --git a/addons/api/addon/generated/models/auth-token.js b/addons/api/addon/generated/models/auth-token.js new file mode 100644 index 000000000..31ce7fd41 --- /dev/null +++ b/addons/api/addon/generated/models/auth-token.js @@ -0,0 +1,8 @@ +import BaseModel from '../../models/base'; + +/** + * NOTE: we don't yet use this model for display in the UI and thus no + * attributes are included at this time. This model is used only for + * validating that an auth token exists. + */ +export default class GeneratedAuthTokenModel extends BaseModel {} diff --git a/addons/api/addon/models/auth-token.js b/addons/api/addon/models/auth-token.js new file mode 100644 index 000000000..4ab39a0aa --- /dev/null +++ b/addons/api/addon/models/auth-token.js @@ -0,0 +1,3 @@ +import GeneratedAuthTokenModel from '../generated/models/auth-token'; + +export default class AuthTokenModel extends GeneratedAuthTokenModel {} diff --git a/addons/api/app/models/auth-token.js b/addons/api/app/models/auth-token.js new file mode 100644 index 000000000..8322bb3f1 --- /dev/null +++ b/addons/api/app/models/auth-token.js @@ -0,0 +1 @@ +export { default } from 'api/models/auth-token'; diff --git a/addons/api/tests/unit/models/auth-token-test.js b/addons/api/tests/unit/models/auth-token-test.js new file mode 100644 index 000000000..1f0826b86 --- /dev/null +++ b/addons/api/tests/unit/models/auth-token-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | auth token', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let store = this.owner.lookup('service:store'); + let model = store.createRecord('auth-token', {}); + assert.ok(model); + }); +}); diff --git a/addons/auth/addon/authenticators/password.js b/addons/auth/addon/authenticators/password.js index 67c8ce422..eb383d6c8 100644 --- a/addons/auth/addon/authenticators/password.js +++ b/addons/auth/addon/authenticators/password.js @@ -13,8 +13,14 @@ import fetch from 'fetch'; * Upon session invalidation, deauthentication is attempted at the URL generated * by `buildDeauthEndpointURL`, but is not guaranteed. * + * When a session is restored, a call is made to the endpoint specified by + * `buildTokenValidationEndpointURL`. If this endpoint responds with 401 or + * 404, then the session is invalid and the restoration is rejected. All other + * responses will resolve the session restoration successfully. + * * This authenticator should not be used directly because it does not specify - * an `buildAuthEndpointURL` or `buildDeauthEndpointURL` of its own. + * an `buildAuthEndpointURL`, `buildDeauthEndpointURL`, or + * `buildTokenValidationEndpointURL` of its own. * To use, generate an application authenticator in your app * `authenticators/application.js` and extend this class. * @@ -31,13 +37,25 @@ export default class PasswordAuthenticator extends BaseAuthenticator { // =methods /** - * Restores the session if data is present. Otherwise rejects. + * Restores the session if data is present and token validation succeeds + * (any response other than 401 or 404 === success). Otherwise rejects. * @override * @param {object} data * @return {Promise} */ - restore(data) { - return data ? resolve(data) : reject(); + async restore(data) { + if (!data) return reject(); + const tokenID = data.id; + const tokenValidationURL = this.buildTokenValidationEndpointURL(tokenID); + const response = await fetch(tokenValidationURL, { method: 'get' }); + // 401 and 404 responses mean the token is invalid, whereas other types of + // error responses do not tell us about the validity of the token. + switch (response.status) { + case 401: + case 404: + return reject(); + } + return resolve(data); } /** diff --git a/addons/auth/tests/dummy/app/authenticators/password.js b/addons/auth/tests/dummy/app/authenticators/password.js index 1953a8f5d..06d437263 100644 --- a/addons/auth/tests/dummy/app/authenticators/password.js +++ b/addons/auth/tests/dummy/app/authenticators/password.js @@ -1,6 +1,10 @@ import PasswordAuthenticator from 'auth/authenticators/password'; export default class ApplicationPasswordAuthenticator extends PasswordAuthenticator { + buildTokenValidationEndpointURL(id) { + return `/validate/${id}`; + } + buildAuthEndpointURL() { return '/authenticate'; } diff --git a/addons/auth/tests/unit/authenticators/password-test.js b/addons/auth/tests/unit/authenticators/password-test.js index 873277469..040ce3758 100644 --- a/addons/auth/tests/unit/authenticators/password-test.js +++ b/addons/auth/tests/unit/authenticators/password-test.js @@ -96,4 +96,45 @@ module('Unit | Authenticator | password', function (hooks) { .then(() => assert.ok(true)) .catch(() => assert.notOk(true, 'uh oh, this should not happen')); }); + + test('it passes token validation on restoration if receiving 200', async function (assert) { + assert.expect(1); + const mockData = { id: 'token_123' }; + const authenticator = this.owner.lookup('authenticator:password'); + this.server.get(authenticator.buildTokenValidationEndpointURL(mockData.id), () => { + assert.ok(true, 'token validation was requested'); + return new Response(200); + }); + await authenticator.restore(mockData); + }); + + test('it fails token validation on restoration if receiving 401', async function (assert) { + assert.expect(2); + const mockData = { id: 'token_123' }; + const authenticator = this.owner.lookup('authenticator:password'); + this.server.get(authenticator.buildTokenValidationEndpointURL(mockData.id), () => { + assert.ok(true, 'token validation was requested'); + return new Response(401); + }); + try { + await authenticator.restore(mockData); + } catch (e) { + assert.ok(true, 'restoration failed'); + } + }); + + test('it fails token validation on restoration if receiving 404', async function (assert) { + assert.expect(2); + const mockData = { id: 'token_123' }; + const authenticator = this.owner.lookup('authenticator:password'); + this.server.get(authenticator.buildTokenValidationEndpointURL(mockData.id), () => { + assert.ok(true, 'token validation was requested'); + return new Response(404); + }); + try { + await authenticator.restore(mockData); + } catch (e) { + assert.ok(true, 'restoration failed'); + } + }); }); diff --git a/ui/core/app/authenticators/password.js b/ui/core/app/authenticators/password.js index 2d64dfc47..a0ceccdd9 100644 --- a/ui/core/app/authenticators/password.js +++ b/ui/core/app/authenticators/password.js @@ -43,6 +43,17 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator { return adapter.buildURL('scope', scopeID, options, 'findRecord'); } + /** + * Generates an auth token validation URL used to check tokens on restoration. + * @override + * @param {string} tokenID + * @return {string} + */ + buildTokenValidationEndpointURL(tokenID) { + const adapter = this.store.adapterFor('auth-token'); + return adapter.buildURL('auth-token', tokenID, {}, 'findRecord'); + } + /** * Intercepts the authenticate response, if any, and assigns the returned * token to all future requests via `addTokenToAuthorization`. diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 59cbf0180..3d332f121 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -1,7 +1,7 @@ import config from '../config/environment'; import { authHandler, deauthHandler } from './route-handlers/auth'; import { pickRandomStatusString } from './factories/session'; -//import { Response } from 'miragejs'; +import { Response } from 'miragejs'; export default function() { @@ -326,6 +326,14 @@ export default function() { return session.update(updatedAttrs); }); + // auth-tokens + + this.get('/auth-tokens/:id', function () { + // Set to 401 or 404 to simulate token invalidation, which will + // cause the session to fail restoration and logout the user. + return new Response(200); + }); + /* Uncomment the following line and the Response import above * Then change the response code to simulate error responses. * this.get('/scopes', () => new Response(505)); diff --git a/ui/core/tests/unit/authenticators/password-test.js b/ui/core/tests/unit/authenticators/password-test.js index 79996c8c0..0314aed17 100644 --- a/ui/core/tests/unit/authenticators/password-test.js +++ b/ui/core/tests/unit/authenticators/password-test.js @@ -1,6 +1,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { Response } from 'miragejs'; module('Unit | Authenticator | password', function (hooks) { setupTest(hooks); @@ -25,7 +26,11 @@ module('Unit | Authenticator | password', function (hooks) { test('it adds an authorization header to application adapter on restore', async function (assert) { assert.expect(1); - authenticator.restore({ token: 'token1234' }); + const mockData = { id: 'token1234', token: 'token1234' }; + this.server.get(authenticator.buildTokenValidationEndpointURL(mockData.id), () => { + return new Response(200); + }); + authenticator.restore(mockData); assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234'); });