Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
cc45755
WIP
panteliselef Sep 8, 2024
243e846
useAssurance internally
panteliselef Sep 9, 2024
2b186f6
minor cleanup
panteliselef Sep 9, 2024
0bef2f6
fix tests
panteliselef Sep 9, 2024
ec63553
remove search params
panteliselef Sep 10, 2024
4d4eeca
Merge branch 'refs/heads/main' into elef/user-665-display-userverific…
panteliselef Sep 10, 2024
1a6446a
Merge branch 'refs/heads/main' into elef/user-665-display-userverific…
panteliselef Sep 16, 2024
374146a
refactor
panteliselef Sep 16, 2024
678a2c6
clean up assurance
panteliselef Sep 16, 2024
85c5cd4
Merge branch 'refs/heads/main' into elef/user-665-display-userverific…
panteliselef Sep 16, 2024
9ae4a62
fix localizations
panteliselef Sep 16, 2024
f025a26
add changeset
panteliselef Sep 16, 2024
274ef1d
update bundlewatch.config.json
panteliselef Sep 16, 2024
0b02ad4
Merge branch 'refs/heads/main' into elef/user-665-display-userverific…
panteliselef Sep 16, 2024
68d078a
update localization
panteliselef Sep 16, 2024
319fab0
update changelog
panteliselef Sep 16, 2024
f5099c8
Merge branch 'main' into elef/user-665-display-userverification-for-s…
panteliselef Sep 16, 2024
fec40a1
remove portals from all menus/select components
panteliselef Sep 18, 2024
3d7a9c5
Merge branch 'main' into elef/user-665-display-userverification-for-s…
panteliselef Sep 18, 2024
4d5a871
restore portals and drop zIndex
panteliselef Sep 18, 2024
063509a
avoid using arbitrary zIndexes
panteliselef Sep 18, 2024
8280af9
Revert "avoid using arbitrary zIndexes"
panteliselef Sep 18, 2024
df6f3a1
Revert "restore portals and drop zIndex"
panteliselef Sep 18, 2024
8f6a7d2
Revert "remove portals from all menus/select components"
panteliselef Sep 18, 2024
8ea72f8
revert changes to indexes and use isolate instead
panteliselef Sep 18, 2024
942ae04
address review comments
panteliselef Sep 26, 2024
3593cf2
Merge branch 'main' into elef/user-665-display-userverification-for-s…
panteliselef Sep 26, 2024
7d6f7c5
remove comments
panteliselef Sep 27, 2024
be1abf5
Merge branch 'main' into elef/user-665-display-userverification-for-s…
panteliselef Sep 27, 2024
acd9f53
Merge branch 'refs/heads/main' into elef/user-665-display-userverific…
panteliselef Oct 1, 2024
d52fb92
fix "broken" back button
panteliselef Oct 1, 2024
f7dad10
Merge branch 'main' into elef/user-665-display-userverification-for-s…
panteliselef Oct 1, 2024
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
31 changes: 31 additions & 0 deletions .changeset/four-oranges-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@clerk/clerk-js": minor
"@clerk/types": minor
---

*Experimental Feature*: `<UserProfile/>` allows users to update their information. Mostly of this information is considered sensitive data.
We want to ensure that only the users themselves can alter any sensitive data.

To increase security we are now, require users to re-verify their credentials when they are about to perform these actions:


| Operation | Reverification | Strategy | Timeframe |
| --- |----------------| --- | --- |
| Update account (first/last name) | ❌ | | |
| Update username | ✅ | Strongest available | 10m |
| Delete account | ✅ | Strongest available | 10m |
| Create/Remove profile image | ❌ | | |
| Update password | ✅ | Strongest available | 10m |
| Remove password | ❌ | | |
| Revoke session | ✅ | Strongest available | 10m |
| Create identification | ✅ | Strongest available | 10m |
| Remove identification | ✅ | Strongest available | 10m |
| Change primary identification | ✅ | Strongest available | 10m |
| Update Passkey name | ❌ | | |
| Enable MFA (TOTP, Phone number) | ✅ | Strongest available | 10m |
| Disable MFA (TOΤP, Phone number) | ✅ | Strongest available | 10m |
| Create/Regenerate Backup Codes | ✅ | Strongest available | 10m |
| Connect External Account | ✅ | Strongest available | 10m |
| Re-authorize External Account | ❌ | | |
| Remove External Account | ✅ | Strongest available | 10m |
| Leave organization | ❌ | | |
Comment on lines +12 to +31
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this table also going into our docs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually, yes we currently have it in our DX gudie

