Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/reverification-reset-breached-password.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@clerk/shared': minor
'@clerk/clerk-js': minor
'@clerk/ui': minor
---

Let users whose password was found in a data breach pass reverification by resetting it. Until now the reverification modal only showed the breached-password error, so these users couldn't make any change that needs reverification, including changing that password. The modal now offers "Reset your password": the user sets a new password and enters a code sent to their email or phone, as in sign-in. Their other verification methods stay available.

From UserProfile's change password form, the modal reuses the new password the user already entered, so they only enter the code.

`session.prepareFirstFactorVerification()` and `session.attemptFirstFactorVerification()` accept the `reset_password_email_code` and `reset_password_phone_code` strategies, so custom flows can build the same reset.
6 changes: 6 additions & 0 deletions packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ export class Session extends BaseResource implements SessionResource {
default: factor.default,
} as PhoneCodeConfig;
break;
case 'reset_password_email_code':
config = { emailAddressId: factor.emailAddressId };
break;
case 'reset_password_phone_code':
config = { phoneNumberId: factor.phoneNumberId };
break;
case 'passkey':
config = {};
break;
Expand Down
76 changes: 76 additions & 0 deletions packages/clerk-js/src/core/resources/__tests__/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,82 @@ describe('Session', () => {
});
});

describe('reset password reverification', () => {
const sessionJSON = {
status: 'active',
id: 'session_1',
object: 'session',
user: createUser({}),
last_active_organization_id: null,
actor: null,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
} as SessionJSON;

const verificationResponse = {
object: 'session_verification',
id: 'sessver_1',
status: 'needs_first_factor',
level: 'first_factor',
session: sessionJSON,
first_factor_verification: null,
second_factor_verification: null,
supported_first_factors: null,
supported_second_factors: null,
};

let requestSpy: Mock;

beforeEach(() => {
BaseResource.clerk = clerkMock();
requestSpy = BaseResource.clerk.getFapiClient().request as Mock;
requestSpy.mockResolvedValue({ payload: { response: verificationResponse }, status: 200 });
});

afterEach(() => {
BaseResource.clerk = null as any;
});

it.each([
[{ strategy: 'reset_password_email_code', emailAddressId: 'idn_1' }],
[{ strategy: 'reset_password_phone_code', phoneNumberId: 'idn_2' }],
] as const)('prepares a first factor with %o', async factor => {
const session = new Session(sessionJSON);

await session.prepareFirstFactorVerification(factor);

expect(requestSpy).toHaveBeenCalledWith(
expect.objectContaining({
body: factor,
method: 'POST',
path: '/client/sessions/session_1/verify/prepare_first_factor',
}),
expect.anything(),
);
});

it('attempts a first factor with the code and the new password', async () => {
const session = new Session(sessionJSON);
const attempt = {
strategy: 'reset_password_email_code',
code: '123456',
password: 'a-much-better-password-123',
signOutOfOtherSessions: true,
} as const;

await session.attemptFirstFactorVerification(attempt);

expect(requestSpy).toHaveBeenCalledWith(
expect.objectContaining({
body: attempt,
method: 'POST',
path: '/client/sessions/session_1/verify/attempt_first_factor',
}),
expect.anything(),
);
});
});

