Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* adds development workflow to mirage config * adds mirage handler and factory for mfa workflow * adds mfa handling to auth service and cluster adapter * moves auth success logic from form to controller * adds mfa form component * shows delayed auth message for all methods * adds new code delay to mfa form * adds error views * fixes merge conflict * adds integration tests for mfa-form component * fixes auth tests * updates mfa response handling to align with backend * updates mfa-form to handle multiple methods and constraints * adds noDefault arg to Select component * updates mirage mfa handler to align with backend and adds generator for various mfa scenarios * adds tests * flaky test fix attempt * reverts test fix attempt * adds changelog entry * updates comments for todo items * removes faker from mfa mirage factory and handler * adds number to word helper * fixes tests
- Loading branch information
Showing
26 changed files
with
1,070 additions
and
164 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:improvement | ||
ui: Adds multi-factor authentication support | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import Component from '@glimmer/component'; | ||
import { inject as service } from '@ember/service'; | ||
import { action } from '@ember/object'; | ||
import { TOTP_NOT_CONFIGURED } from 'vault/services/auth'; | ||
|
||
const TOTP_NA_MSG = | ||
'Multi-factor authentication is required, but you have not set it up. In order to do so, please contact your administrator.'; | ||
const MFA_ERROR_MSG = | ||
'Multi-factor authentication is required, but failed. Go back and try again, or contact your administrator.'; | ||
|
||
export { TOTP_NA_MSG, MFA_ERROR_MSG }; | ||
|
||
/** | ||
* @module MfaError | ||
* MfaError components are used to display mfa errors | ||
* | ||
* @example | ||
* ```js | ||
* <MfaError /> | ||
* ``` | ||
*/ | ||
|
||
export default class MfaError extends Component { | ||
@service auth; | ||
|
||
get isTotp() { | ||
return this.auth.mfaErrors.includes(TOTP_NOT_CONFIGURED); | ||
} | ||
get title() { | ||
return this.isTotp ? 'TOTP not set up' : 'Unauthorized'; | ||
} | ||
get description() { | ||
return this.isTotp ? TOTP_NA_MSG : MFA_ERROR_MSG; | ||
} | ||
|
||
@action | ||
onClose() { | ||
this.auth.set('mfaErrors', null); | ||
if (this.args.onClose) { | ||
this.args.onClose(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import Component from '@glimmer/component'; | ||
import { inject as service } from '@ember/service'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import { action, set } from '@ember/object'; | ||
import { task, timeout } from 'ember-concurrency'; | ||
import { numberToWord } from 'vault/helpers/number-to-word'; | ||
/** | ||
* @module MfaForm | ||
* The MfaForm component is used to enter a passcode when mfa is required to login | ||
* | ||
* @example | ||
* ```js | ||
* <MfaForm @clusterId={this.model.id} @authData={this.authData} /> | ||
* ``` | ||
* @param {string} clusterId - id of selected cluster | ||
* @param {object} authData - data from initial auth request -- { mfa_requirement, backend, data } | ||
* @param {function} onSuccess - fired when passcode passes validation | ||
*/ | ||
|
||
export default class MfaForm extends Component { | ||
@service auth; | ||
|
||
@tracked passcode; | ||
@tracked countdown; | ||
@tracked errors; | ||
|
||
get constraints() { | ||
return this.args.authData.mfa_requirement.mfa_constraints; | ||
} | ||
get multiConstraint() { | ||
return this.constraints.length > 1; | ||
} | ||
get singleConstraintMultiMethod() { | ||
return !this.isMultiConstraint && this.constraints[0].methods.length > 1; | ||
} | ||
get singlePasscode() { | ||
return ( | ||
!this.isMultiConstraint && | ||
this.constraints[0].methods.length === 1 && | ||
this.constraints[0].methods[0].uses_passcode | ||
); | ||
} | ||
get description() { | ||
let base = 'Multi-factor authentication is enabled for your account.'; | ||
if (this.singlePasscode) { | ||
base += ' Enter your authentication code to log in.'; | ||
} | ||
if (this.singleConstraintMultiMethod) { | ||
base += ' Select the MFA method you wish to use.'; | ||
} | ||
if (this.multiConstraint) { | ||
const num = this.constraints.length; | ||
base += ` ${numberToWord(num, true)} methods are required for successful authentication.`; | ||
} | ||
return base; | ||
} | ||
|
||
@task *validate() { | ||
try { | ||
const response = yield this.auth.totpValidate({ | ||
clusterId: this.args.clusterId, | ||
...this.args.authData, | ||
}); | ||
this.args.onSuccess(response); | ||
} catch (error) { | ||
this.errors = error.errors; | ||
// TODO: update if specific error can be parsed for incorrect passcode | ||
// this.newCodeDelay.perform(); | ||
} | ||
} | ||
|
||
@task *newCodeDelay() { | ||
this.passcode = null; | ||
this.countdown = 30; | ||
while (this.countdown) { | ||
yield timeout(1000); | ||
this.countdown--; | ||
} | ||
} | ||
|
||
@action onSelect(constraint, id) { | ||
set(constraint, 'selectedId', id); | ||
set(constraint, 'selectedMethod', constraint.methods.findBy('id', id)); | ||
} | ||
@action submit(e) { | ||
e.preventDefault(); | ||
this.validate.perform(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { helper } from '@ember/component/helper'; | ||
|
||
export function numberToWord(number, capitalize) { | ||
const word = | ||
{ | ||
0: 'zero', | ||
1: 'one', | ||
2: 'two', | ||
3: 'three', | ||
4: 'four', | ||
5: 'five', | ||
6: 'six', | ||
7: 'seven', | ||
8: 'eight', | ||
9: 'nine', | ||
}[number] || number; | ||
return capitalize && typeof word === 'string' ? `${word.charAt(0).toUpperCase()}${word.slice(1)}` : word; | ||
} | ||
|
||
export default helper(function ([number], { capitalize }) { | ||
return numberToWord(number, capitalize); | ||
}); |
Oops, something went wrong.