Files
threshold-ui/addons/auth/addon/authenticators/base.js
T
Lisbet Alvarez faf4ecfac6 Fix memory leak issue when using fetch in oidc authentication (#2640)
* test: 💍 test 1

* chore: 🤖 remove waitForPromise use

* chore: 🤖 remove all other waitforpromise usage

* test: 💍 turn down polling

* test: 💍 use .json

* refactor: 💡 add waitForPromise back in & consume response body

* test: 💍 some testing

* test: 💍 more testing

* refactor: 💡 clean-up a bit

* refactor: 💡 use head method request & clean up comments

* refactor: 💡 use get method & consume response body

* test: 💍 fix token validation test cases
2025-01-03 14:06:27 -08:00

138 lines
4.5 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import SimpleAuthBaseAuthenticator from 'ember-simple-auth/authenticators/base';
import { resolve, reject } from 'rsvp';
import { waitForPromise } from '@ember/test-waiters';
/**
* Encapsulates common authenticator functionality.
*
* Upon session invalidation, deauthentication should be attempted at the URL
* generated by `buildDeauthEndpointURL` on a best-effort basis.
*
* When a session is restored, a call should be made to the endpoint specified
* by `buildTokenValidationEndpointURL`. If this endpoint responds with 401 or
* 404, then the session is invalid and the restoration should be rejected.
* All other responses should resolve the session restoration successfully.
*/
export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
// =unimplemented methods
/**
* Generates an auth method URL with which to authenticate.
* @override
* @param {object} options
* @param {string} options.scope.scope_id
* @param {string} options.authMethod.id
* @return {string}
*/
buildAuthEndpointURL(/* {
scope: { id: scopeID },
authMethod: { id: authMethodID },
} */) {}
/**
* 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);
// Note: waitForPromise is needed to provide the necessary integration with @ember/test-helpers
// visit https://www.npmjs.com/package/@ember/test-waiters for more info.
const response = await waitForPromise(
fetch(tokenValidationURL, {
method: 'get',
headers: { Authorization: `Bearer ${token}` },
}),
);
// Note: Always consume response body in order to avoid memory leaks
// visit https://undici.nodejs.org/#/?id=garbage-collection for more info.
// We do not use the undici package but the link informs us that garbage
// collection is undefined when response body is not consumed.
await response.json();
// 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.attributes.token, data.attributes.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) {
// Pull fields up from `data.attributes` for easier access in JavaScript.
// The `attributes` field exists on the Go side for its convenience but is
// unnecessary here.
Object.assign(data, data.attributes);
// 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 `deauthEndpoint` on a best-effort basis and then returns.
* Deauthentication with the server is not guaranteed and request failures
* are ignored.
*
* @override
* @return {Promise}
*/
async invalidate(options) {
const { token } = options;
const deauthEndpointURL = this.buildDeauthEndpointURL(options);
await waitForPromise(
fetch(deauthEndpointURL, {
method: 'delete',
headers: { Authorization: `Bearer ${token}` },
}).catch(() => {
/* no op */
}),
);
return super.invalidate(...arguments);
}
}