Files
threshold-ui/addons/auth/addon/authenticators/oidc.js
T
2025-03-12 08:29:54 -07:00

121 lines
4.6 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import BaseAuthenticator from './base';
import { service } from '@ember/service';
import { reject } from 'rsvp';
import { waitForPromise } from '@ember/test-waiters';
/**
* The OIDC base authenticator encapsulates the multi-step OIDC flow.
*
* 1. Start authentication flow: this step is actually a combination of two
* sub steps:
* 1. Initiate a call via `startAuthentication` to the API at
* `buildAuthEndpointURL` using the `start` command to fetch the URL of
* the external authentication provider (`auth_url`).
* 2. Open `auth_url`. This may be either inline (same tab)
* or external in a new tab or browser window.
* 2. Requests the token from `buildAuthEndpointURL` using the `token` command.
* A token isn't guaranteed to be ready, so the OIDC flow can be in a
* "pending" state.
*
* This authenticator should not be used directly because it does not specify
* URL-building endpoints of its own.
* To use, generate an application authenticator in your app
* `authenticators/application.js` and extend this class.
*/
export default class OIDCAuthenticator extends BaseAuthenticator {
// =services
@service session;
// =methods
/**
* Kicks-off the OIDC flow: calls the `authenticate:start` action on the
* specified auth method, which returns some meta data along with an
* authorization request URL. The response data is saved for later and
* authentication enters a pending state.
*
* @param {boolean} requestCookies request cookie tokens (default `true`)
* @param {object} options
* @return {object}
*/
async startAuthentication(options) {
const url = this.buildAuthEndpointURL(options);
const body = JSON.stringify({ command: 'start' });
// 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(url, { method: 'post', body }));
const json = await response.json();
if (response.status < 400) {
// Store meta about the pending OIDC flow
this.session.set('data.pending', {
oidc: json,
});
return json;
} else {
return reject();
}
}
/**
* OIDC is a _process_ and does not have a singular "authenticate" step. Thus
* this authenticate method simply normalizes the data passed into it once
* `attemptFetchToken` completes. Notably, no requests are made by calling
* authenticate. A Boundary token must already have been obtained.
* @param {object} json
* @return {object}
*/
async authenticate(json) {
return this.normalizeData(json);
}
/**
* This request attempts to fetch a Boundary token. This method represents a
* single request the polling sequence used to wait for authentication.
* There are three possible outcomes:
*
* 1. If the token is ready, it is persisted into the session, the session
* enters an authenticated state, and this method returns `true`.
* 2. If the token is not yet ready, this method returns false.
* 3. If the request errors, this method returns to a rejection,
* requiring appropriate error handling.
*/
async attemptFetchToken(options) {
// Get the URL and setup the request body
const url = this.buildAuthEndpointURL(options);
const body = JSON.stringify({
command: 'token',
attributes: {
//state: this.session.get('data.pending.oidc.attributes.state'),
token_id: this.session.get('data.pending.oidc.attributes.token_id'),
},
});
// Fetch the endpoint and get the response JSON
const response = await waitForPromise(fetch(url, { method: 'post', body }));
// 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.
const json = await response.json();
if (response.status === 202) {
// The token isn't ready yet, keep trying.
return false;
} else if (response.status < 400) {
// Response was successful, meaning a token was obtained.
// Authenticate with the session service using the response JSON.
await this.session.authenticate('authenticator:oidc', json);
return true;
} else {
// Response errors, return a rejection for further handling.
return reject();
}
}
}