Skip to content

Commit

Permalink
refactor(passcodes): update expired functionality for register/reset-…
Browse files Browse the repository at this point in the history
…password

Previously when the passcode had expired, or had been incorrect multiple times, we'd redirect users to the older `/welcome/expired` page for create account (register), and `/reset-password/expired` for reset password.

The problem with these pages is that the copy and UX was more suited to when a link rather than code had expired/invalidated.

Instead we now take the same approach as taken on the Sign In page, where we redirect back to the initial page the user took the action on, but show an error with context saying their code had expired, and to receive a new one instead.
  • Loading branch information
coldlink committed Feb 6, 2025
1 parent 563cdb1 commit 45a9255
Show file tree
Hide file tree
Showing 17 changed files with 237 additions and 40 deletions.
4 changes: 3 additions & 1 deletion cypress/integration/ete/registration_1.2.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,8 @@ describe('Registration flow - Split 1/2', () => {
// attempt 5
cy.get('input[name=code]').type('000000');
cy.contains('Submit verification code').click();
cy.url().should('include', '/welcome/expired');
cy.url().should('include', '/register/email');
cy.contains('Your code has expired');
},
);
});
Expand Down Expand Up @@ -710,6 +711,7 @@ describe('Registration flow - Split 1/2', () => {
cy.get('input[name=code]').type('000000');
cy.contains('Submit verification code').click();
cy.url().should('include', '/register/email');
cy.contains('Your code has expired');
},
);
});
Expand Down
6 changes: 4 additions & 2 deletions cypress/integration/ete/reset_password_passcode.7.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@ describe('Password reset recovery flows - with Passcodes', () => {
// attempt 5 - manual submit
cy.get('input[name=code]').type(`${+code! + 1}`);
cy.contains('Submit one-time code').click();
cy.url().should('include', '/reset-password/expired');
cy.url().should('include', '/reset-password');
cy.contains('Your code has expired');
},
);
});
Expand Down Expand Up @@ -844,7 +845,8 @@ describe('Password reset recovery flows - with Passcodes', () => {
// attempt 5
cy.get('input[name=code]').type('123456');
cy.contains('Submit one-time code').click();
cy.url().should('include', '/reset-password/expired');
cy.url().should('include', '/reset-password');
cy.contains('Your code has expired');
});
});
});
12 changes: 12 additions & 0 deletions src/client/pages/RegisterWithEmail.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Meta } from '@storybook/react';

import { RegisterWithEmail } from '@/client/pages/RegisterWithEmail';
import { RegistrationProps } from '@/client/pages/Registration';
import { PasscodeErrors } from '@/shared/model/Errors';

