Skip to content

Commit

Permalink
Sidebranch: MFA end user setup (#15273)
Browse files Browse the repository at this point in the history
* initial setup of components and route

* fix navbar

* replace parent component with controller

* use auth service to return entity id

* adapter and some error handling:

* clean up adapter and handle warning

* wip

* use library for qrCode generation

* clear warning and QR code display fix

* flow for restart setup

* add documentation

* clean up

* fix warning issue

* handle root user

* remove comment

* update copy

* fix margin

* address comment
  • Loading branch information
Monkeychip committed May 17, 2022
1 parent b8dbd68 commit 4c585c9
Show file tree
Hide file tree
Showing 18 changed files with 318 additions and 14 deletions.
13 changes: 13 additions & 0 deletions ui/app/adapters/mfa-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ApplicationAdapter from './application';

export default class MfaSetupAdapter extends ApplicationAdapter {
adminGenerate(data) {
let url = `/v1/identity/mfa/method/totp/admin-generate`;
return this.ajax(url, 'POST', { data });
}

adminDestroy(data) {
let url = `/v1/identity/mfa/method/totp/admin-destroy`;
return this.ajax(url, 'POST', { data });
}
}
9 changes: 7 additions & 2 deletions ui/app/components/auth-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ export default class AuthInfoComponent extends Component {
@service wizard;
@service router;

@tracked
fakeRenew = false;
@tracked fakeRenew = false;

get hasEntityId() {
// root users will not have an entity_id because they are not associated with an entity.
// in order to use the MFA end user setup they need an entity_id
return this.auth.authData.entity_id ? true : false;
}

get isRenewing() {
return this.fakeRenew || this.auth.isRenewing;
Expand Down
77 changes: 77 additions & 0 deletions ui/app/components/mfa-setup-step-one.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

/**
* @module MfaSetupStepOne
* MfaSetupStepOne component is a child component used in the end user setup for MFA. It records the UUID (aka method_id) and sends a admin-generate request.
*
* @param {string} entityId - the entityId of the user. This comes from the auth service which records it on loading of the cluster. A root user does not have an entityId.
* @param {function} isUUIDVerified - a function that consumes a boolean. Is true if the admin-generate is successful and false if it throws a warning or error.
* @param {boolean} restartFlow - a boolean that is true that is true if the user should proceed to step two or false if they should stay on step one.
* @param {function} saveUUIDandQrCode - A function that sends the inputted UUID and return qrCode from step one to the parent.
* @param {boolean} showWarning - whether a warning is returned from the admin-generate query. Needs to be passed to step two.
*/

export default class MfaSetupStepOne extends Component {
@service store;
@tracked error = '';
@tracked warning = '';
@tracked qrCode = '';
@action
redirectPreviousPage() {
this.args.restartFlow();
window.history.back();
}
@action
async verifyUUID(evt) {
evt.preventDefault();
let response = await this.postAdminGenerate();
if (response === 'stop_progress') {
this.args.isUUIDVerified(false);
} else if (response === 'reset_method') {
this.args.showWarning(this.warning);
} else {
this.args.isUUIDVerified(true);
}
}
async postAdminGenerate() {
this.error = '';
this.warning = '';
let adapter = this.store.adapterFor('mfa-setup');
let response;
try {
response = await adapter.adminGenerate({
entity_id: this.args.entityId,
method_id: this.UUID, // comes from value on the input
});
this.args.saveUUIDandQrCode(this.UUID, response.data?.url);
// if there was a warning it won't fail but needs to be handled here and the flow needs to be interrupted
let warnings = response.warnings || [];
if (warnings.length > 0) {
this.UUID = ''; // clear UUID
const alreadyGenerated = warnings.find((w) =>
w.includes('Entity already has a secret for MFA method')
);
if (alreadyGenerated) {
this.warning =
'A QR code has already been generated, scanned, and MFA set up for this entity. If a new code is required, contact your administrator.';
return 'reset_method';
}
this.warning = warnings; // in case other kinds of warnings comes through.
return 'reset_method';
}
} catch (error) {
this.UUID = ''; // clear the UUID
this.error = error.errors;
return 'stop_progress';
}
return response;
}
}
40 changes: 40 additions & 0 deletions ui/app/components/mfa-setup-step-two.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

/**
* @module MfaSetupStepTwo
* MfaSetupStepTwo component is a child component used in the end user setup for MFA. It displays a qrCode or a warning and allows a user to reset the method.
*
* @param {string} entityId - the entityId of the user. This comes from the auth service which records it on loading of the cluster. A root user does not have an entityId.
* @param {string} uuid - the UUID that is entered in the input on step one.
* @param {string} qrCode - the returned url from the admin-generate post. Used to create the qrCode.
* @param {boolean} restartFlow - a boolean that is true that is true if the user should proceed to step two or false if they should stay on step one.
* @param {string} warning - if there is a warning returned from the admin-generate post then it's sent to the step two component in this param.
*/

export default class MfaSetupStepTwo extends Component {
@service store;

@action
redirectPreviousPage() {
this.args.restartFlow();
window.history.back();
}

@action
async restartSetup() {
this.error = null;
let adapter = this.store.adapterFor('mfa-setup');
try {
await adapter.adminDestroy({
entity_id: this.args.entityId,
method_id: this.args.uuid,
});
} catch (error) {
this.error = error.errors;
return 'stop_progress';
}
this.args.restartFlow();
}
}
1 change: 1 addition & 0 deletions ui/app/components/splash-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default Component.extend({
auth: service(),
store: service(),
tagName: '',
showTruncatedNavBar: true,

activeCluster: computed('auth.activeCluster', function () {
return this.store.peekRecord('cluster', this.auth.activeCluster);
Expand Down
43 changes: 43 additions & 0 deletions ui/app/controllers/vault/cluster/mfa-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

export default class VaultClusterMfaSetupController extends Controller {
@service auth;
@tracked onStep = 1;
@tracked warning = '';
@tracked uuid = '';
@tracked qrCode = '';
get entityId() {
return this.auth.authData.entity_id;
}
@action isUUIDVerified(verified) {
this.warning = ''; // clear the warning, otherwise it persists.
if (verified) {
this.onStep = 2;
} else {
this.restartFlow();
}
}

@action
restartFlow() {
this.onStep = 1;
}

@action
saveUUIDandQrCode(uuid, qrCode) {
// qrCode could be an empty string if the admin-generate was not successful
this.uuid = uuid;
this.qrCode = qrCode;
}

@action
showWarning(warning) {
this.warning = warning;
this.onStep = 2;
}
}
1 change: 1 addition & 0 deletions ui/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Router.map(function () {
this.route('logout');
this.mount('open-api-explorer', { path: '/api-explorer' });
this.route('license');
this.route('mfa-setup');
this.route('clients', function () {
this.route('current');
this.route('history');
Expand Down
3 changes: 3 additions & 0 deletions ui/app/routes/vault/cluster/mfa-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Route from '@ember/routing/route';

export default class MfaSetupRoute extends Route {}
6 changes: 6 additions & 0 deletions ui/app/styles/components/list-item-row.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
font-weight: $font-weight-semibold;
color: $ui-gray-500;
}

.center-display {
width: 50%;
margin-left: auto;
margin-right: auto;
}
}

a.list-item-row,
Expand Down
4 changes: 4 additions & 0 deletions ui/app/styles/core/buttons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
font-weight: $font-weight-semibold;
}

&.has-text-danger {
border: 1px solid $red-500;
}

&.tool-tip-trigger {
color: $grey-dark;
min-width: auto;
Expand Down
4 changes: 4 additions & 0 deletions ui/app/styles/core/helpers.scss
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@
.is-flex-start {
display: flex !important;
justify-content: flex-start;

&.has-gap {
gap: $spacing-m;
}
}
.is-flex-full {
flex-basis: 100%;
Expand Down
7 changes: 7 additions & 0 deletions ui/app/templates/components/auth-info.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
/>
</li>
{{/if}}
{{#if this.hasEntityId}}
<li class="action">
<LinkTo @route="vault.cluster.mfa-setup">
Multi-factor authentication
</LinkTo>
</li>
{{/if}}
<li class="action">
<button type="button" class="link" onclick={{action "restartGuide"}}>
Restart guide
Expand Down
26 changes: 26 additions & 0 deletions ui/app/templates/components/mfa-setup-step-one.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<p>
TOTP Multi-factor authentication (MFA) can be enabled here if it is required by your administrator. This will ensure that
you are not prevented from logging into Vault in the future, once MFA is fully enforced.
</p>
<form id="mfa-setup-step-one" {{on "submit" this.verifyUUID}}>
<MessageError @errorMessage={{this.error}} class="has-top-margin-s" />
<div class="field has-top-margin-l">
<label class="is-label">
Method ID
</label>

{{! template-lint-disable no-autofocus-attribute}}
<p class="sub-text">Enter the UUID for your multi-factor authentication method. This can be provided to you by your
administrator.</p>
<Input id="uuid" name="uuid" class="input" autocomplete="off" spellcheck="false" autofocus="true" @value={{this.UUID}} />
</div>

<div class="is-flex-start has-gap">
<button id="continue" type="submit" class="button is-primary" disabled={{(is-empty-value this.UUID)}}>
Verify
</button>
<button id="cancel" type="button" {{on "click" this.redirectPreviousPage}} class="button">
Cancel
</button>
</div>
</form>
35 changes: 35 additions & 0 deletions ui/app/templates/components/mfa-setup-step-two.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<p>
TOTP Multi-factor authentication (MFA) can be enabled here if it is required by your administrator. This will ensure that
you are not prevented from logging into Vault in the future, once MFA is fully enforced.
</p>
<div class="field has-top-margin-l">
<MessageError @errorMessage={{this.error}} class="has-top-margin-s" />
{{#if @warning}}
<AlertBanner @type="info" @title="MFA enabled" @message={{@warning}} class="has-top-margin-l" />
{{else}}
<div class="list-item-row">
<div class="center-display">
{{! template-lint-disable no-curly-component-invocation }}
{{qr-code text=@qrCode colorLight="#F7F7F7" width=155 height=155 correctLevel="L"}}
</div>
</div>
<div class="has-top-margin-s">
<div class="info-table-row has-no-shadow">
<div class="column info-table-row-edit"><Icon @name="alert-triangle-fill" class="has-text-highlight" /></div>
<p class="is-size-8">
After you leave this page, this QR code will be removed and
<strong>cannot</strong>
be regenerated.
</p>
</div>
</div>
{{/if}}
<div class="is-flex-start has-gap has-top-margin-l">
<button id="restart" type="button" class="button has-text-danger" {{on "click" this.restartSetup}}>
Restart setup
</button>
<button id="cancel" type="button" {{on "click" this.redirectPreviousPage}} class="button is-primary">
Done
</button>
</div>
</div>
26 changes: 14 additions & 12 deletions ui/app/templates/components/splash-page.hbs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
<NavHeader as |Nav|>
<Nav.home>
<HomeLink @class="navbar-item splash-page-logo has-text-white">
<LogoEdition />
</HomeLink>
</Nav.home>
<Nav.items>
<div class="navbar-item status-indicator-button" data-status={{if this.activeCluster.unsealed "good" "bad"}}>
<StatusMenu @label="Status" @onLinkClick={{action Nav.closeDrawer}} />
</div>
</Nav.items>
</NavHeader>
{{#if this.showTruncatedNavBar}}
<NavHeader as |Nav|>
<Nav.home>
<HomeLink @class="navbar-item splash-page-logo has-text-white">
<LogoEdition />
</HomeLink>
</Nav.home>
<Nav.items>
<div class="navbar-item status-indicator-button" data-status={{if this.activeCluster.unsealed "good" "bad"}}>
<StatusMenu @label="Status" @onLinkClick={{action Nav.closeDrawer}} />
</div>
</Nav.items>
</NavHeader>
{{/if}}
{{! bypass UiWizard and container styling }}
{{#if this.hasAltContent}}
{{yield (hash altContent=(component "splash-page/splash-content"))}}
Expand Down
29 changes: 29 additions & 0 deletions ui/app/templates/vault/cluster/mfa-setup.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<SplashPage @showTruncatedNavBar={{false}} as |Page|>
<Page.header>
<h1 class="title is-4">MFA setup</h1>
</Page.header>
<Page.content>
<div class="auth-form" data-test-mfa-form>
<div class="box">
{{#if (eq this.onStep 1)}}
<MfaSetupStepOne
@entityId={{this.entityId}}
@isUUIDVerified={{this.isUUIDVerified}}
@restartFlow={{this.restartFlow}}
@saveUUIDandQrCode={{this.saveUUIDandQrCode}}
@showWarning={{this.showWarning}}
/>
{{/if}}
{{#if (eq this.onStep 2)}}
<MfaSetupStepTwo
@entityId={{this.entityId}}
@uuid={{this.uuid}}
@qrCode={{this.qrCode}}
@restartFlow={{this.restartFlow}}
@warning={{this.warning}}
/>
{{/if}}
</div>
</div>
</Page.content>
</SplashPage>
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"ember-modifier": "^3.1.0",
"ember-page-title": "^6.2.2",
"ember-power-select": "^5.0.3",
"ember-qrcode-shim": "^0.4.0",
"ember-qunit": "^5.1.5",
"ember-resolver": "^8.0.3",
"ember-responsive": "^3.0.0-beta.3",
Expand Down
Loading

0 comments on commit 4c585c9

Please sign in to comment.