claire bontempo 2482674312
UI: Create starter Auth::Page component (#27478)
* move OktaNumberChallenge and AuthForm to AuthPage component

* return from didReceiveAttrs if component is being torn down

* update auth form test

* change passed task to an auth action

* update auth form unit test

* fix return

* update jsdoc for auth form

* add docs

* add comments, last little cleanup, pass API error to okta number challenge

* separate tests and move Auth::Page specific logic out of auth form integration test

* fix test typos

* fix page tests
2024-06-20 12:40:28 -07:00

109 lines
3.6 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import Ember from 'ember';
import { service } from '@ember/service';
import { task, timeout } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
/**
* @module AuthPage
* The Auth::Page wraps OktaNumberChallenge and AuthForm to manage the login flow and is responsible for calling the authenticate method
*
* @example
* <Auth::Page @wrappedToken={{this.wrappedToken}} @cluster={{this.model}} @namespace={{this.namespaceQueryParam}} @selectedAuth={{this.authMethod}} @onSuccess={{action "onAuthResponse"}} />
*
* @param {string} wrappedToken - Query param value of a wrapped token that can be used to login when added directly to the URL via the "wrapped_token" query param
* @param {object} cluster - The route model which is the ember data cluster model. contains information such as cluster id, name and boolean for if the cluster is in standby
* @param {string} namespace- Namespace query param, passed to AuthForm and set by typing in namespace input or URL
* @param {string} selectedAuth - The auth method selected in the dropdown, passed to auth service's authenticate method
* @param {function} onSuccess - Callback that fires the "onAuthResponse" action in the auth controller and handles transitioning after success
*/
export default class AuthPageComponent extends Component {
@service auth;
@tracked authError = null;
@tracked oktaNumberChallengeAnswer = '';
@tracked waitingForOktaNumberChallenge = false;
@action
performAuth(backendType, data) {
this.authenticate.unlinked().perform(backendType, data);
}
@task
@waitFor
*delayAuthMessageReminder() {
if (Ember.testing) {
yield timeout(0);
} else {
yield timeout(5000);
}
}
@task
@waitFor
*authenticate(backendType, data) {
const {
selectedAuth,
cluster: { id: clusterId },
} = this.args;
try {
if (backendType === 'okta') {
this.pollForOktaNumberChallenge.perform(data.nonce, data.path);
} else {
this.delayAuthMessageReminder.perform();
}
const authResponse = yield this.auth.authenticate({
clusterId,
backend: backendType,
data,
selectedAuth,
});
this.args.onSuccess(authResponse, backendType, data);
} catch (e) {
if (!this.auth.mfaError) {
this.authError = `Authentication failed: ${this.auth.handleError(e)}`;
}
}
}
@task
@waitFor
*pollForOktaNumberChallenge(nonce, mount) {
// yield for 1s to wait to see if there is a login error before polling
yield timeout(1000);
if (this.authError) return;
this.waitingForOktaNumberChallenge = true;
// keep polling /auth/okta/verify/:nonce API every 1s until response returns with correct_number
let response = null;
while (response === null) {
// disable polling for tests otherwise promises reject and acceptance tests fail
if (Ember.testing) return;
yield timeout(1000);
response = yield this.auth.getOktaNumberChallengeAnswer(nonce, mount);
}
// display correct number so user can select on personal MFA device
this.oktaNumberChallengeAnswer = response;
}
@action
onCancel() {
// reset variables and stop polling tasks if canceling login
this.authError = null;
this.oktaNumberChallengeAnswer = null;
this.waitingForOktaNumberChallenge = false;
this.authenticate.cancelAll();
this.pollForOktaNumberChallenge.cancelAll();
}
}