Skip to content

Commit 5e54d49

Browse files
authored
updates mfa error handling (#14147)
1 parent 984bf7f commit 5e54d49

File tree

12 files changed

+99
-141
lines changed

12 files changed

+99
-141
lines changed

ui/app/components/mfa-error.js

Lines changed: 0 additions & 43 deletions
This file was deleted.

ui/app/components/mfa-form.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ import { numberToWord } from 'vault/helpers/number-to-word';
1717
* @param {function} onSuccess - fired when passcode passes validation
1818
*/
1919

20+
export const VALIDATION_ERROR =
21+
'The passcode failed to validate. If you entered the correct passcode, contact your administrator.';
22+
2023
export default class MfaForm extends Component {
2124
@service auth;
2225

23-
@tracked passcode;
2426
@tracked countdown;
25-
@tracked errors;
27+
@tracked error;
2628

2729
get constraints() {
2830
return this.args.authData.mfa_requirement.mfa_constraints;
@@ -57,21 +59,26 @@ export default class MfaForm extends Component {
5759

5860
@task *validate() {
5961
try {
62+
this.error = null;
6063
const response = yield this.auth.totpValidate({
6164
clusterId: this.args.clusterId,
6265
...this.args.authData,
6366
});
6467
this.args.onSuccess(response);
6568
} catch (error) {
66-
this.errors = error.errors;
67-
// TODO: update if specific error can be parsed for incorrect passcode
68-
// this.newCodeDelay.perform();
69+
const codeUsed = (error.errors || []).find((e) => e.includes('code already used;'));
70+
if (codeUsed) {
71+
// parse validity period from error string to initialize countdown
72+
const seconds = parseInt(codeUsed.split('in ')[1].split(' seconds')[0]);
73+
this.newCodeDelay.perform(seconds);
74+
} else {
75+
this.error = VALIDATION_ERROR;
76+
}
6977
}
7078
}
7179

72-
@task *newCodeDelay() {
73-
this.passcode = null;
74-
this.countdown = 30;
80+
@task *newCodeDelay(timePeriod) {
81+
this.countdown = timePeriod;
7582
while (this.countdown) {
7683
yield timeout(1000);
7784
this.countdown--;

ui/app/controllers/vault/cluster/auth.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,9 @@ export default Controller.extend({
8080
onMfaSuccess(authResponse) {
8181
this.authSuccess(authResponse);
8282
},
83+
onMfaErrorDismiss() {
84+
this.set('mfaAuthData', null);
85+
this.auth.set('mfaErrors', null);
86+
},
8387
},
8488
});

ui/app/services/auth.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ import { task, timeout } from 'ember-concurrency';
1515
const TOKEN_SEPARATOR = '☃';
1616
const TOKEN_PREFIX = 'vault-';
1717
const ROOT_PREFIX = '_root_';
18-
const TOTP_NOT_CONFIGURED = 'TOTP mfa required but not configured';
1918
const BACKENDS = supportedAuthBackends();
2019

21-
export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX, TOTP_NOT_CONFIGURED };
20+
export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX };
2221

2322
export default Service.extend({
2423
permissions: service(),
@@ -362,21 +361,10 @@ export default Service.extend({
362361
async authenticate(/*{clusterId, backend, data}*/) {
363362
const [options] = arguments;
364363
const adapter = this.clusterAdapter();
365-
let resp;
366-
367-
try {
368-
resp = await adapter.authenticate(options);
369-
} catch (e) {
370-
// TODO: check for totp not configured mfa error before throwing
371-
const errors = this.handleError(e);
372-
// stubbing error - verify once API is finalized
373-
if (errors.includes(TOTP_NOT_CONFIGURED)) {
374-
this.set('mfaErrors', errors);
375-
}
376-
throw e;
377-
}
378364

365+
let resp = await adapter.authenticate(options);
379366
const { mfa_requirement, requiresAction } = this._parseMfaResponse(resp.auth?.mfa_requirement);
367+
380368
if (mfa_requirement) {
381369
if (requiresAction) {
382370
return { mfa_requirement };

ui/app/templates/components/auth-form.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="auth-form">
1+
<div class="auth-form" data-test-auth-form>
22
{{#if this.hasMethodsWithPath}}
33
<nav class="tabs is-marginless">
44
<ul>

ui/app/templates/components/mfa-error.hbs

Lines changed: 0 additions & 15 deletions
This file was deleted.

ui/app/templates/components/mfa-form.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{{this.description}}
55
</p>
66
<form id="auth-form" {{on "submit" this.submit}}>
7-
<MessageError @errors={{this.errors}} class="has-top-margin-s" />
7+
<MessageError @errorMessage={{this.error}} class="has-top-margin-s" />
88
<div class="field has-top-margin-l">
99
{{#each this.constraints as |constraint index|}}
1010
{{#if index}}

ui/app/templates/vault/cluster/auth.hbs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
<SplashPage @hasAltContent={{this.auth.mfaErrors}} as |Page|>
22
<Page.altContent>
3-
<MfaError @onClose={{fn (mut this.mfaAuthData) null}} />
3+
<div class="has-top-margin-xxl" data-test-mfa-error>
4+
<EmptyState
5+
@title="Unauthorized"
6+
@message="Multi-factor authentication is required, but failed. Go back and try again, or contact your administrator."
7+
@icon="alert-circle"
8+
@bottomBorder={{true}}
9+
@subTitle={{join ". " this.auth.mfaErrors}}
10+
class="is-box-shadowless"
11+
>
12+
<button type="button" class="button is-ghost is-transparent" {{on "click" (action "onMfaErrorDismiss")}}>
13+
<Icon @name="chevron-left" />
14+
Go back
15+
</button>
16+
</EmptyState>
17+
</div>
418
</Page.altContent>
519
<Page.header>
620
{{#if this.oidcProvider}}

ui/mirage/handlers/mfa.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export default function (server) {
4343
[mfa_constraints, methods] = generator([m('totp')], [m('okta')]); // 2 constraints 1 passcode 1 non-passcode
4444
} else if (user === 'mfa-i') {
4545
[mfa_constraints, methods] = generator([m('okta'), m('totp')], [m('totp')]); // 2 constraints 1 passcode/1 non-passcode 1 non-passcode
46+
} else if (user === 'mfa-j') {
47+
[mfa_constraints, methods] = generator([m('pingid')]); // use to test push failures
4648
}
4749
const numbers = (length) =>
4850
Math.random()
@@ -129,7 +131,10 @@ export default function (server) {
129131
const passcode = mfa_payload[constraintId][0];
130132
if (method.uses_passcode) {
131133
if (passcode !== 'test') {
132-
const error = !passcode ? 'TOTP passcode not provided' : 'Incorrect TOTP passcode provided';
134+
const error =
135+
passcode === 'used'
136+
? 'code already used; new code is available in 30 seconds'
137+
: 'failed to validate';
133138
return new Response(403, {}, { errors: [error] });
134139
}
135140
} else if (passcode) {

ui/tests/acceptance/mfa-test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,21 @@ module('Acceptance | mfa', function (hooks) {
132132
await click('[data-test-mfa-validate]');
133133
didLogin(assert);
134134
});
135+
136+
test('it should render unauthorized message for push failure', async function (assert) {
137+
await login('mfa-j');
138+
assert.dom('[data-test-auth-form]').doesNotExist('Auth form hidden when mfa fails');
139+
assert.dom('[data-test-empty-state-title]').hasText('Unauthorized', 'Error title renders');
140+
assert
141+
.dom('[data-test-empty-state-subText]')
142+
.hasText('PingId MFA validation failed', 'Error message from server renders');
143+
assert
144+
.dom('[data-test-empty-state-message]')
145+
.hasText(
146+
'Multi-factor authentication is required, but failed. Go back and try again, or contact your administrator.',
147+
'Error description renders'
148+
);
149+
await click('[data-test-mfa-error] button');
150+
assert.dom('[data-test-auth-form]').exists('Auth form renders after mfa error dismissal');
151+
});
135152
});

0 commit comments

Comments
 (0)