3 changes: 3 additions & 0 deletions packages/clerk-js/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { TextDecoder, TextEncoder } from 'node:util';

import { jest } from '@jest/globals';

class FakeResponse {}

if (typeof window !== 'undefined') {
Object.defineProperties(globalThis, {
TextDecoder: { value: TextDecoder },
TextEncoder: { value: TextEncoder },
Response: { value: FakeResponse },
crypto: { value: crypto.webcrypto },
});

Expand Down
49 changes: 46 additions & 3 deletions packages/clerk-js/src/ui/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ export type ComponentControls = {
| 'organizationProfile'
| 'createOrganization'
| 'userVerification',
options?: {
notify?: boolean;
},
Comment on lines +87 to +89
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only internal

) => void;
// Special case, as the impersonation fab mounts automatically
mountImpersonationFab: () => void;
Expand Down Expand Up @@ -245,13 +248,53 @@ const Components = (props: ComponentsProps) => {
setState(s => ({ ...s, ...restProps, options: { ...s.options, ...restProps.options } }));
};

componentsControls.closeModal = name => {
componentsControls.closeModal = (name, options = {}) => {
const { notify = true } = options;
clearUrlStateParam();
setState(s => ({ ...s, [name + 'Modal']: null }));
setState(s => {
function handleCloseModalForExperimentalUserVerification() {
const modal = s[`${name}Modal`] || {};
if ('afterVerificationCancelled' in modal && notify) {
modal.afterVerificationCancelled?.();
}
}

/**
* We need this in order for `Clerk.__experimental_closeUserVerification()`
* to properly trigger the previously defined `afterVerificationCancelled` callback
*/
handleCloseModalForExperimentalUserVerification();

return { ...s, [`${name}Modal`]: null };
});
};

componentsControls.openModal = (name, props) => {
setState(s => ({ ...s, [name + 'Modal']: props }));
function handleCloseModalForExperimentalUserVerification() {
if (!('afterVerificationCancelled' in props)) {
return;
}

setState(s => ({
...s,
[`${name}Modal`]: {
...props,
/**
* When a UserVerification flow is completed, we need to close the modal without trigger a cancellation callback
*/
afterVerification() {
props.afterVerification?.();
componentsControls.closeModal(name, { notify: false });
},
},
}));
}

if ('afterVerificationCancelled' in props) {
handleCloseModalForExperimentalUserVerification();
} else {
setState(s => ({ ...s, [`${name}Modal`]: props }));
}
};

componentsControls.mountImpersonationFab = () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/clerk-js/src/ui/common/RemoveResourceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { localizationKeys, Text } from '../customizables';
import type { FormProps } from '../elements';
import { Form, FormButtons, FormContainer, useCardState, withCardStateProvider } from '../elements';
import { useAssurance } from '../hooks/useAssurance';
import type { LocalizationKey } from '../localization';
import { handleError } from '../utils';

Expand All @@ -15,10 +16,11 @@ type RemoveFormProps = FormProps & {
export const RemoveResourceForm = withCardStateProvider((props: RemoveFormProps) => {
const { title, messageLine1, messageLine2, deleteResource, onSuccess, onReset } = props;
const card = useCardState();
const { handleAssurance } = useAssurance();

const handleSubmit = async () => {
try {
await deleteResource().then(onSuccess);
await handleAssurance(deleteResource).then(onSuccess);
} catch (e) {
handleError(e, [], card.setError);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export const AccountPage = withCardStateProvider(() => {
{showEmail && <EmailsSection shouldAllowCreation={shouldAllowIdentificationCreation} />}
{showPhone && <PhoneSection shouldAllowCreation={shouldAllowIdentificationCreation} />}
{showConnectedAccounts && <ConnectedAccountsSection shouldAllowCreation={shouldAllowIdentificationCreation} />}

{/*TODO-STEP-UP: Verify that these work as expected*/}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these verified they work? 😄

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, although thorough testing will still need to be done. E2E tests will also be introduced before this goes to public beta

{showSamlAccounts && <EnterpriseAccountsSection />}
{showWeb3 && <Web3Section shouldAllowCreation={shouldAllowIdentificationCreation} />}
</Col>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { SessionWithActivitiesResource } from '@clerk/types';
import { Badge, Col, descriptors, Flex, Icon, localizationKeys, Text, useLocalizations } from '../../customizables';
import { FullHeightLoader, ProfileSection, ThreeDotsMenu } from '../../elements';
import { useFetch, useLoadingStatus } from '../../hooks';
import { useAssurance } from '../../hooks/useAssurance';
import { DeviceLaptop, DeviceMobile } from '../../icons';
import { mqu, type PropsOfComponent } from '../../styledSystem';
import { getRelativeToNowDateKey } from '../../utils';
Expand Down Expand Up @@ -48,12 +49,19 @@ export const ActiveDevicesSection = () => {
const DeviceItem = ({ session }: { session: SessionWithActivitiesResource }) => {
const isCurrent = useSession().session?.id === session.id;
const status = useLoadingStatus();
const { handleAssurance } = useAssurance();

const revoke = async () => {
if (isCurrent || !session) {
return;
}
status.setLoading();
return session.revoke().finally(() => status.setIdle());
return (
handleAssurance(() => session.revoke())
// TODO-STEPUP: Properly handler the response with a setCardError
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@panteliselef is this to-do for later or it's forgotten?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this is not step-up specific, just something that i discovered (for later)

.catch(() => {})
.finally(() => status.setIdle())
);
};

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isClerkRuntimeError } from '@clerk/shared';
import { useUser } from '@clerk/shared/react';
import type { TOTPResource } from '@clerk/types';
import React from 'react';
Expand All @@ -14,6 +15,8 @@ import {
useCardState,
withCardStateProvider,
} from '../../elements';
import { useActionContext } from '../../elements/Action/ActionRoot';
import { useAssurance } from '../../hooks/useAssurance';
import { handleError } from '../../utils';

type AddAuthenticatorAppProps = FormProps & {
Expand All @@ -26,16 +29,26 @@ export const AddAuthenticatorApp = withCardStateProvider((props: AddAuthenticato
const { title, onSuccess, onReset } = props;
const { user } = useUser();
const card = useCardState();
const { handleAssurance } = useAssurance();
const { close } = useActionContext();
const [totp, setTOTP] = React.useState<TOTPResource | undefined>(undefined);
const [displayFormat, setDisplayFormat] = React.useState<DisplayFormat>('qr');

// TODO: React18
// Non-idempotent useEffect
React.useEffect(() => {
void user
?.createTOTP()
if (!user) {
return;
}

void handleAssurance(user.createTOTP)
.then((totp: TOTPResource) => setTOTP(totp))
.catch(err => handleError(err, [], card.setError));
.catch(err => {
if (isClerkRuntimeError(err) && err.code === 'assurance_cancelled') {
return close();
}
return handleError(err, [], card.setError);
});
}, []);

if (card.error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useUserProfileContext } from '../../contexts';
import { descriptors, Image, localizationKeys } from '../../customizables';
import { ProfileSection, useCardState } from '../../elements';
import { useEnabledThirdPartyProviders } from '../../hooks';
import { useAssurance } from '../../hooks/useAssurance';
import { useRouter } from '../../router';
import { handleError, sleep } from '../../utils';

Expand All @@ -16,6 +17,8 @@ export const AddConnectedAccount = () => {
const { navigate } = useRouter();
const { strategies, strategyToDisplayData } = useEnabledThirdPartyProviders();
const { additionalOAuthScopes, componentName, mode } = useUserProfileContext();

const { handleAssurance } = useAssurance();
const isModal = mode === 'modal';

const enabledStrategies = strategies.filter(s => s.startsWith('oauth')) as OAuthStrategy[];
Expand All @@ -26,6 +29,9 @@ export const AddConnectedAccount = () => {
});

const connect = (strategy: OAuthStrategy) => {
if (!user) {
return;
}
const socialProvider = strategy.replace('oauth_', '') as OAuthProvider;
const redirectUrl = isModal
? appendModalState({ url: window.location.href, componentName, socialProvider: socialProvider })
Expand All @@ -35,12 +41,13 @@ export const AddConnectedAccount = () => {
// TODO: Decide if we should keep using this strategy
// If yes, refactor and cleanup:
card.setLoading(strategy);
user
?.createExternalAccount({
return handleAssurance(() =>
user.createExternalAccount({
strategy,
redirectUrl,
additionalScopes,
})
}),
)
.then(res => {
if (res.verification?.externalVerificationRedirectURL) {
void sleep(2000).then(() => card.setIdle(strategy));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Card, ProfileSection, ThreeDotsMenu, useCardState, withCardStateProvide
import { Action } from '../../elements/Action';
import { useActionContext } from '../../elements/Action/ActionRoot';
import { useEnabledThirdPartyProviders } from '../../hooks';
import { useAssurance } from '../../hooks/useAssurance';
import { useRouter } from '../../router';
import type { PropsOfComponent } from '../../styledSystem';
import { handleError } from '../../utils';
Expand Down Expand Up @@ -89,6 +90,7 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) =>
const { navigate } = useRouter();
const { user } = useUser();
const card = useCardState();
const { handleAssurance } = useAssurance();

if (!user) {
return null;
Expand All @@ -115,11 +117,13 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) =>
if (reauthorizationRequired) {
response = await account.reauthorize({ additionalScopes, redirectUrl });
} else {
response = await user.createExternalAccount({
strategy: account.verification!.strategy as OAuthStrategy,
redirectUrl,
additionalScopes,
});
response = await handleAssurance(() =>
user.createExternalAccount({
strategy: account.verification!.strategy as OAuthStrategy,
redirectUrl,
additionalScopes,
}),
);
}

await navigate(response.verification!.externalVerificationRedirectURL?.href || '');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSignOutContext } from '../../contexts';
import { Col, localizationKeys, Text, useLocalizations } from '../../customizables';
import type { FormProps } from '../../elements';
import { Form, FormButtons, FormContainer, useCardState, withCardStateProvider } from '../../elements';
import { useAssurance } from '../../hooks/useAssurance';
import { useMultipleSessions } from '../../hooks/useMultipleSessions';
import { handleError, useFormControl } from '../../utils';

Expand All @@ -16,6 +17,7 @@ export const DeleteUserForm = withCardStateProvider((props: DeleteUserFormProps)
const { t } = useLocalizations();
const { otherSessions } = useMultipleSessions({ user });
const { setActive } = useClerk();
const { handleAssurance } = useAssurance();

const confirmationField = useFormControl('deleteConfirmation', '', {
type: 'text',
Expand All @@ -38,8 +40,7 @@ export const DeleteUserForm = withCardStateProvider((props: DeleteUserFormProps)
throw Error('user is not defined');
}

await user.delete();
// TODO: Investigate if we need to call `setActive` with {session: null}
await handleAssurance(user.delete);
const navigationCallback =
otherSessions.length === 0 ? navigateAfterSignOut : navigateAfterMultiSessionSingleSignOutUrl;
return await setActive({
Expand Down
8 changes: 6 additions & 2 deletions packages/clerk-js/src/ui/components/UserProfile/EmailForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useEnvironment } from '../../contexts';
import { localizationKeys } from '../../customizables';
import type { FormProps } from '../../elements';
import { Form, FormButtons, FormContainer, useCardState, withCardStateProvider } from '../../elements';
import { useAssurance } from '../../hooks/useAssurance';
import { handleError, useFormControl } from '../../utils';
import { emailLinksEnabledForInstance } from './utils';
import { VerifyWithCode } from './VerifyWithCode';
Expand All @@ -19,6 +20,7 @@ export const EmailForm = withCardStateProvider((props: EmailFormProps) => {
const { emailId: id, onSuccess, onReset } = props;
const card = useCardState();
const { user } = useUser();
const { handleAssurance } = useAssurance();
const environment = useEnvironment();
const preferEmailLinks = emailLinksEnabledForInstance(environment);

Expand All @@ -39,8 +41,10 @@ export const EmailForm = withCardStateProvider((props: EmailFormProps) => {

const addEmail = async (e: React.FormEvent) => {
e.preventDefault();
return user
?.createEmailAddress({ email: emailField.value })
if (!user) {
return;
}
return handleAssurance(() => user.createEmailAddress({ email: emailField.value }))
.then(res => {
emailAddressRef.current = res;
wizard.nextStep();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
useCardState,
withCardStateProvider,
} from '../../elements';
import { useAssurance } from '../../hooks/useAssurance';
import { handleError } from '../../utils';
import { MfaBackupCodeList } from './MfaBackupCodeList';

Expand All @@ -19,15 +20,15 @@ export const MfaBackupCodeCreateForm = withCardStateProvider((props: MfaBackupCo
const { onSuccess } = props;
const { user } = useUser();
const card = useCardState();
const { handleAssurance } = useAssurance();
const [backupCode, setBackupCode] = React.useState<BackupCodeResource | undefined>(undefined);

React.useEffect(() => {
if (backupCode) {
if (backupCode || !user) {
return;
}

void user
?.createBackupCode()
void handleAssurance(user.createBackupCode)
.then((backupCode: BackupCodeResource) => setBackupCode(backupCode))
.catch(err => handleError(err, [], card.setError));
}, []);
Expand Down
Loading