UI Support for Okta Number Challenge (#15998)

* Imported uuid library for initial commit to push a clean branch.

* Removed import statement in auth-form file since it was causing UI tests to fail as the import was not being used.

* Added nonce field to payload for okta sign in. (#16001)

* Added nonce field to payload for okta sign in.

* Added missing yarn package for uuid

* Fixed failing ui tests in cluster-test file to take into account of nonce field in the payload of okta login

* Removed uuid library and used crypto.randomUUID() to generate unique uuid values instead

* Fixed indent in package.json

* Removed uuid library since decided to use crypto.randomUUID() instead to generate unique uuid values

* Create polling function for correct answer in okta number challenge (#16070)

* Implemented polling function to get correct answer for okta number challenge.

* Disabled polling function for testing as it was causing acceptance test to fail in auth-test.js

* Changed API call to be the auth mount path instead of being static and created a variable to store the oktaNumberChallengeAnswer to be used later for the display screens

* Create component for okta number challenge screen (#16195)

* Implemented loading screen and display screen for correct answer for Okta Number Challenge

* Fixed linting issues on hbs files

* Added periods to parameter descriptions and made parameters optional

* Removed optional parameters from calling AuthForm component if authMethod is not Okta

* Implement error handling and screens for okta number challenge (#16276)

* Implemented loading screen and display screen for correct answer for Okta Number Challenge

* Fixed linting issues on hbs files

* Temporary changes to include error screen in okta number challenge

* Created error screen tests and made minor fixes

* Fixed error for wrong parameter name being passed in

* Fixed linting issues causing ui tests to fail

* Added periods at the end of param descriptions

* Imported uuid library for initial commit to push a clean branch.

* Removed import statement in auth-form file since it was causing UI tests to fail as the import was not being used.

* Removed uuid library since decided to use crypto.randomUUID() instead to generate unique uuid values

* Added nonce field to payload for okta sign in. (#16001)

* Added nonce field to payload for okta sign in.

* Added missing yarn package for uuid

* Fixed failing ui tests in cluster-test file to take into account of nonce field in the payload of okta login

* Removed uuid library and used crypto.randomUUID() to generate unique uuid values instead

* Fixed indent in package.json

* Create polling function for correct answer in okta number challenge (#16070)

* Implemented polling function to get correct answer for okta number challenge.

* Disabled polling function for testing as it was causing acceptance test to fail in auth-test.js

* Changed API call to be the auth mount path instead of being static and created a variable to store the oktaNumberChallengeAnswer to be used later for the display screens

* Create component for okta number challenge screen (#16195)

* Implemented loading screen and display screen for correct answer for Okta Number Challenge

* Fixed linting issues on hbs files

* Added periods to parameter descriptions and made parameters optional

* Removed optional parameters from calling AuthForm component if authMethod is not Okta

* Implement error handling and screens for okta number challenge (#16276)

* Implemented loading screen and display screen for correct answer for Okta Number Challenge

* Fixed linting issues on hbs files

* Temporary changes to include error screen in okta number challenge

* Created error screen tests and made minor fixes

* Fixed error for wrong parameter name being passed in

* Fixed linting issues causing ui tests to fail

* Added periods at the end of param descriptions

* UI/vault 7312/fix vault enterprise error for okta number challenge (#16568)

* Fixed bug with okta not working when selecting okta tab after being on other tab

* Fixed vault enterprise errors

* Fixed error when logging in with Okta in 'Other' tab

* Removed namespace parameter in option to use the default

* Added changelog
This commit is contained in:
linda9379 2022-08-10 15:46:04 -04:00 committed by GitHub
parent 5c4b1cc4ac
commit 5cd1a12178
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 396 additions and 168 deletions

3
changelog/15998.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
ui: UI support for Okta Number Challenge.
```

View File

@ -107,7 +107,7 @@ export default ApplicationAdapter.extend({
},
authenticate({ backend, data }) {
const { role, jwt, token, password, username, path } = data;
const { role, jwt, token, password, username, path, nonce } = data;
const url = this.urlForAuth(backend, username, path);
const verb = backend === 'token' ? 'GET' : 'POST';
let options = {
@ -119,6 +119,8 @@ export default ApplicationAdapter.extend({
};
} else if (backend === 'jwt' || backend === 'oidc') {
options.data = { role, jwt };
} else if (backend === 'okta') {
options.data = { password, nonce };
} else {
options.data = token ? { token, password } : { password };
}

View File

@ -24,7 +24,11 @@ const BACKENDS = supportedAuthBackends();
* @param {object} cluster - The auth method that is currently selected in the dropdown. This corresponds to an Ember Model.
* @param {string} namespace- The currently active namespace.
* @param {string} selectedAuth - The auth method that is currently selected in the dropdown.
* @param {function} onSuccess - Fired on auth success
* @param {function} onSuccess - Fired on auth success.
* @param {function} [setOktaNumberChallenge] - Sets whether we are waiting for okta number challenge to be used to sign in.
* @param {boolean} [waitingForOktaNumberChallenge=false] - Determines if we are waiting for the Okta Number Challenge to sign in.
* @param {function} [setCancellingAuth] - Sets whether we are cancelling or not the login authentication for Okta Number Challenge.
* @param {boolean} [cancelAuthForOktaNumberChallenge=false] - Determines if we are cancelling the login authentication for the Okta Number Challenge.
*/
const DEFAULTS = {
@ -51,6 +55,9 @@ export default Component.extend(DEFAULTS, {
oldNamespace: null,
authMethods: BACKENDS,
// number answer for okta number challenge if applicable
oktaNumberChallengeAnswer: null,
didReceiveAttrs() {
this._super(...arguments);
let {
@ -60,8 +67,14 @@ export default Component.extend(DEFAULTS, {
namespace: ns,
selectedAuth: newMethod,
oldSelectedAuth: oldMethod,
cancelAuthForOktaNumberChallenge: cancelAuth,
} = this;
// if we are cancelling the login then we reset the number challenge answer and cancel the current authenticate and polling tasks
if (cancelAuth) {
this.set('oktaNumberChallengeAnswer', null);
this.authenticate.cancelAll();
this.pollForOktaNumberChallenge.cancelAll();
}
next(() => {
if (!token && (oldNS === null || oldNS !== ns)) {
this.fetchMethods.perform();
@ -219,7 +232,11 @@ export default Component.extend(DEFAULTS, {
cluster: { id: clusterId },
} = this;
try {
if (backendType === 'okta') {
this.pollForOktaNumberChallenge.perform(data.nonce, data.path);
} else {
this.delayAuthMessageReminder.perform();
}
const authResponse = yield this.auth.authenticate({
clusterId,
backend: backendType,
@ -236,6 +253,28 @@ export default Component.extend(DEFAULTS, {
})
),
pollForOktaNumberChallenge: task(function* (nonce, mount) {
// yield for 1s to wait to see if there is a login error before polling
yield timeout(1000);
if (this.error) {
return;
}
let response = null;
this.setOktaNumberChallenge(true);
this.setCancellingAuth(false);
// keep polling /auth/okta/verify/:nonce API every 1s until a response is given with the correct number for the Okta Number Challenge
while (response === null) {
// when testing, the polling loop causes promises to be rejected making acceptance tests fail
// so disable the poll in tests
if (Ember.testing) {
return;
}
yield timeout(1000);
response = yield this.auth.getOktaNumberChallengeAnswer(nonce, mount);
}
this.set('oktaNumberChallengeAnswer', response);
}),
delayAuthMessageReminder: task(function* () {
if (Ember.testing) {
this.showLoading = true;
@ -275,6 +314,14 @@ export default Component.extend(DEFAULTS, {
if (this.customPath || backend.id) {
data.path = this.customPath || backend.id;
}
// add nonce field for okta backend
if (backend.type === 'okta') {
data.nonce = crypto.randomUUID();
// add a default path of okta if it doesn't exist to be used for Okta Number Challenge
if (!data.path) {
data.path = 'okta';
}
}
return this.authenticate.unlinked().perform(backend.type, data);
},
handleError(e) {
@ -283,5 +330,9 @@ export default Component.extend(DEFAULTS, {
error: e ? this.auth.handleError(e) : null,
});
},
returnToLoginFromOktaNumberChallenge() {
this.setOktaNumberChallenge(false);
this.set('oktaNumberChallengeAnswer', null);
},
},
});

View File

@ -0,0 +1,24 @@
import Component from '@glimmer/component';
/**
* @module OktaNumberChallenge
* OktaNumberChallenge components are used to display loading screen and correct answer for Okta Number Challenge when signing in through Okta
*
* @example
* ```js
* <OktaNumberChallenge @correctAnswer={this.oktaNumberChallengeAnswer} @hasError={this.error} @onReturnToLogin={this.returnToLoginFromOktaNumberChallenge}/>
* ```
* @param {number} correctAnswer - The correct answer to click for the okta number challenge.
* @param {boolean} hasError - Determines if there is an error being thrown.
* @param {function} onReturnToLogin - Sets waitingForOktaNumberChallenge to false if want to return to main login.
*/
export default class OktaNumberChallenge extends Component {
get oktaNumberChallengeCorrectAnswer() {
return this.args.correctAnswer;
}
get errorThrown() {
return this.args.hasError;
}
}

View File

@ -85,5 +85,9 @@ export default Controller.extend({
mfaErrors: null,
});
},
cancelAuthentication() {
this.set('cancelAuth', true);
this.set('waitingForOktaNumberChallenge', false);
},
},
});

View File

@ -441,4 +441,21 @@ export default Service.extend({
backend: BACKENDS.findBy('type', backend),
});
}),
getOktaNumberChallengeAnswer(nonce, mount) {
const url = `/v1/auth/${mount}/verify/${nonce}`;
return this.ajax(url, 'GET', {}).then(
(resp) => {
return resp.data.correct_answer;
},
(e) => {
// if error status is 404, return and keep polling for a response
if (e.status === 404) {
return null;
} else {
throw e;
}
}
);
},
});

View File

@ -1,4 +1,11 @@
<div class="auth-form" data-test-auth-form>
{{#if (or (this.error) (and this.waitingForOktaNumberChallenge (not this.cancelAuthForOktaNumberChallenge)))}}
<OktaNumberChallenge
@correctAnswer={{this.oktaNumberChallengeAnswer}}
@hasError={{(not-eq this.error null)}}
@onReturnToLogin={{action "returnToLoginFromOktaNumberChallenge"}}
/>
{{else}}
{{#if this.hasMethodsWithPath}}
<nav class="tabs is-marginless">
<ul>
@ -6,7 +13,9 @@
{{#let (or method.path method.type) as |methodKey|}}
<li
class={{if
(and this.selectedAuthIsPath (eq (or this.selectedAuthBackend.path this.selectedAuthBackend.type) methodKey))
(and
this.selectedAuthIsPath (eq (or this.selectedAuthBackend.path this.selectedAuthBackend.type) methodKey)
)
"is-active"
""
}}
@ -173,4 +182,5 @@
</form>
{{/if}}
</div>
{{/if}}
</div>

View File

@ -0,0 +1,38 @@
<div class="auth-form" data-test-okta-number-challenge>
<div class="box is-marginless is-shadowless">
<div class="field has-top-margin-xs">
<p data-test-okta-number-challenge-description>
To finish signing in, you will need to complete an additional MFA step.</p>
{{#if this.errorThrown}}
<div class="has-top-margin-s">
<MessageError @errorMessage="There was a problem" />
<button
type="button"
class="button"
{{on "click" @onReturnToLogin}}
data-test-return-from-okta-number-challenge
>Return to login</button>
</div>
{{else if this.oktaNumberChallengeCorrectAnswer}}
<div class="has-top-margin-s">
<p class="has-text-black has-text-weight-semibold" data-test-okta-number-challenge-verification-type>Okta
verification</p>
<p data-test-okta-number-challenge-verification-description>Select the following number to complete verification:</p>
<h1
class="title has-font-weight-normal has-top-margin-m has-bottom-margin-s"
data-test-okta-number-challenge-answer
>{{this.oktaNumberChallengeCorrectAnswer}}</h1>
</div>
{{else}}
<div class="has-top-margin-l has-bottom-margin-m">
<div class="is-flex-row">
<FlightIcon @name="loading" />
<div class="has-left-margin-xs">
<p data-test-okta-number-challenge-loading>Please wait...</p>
</div>
</div>
</div>
{{/if}}
</div>
</div>
</div>

View File

@ -27,9 +27,13 @@
<button type="button" class="icon-button" {{on "click" (fn (mut this.mfaAuthData) null)}}>
<Icon @name="arrow-left" @size="24" aria-label="Back to login" class="icon-blue" />
</button>
{{else if this.waitingForOktaNumberChallenge}}
<button type="button" class="icon-button" {{on "click" (action "cancelAuthentication")}}>
<Icon @name="arrow-left" @size="24" aria-label="Back to login" class="icon-blue" />
</button>
{{/if}}
<h1 class="title is-3">
{{if this.mfaAuthData "Authenticate" "Sign in to Vault"}}
{{if (or this.mfaAuthData this.waitingForOktaNumberChallenge) "Authenticate" "Sign in to Vault"}}
</h1>
</div>
{{/if}}
@ -113,6 +117,10 @@
@redirectTo={{this.redirectTo}}
@selectedAuth={{this.authMethod}}
@onSuccess={{action "onAuthResponse"}}
@setOktaNumberChallenge={{fn (mut this.waitingForOktaNumberChallenge)}}
@waitingForOktaNumberChallenge={{this.waitingForOktaNumberChallenge}}
@setCancellingAuth={{fn (mut this.cancelAuth)}}
@cancelAuthForOktaNumberChallenge={{this.cancelAuth}}
/>
{{/if}}
</Page.content>

View File

@ -0,0 +1,69 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | okta-number-challenge', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.oktaNumberChallengeAnswer = null;
this.hasError = false;
});
test('it should render correct descriptions', async function (assert) {
await render(hbs`<OktaNumberChallenge @correctAnswer={{this.oktaNumberChallengeAnswer}}/>`);
assert
.dom('[data-test-okta-number-challenge-description]')
.includesText(
'To finish signing in, you will need to complete an additional MFA step.',
'Correct description renders'
);
assert
.dom('[data-test-okta-number-challenge-loading]')
.includesText('Please wait...', 'Correct loading description renders');
});
test('it should show correct number for okta number challenge', async function (assert) {
this.set('oktaNumberChallengeAnswer', 1);
await render(hbs`<OktaNumberChallenge @correctAnswer={{this.oktaNumberChallengeAnswer}}/>`);
assert
.dom('[data-test-okta-number-challenge-description]')
.includesText(
'To finish signing in, you will need to complete an additional MFA step.',
'Correct description renders'
);
assert
.dom('[data-test-okta-number-challenge-verification-type]')
.includesText('Okta verification', 'Correct verification type renders');
assert
.dom('[data-test-okta-number-challenge-verification-description]')
.includesText(
'Select the following number to complete verification:',
'Correct verification description renders'
);
assert
.dom('[data-test-okta-number-challenge-answer]')
.includesText('1', 'Correct okta number challenge answer renders');
});
test('it should show error screen', async function (assert) {
this.set('hasError', true);
await render(
hbs`<OktaNumberChallenge @correctAnswer={{this.oktaNumberChallengeAnswer}} @hasError={{this.hasError}} @onReturnToLogin={{fn (mut this.returnToLogin) true}}/>`
);
assert
.dom('[data-test-okta-number-challenge-description]')
.includesText(
'To finish signing in, you will need to complete an additional MFA step.',
'Correct description renders'
);
assert
.dom('[data-test-error]')
.includesText('There was a problem', 'Displays error that there was a problem');
await click('[data-test-return-from-okta-number-challenge]');
assert.true(this.returnToLogin, 'onReturnToLogin was triggered');
});
});

View File

@ -114,11 +114,12 @@ module('Unit | Adapter | cluster', function (hooks) {
'ldap:userpass options OK'
);
data = { password: 'password', username: 'username', nonce: 'uuid' };
adapter.authenticate({ backend: 'okta', data });
assert.equal(url, '/v1/auth/okta/login/username', 'okta:userpass url OK');
assert.equal(method, 'POST', 'ldap:userpass method OK');
assert.deepEqual(
{ data: { password: 'password' }, unauthenticated: true },
{ data: { password: 'password', nonce: 'uuid' }, unauthenticated: true },
options,
'okta:userpass options OK'
);
@ -132,6 +133,7 @@ module('Unit | Adapter | cluster', function (hooks) {
adapter.authenticate({ backend: 'LDAP', data });
assert.equal(url, '/v1/auth/path/login/username', 'auth:LDAP with path url OK');
data = { password: 'password', username: 'username', path: 'path', nonce: 'uuid' };
adapter.authenticate({ backend: 'Okta', data });
assert.equal(url, '/v1/auth/path/login/username', 'auth:Okta with path url OK');
});