diff --git a/.changeset/rotten-baths-wish.md b/.changeset/rotten-baths-wish.md new file mode 100644 index 00000000000..82eb2fa0ee8 --- /dev/null +++ b/.changeset/rotten-baths-wish.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Hide passkeys section when user has an enterprise account with the disable additional identifiers setting enabled diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 271d9cd5159..043117dcec0 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,10 +1,10 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "626.1KB" }, + { "path": "./dist/clerk.js", "maxSize": "629KB" }, { "path": "./dist/clerk.browser.js", "maxSize": "78KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "119KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "61KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "114.1KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "117.1KB" }, { "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" }, { "path": "./dist/vendors*.js", "maxSize": "41KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, diff --git a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx index e7e5a455b7f..087a0e245b4 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx @@ -4,7 +4,7 @@ import { Card } from '@/ui/elements/Card'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; import { Header } from '@/ui/elements/Header'; -import { useEnvironment } from '../../contexts'; +import { useEnvironment, useUserProfileContext } from '../../contexts'; import { Col, descriptors, localizationKeys } from '../../customizables'; import { ConnectedAccountsSection } from './ConnectedAccountsSection'; import { EmailsSection } from './EmailsSection'; @@ -18,6 +18,7 @@ export const AccountPage = withCardStateProvider(() => { const { attributes, social, enterpriseSSO } = useEnvironment().userSettings; const card = useCardState(); const { user } = useUser(); + const { shouldAllowIdentificationCreation } = useUserProfileContext(); const showUsername = attributes.username?.enabled; const showEmail = attributes.email_address?.enabled; @@ -26,13 +27,6 @@ export const AccountPage = withCardStateProvider(() => { const showEnterpriseAccounts = user && enterpriseSSO.enabled; const showWeb3 = attributes.web3_wallet?.enabled; - const shouldAllowIdentificationCreation = - !showEnterpriseAccounts || - !user.enterpriseAccounts.some( - enterpriseAccount => - enterpriseAccount.active && enterpriseAccount.enterpriseConnection?.disableAdditionalIdentifications, - ); - return ( { const { attributes, instanceIsPasswordBased } = useEnvironment().userSettings; const card = useCardState(); const { user } = useUser(); + const { shouldAllowIdentificationCreation } = useUserProfileContext(); const showPassword = instanceIsPasswordBased; - const showPasskey = attributes.passkey?.enabled; + const showPasskey = attributes.passkey?.enabled && shouldAllowIdentificationCreation; const showMfa = getSecondFactors(attributes).length > 0; const showDelete = user?.deleteSelfEnabled; diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx index f242101e542..a06a8a738c1 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx @@ -70,7 +70,6 @@ describe('SecurityPage', () => { last_used_at: Date.now(), verification: null, updated_at: Date.now(), - credential_id: 'some_id', }, ], }); @@ -85,6 +84,195 @@ describe('SecurityPage', () => { getByText(/^Last used:/); }); + describe('Passkeys section visibility', () => { + describe('with active enterprise connection', () => { + it('shows the passkeys section when passkeys are enabled', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withEmailAddress(); + f.withEnterpriseSso(); + f.withPasskey(); + f.withUser({ + email_addresses: ['test@clerk.com'], + passkeys: [ + { + object: 'passkey', + id: '1234', + name: 'Chrome on Mac', + created_at: Date.now(), + last_used_at: Date.now(), + verification: null, + updated_at: Date.now(), + }, + ], + enterprise_accounts: [ + { + object: 'enterprise_account', + active: true, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'saml_okta', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'Okta Workforce', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: false, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', + }, + ], + }); + }); + fixtures.clerk.user?.getSessions.mockReturnValue(Promise.resolve([])); + + render(, { wrapper }); + await waitFor(() => expect(fixtures.clerk.user?.getSessions).toHaveBeenCalled()); + screen.getByText('Passkeys'); + screen.getByText('Chrome on Mac'); + }); + + it('hides the passkeys section when disable_additional_identifications is true', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withEmailAddress(); + f.withEnterpriseSso(); + f.withPasskey(); + f.withUser({ + email_addresses: ['test@clerk.com'], + passkeys: [ + { + object: 'passkey', + id: '1234', + name: 'Chrome on Mac', + created_at: Date.now(), + last_used_at: Date.now(), + verification: null, + updated_at: Date.now(), + }, + ], + enterprise_accounts: [ + { + object: 'enterprise_account', + active: true, + first_name: 'Laura', + last_name: 'Serafim', + protocol: 'saml', + provider_user_id: null, + public_metadata: {}, + email_address: 'test@clerk.com', + provider: 'saml_okta', + enterprise_connection: { + object: 'enterprise_connection', + provider: 'saml_okta', + name: 'Okta Workforce', + id: 'ent_123', + active: true, + allow_idp_initiated: false, + allow_subdomains: false, + disable_additional_identifications: true, + sync_user_attributes: false, + domain: 'foocorp.com', + created_at: 123, + updated_at: 123, + logo_public_url: null, + protocol: 'saml', + }, + verification: { + status: 'verified', + strategy: 'saml', + verified_at_client: 'foo', + attempts: 0, + error: { + code: 'identifier_already_signed_in', + long_message: "You're already signed in", + message: "You're already signed in", + }, + expire_at: 123, + id: 'ver_123', + object: 'verification', + }, + id: 'eac_123', + }, + ], + }); + }); + fixtures.clerk.user?.getSessions.mockReturnValue(Promise.resolve([])); + + render(, { wrapper }); + await waitFor(() => expect(fixtures.clerk.user?.getSessions).toHaveBeenCalled()); + expect(screen.queryByText('Passkeys')).not.toBeInTheDocument(); + expect(screen.queryByText('Chrome on Mac')).not.toBeInTheDocument(); + }); + }); + + describe('without enterprise connection', () => { + it('shows the passkeys section when passkeys are enabled', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withPasskey(); + f.withUser({ + email_addresses: ['test@clerk.com'], + passkeys: [ + { + object: 'passkey', + id: '1234', + name: 'Chrome on Mac', + created_at: Date.now(), + last_used_at: Date.now(), + verification: null, + updated_at: Date.now(), + }, + ], + }); + }); + fixtures.clerk.user?.getSessions.mockReturnValue(Promise.resolve([])); + + render(, { wrapper }); + await waitFor(() => expect(fixtures.clerk.user?.getSessions).toHaveBeenCalled()); + screen.getByText('Passkeys'); + screen.getByText('Chrome on Mac'); + }); + + it('hides the passkeys section when passkeys are not enabled', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ + email_addresses: ['test@clerk.com'], + }); + }); + fixtures.clerk.user?.getSessions.mockReturnValue(Promise.resolve([])); + + render(, { wrapper }); + await waitFor(() => expect(fixtures.clerk.user?.getSessions).toHaveBeenCalled()); + expect(screen.queryByText('Passkeys')).not.toBeInTheDocument(); + }); + }); + }); + it('shows the active devices of the user and has appropriate buttons', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withSocialProvider({ provider: 'google' }); diff --git a/packages/clerk-js/src/ui/contexts/components/UserProfile.ts b/packages/clerk-js/src/ui/contexts/components/UserProfile.ts index fec7de5a4c4..9ed47562e21 100644 --- a/packages/clerk-js/src/ui/contexts/components/UserProfile.ts +++ b/packages/clerk-js/src/ui/contexts/components/UserProfile.ts @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/shared/react'; +import { useClerk, useUser } from '@clerk/shared/react'; import { createContext, useContext, useMemo } from 'react'; import type { NavbarRoute } from '@/ui/elements/Navbar'; @@ -20,6 +20,7 @@ export type UserProfileContextType = UserProfileCtx & { queryParams: ParsedQueryString; authQueryString: string | null; pages: PagesType; + shouldAllowIdentificationCreation: boolean; }; export const UserProfileContext = createContext(null); @@ -29,6 +30,7 @@ export const useUserProfileContext = (): UserProfileContextType => { const { queryParams } = useRouter(); const clerk = useClerk(); const environment = useEnvironment(); + const { user } = useUser(); if (!context || context.componentName !== 'UserProfile') { throw new Error('Clerk: useUserProfileContext called outside of the mounted UserProfile component.'); @@ -40,11 +42,25 @@ export const useUserProfileContext = (): UserProfileContextType => { return createUserProfileCustomPages(customPages || [], clerk, environment); }, [customPages]); + const shouldAllowIdentificationCreation = useMemo(() => { + const { enterpriseSSO } = environment.userSettings; + const showEnterpriseAccounts = user && enterpriseSSO.enabled; + + return ( + !showEnterpriseAccounts || + !user?.enterpriseAccounts.some( + enterpriseAccount => + enterpriseAccount.active && enterpriseAccount.enterpriseConnection?.disableAdditionalIdentifications, + ) + ); + }, [user, environment.userSettings.enterpriseSSO]); + return { ...ctx, pages, componentName, queryParams, authQueryString: '', + shouldAllowIdentificationCreation, }; };