describe('trusted device reverification', () => {
const sessionJSON = {
status: 'active',
Expand Down
16 changes: 15 additions & 1 deletion packages/shared/src/types/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1972,7 +1972,7 @@ export type __internal_UserVerificationProps = RoutingOptions & {
/**
* Non-awaitable callback for when verification is completed successfully
*/
afterVerification?: () => void;
afterVerification?: (result?: __internal_UserVerificationResult) => void;

/**
* Non-awaitable callback for when verification is cancelled, (i.e modal is closed)
Expand All @@ -1987,12 +1987,26 @@ export type __internal_UserVerificationProps = RoutingOptions & {
*/
level?: SessionVerificationLevel;

/**
* A new password the caller already collected. When the user resets their
* password to verify, it's used instead of asking for one.
*/
resetPassword?: {
password: string;
signOutOfOtherSessions: boolean;
};

/**
* Customization options to fully match the Clerk components to your own brand. These options serve as overrides and will be merged with the global `appearance` configuration (if one is provided). See the [`Appearance`](https://clerk.com/docs/guides/customizing-clerk/appearance-prop/overview) docs for more information.
*/
appearance?: ClerkAppearanceTheme;
};

export type __internal_UserVerificationResult = {
/** True when the user reset their password to verify. */
passwordReset?: boolean;
};

export type __internal_UserVerificationModalProps = WithoutRouting<__internal_UserVerificationProps>;

export type ProtectCheckFlow = 'signIn' | 'signUp';
Expand Down
27 changes: 27 additions & 0 deletions packages/shared/src/types/factors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,21 +128,25 @@ export type EnterpriseSSOFactor = {
strategy: EnterpriseSSOStrategy;
/**
* The ID of the enterprise connection.
*
* @experimental
*/
enterpriseConnectionId?: string;
/**
* The name of the enterprise connection.
*
* @experimental
*/
enterpriseConnectionName?: string;
/**
* The public URL of the enterprise connection logo.
*
* @experimental
*/
enterpriseConnectionLogoPublicUrl?: string | null;
/**
* The provider of the enterprise connection.
*
* @experimental
*/
enterpriseConnectionProvider?: string;
Expand Down Expand Up @@ -255,11 +259,13 @@ export type EnterpriseSSOConfig = EnterpriseSSOFactor & {
oidcPrompt?: string;
/**
* The ID of the email address used for the enterprise SSO factor.
*
* @experimental
*/
emailAddressId?: string;
/**
* The ID of the enterprise connection used for the enterprise SSO factor.
*
* @experimental
*/
enterpriseConnectionId?: string;
Expand Down Expand Up @@ -442,3 +448,24 @@ export type ResetPasswordEmailCodeAttempt = {
*/
password?: string;
};

/** @generateWithEmptyComment */
export type SessionVerifyResetPasswordAttempt = {
/**
* The strategy type.
*/
strategy: ResetPasswordEmailCodeStrategy | ResetPasswordPhoneCodeStrategy;
/**
* The one-time code sent to the user's email address or phone number.
*/
code: string;
/**
* The new password, which replaces the current one. Unlike a sign-in's reset
* attempt, it's required: the code only counts together with it.
*/
password: string;
/**
* Whether to sign the user out of their other sessions once the new password is set.
*/
signOutOfOtherSessions?: boolean;
};
17 changes: 17 additions & 0 deletions packages/shared/src/types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import type {
PhoneCodeAttempt,
PhoneCodeConfig,
PhoneCodeSecondFactorConfig,
ResetPasswordEmailCodeFactorConfig,
ResetPasswordPhoneCodeFactorConfig,
SessionVerifyResetPasswordAttempt,
TOTPAttempt,
TrustedDeviceAttempt,
TrustedDeviceConfig,
Expand Down Expand Up @@ -291,6 +294,7 @@ export interface SessionResource extends ClerkResource {
getToken: GetToken;
/**
* Checks if the user is [authorized for the specified Role, Permission, Feature, or Plan](https://clerk.com/docs/guides/secure/authorization-checks) or requires the user to [reverify their credentials](https://clerk.com/docs/guides/secure/reverification) if their last verification is older than allowed.
*
* @skipParametersSection
*/
checkAuthorization: CheckAuthorization;
Expand All @@ -308,36 +312,45 @@ export interface SessionResource extends ClerkResource {
updatedAt: Date;
/**
* Initiates the reverification flow.
*
* @returns A [`SessionVerification`](https://clerk.com/docs/reference/types/session-verification) instance with its status and supported factors.
*/
startVerification: (params: SessionVerifyCreateParams) => Promise<SessionVerificationResource>;
/**
* Initiates the [first factor verification](!first-factor-verification) process. This is a required step to complete a reverification flow when using a preparable factor.
*
* @returns A [`SessionVerification`](https://clerk.com/docs/reference/types/session-verification) instance with its status and supported factors.
*
* @skipParametersSection
*/
prepareFirstFactorVerification: (
factor: SessionVerifyPrepareFirstFactorParams,
) => Promise<SessionVerificationResource>;
/**
* Attempts to complete the [first factor verification](!first-factor-verification) process.
*
* @returns A [`SessionVerification`](https://clerk.com/docs/reference/types/session-verification) instance with its status and supported factors.
*
* @skipParametersSection
*/
attemptFirstFactorVerification: (
attemptFactor: SessionVerifyAttemptFirstFactorParams,
) => Promise<SessionVerificationResource>;
/**
* Initiates the [second factor verification](!second-factor-verification) process. This is a required step to complete a reverification flow when using a preparable factor.
*
* @returns A [`SessionVerification`](https://clerk.com/docs/reference/types/session-verification) instance with its status and supported factors.
*
* @skipParametersSection
*/
prepareSecondFactorVerification: (
params: SessionVerifyPrepareSecondFactorParams,
) => Promise<SessionVerificationResource>;
/**
* Attempts to complete the [second factor verification](!second-factor-verification) process.
*
* @returns A [`SessionVerification`](https://clerk.com/docs/reference/types/session-verification) instance with its status and supported factors.
*
* @skipParametersSection
*/
attemptSecondFactorVerification: (
Expand All @@ -351,6 +364,7 @@ export interface SessionResource extends ClerkResource {
* in-progress multi-factor reverification instead. Any other `level`
* value is rejected. Throws a `ClerkWebAuthnError` when WebAuthn is
* unsupported or the passkey ceremony fails.
*
* @returns A [`SessionVerification`](https://clerk.com/docs/reference/types/session-verification) instance with its status and supported factors.
*/
verifyWithPasskey: (params?: SessionVerifyWithPasskeyParams) => Promise<SessionVerificationResource>;
Expand Down Expand Up @@ -537,6 +551,8 @@ export type SessionVerifyPrepareFirstFactorParams =
| EmailCodeConfig
| PhoneCodeConfig
| PassKeyConfig
| ResetPasswordEmailCodeFactorConfig
| ResetPasswordPhoneCodeFactorConfig
/**
* @experimental
*/
Expand All @@ -551,6 +567,7 @@ export type SessionVerifyAttemptFirstFactorParams =
| PhoneCodeAttempt
| PasswordAttempt
| PasskeyAttempt
| SessionVerifyResetPasswordAttempt
/**
* @experimental
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/types/sessionVerification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type {
PasskeyFactor,
PasswordFactor,
PhoneCodeFactor,
ResetPasswordEmailCodeFactor,
ResetPasswordPhoneCodeFactor,
TOTPFactor,
} from './factors';
import type { ClerkResource } from './resource';
Expand Down Expand Up @@ -55,6 +57,8 @@ export type SessionVerificationFirstFactor =
| PhoneCodeFactor
| PasswordFactor
| PasskeyFactor
| ResetPasswordEmailCodeFactor
| ResetPasswordPhoneCodeFactor
/**
* @experimental
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{ "path": "./dist/ui.shared.browser.js", "maxSize": "44KB" },
{ "path": "./dist/framework*.js", "maxSize": "44KB" },
{ "path": "./dist/vendors*.js", "maxSize": "73KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "139KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "140KB" },
{ "path": "./dist/signin*.js", "maxSize": "19KB" },
{ "path": "./dist/signup*.js", "maxSize": "13KB" },
{ "path": "./dist/userprofile*.js", "maxSize": "16KB" },
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/src/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
__internal_ProtectCheckModalProps,
__internal_SubscriptionDetailsProps,
__internal_UserVerificationProps,
__internal_UserVerificationResult,
Clerk,
ClerkOptions,
CreateOrganizationModalProps,
Expand Down Expand Up @@ -468,8 +469,8 @@ const Components = (props: ComponentsProps) => {
/**
* When a UserVerification flow is completed, we need to close the modal without trigger a cancellation callback
*/
afterVerification() {
props.afterVerification?.();
afterVerification(result?: __internal_UserVerificationResult) {
props.afterVerification?.(result);
componentsControls.closeModal(name, { notify: false });
},
},
Expand Down
Loading
Loading