From c6da26a46f1f3dd528a0b4bf6c73449f25c6a197 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 18 Dec 2020 14:29:42 -0500 Subject: [PATCH] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20extract=20common=20a?= =?UTF-8?q?uthenticator=20bits=20into=20base=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/auth/addon/authenticators/base.js | 89 +++++++++++++++++++ addons/auth/addon/authenticators/password.js | 72 +++------------ .../tests/dummy/app/authenticators/base.js | 11 +++ .../tests/unit/authenticators/base-test.js | 73 +++++++++++++++ .../unit/authenticators/password-test.js | 64 ------------- 5 files changed, 187 insertions(+), 122 deletions(-) create mode 100644 addons/auth/addon/authenticators/base.js create mode 100644 addons/auth/tests/dummy/app/authenticators/base.js create mode 100644 addons/auth/tests/unit/authenticators/base-test.js diff --git a/addons/auth/addon/authenticators/base.js b/addons/auth/addon/authenticators/base.js new file mode 100644 index 000000000..d0b394d32 --- /dev/null +++ b/addons/auth/addon/authenticators/base.js @@ -0,0 +1,89 @@ +import SimpleAuthBaseAuthenticator from 'ember-simple-auth/authenticators/base'; +import { resolve, reject } from 'rsvp'; +import fetch from 'fetch'; + +/** + * Encapsulates common authenticator functionality. + */ +export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator { + // =unimplemented methods + + /** + * Generates a scope URL with which to deauthenticate. + * @override + * @param {object} options + * @param {string} scopeID + * @return {string} + */ + buildDeauthEndpointURL(/* { scope: { id: scopeID } } */) { } + + /** + * Generates an auth token validation URL used to check tokens on restoration. + * @override + * @param {string} tokenID + * @return {string} + */ + buildTokenValidationEndpointURL(/* tokenID */) { } + + // =methods + + /** + * Checks that the given token is valid and resolves, otherwise rejects. + */ + async validateToken(token, tokenID) { + const tokenValidationURL = this.buildTokenValidationEndpointURL(tokenID); + const response = await fetch(tokenValidationURL, { + method: 'get', + headers: { 'Authorization': `Bearer ${token}` } + }); + // 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. + if (response.status === 401 || response.status === 404) return reject(); + return resolve(); + } + + /** + * 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} + */ + async restore(data) { + if (!data) return reject(); + return this.validateToken(data.token, data.id) + .then(() => this.normalizeData(data)); + } + + /** + * Normalizes the auth data. Adds convenience booleans depending on the + * scope within which the session is authenticated: isGlobal, isOrg. + * If a `username` is provided, appends this to the data. + * @param {object} data + * @param {string} username + * @return {object} + */ + normalizeData(data, username) { + // Add booleans indicated the scope type + data.isGlobal = (data?.scope?.type === 'global'); + data.isOrg = (data?.scope?.type === 'org'); + if (username) data.username = username; + return data; + } + + /** + * Posts to the `deauthEntpoint` on a best-effort basis and then returns. + * Deauthentication with the server is not guaranteed and request failures + * are ignored. + * + * @override + * @return {Promise} + */ + invalidate(options) { + const deauthEndpointURL = this.buildDeauthEndpointURL(options); + fetch(deauthEndpointURL, { method: 'post' }).catch(() => { + /* no op */ + }); + return super.invalidate(...arguments); + } +} diff --git a/addons/auth/addon/authenticators/password.js b/addons/auth/addon/authenticators/password.js index 6c793576c..6f70946e8 100644 --- a/addons/auth/addon/authenticators/password.js +++ b/addons/auth/addon/authenticators/password.js @@ -1,4 +1,4 @@ -import BaseAuthenticator from 'ember-simple-auth/authenticators/base'; +import BaseAuthenticator from './base'; import { resolve, reject } from 'rsvp'; import fetch from 'fetch'; @@ -34,35 +34,23 @@ import fetch from 'fetch'; * */ export default class PasswordAuthenticator extends BaseAuthenticator { - // =methods + + // =unimplemented methods /** - * Checks that the given token is valid and resolves, otherwise rejects. - */ - async validateToken(token, tokenID) { - const tokenValidationURL = this.buildTokenValidationEndpointURL(tokenID); - const response = await fetch(tokenValidationURL, { - method: 'get', - headers: { 'Authorization': `Bearer ${token}` } - }); - // 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. - if (response.status === 401 || response.status === 404) return reject(); - return resolve(); - } - - /** - * Restores the session if data is present and token validation succeeds - * (any response other than 401 or 404 === success). Otherwise rejects. + * Generates an auth method URL with which to authenticate. * @override - * @param {object} data - * @return {Promise} + * @param {object} options + * @param {string} options.scope.scope_id + * @param {string} options.authMethod.id + * @return {string} */ - async restore(data) { - if (!data) return reject(); - return this.validateToken(data.token, data.id) - .then(() => this.normalizeData(data)); - } + buildAuthEndpointURL(/* { + scope: { id: scopeID }, + authMethod: { id: authMethodID }, + } */) { } + + // =methods /** * Posts credentials to the URL specified in `authEndpoint` and resolves @@ -94,36 +82,4 @@ export default class PasswordAuthenticator extends BaseAuthenticator { ? resolve(this.normalizeData(json, login_name)) : reject(); } - - /** - * Normalizes the auth data. Adds convenience booleans depending on the - * scope within which the session is authenticated: isGlobal, isOrg. - * If a `username` is provided, appends this to the data. - * @param {object} data - * @param {string} username - * @return {object} - */ - normalizeData(data, username) { - // Add booleans indicated the scope type - data.isGlobal = (data?.scope?.type === 'global'); - data.isOrg = (data?.scope?.type === 'org'); - if (username) data.username = username; - return data; - } - - /** - * Posts to the `deauthEntpoint` on a best-effort basis and then returns. - * Deauthentication with the server is not guaranteed and request failures - * are ignored. - * - * @override - * @return {Promise} - */ - invalidate(options) { - const deauthEndpointURL = this.buildDeauthEndpointURL(options); - fetch(deauthEndpointURL, { method: 'post' }).catch(() => { - /* no op */ - }); - return super.invalidate(...arguments); - } } diff --git a/addons/auth/tests/dummy/app/authenticators/base.js b/addons/auth/tests/dummy/app/authenticators/base.js new file mode 100644 index 000000000..661b25d18 --- /dev/null +++ b/addons/auth/tests/dummy/app/authenticators/base.js @@ -0,0 +1,11 @@ +import BaseAuthenticator from 'auth/authenticators/base'; + +export default class ApplicationBaseAuthenticator extends BaseAuthenticator { + buildTokenValidationEndpointURL(id) { + return `/validate/${id}`; + } + + buildDeauthEndpointURL() { + return '/deauthenticate'; + } +} diff --git a/addons/auth/tests/unit/authenticators/base-test.js b/addons/auth/tests/unit/authenticators/base-test.js new file mode 100644 index 000000000..7cb7a3e4b --- /dev/null +++ b/addons/auth/tests/unit/authenticators/base-test.js @@ -0,0 +1,73 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { Response } from 'miragejs'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; + +module('Unit | Authenticator | base', function (hooks) { + setupTest(hooks); + setupMirage(hooks); + + test('it deauthenticates on invalidation', async function (assert) { + assert.expect(1); + const authenticator = this.owner.lookup('authenticator:base'); + this.server.post(authenticator.buildDeauthEndpointURL(), () => { + assert.ok(true, 'deauthentication occurred'); + return new Response(200); + }); + await authenticator.invalidate(); + }); + + test('invalidation succeeds even if the deauthentication request fails', async function (assert) { + assert.expect(2); + const authenticator = this.owner.lookup('authenticator:base'); + this.server.post(authenticator.buildDeauthEndpointURL(), () => { + assert.ok(true, 'deauthentication was requested'); + return new Response(500); + }); + await authenticator + .invalidate() + .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:base'); + 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:base'); + 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:base'); + 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/addons/auth/tests/unit/authenticators/password-test.js b/addons/auth/tests/unit/authenticators/password-test.js index 040ce3758..3bac47edb 100644 --- a/addons/auth/tests/unit/authenticators/password-test.js +++ b/addons/auth/tests/unit/authenticators/password-test.js @@ -73,68 +73,4 @@ module('Unit | Authenticator | password', function (hooks) { assert.ok(true, 'authentication failed'); }); }); - - test('it deauthenticates on invalidation', async function (assert) { - assert.expect(1); - const authenticator = this.owner.lookup('authenticator:password'); - this.server.post(authenticator.buildDeauthEndpointURL(), () => { - assert.ok(true, 'deauthentication occurred'); - return new Response(200); - }); - await authenticator.invalidate(); - }); - - test('it invalidation succeeds even if the deauthentication request fails', async function (assert) { - assert.expect(2); - const authenticator = this.owner.lookup('authenticator:password'); - this.server.post(authenticator.buildDeauthEndpointURL(), () => { - assert.ok(true, 'deauthentication was requested'); - return new Response(500); - }); - await authenticator - .invalidate() - .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'); - } - }); });