From eeac1d1ebc4c9cc5179d438c4fd9c715f1b885a4 Mon Sep 17 00:00:00 2001 From: Denys Oblohin <72614880+denysoblohin-okta@users.noreply.github.com> Date: Wed, 1 Nov 2023 19:59:40 +0200 Subject: [PATCH] Added remediation ReEnrollAuthenticatorWarning (#1472) OKTA-644785 Added remediation ReEnrollAuthenticatorWarning --- CHANGELOG.md | 1 + lib/idx/flow/AccountUnlockFlow.ts | 4 +- lib/idx/flow/AuthenticationFlow.ts | 2 + lib/idx/flow/PasswordRecoveryFlow.ts | 2 + .../ReEnrollAuthenticatorWarning.ts | 18 +++ lib/idx/remediators/index.ts | 1 + test/spec/idx/authenticate.ts | 143 ++++++++++++++++++ 7 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 lib/idx/remediators/ReEnrollAuthenticatorWarning.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aeae5a91..3ad9acb62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Bug Fix - [#1462](https://github.com/okta/okta-auth-js/pull/1462) Fixes ESM build for Node.js + - [#1472](https://github.com/okta/okta-auth-js/pull/1472) Added missing remediator `ReEnrollAuthenticatorWarning` ### Other diff --git a/lib/idx/flow/AccountUnlockFlow.ts b/lib/idx/flow/AccountUnlockFlow.ts index 465a872bb..d87f8f47f 100644 --- a/lib/idx/flow/AccountUnlockFlow.ts +++ b/lib/idx/flow/AccountUnlockFlow.ts @@ -18,7 +18,8 @@ import { SelectAuthenticatorAuthenticate, ChallengeAuthenticator, ChallengePoll, - AuthenticatorVerificationData + AuthenticatorVerificationData, + ReEnrollAuthenticatorWarning } from '../remediators'; export const AccountUnlockFlow: RemediationFlow = { @@ -31,4 +32,5 @@ export const AccountUnlockFlow: RemediationFlow = { 'challenge-authenticator': ChallengeAuthenticator, 'challenge-poll': ChallengePoll, 'authenticator-verification-data': AuthenticatorVerificationData, + 'reenroll-authenticator-warning': ReEnrollAuthenticatorWarning, }; diff --git a/lib/idx/flow/AuthenticationFlow.ts b/lib/idx/flow/AuthenticationFlow.ts index e14d3bc8a..d96953d21 100644 --- a/lib/idx/flow/AuthenticationFlow.ts +++ b/lib/idx/flow/AuthenticationFlow.ts @@ -17,6 +17,7 @@ import { SelectAuthenticatorAuthenticate, ChallengeAuthenticator, ReEnrollAuthenticator, + ReEnrollAuthenticatorWarning, RedirectIdp, AuthenticatorEnrollmentData, SelectAuthenticatorEnroll, @@ -39,6 +40,7 @@ export const AuthenticationFlow: RemediationFlow = { 'challenge-authenticator': ChallengeAuthenticator, 'challenge-poll': ChallengePoll, 'reenroll-authenticator': ReEnrollAuthenticator, + 'reenroll-authenticator-warning': ReEnrollAuthenticatorWarning, 'enroll-poll': EnrollPoll, 'select-enrollment-channel': SelectEnrollmentChannel, 'enrollment-channel-data': EnrollmentChannelData, diff --git a/lib/idx/flow/PasswordRecoveryFlow.ts b/lib/idx/flow/PasswordRecoveryFlow.ts index c319faeab..7a236bbd9 100644 --- a/lib/idx/flow/PasswordRecoveryFlow.ts +++ b/lib/idx/flow/PasswordRecoveryFlow.ts @@ -19,6 +19,7 @@ import { AuthenticatorVerificationData, ResetAuthenticator, ReEnrollAuthenticator, + ReEnrollAuthenticatorWarning, SelectAuthenticatorEnroll, AuthenticatorEnrollmentData, EnrollPoll @@ -34,5 +35,6 @@ export const PasswordRecoveryFlow: RemediationFlow = { 'authenticator-enrollment-data': AuthenticatorEnrollmentData, 'reset-authenticator': ResetAuthenticator, 'reenroll-authenticator': ReEnrollAuthenticator, + 'reenroll-authenticator-warning': ReEnrollAuthenticatorWarning, 'enroll-poll': EnrollPoll, }; diff --git a/lib/idx/remediators/ReEnrollAuthenticatorWarning.ts b/lib/idx/remediators/ReEnrollAuthenticatorWarning.ts new file mode 100644 index 000000000..27c9eeca7 --- /dev/null +++ b/lib/idx/remediators/ReEnrollAuthenticatorWarning.ts @@ -0,0 +1,18 @@ +/*! + * Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + + +import { ReEnrollAuthenticator } from './ReEnrollAuthenticator'; + +export class ReEnrollAuthenticatorWarning extends ReEnrollAuthenticator { + static remediationName = 'reenroll-authenticator-warning'; +} diff --git a/lib/idx/remediators/index.ts b/lib/idx/remediators/index.ts index 100051a61..cacb130c4 100644 --- a/lib/idx/remediators/index.ts +++ b/lib/idx/remediators/index.ts @@ -22,6 +22,7 @@ export * from './ResetAuthenticator'; export * from './EnrollProfile'; export * from './Identify'; export * from './ReEnrollAuthenticator'; +export * from './ReEnrollAuthenticatorWarning'; export * from './RedirectIdp'; export * from './SelectAuthenticatorAuthenticate'; export * from './SelectAuthenticatorEnroll'; diff --git a/test/spec/idx/authenticate.ts b/test/spec/idx/authenticate.ts index 919b78f31..66242de2f 100644 --- a/test/spec/idx/authenticate.ts +++ b/test/spec/idx/authenticate.ts @@ -67,6 +67,7 @@ import { IdxAuthenticatorFactory, SelectEnrollmentChannelRemediationFactory, EnrollmentChannelDataSmsRemediationFactory, + ReEnrollPasswordAuthenticatorRemediationFactory, } from '@okta/test.support/idx'; import { IdxMessagesFactory } from '@okta/test.support/idx/factories/messages'; @@ -614,6 +615,148 @@ describe('idx/authenticate', () => { }); + + describe('authentication with optional password change', () => { + beforeEach(() => { + const { successResponse } = testContext; + const identifyResponse = IdentifyResponseFactory.build(); + const selectAuthenticatorResponse = IdxResponseFactory.build({ + neededToProceed: [ + SelectAuthenticatorAuthenticateRemediationFactory.build({ + value: [ + AuthenticatorValueFactory.build({ + options: [ + PasswordAuthenticatorOptionFactory.build(), + ] + }) + ] + }) + ] + }); + const verifyPasswordResponse = VerifyPasswordResponseFactory.build(); + const changePasswordResponse = IdxResponseFactory.build({ + neededToProceed: [ + ReEnrollPasswordAuthenticatorRemediationFactory.build({ + name: 'reenroll-authenticator-warning' + }), + SkipRemediationFactory.build() + ] + }); + chainResponses([ + identifyResponse, + selectAuthenticatorResponse, + verifyPasswordResponse, + changePasswordResponse, + successResponse, + ]); + jest.spyOn(mocked.introspect, 'introspect') + .mockResolvedValueOnce(identifyResponse) + .mockResolvedValueOnce(changePasswordResponse); + jest.spyOn(identifyResponse, 'proceed'); + jest.spyOn(selectAuthenticatorResponse, 'proceed'); + jest.spyOn(verifyPasswordResponse, 'proceed'); + jest.spyOn(changePasswordResponse, 'proceed'); + Object.assign(testContext, { + identifyResponse, + selectAuthenticatorResponse, + verifyPasswordResponse, + changePasswordResponse, + }); + }); + + it('can authenticate with current password and then change password to new one', async () => { + const { + authClient, + identifyResponse, + selectAuthenticatorResponse, + verifyPasswordResponse, + changePasswordResponse, + tokenResponse + } = testContext; + // authenticate + jest.spyOn(mocked.introspect, 'introspect').mockResolvedValue(identifyResponse); + let res = await authenticate(authClient, { username: 'fakeuser', password: 'fakepass' }); + expect(res).toMatchObject({ + status: IdxStatus.PENDING, + neededToProceed: [{ + name: 'reenroll-authenticator-warning' + }, { + name: 'skip' + }] + }); + expect(identifyResponse.proceed).toHaveBeenCalledWith('identify', { identifier: 'fakeuser' }); + expect(selectAuthenticatorResponse.proceed).toHaveBeenCalledWith('select-authenticator-authenticate', { + authenticator: { id: 'id-password' } + }); + expect(verifyPasswordResponse.proceed).toHaveBeenCalledWith('challenge-authenticator', { credentials: { passcode: 'fakepass' } }); + expect(res.nextStep).toMatchObject({ + name: 'reenroll-authenticator-warning', + type: 'password', + authenticator: { + key: 'okta_password', + }, + inputs: [{ + name: 'newPassword', + label: 'New password', + secret: true, + }], + }); + // proceed + res = await proceed(authClient, { newPassword: 'newpass' }); + expect(changePasswordResponse.proceed).toHaveBeenCalledWith('reenroll-authenticator-warning', { + credentials: { passcode: 'newpass' } + }); + expect(res).toMatchObject({ + status: IdxStatus.SUCCESS, + tokens: tokenResponse.tokens, + }); + }); + + it('can authenticate with current password and skip optional password change', async () => { + const { + authClient, + identifyResponse, + selectAuthenticatorResponse, + verifyPasswordResponse, + tokenResponse + } = testContext; + // authenticate + jest.spyOn(mocked.introspect, 'introspect').mockResolvedValue(identifyResponse); + let res = await authenticate(authClient, { username: 'fakeuser', password: 'fakepass' }); + expect(res).toMatchObject({ + status: IdxStatus.PENDING, + neededToProceed: [{ + name: 'reenroll-authenticator-warning' + }, { + name: 'skip' + }] + }); + expect(identifyResponse.proceed).toHaveBeenCalledWith('identify', { identifier: 'fakeuser' }); + expect(selectAuthenticatorResponse.proceed).toHaveBeenCalledWith('select-authenticator-authenticate', { + authenticator: { id: 'id-password' } + }); + expect(verifyPasswordResponse.proceed).toHaveBeenCalledWith('challenge-authenticator', { credentials: { passcode: 'fakepass' } }); + expect(res.nextStep).toMatchObject({ + name: 'reenroll-authenticator-warning', + type: 'password', + authenticator: { + key: 'okta_password', + }, + inputs: [{ + name: 'newPassword', + label: 'New password', + secret: true, + }], + }); + // proceed + res = await proceed(authClient, { skip: true }); + expect(res).toMatchObject({ + status: IdxStatus.SUCCESS, + tokens: tokenResponse.tokens, + }); + }); + }); + describe('mfa authentication', () => { describe('phone', () => {