mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-26 00:51:08 +02:00
* UI: Move `wrapped_token` login functionality to route (#30465) * move token unwrap functionality to page component * update mfa test * remove wrapped_token logic from page component * more cleanup to relocate unwrap logic * move wrapped_token to route * move unwrap tests to acceptance * move mfa form back * add some padding * update mfa-form tests * get param from params * wait for auth form on back * run rests * UI: Add MFA support for SSO methods (#30489) * initial implementation of mfa validation for sso methods * update typescript interfaces * add stopgap changes to auth service * switch order backend is defined * update login form for tests even though it will be deleted * attempt to stabilize wrapped_query test * =update login form test why not * Update ui/app/components/auth/form/saml.ts Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com> --------- Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com> * Move CSP error to page component (#30492) * initial implementation of mfa validation for sso methods * update typescript interfaces * add stopgap changes to auth service * switch order backend is defined * update login form for tests even though it will be deleted * attempt to stabilize wrapped_query test * =update login form test why not * move csp error to page component * move csp error to page component * Move fetching unauthenticated mounts to the route (#30509) * rename namespace arg to namespaceQueryParam * move fetch mounts to route * add margin to sign in button spacing * update selectors for oidc provider test * add todo delete comments * fix arg typo in test * change method name * fix args handling tab click * remove tests that no longer relate to components functionality * add tests for preselectedAuthType functionality * move typescript interfaces, fix selector * add await * oops * move format method down, make private * move tab formatting to the route * move to page object * fix token unwrap aborting transition * not sure what that is doing there.. * add comments * rename to presetAuthType * use did-insert instead * UI: Implement `Auth::FormTemplate` (#30521) * replace Auth::LoginForm with Auth::FormTemplate * first round of test updates * return null if mounts object is empty * add comment and test for empty sys/internal/mounts data * more test updates * delete listing_visibility test, delete login-form component test * update divs to Hds::Card::Container * add overflow class * remove unused getters * move requesting stored auth type to page component * fix typo * Update ui/app/components/auth/form/oidc-jwt.ts make comment make more sense * small cleanup items, update imports * Delete old auth components (#30527) * delete old components * update codeowners * Update `with` query param functionality (#30537) * update path input to type=hidden * add test coverage * update page test * update auth route * delete login form * update ent test * consolidate logic in getter * add more comments * more comments.. * rename selector * refresh model as well * redirect for invalid query params * move unwrap to redirect * only redirect on invalid query params * add tests for query param * test selector updates * remove todos, update relevant ones with initials * add changelog --------- Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>
214 lines
6.5 KiB
TypeScript
214 lines
6.5 KiB
TypeScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import AuthBase from './base';
|
|
import Ember from 'ember';
|
|
import { service } from '@ember/service';
|
|
import { task, timeout, waitForEvent } from 'ember-concurrency';
|
|
import { tracked } from '@glimmer/tracking';
|
|
import errorMessage from 'vault/utils/error-message';
|
|
|
|
import type AdapterError from 'vault/@ember-data/adapter/error';
|
|
import type AuthMethodAdapter from 'vault/vault/adapters/auth-method';
|
|
import type AuthService from 'vault/vault/services/auth';
|
|
import type RoleSamlModel from 'vault/models/role-saml';
|
|
import type Store from '@ember-data/store';
|
|
|
|
/**
|
|
* @module Auth::Form::Saml
|
|
* see Auth::Base
|
|
*/
|
|
|
|
const ERROR_WINDOW_CLOSED =
|
|
'The provider window was closed before authentication was complete. Your web browser may have blocked or closed a pop-up window. Please check your settings and click "Sign in" to try again.';
|
|
const ERROR_TIMEOUT = 'The authentication request has timed out. Please click "Sign in" to try again.';
|
|
|
|
export { ERROR_WINDOW_CLOSED };
|
|
|
|
export default class AuthFormSaml extends AuthBase {
|
|
@service declare readonly auth: AuthService;
|
|
@service declare readonly store: Store;
|
|
|
|
@tracked fetchedRole: RoleSamlModel | null = null;
|
|
|
|
loginFields = [
|
|
{
|
|
name: 'role',
|
|
helperText: 'Vault will use the default role to sign in if this field is left blank.',
|
|
},
|
|
];
|
|
|
|
get canLoginSaml() {
|
|
return window.isSecureContext;
|
|
}
|
|
|
|
get tasksAreRunning() {
|
|
return this.login.isRunning || this.exchangeSAMLTokenPollID.isRunning;
|
|
}
|
|
|
|
/* Saml auth flow on login button click:
|
|
* 1. find role-saml record which returns role info
|
|
* 2. open popup at url defined returned from role
|
|
* 3. watch popup window for close (and cancel polling if it closes)
|
|
* 4. poll vault for 200 token response
|
|
* 5. close popup, stop polling, and trigger onSubmit with token data
|
|
*/
|
|
login = task(async (formData) => {
|
|
// submit data is parsed by base.ts and a path will always have a value.
|
|
// either the default of auth type, or the custom inputted path
|
|
const { role, path, namespace } = formData;
|
|
|
|
await this.startSAMLAuth({ role, path, namespace });
|
|
});
|
|
|
|
// Fetch role to get sso_service_url and open popup
|
|
async startSAMLAuth({ role = '', path = '', namespace = '' }) {
|
|
try {
|
|
const id = JSON.stringify([path, role]);
|
|
this.fetchedRole = await this.store.findRecord('role-saml', id, {
|
|
adapterOptions: { namespace },
|
|
});
|
|
} catch (error) {
|
|
this.onError(errorMessage(error));
|
|
return;
|
|
}
|
|
|
|
if (this.fetchedRole) {
|
|
const win = window;
|
|
const POPUP_WIDTH = 500;
|
|
const POPUP_HEIGHT = 600;
|
|
const left = win.screen.width / 2 - POPUP_WIDTH / 2;
|
|
const top = win.screen.height / 2 - POPUP_HEIGHT / 2;
|
|
const samlWindow = win.open(
|
|
this.fetchedRole.ssoServiceURL,
|
|
'vaultSAMLWindow',
|
|
`width=${POPUP_WIDTH},height=${POPUP_HEIGHT},resizable,scrollbars=yes,top=${top},left=${left}`
|
|
);
|
|
|
|
await this.exchangeSAMLTokenPollID.perform(samlWindow, { path });
|
|
}
|
|
}
|
|
|
|
exchangeSAMLTokenPollID = task(async (samlWindow, { path }) => {
|
|
// start watching the popup window and the current one
|
|
this.watchPopup.perform(samlWindow);
|
|
this.watchCurrent.perform(samlWindow);
|
|
|
|
let resp;
|
|
try {
|
|
resp = await this.pollForToken(samlWindow, { path });
|
|
this.closeWindow(samlWindow);
|
|
} catch (error) {
|
|
this.cancelLogin(samlWindow, errorMessage(error));
|
|
return;
|
|
}
|
|
|
|
// We've got a response from the polling request
|
|
// pass MFA data or use the Vault token (client_token) to continue the auth
|
|
const mfa_requirement = resp?.mfa_requirement;
|
|
const client_token = resp?.client_token;
|
|
if (mfa_requirement) {
|
|
this.handleMfa(mfa_requirement, path);
|
|
return;
|
|
}
|
|
if (client_token) {
|
|
this.continueLogin({ token: client_token });
|
|
return;
|
|
}
|
|
|
|
// If there's a problem with the SAML exchange the auth workflow should fail earlier.
|
|
// Including this catch just in case, though it's unlikely this will be hit.
|
|
this.handleSAMLError('Missing token. Please try again.');
|
|
return;
|
|
});
|
|
|
|
async pollForToken(samlWindow: Window, { path = '' }) {
|
|
// Poll every one second for the token to become available
|
|
const WAIT_TIME = Ember.testing ? 50 : 1000;
|
|
const MAX_TIME = 180; // 3 minutes in seconds
|
|
|
|
const adapter = this.store.adapterFor('auth-method') as AuthMethodAdapter;
|
|
// Wait up to 3 minutes for a token to become available
|
|
for (let attempt = 0; attempt < MAX_TIME; attempt++) {
|
|
await timeout(WAIT_TIME);
|
|
|
|
try {
|
|
const resp = await adapter.pollSAMLToken(
|
|
path,
|
|
this.fetchedRole?.tokenPollID,
|
|
this.fetchedRole?.clientVerifier
|
|
);
|
|
|
|
if (resp?.auth) {
|
|
// Exit loop if response
|
|
return resp.auth;
|
|
}
|
|
} catch (e) {
|
|
const error = e as AdapterError;
|
|
if (error.httpStatus === 401) {
|
|
// Continue to retry on 401 Unauthorized
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
this.cancelLogin(samlWindow, ERROR_TIMEOUT);
|
|
return;
|
|
}
|
|
|
|
async continueLogin(data: { token: string }) {
|
|
try {
|
|
const authResponse = await this.auth.authenticate({
|
|
clusterId: this.args.cluster.id,
|
|
backend: 'token',
|
|
data,
|
|
selectedAuth: this.args.authType,
|
|
});
|
|
|
|
// responsible for redirect after auth data is persisted
|
|
this.handleAuthResponse(authResponse);
|
|
} catch (e) {
|
|
const error = e as AdapterError;
|
|
this.onError(errorMessage(error));
|
|
}
|
|
}
|
|
|
|
// MANAGE POPUPS
|
|
watchPopup = task(async (samlWindow) => {
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
const WAIT_TIME = Ember.testing ? 50 : 500;
|
|
await timeout(WAIT_TIME);
|
|
|
|
if (!samlWindow || samlWindow.closed) {
|
|
return this.handleSAMLError(ERROR_WINDOW_CLOSED);
|
|
}
|
|
}
|
|
});
|
|
|
|
watchCurrent = task(async (samlWindow) => {
|
|
// when user is about to change pages, close the popup window
|
|
await waitForEvent(window, 'beforeunload');
|
|
samlWindow?.close();
|
|
});
|
|
|
|
cancelLogin(samlWindow: Window, errorMessage: string) {
|
|
this.closeWindow(samlWindow);
|
|
this.handleSAMLError(errorMessage);
|
|
}
|
|
|
|
closeWindow(samlWindow: Window) {
|
|
this.watchPopup.cancelAll();
|
|
this.watchCurrent.cancelAll();
|
|
samlWindow.close();
|
|
}
|
|
|
|
handleSAMLError(errorMessage: string) {
|
|
this.exchangeSAMLTokenPollID.cancelAll();
|
|
this.onError(errorMessage);
|
|
}
|
|
}
|