Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sidebranch: MFA end user setup #15273

Merged
merged 18 commits into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
zofskeez marked this conversation as resolved.
Show resolved Hide resolved
}

@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,
zofskeez marked this conversation as resolved.
Show resolved Hide resolved

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;
}

zofskeez marked this conversation as resolved.
Show resolved Hide resolved
.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 {
zofskeez marked this conversation as resolved.
Show resolved Hide resolved
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}}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on removing the autofocus attr on the Input?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we have a consistent pattern with autofocusing but I don't have a problem with removing it.

<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"}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no positional parameters can this be changed to angle bracket invocation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, it borked everything. hastag oldLibrary

</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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jordan I'll leave some additional comments in slack about this, but for now, this solves the qrCode issue. It's a wrapped that goes around the qrCode.js library, which ALSO hasn't been updated in a long time, but is still recommended as the way to handle qr codes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. Let's roll with it if it suits our needs and doesn't introduce deprecation warnings or those type of things in the build.

"ember-qunit": "^5.1.5",
"ember-resolver": "^8.0.3",
"ember-responsive": "^3.0.0-beta.3",
Expand Down
Loading