export default {
title: 'Pages/RegisterWithEmail',
Expand Down Expand Up @@ -111,3 +112,14 @@ export const JobsSite = (args: RegistrationProps) => (
JobsSite.story = {
name: 'with Jobs site',
};

export const WithPasscodeExpiredError = (args: RegistrationProps) => (
<RegisterWithEmail
{...args}
pageError={PasscodeErrors.PASSCODE_EXPIRED}
shortRequestId="123e4567"
/>
);
WithPasscodeExpiredError.story = {
name: 'with defaults',
};
28 changes: 28 additions & 0 deletions src/client/pages/RegisterWithEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,36 @@ import { Divider } from '@guardian/source-development-kitchen/react-components';
import { divider } from '@/client/styles/Shared';
import { MainBodyText } from '@/client/components/MainBodyText';
import ThemedLink from '@/client/components/ThemedLink';
import locations from '@/shared/lib/locations';
import { SUPPORT_EMAIL } from '@/shared/model/Configuration';
import { PasscodeErrors } from '@/shared/model/Errors';

type RegisterWithEmailProps = RegistrationProps & {
geolocation?: GeoLocation;
appName?: AppName;
shortRequestId?: string;
pageError?: string;
};

const getErrorContext = (pageError?: string) => {
if (pageError === PasscodeErrors.PASSCODE_EXPIRED) {
return (
<>
<div>
Please request a new verification code to create your account.
</div>
<br />
<div>
If you are still having trouble, please contact our customer service
team at{' '}
<ThemedLink href={locations.SUPPORT_EMAIL_MAILTO}>
{SUPPORT_EMAIL}
</ThemedLink>
.
</div>
</>
);
}
};

export const RegisterWithEmail = ({
Expand All @@ -29,6 +54,7 @@ export const RegisterWithEmail = ({
geolocation,
appName,
shortRequestId,
pageError,
}: RegisterWithEmailProps) => {
const formTrackingName = 'register';

Expand All @@ -40,6 +66,8 @@ export const RegisterWithEmail = ({
<MinimalLayout
pageHeader="Create your account"
shortRequestId={shortRequestId}
errorContext={getErrorContext(pageError)}
errorOverride={pageError}
>
<MainForm
formAction={buildUrlWithQueryParams('/register', {}, queryParams)}
Expand Down
3 changes: 3 additions & 0 deletions src/client/pages/RegisterWithEmailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ export const RegisterWithEmailPage = () => {
recaptchaConfig,
queryParams,
shortRequestId,
globalMessage = {},
} = clientState;
const { email, formError } = pageData;
const { recaptchaSiteKey } = recaptchaConfig;
const { error: pageError } = globalMessage;

return (
<RegisterWithEmail
Expand All @@ -22,6 +24,7 @@ export const RegisterWithEmailPage = () => {
geolocation={pageData.geolocation}
appName={pageData.appName}
shortRequestId={shortRequestId}
pageError={pageError}
/>
);
};
19 changes: 19 additions & 0 deletions src/client/pages/ResetPassword.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Meta } from '@storybook/react';
import { ResetPassword } from '@/client/pages/ResetPassword';
import { MainBodyText } from '@/client/components/MainBodyText';
import { ResetPasswordSessionExpiredPage } from '@/client/pages/ResetPasswordSessionExpiredPage';
import { PasscodeErrors } from '@/shared/model/Errors';

export default {
title: 'Pages/ResetPassword',
Expand Down Expand Up @@ -84,3 +85,21 @@ export const RecaptchaError = () => (
RecaptchaError.story = {
name: 'with reCAPTCHA error',
};

export const PasscodeExpiredError = () => (
<ResetPassword
shortRequestId="123e4567"
headerText="Reset password"
buttonText="Request password reset"
queryString={{ returnUrl: 'http://theguardian.com' }}
pageError={PasscodeErrors.PASSCODE_EXPIRED}
>
<MainBodyText>
Enter your email address and we’ll send you instructions to reset your
password.
</MainBodyText>
</ResetPassword>
);
PasscodeExpiredError.story = {
name: 'with passcode expired error',
};
40 changes: 27 additions & 13 deletions src/client/pages/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { PropsWithChildren, ReactNode, useState } from 'react';

import React, { PropsWithChildren } from 'react';
import { MinimalLayout } from '@/client/layouts/MinimalLayout';
import { MainForm } from '@/client/components/MainForm';
import { EmailInput } from '@/client/components/EmailInput';
Expand All @@ -16,8 +15,9 @@ import {
import { MainBodyText } from '@/client/components/MainBodyText';
import { divider } from '@/client/styles/Shared';
import { Divider } from '@guardian/source-development-kitchen/react-components';

import { GatewayError } from '@/shared/model/Errors';
import { GatewayError, PasscodeErrors } from '@/shared/model/Errors';
import { SUPPORT_EMAIL } from '@/shared/model/Configuration';
import ThemedLink from '@/client/components/ThemedLink';

interface ResetPasswordProps {
email?: string;
Expand All @@ -32,8 +32,28 @@ interface ResetPasswordProps {
formPageTrackingName?: string;
formError?: GatewayError;
shortRequestId?: string;
pageError?: string;
}

const getErrorContext = (pageError?: string) => {
if (pageError === PasscodeErrors.PASSCODE_EXPIRED) {
return (
<>
<div>Please request a new one-time code to reset your password.</div>
<br />
<div>
If you are still having trouble, please contact our customer service
team at{' '}
<ThemedLink href={locations.SUPPORT_EMAIL_MAILTO}>
{SUPPORT_EMAIL}
</ThemedLink>
.
</div>
</>
);
}
};

export const ResetPassword = ({
email = '',
headerText,
Expand All @@ -48,20 +68,16 @@ export const ResetPassword = ({
formPageTrackingName,
formError,
shortRequestId,
pageError,
}: PropsWithChildren<ResetPasswordProps>) => {
// track page/form load
usePageLoadOphanInteraction(formPageTrackingName);

const [recaptchaErrorMessage, setRecaptchaErrorMessage] = useState('');
const [recaptchaErrorContext, setRecaptchaErrorContext] =
useState<ReactNode>(null);

return (
<MinimalLayout
shortRequestId={shortRequestId}
pageHeader={headerText}
errorContext={recaptchaErrorContext}
errorOverride={recaptchaErrorMessage}
errorContext={getErrorContext(pageError)}
errorOverride={pageError}
>
{children}
<MainForm
Expand All @@ -72,8 +88,6 @@ export const ResetPassword = ({
}
submitButtonText={buttonText}
recaptchaSiteKey={recaptchaSiteKey}
setRecaptchaErrorMessage={setRecaptchaErrorMessage}
setRecaptchaErrorContext={setRecaptchaErrorContext}
formTrackingName={formPageTrackingName}
disableOnSubmit
formErrorMessageFromParent={formError}
Expand Down
3 changes: 3 additions & 0 deletions src/client/pages/ResetPasswordPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export const ResetPasswordPage = () => {
queryParams,
recaptchaConfig,
shortRequestId,
globalMessage = {},
} = clientState;
const { recaptchaSiteKey } = recaptchaConfig;
const { error: pageError } = globalMessage;

return (
<ResetPassword
Expand All @@ -24,6 +26,7 @@ export const ResetPasswordPage = () => {
formPageTrackingName="forgot-password"
showHelpCentreMessage
shortRequestId={shortRequestId}
pageError={pageError}
>
<MainBodyText>
Enter your email address and we’ll send you instructions to reset your
Expand Down
4 changes: 2 additions & 2 deletions src/client/pages/SignIn.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { Meta } from '@storybook/react';

import { SignIn, SignInProps } from '@/client/pages/SignIn';
import { SignInErrors } from '@/shared/model/Errors';
import { PasscodeErrors, SignInErrors } from '@/shared/model/Errors';

export default {
title: 'Pages/SignIn',
Expand Down Expand Up @@ -138,7 +138,7 @@ export const SignInWithPasscodeError = (args: SignInProps) => (
{...{
...args,
usePasscodeSignIn: true,
pageError: SignInErrors.PASSCODE_EXPIRED,
pageError: PasscodeErrors.PASSCODE_EXPIRED,
}}
/>
);
Expand Down
3 changes: 2 additions & 1 deletion src/client/pages/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import {
extractMessage,
GatewayError,
PasscodeErrors,
RegistrationErrors,
SignInErrors,
} from '@/shared/model/Errors';
Expand Down Expand Up @@ -93,7 +94,7 @@ const getErrorContext = (
<a href={locations.SUPPORT_EMAIL_MAILTO}>{SUPPORT_EMAIL}</a>
</>
);
} else if (error === SignInErrors.PASSCODE_EXPIRED) {
} else if (error === PasscodeErrors.PASSCODE_EXPIRED) {
return (
<>
<div>Please request a new one-time code to sign in.</div>
Expand Down
11 changes: 7 additions & 4 deletions src/server/controllers/signInControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ import { changePasswordEmailIdx } from '@/server/controllers/sendChangePasswordE
import {
GatewayError,
GenericErrors,
RegistrationErrors,
PasscodeErrors,
SignInErrors,
} from '@/shared/model/Errors';
import { convertExpiresAtToExpiryTimeInMs } from '@/server/lib/okta/idx/shared/convertExpiresAtToExpiryTimeInMs';
import { handlePasscodeError } from '@/server/lib/okta/idx/shared/errorHandling';
import {
handlePasscodeError,
HandlePasscodeErrorParams,
} from '@/server/lib/okta/idx/shared/errorHandling';
import { validateEmailAndPasswordSetSecurely } from '@/server/lib/okta/validateEmail';
import { UserResponse } from '@/server/models/okta/User';
import {
Expand Down Expand Up @@ -653,7 +656,7 @@ export const oktaIdxApiSubmitPasscodeController = async ({
req: Request;
res: ResponseWithRequestState;
emailSentPage?: Extract<RoutePaths, '/signin/code' | '/register/email-sent'>;
expiredPage?: Extract<RoutePaths, '/signin/code/expired' | '/register/email'>;
expiredPage?: HandlePasscodeErrorParams['expiredPage'];
}) => {
const { code } = req.body;

Expand All @@ -668,7 +671,7 @@ export const oktaIdxApiSubmitPasscodeController = async ({
if (userState === 'NON_EXISTENT') {
throw new OAuthError({
error: 'api.authn.error.PASSCODE_INVALID',
error_description: RegistrationErrors.PASSCODE_INVALID,
error_description: PasscodeErrors.PASSCODE_INVALID,
});
}

Expand Down
13 changes: 9 additions & 4 deletions src/server/lib/okta/idx/shared/errorHandling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ import { mergeRequestState } from '@/server/lib/requestState';
import { ResponseWithRequestState } from '@/server/models/Express';
import { OAuthError } from '@/server/models/okta/Error';
import { addQueryParamsToPath } from '@/shared/lib/queryParams';
import { RegistrationErrors } from '@/shared/model/Errors';
import { PasscodeErrors } from '@/shared/model/Errors';
import { convertExpiresAtToExpiryTimeInMs } from './convertExpiresAtToExpiryTimeInMs';
import { RoutePaths } from '@/shared/model/Routes';

type HandlePasscodeErrorParams = {
export type HandlePasscodeErrorParams = {
error: unknown;
req: Request;
res: ResponseWithRequestState;
emailSentPage: RoutePaths;
expiredPage: RoutePaths;
expiredPage: Extract<
RoutePaths,
| '/register/code/expired'
| '/reset-password/code/expired'
| '/signin/code/expired'
>;
};

/**
Expand Down Expand Up @@ -111,7 +116,7 @@ export const handlePasscodeError = ({
fieldErrors: [
{
field: 'code',
message: RegistrationErrors.PASSCODE_INVALID,
message: PasscodeErrors.PASSCODE_INVALID,
},
],
token: code,
Expand Down
Loading

0 comments on commit 45a9255

Please sign in to comment.