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,
};
};