refactor: 💡 extract common authenticator bits into base class
This commit is contained in:
5 files changed
+187
-122
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import BaseAuthenticator from 'auth/authenticators/base';
|
||||
|
||||
export default class ApplicationBaseAuthenticator extends BaseAuthenticator {
|
||||
buildTokenValidationEndpointURL(id) {
|
||||
return `/validate/${id}`;
|
||||
}
|
||||
|
||||
buildDeauthEndpointURL() {
|
||||
return '/deauthenticate';
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user