From 8662eebf0b97fa9e350b89e198233819505d52b6 Mon Sep 17 00:00:00 2001 From: Indira Thirumalaimurugan Date: Thu, 18 Nov 2021 22:05:50 +0000 Subject: [PATCH] feat: add interstitial email page on authenticating via email OKTA-443678 <<>> Artifact: okta-signin-widget Files changed count: 6 PR Link: "https://github.com/okta/okta-signin-widget/pull/2298" --- .../i18n/src/properties/login.properties | 3 + .../src/properties/login_ok_PL.properties | 2 + ...authenticator-verification-data-email.json | 216 ++++++++++++++++++ src/v2/view-builder/ViewFactory.js | 4 +- .../ChallengeAuthenticatorDataEmailView.js | 55 +++++ .../spec/ChallengeAuthenticatorEmail_spec.js | 57 ++++- 6 files changed, 332 insertions(+), 5 deletions(-) create mode 100644 playground/mocks/data/idp/idx/authenticator-verification-data-email.json create mode 100644 src/v2/view-builder/views/email/ChallengeAuthenticatorDataEmailView.js diff --git a/packages/@okta/i18n/src/properties/login.properties b/packages/@okta/i18n/src/properties/login.properties index 6ebe8777f2..93d1867a0b 100644 --- a/packages/@okta/i18n/src/properties/login.properties +++ b/packages/@okta/i18n/src/properties/login.properties @@ -915,6 +915,9 @@ oie.phone.invalid = Invalid Phone Number. oie.email.label = Email oie.email.authenticator.description = Verify with a link or code sent to your email oie.email.mfa.title = Verify with your email +oie.email.verify.primaryButton = Send me an email +# {0} is a user's email address +oie.email.verify.subtitle = Verify with an email link or enter a code sent to <$1>{0} # {0} is a user's email address oie.email.verify.alternate.magicLinkToEmailAddress = An email magic link was sent to <$1>{0}. oie.email.verify.alternate.magicLinkToYourEmail = An email magic link was sent to <$1>your email. diff --git a/packages/@okta/i18n/src/properties/login_ok_PL.properties b/packages/@okta/i18n/src/properties/login_ok_PL.properties index 013dd467d2..f565706731 100644 --- a/packages/@okta/i18n/src/properties/login_ok_PL.properties +++ b/packages/@okta/i18n/src/properties/login_ok_PL.properties @@ -665,6 +665,8 @@ oie.phone.invalid = 》Îñṽåļîð Þĥöñé Ñûɱƀéŕ· Ӝฐโ⾼ئ oie.email.label = 》Éɱåîļ 홝한Ӝฐโ《 oie.email.authenticator.description = 》Ṽéŕîƒý ŵîţĥ å ļîñķ öŕ çöðé šéñţ ţö ýöûŕ éɱåîļ 䀕ヸ€홝한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ《 oie.email.mfa.title = 》Ṽéŕîƒý ŵîţĥ ýöûŕ éɱåîļ Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ《 +oie.email.verify.primaryButton = 》Šéñð ɱé åñ éɱåîļ Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ《 +oie.email.verify.subtitle = 》Ṽéŕîƒý ŵîţĥ åñ éɱåîļ ļîñķ öŕ éñţéŕ å çöðé šéñţ ţö ≤€①≥ €홝한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ{0}≤⁄€①≥ ฐโ《 oie.email.verify.alternate.magicLinkToEmailAddress = 》Åñ éɱåîļ ɱåĝîç ļîñķ ŵåš šéñţ ţö ≤€①≥ 한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ{0}≤⁄€①≥· ฐโ 《 oie.email.verify.alternate.magicLinkToYourEmail = 》Åñ éɱåîļ ɱåĝîç ļîñķ ŵåš šéñţ ţö ≤€①≥ýöûŕ éɱåîļ≤⁄€①≥· €홝한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ 《 oie.email.verify.alternate.instructions = 》Çļîçķ ţĥé ļîñķ îñ ţĥé éɱåîļ öŕ éñţéŕ ţĥé çöðé ƀéļöŵ ţö çöñţîñûé· ئ䀕ヸ€홝한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ《 diff --git a/playground/mocks/data/idp/idx/authenticator-verification-data-email.json b/playground/mocks/data/idp/idx/authenticator-verification-data-email.json new file mode 100644 index 0000000000..0d57fe62ac --- /dev/null +++ b/playground/mocks/data/idp/idx/authenticator-verification-data-email.json @@ -0,0 +1,216 @@ +{ + "version": "1.0.0", + "stateHandle": "02bINNCEMoUSz5zHuyfW0xElLeOHx7RprUV7rWlSJ3", + "expiresAt": "2021-11-10T00:26:42.000Z", + "intent": "LOGIN", + "remediation": { + "type": "array", + "value": [ + { + "rel": [ + "create-form" + ], + "name": "authenticator-verification-data", + "relatesTo": [ + "$.currentAuthenticatorEnrollment" + ], + "href": "http://localhost:3000/idp/idx/challenge", + "method": "POST", + "produces": "application/ion+json; okta-version=1.0.0", + "value": [ + { + "name": "authenticator", + "label": "Email", + "form": { + "value": [ + { + "name": "id", + "required": true, + "value": "aut1v94GMPPMvkjbp0g4", + "mutable": false + }, + { + "name": "methodType", + "type": "string", + "required": true, + "options": [ + { + "label": "Email", + "value": "email" + } + ] + } + ] + } + }, + { + "name": "stateHandle", + "required": true, + "value": "02bINNCEMoUSz5zHuyfW0xElLeOHx7RprUV7rWlSJ3", + "visible": false, + "mutable": false + } + ], + "accepts": "application/json; okta-version=1.0.0" + }, + { + "rel": [ + "create-form" + ], + "name": "select-authenticator-authenticate", + "href": "http://localhost:3000/idp/idx/challenge", + "method": "POST", + "produces": "application/ion+json; okta-version=1.0.0", + "value": [ + { + "name": "authenticator", + "type": "object", + "options": [ + { + "label": "Email", + "value": { + "form": { + "value": [ + { + "name": "id", + "required": true, + "value": "aut1v94GMPPMvkjbp0g4", + "mutable": false + }, + { + "name": "methodType", + "required": false, + "value": "email", + "mutable": false + } + ] + } + }, + "relatesTo": "$.authenticatorEnrollments.value[0]" + } + ] + }, + { + "name": "stateHandle", + "required": true, + "value": "02bINNCEMoUSz5zHuyfW0xElLeOHx7RprUV7rWlSJ3", + "visible": false, + "mutable": false + } + ], + "accepts": "application/json; okta-version=1.0.0" + } + ] + }, + "currentAuthenticatorEnrollment": { + "type": "object", + "value": { + "profile": { + "email": "i***1@okta.com" + }, + "resend": { + "rel": [ + "create-form" + ], + "name": "resend", + "href": "http://localhost:3000/idp/idx/challenge/resend", + "method": "POST", + "produces": "application/ion+json; okta-version=1.0.0", + "value": [ + { + "name": "stateHandle", + "required": true, + "value": "02bINNCEMoUSz5zHuyfW0xElLeOHx7RprUV7rWlSJ3", + "visible": false, + "mutable": false + } + ], + "accepts": "application/json; okta-version=1.0.0" + }, + "type": "email", + "key": "okta_email", + "id": "eae2ep99TyJZdLB8R0g4", + "displayName": "Email", + "methods": [ + { + "type": "email" + } + ] + } + }, + "authenticators": { + "type": "array", + "value": [ + { + "type": "email", + "key": "okta_email", + "id": "aut1v94GMPPMvkjbp0g4", + "displayName": "Email", + "methods": [ + { + "type": "email" + } + ] + } + ] + }, + "authenticatorEnrollments": { + "type": "array", + "value": [ + { + "profile": { + "email": "i***1@okta.com" + }, + "type": "email", + "key": "okta_email", + "id": "eae2ep99TyJZdLB8R0g4", + "displayName": "Email", + "methods": [ + { + "type": "email" + } + ] + } + ] + }, + "user": { + "type": "object", + "value": { + "id": "00u2ep8CG3BouMb750g4", + "identifier": "testUser@okta.com", + "profile": { + "firstName": "Test", + "lastName": "user1", + "timeZone": "America/Los_Angeles", + "locale": "en_US" + } + } + }, + "cancel": { + "rel": [ + "create-form" + ], + "name": "cancel", + "href": "http://localhost:3000/idp/idx/cancel", + "method": "POST", + "produces": "application/ion+json; okta-version=1.0.0", + "value": [ + { + "name": "stateHandle", + "required": true, + "value": "02bINNCEMoUSz5zHuyfW0xElLeOHx7RprUV7rWlSJ3", + "visible": false, + "mutable": false + } + ], + "accepts": "application/json; okta-version=1.0.0" + }, + "app": { + "type": "object", + "value": { + "name": "okta_enduser", + "label": "Okta Dashboard", + "id": "0oazy63LqpLSP2B6r0g3" + } + } +} diff --git a/src/v2/view-builder/ViewFactory.js b/src/v2/view-builder/ViewFactory.js index 2224ecbb75..c007a71f9a 100644 --- a/src/v2/view-builder/ViewFactory.js +++ b/src/v2/view-builder/ViewFactory.js @@ -63,6 +63,7 @@ import ChallengeWebauthnView from './views/webauthn/ChallengeWebauthnView'; // email import EnrollAuthenticatorEmailView from './views/email/EnrollAuthenticatorEmailView'; import ChallengeAuthenticatorEmailView from './views/email/ChallengeAuthenticatorEmailView'; +import ChallengeAuthenticatorDataEmailView from './views/email/ChallengeAuthenticatorDataEmailView'; // app (okta verify) import EnrollPollOktaVerifyView from './views/ov/EnrollPollOktaVerifyView'; @@ -229,7 +230,8 @@ const VIEWS_MAPPING = { }, [RemediationForms.AUTHENTICATOR_VERIFICATION_DATA]: { [AUTHENTICATOR_KEY.PHONE]: ChallengeAuthenticatorDataPhoneView, - [AUTHENTICATOR_KEY.OV]: ChallengeAuthenticatorDataOktaVerifyView + [AUTHENTICATOR_KEY.OV]: ChallengeAuthenticatorDataOktaVerifyView, + [AUTHENTICATOR_KEY.EMAIL]: ChallengeAuthenticatorDataEmailView, }, [RemediationForms.FAILURE_REDIRECT]: { [DEFAULT]: AutoRedirectView, diff --git a/src/v2/view-builder/views/email/ChallengeAuthenticatorDataEmailView.js b/src/v2/view-builder/views/email/ChallengeAuthenticatorDataEmailView.js new file mode 100644 index 0000000000..b0af95fb6a --- /dev/null +++ b/src/v2/view-builder/views/email/ChallengeAuthenticatorDataEmailView.js @@ -0,0 +1,55 @@ +import { loc, View } from 'okta'; +import hbs from 'handlebars-inline-precompile'; +import BaseAuthenticatorEmailView from './BaseAuthenticatorEmailView'; + +const BaseAuthenticatorEmailForm = BaseAuthenticatorEmailView.prototype.Body; + +const SubtitleView = View.extend({ + template: hbs` +
+ {{i18n + code="oie.email.verify.subtitle" + bundle="login" + arguments="email" + $1="$1" + }} +
+ `, + + getTemplateData() { + const email = this.options.appState.get('currentAuthenticatorEnrollment')?.profile?.email; + return { + email, + }; + }, +}); + +const Body = BaseAuthenticatorEmailForm.extend( + { + title() { + return loc('oie.email.mfa.title', 'login'); + }, + + save() { + return loc('oie.email.verify.primaryButton', 'login'); + }, + + postRender() { + BaseAuthenticatorEmailForm.prototype.postRender.apply(this, arguments); + this.add(SubtitleView, { + prepend: true, + selector: '.o-form-info-container', + }); + }, + + getUISchema() { + // Prevent from displaying radio buttons on the UI + const uiSchemas = BaseAuthenticatorEmailForm.prototype.getUISchema.apply(this, arguments); + return uiSchemas.filter(schema => schema.name !== 'authenticator.methodType'); + }, + }, +); + +export default BaseAuthenticatorEmailView.extend({ + Body, +}); diff --git a/test/testcafe/spec/ChallengeAuthenticatorEmail_spec.js b/test/testcafe/spec/ChallengeAuthenticatorEmail_spec.js index f63b3c1241..44958c33af 100644 --- a/test/testcafe/spec/ChallengeAuthenticatorEmail_spec.js +++ b/test/testcafe/spec/ChallengeAuthenticatorEmail_spec.js @@ -21,6 +21,7 @@ import terminalTransferedEmail from '../../../playground/mocks/data/idp/idx/term import sessionExpired from '../../../playground/mocks/data/idp/idx/error-session-expired'; import tooManyRequest from '../../../playground/mocks/data/idp/idx/error-429-too-many-request'; import apiLimitExeeeded from '../../../playground/mocks/data/idp/idx/error-429-api-limit-exceeded'; +import emailVerificationSendEmailData from '../../../playground/mocks/data/idp/idx/authenticator-verification-data-email'; const emailVerificationEmptyProfile = JSON.parse(JSON.stringify(emailVerificationNoProfile)); // add empty profile to test @@ -33,6 +34,12 @@ const logger = RequestLogger(/challenge|challenge\/poll|challenge\/answer/, } ); +const sendEmailMock = RequestMock() + .onRequestTo('http://localhost:3000/idp/idx/introspect') + .respond(emailVerificationSendEmailData) + .onRequestTo('http://localhost:3000/idp/idx/challenge') + .respond(emailVerification); + const validOTPmock = RequestMock() .onRequestTo('http://localhost:3000/idp/idx/introspect') .respond(emailVerification) @@ -157,6 +164,48 @@ async function setup(t) { return challengeEmailPageObject; } +test + .requestHooks(sendEmailMock)('send email screen should have right labels', async t => { + const challengeEmailPageObject = await setup(t); + await checkConsoleMessages({ + controller: null, + formName: 'authenticator-verification-data', + authenticatorKey: 'okta_email', + methodType: 'email', + }); + + const pageTitle = challengeEmailPageObject.getFormTitle(); + const saveBtnText = challengeEmailPageObject.getSaveButtonLabel(); + await t.expect(pageTitle).eql('Verify with your email'); + await t.expect(saveBtnText).eql('Send me an email'); + + const emailAddress = emailVerificationSendEmailData.currentAuthenticatorEnrollment.value.profile.email; + await t.expect(challengeEmailPageObject.getFormSubtitle()) + .eql(`Verify with an email link or enter a code sent to ${emailAddress}`); + + // Verify links (switch authenticator link not present since there are no other authenticators available) + await t.expect(await challengeEmailPageObject.switchAuthenticatorLinkExists()).notOk(); + await t.expect(await challengeEmailPageObject.signoutLinkExists()).ok(); + await t.expect(challengeEmailPageObject.getSignoutLinkText()).eql('Back to sign in'); + }); + +test + .requestHooks(sendEmailMock)('send me an email button should take to challenge email authenticator screen', async t => { + const challengeEmailPageObject = await setup(t); + await challengeEmailPageObject.clickNextButton(); + const pageTitle = challengeEmailPageObject.getFormTitle(); + await t.expect(pageTitle).eql('Verify with your email'); + + const emailAddress = emailVerification.currentAuthenticatorEnrollment.value.profile.email; + await t.expect(challengeEmailPageObject.getFormSubtitle()) + .eql(`An email magic link was sent to ${emailAddress}. Click the link in the email or enter the code below to continue.`); + + // Verify links (switch authenticator link not present since there are no other authenticators available) + await t.expect(await challengeEmailPageObject.switchAuthenticatorLinkExists()).notOk(); + await t.expect(await challengeEmailPageObject.signoutLinkExists()).ok(); + await t.expect(challengeEmailPageObject.getSignoutLinkText()).eql('Back to sign in'); + }); + test .requestHooks(validOTPmock)('challenge email authenticator screen has right labels', async t => { const challengeEmailPageObject = await setup(t); @@ -168,14 +217,14 @@ test }); await challengeEmailPageObject.clickEnterCodeLink(); - const pageTitle = challengeEmailPageObject.getPageTitle(); + const pageTitle = challengeEmailPageObject.getFormTitle(); const saveBtnText = challengeEmailPageObject.getSaveButtonLabel(); - await t.expect(saveBtnText).contains('Verify'); - await t.expect(pageTitle).contains('Verify with your email'); + await t.expect(pageTitle).eql('Verify with your email'); + await t.expect(saveBtnText).eql('Verify'); const emailAddress = emailVerification.currentAuthenticatorEnrollment.value.profile.email; await t.expect(challengeEmailPageObject.getFormSubtitle()) - .contains(`An email magic link was sent to ${emailAddress}. Click the link in the email or enter the code below to continue.`); + .eql(`An email magic link was sent to ${emailAddress}. Click the link in the email or enter the code below to continue.`); // Verify links (switch authenticator link not present since there are no other authenticators available) await t.expect(await challengeEmailPageObject.switchAuthenticatorLinkExists()).notOk();