diff --git a/playwright/tests/e2e/sign_in_passcode_active_user.spec.ts b/playwright/tests/e2e/sign_in_passcode_active_user.spec.ts index 952a05551..414d8ba8c 100644 --- a/playwright/tests/e2e/sign_in_passcode_active_user.spec.ts +++ b/playwright/tests/e2e/sign_in_passcode_active_user.spec.ts @@ -134,6 +134,54 @@ test.describe('Sign In flow, with passcode', () => { expect(updatedUser.profile.emailValidated).toBe(true); }); + test('should sign in with passcode - preserve firstName & lastName', async ({ + request, + page, + }) => { + const { emailAddress } = await createTestUser(request, { + isUserEmailValidated: true, + }); + + await page.goto(`/signin?appClientId=${appClientId}`); + const element = await page.locator('input[name=email]').elementHandle(); + + // TODO: Use real form elements once implemented. + await element?.evaluate((element) => { + /* eslint-disable functional/immutable-data */ + const firstNameInput = document.createElement('input'); + firstNameInput.name = 'firstName'; + firstNameInput.value = 'TestFirstName'; + element.append(firstNameInput); + const secondNameInput = document.createElement('input'); + secondNameInput.name = 'secondName'; + secondNameInput.value = 'TestLastName'; + element.append(secondNameInput); + /* eslint-enable functional/immutable-data */ + }); + + await page.locator('input[name=email]').fill(emailAddress); + + const passcodeRequestTime = new Date(); + await page.locator('[data-cy="main-form-submit-button"]').click(); + + await expect(page).toHaveURL(/\/passcode/); + await expect(page.getByText('Enter your one-time code')).toBeVisible(); + + const { codes } = await checkForEmailAndGetDetails( + emailAddress, + passcodeRequestTime, + ); + + const code = codes?.[0].value; + await page.locator('input[name=code]').fill(code!); + + await expect(page).toHaveURL(/\/welcome\/existing/); + + const updatedUser = await getTestOktaUser(request, emailAddress); + expect(updatedUser.profile.firstName).toBe('TestFirstName'); + expect(updatedUser.profile.lastName).toBe('TestLastName'); + }); + test('selects password option to sign in from initial sign in page', async ({ request, page, diff --git a/src/server/controllers/signInControllers.ts b/src/server/controllers/signInControllers.ts index dcb9b2026..c6c476df5 100644 --- a/src/server/controllers/signInControllers.ts +++ b/src/server/controllers/signInControllers.ts @@ -201,7 +201,7 @@ export const oktaIdxApiSignInPasscodeController = async ({ loopDetectionFlag?: boolean; confirmationPagePath?: StartIdxFlowParams['authorizationCodeFlowOptions']['confirmationPagePath']; }): Promise => { - const { email = '' } = req.body; + const { email = '', firstName, secondName } = req.body; const referrerUrl = new URL(req.body.ref); const referrerPath = referrerUrl.pathname; const emailSentPage = referrerPath.includes('/iframed') @@ -249,6 +249,8 @@ export const oktaIdxApiSignInPasscodeController = async ({ extraData: { flow: 'sign-in-passcode', appLabel: res.locals.appLabel, + firstName, + lastName: secondName, }, }, }); diff --git a/src/server/lib/okta/idx/interact.ts b/src/server/lib/okta/idx/interact.ts index da9a48b7e..b3c1c4dac 100644 --- a/src/server/lib/okta/idx/interact.ts +++ b/src/server/lib/okta/idx/interact.ts @@ -95,6 +95,8 @@ export const interact = async ( codeVerifier, flow: extraData?.flow, appLabel: extraData?.appLabel, + firstName: extraData?.firstName, + lastName: extraData?.lastName, }, ); setAuthorizationStateCookie(authState, res); diff --git a/src/server/lib/okta/openid-connect.ts b/src/server/lib/okta/openid-connect.ts index 78a152104..27d4c7d5d 100644 --- a/src/server/lib/okta/openid-connect.ts +++ b/src/server/lib/okta/openid-connect.ts @@ -50,6 +50,8 @@ export interface AuthorizationState { // used to track the flow the user is in for basic metrics/analytics flow?: UserFlow; // password reset, and email verification flows outside of create account appLabel?: string; // used to track the app used to start the flow + firstName?: string; // used to persist the first name during the signin flow, so that we can set it after login + lastName?: string; // used to persist the last name during the signin flow, so that we can set it after login }; } diff --git a/src/server/routes/oauth.ts b/src/server/routes/oauth.ts index dbfda9d95..446e868ff 100644 --- a/src/server/routes/oauth.ts +++ b/src/server/routes/oauth.ts @@ -426,6 +426,20 @@ const authenticationHandler = async ( } } + // Update the users first and last name if they exist in the authState + if (authState.data?.firstName || authState.data?.lastName) { + try { + await updateUser(sub, { + profile: { + firstName: authState.data.firstName, + lastName: authState.data.lastName, + }, + }); + } catch (error) { + logger.error('OAuth authentication: failed to update user name', error); + } + } + // clear any existing oauth application cookies if they exist checkAndDeleteOAuthTokenCookies(req, res); diff --git a/src/server/routes/register.ts b/src/server/routes/register.ts index 6b8e42a2a..1e9e31e3a 100644 --- a/src/server/routes/register.ts +++ b/src/server/routes/register.ts @@ -368,7 +368,12 @@ const oktaIdxCreateAccountOrSignIn = async ( req: Request, res: ResponseWithRequestState, ) => { - const { email = '', isCombinedSigninAndRegisterFlow = false } = req.body; + const { + email = '', + firstName, + secondName, + isCombinedSigninAndRegisterFlow = false, + } = req.body; const { queryParams: { appClientId, clientId }, @@ -406,6 +411,8 @@ const oktaIdxCreateAccountOrSignIn = async ( extraData: { flow: 'create-account', appLabel: res.locals.appLabel, + firstName, + lastName: secondName, }, }, consents,