Skip to content

Commit 4749ed4

Browse files
feat(clerk-js,backend,types): Implement "disable additional identifiers" for SAML connection (#4211)
1 parent ef9b239 commit 4749ed4

File tree

16 files changed

+382
-135
lines changed

16 files changed

+382
-135
lines changed

.changeset/fresh-forks-talk.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@clerk/clerk-js": patch
3+
"@clerk/backend": patch
4+
"@clerk/types": patch
5+
---
6+
7+
Conditionally renders identification sections on `UserProfile` based on the SAML connection configuration for disabling additional identifiers.

packages/backend/src/api/resources/JSON.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export interface SamlAccountJSON extends ClerkResourceJSON {
114114
first_name: string;
115115
last_name: string;
116116
verification: VerificationJSON | null;
117+
saml_connection: SamlAccountConnectionJSON | null;
117118
}
118119

119120
export interface IdentificationLinkJSON extends ClerkResourceJSON {
@@ -399,3 +400,17 @@ export interface PermissionJSON extends ClerkResourceJSON {
399400
created_at: number;
400401
updated_at: number;
401402
}
403+
404+
export interface SamlAccountConnectionJSON extends ClerkResourceJSON {
405+
id: string;
406+
name: string;
407+
domain: string;
408+
active: boolean;
409+
provider: string;
410+
sync_user_attributes: boolean;
411+
allow_subdomains: boolean;
412+
allow_idp_initiated: boolean;
413+
disable_additional_identifications: boolean;
414+
created_at: number;
415+
updated_at: number;
416+
}

packages/backend/src/api/resources/SamlAccount.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SamlAccountJSON } from './JSON';
2+
import { SamlAccountConnection } from './SamlConnection';
23
import { Verification } from './Verification';
34

45
export class SamlAccount {
@@ -11,6 +12,7 @@ export class SamlAccount {
1112
readonly firstName: string,
1213
readonly lastName: string,
1314
readonly verification: Verification | null,
15+
readonly samlConnection: SamlAccountConnection | null,
1416
) {}
1517

1618
static fromJSON(data: SamlAccountJSON): SamlAccount {
@@ -23,6 +25,7 @@ export class SamlAccount {
2325
data.first_name,
2426
data.last_name,
2527
data.verification && Verification.fromJSON(data.verification),
28+
data.saml_connection && SamlAccountConnection.fromJSON(data.saml_connection),
2629
);
2730
}
2831
}

packages/backend/src/api/resources/SamlConnection.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AttributeMappingJSON, SamlConnectionJSON } from './JSON';
1+
import type { AttributeMappingJSON, SamlAccountConnectionJSON, SamlConnectionJSON } from './JSON';
22

33
export class SamlConnection {
44
constructor(
@@ -49,6 +49,35 @@ export class SamlConnection {
4949
}
5050
}
5151

52+
export class SamlAccountConnection {
53+
constructor(
54+
readonly id: string,
55+
readonly name: string,
56+
readonly domain: string,
57+
readonly active: boolean,
58+
readonly provider: string,
59+
readonly syncUserAttributes: boolean,
60+
readonly allowSubdomains: boolean,
61+
readonly allowIdpInitiated: boolean,
62+
readonly createdAt: number,
63+
readonly updatedAt: number,
64+
) {}
65+
static fromJSON(data: SamlAccountConnectionJSON): SamlAccountConnection {
66+
return new SamlAccountConnection(
67+
data.id,
68+
data.name,
69+
data.domain,
70+
data.active,
71+
data.provider,
72+
data.sync_user_attributes,
73+
data.allow_subdomains,
74+
data.allow_idp_initiated,
75+
data.created_at,
76+
data.updated_at,
77+
);
78+
}
79+
}
80+
5281
class AttributeMapping {
5382
constructor(
5483
readonly userId: string,

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"files": [
3-
{ "path": "./dist/clerk.browser.js", "maxSize": "64.1kB" },
3+
{ "path": "./dist/clerk.browser.js", "maxSize": "65kB" },
44
{ "path": "./dist/clerk.headless.js", "maxSize": "43kB" },
55
{ "path": "./dist/ui-common*.js", "maxSize": "86KB" },
66
{ "path": "./dist/vendors*.js", "maxSize": "70KB" },

packages/clerk-js/src/core/resources/SamlAccount.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import type { SamlAccountJSON, SamlAccountResource, SamlIdpSlug, VerificationResource } from '@clerk/types';
1+
import type {
2+
SamlAccountConnectionJSON,
3+
SamlAccountConnectionResource,
4+
SamlAccountJSON,
5+
SamlAccountResource,
6+
SamlIdpSlug,
7+
VerificationResource,
8+
} from '@clerk/types';
29

10+
import { unixEpochToDate } from '../../utils/date';
311
import { BaseResource } from './Base';
412
import { Verification } from './Verification';
513

@@ -12,6 +20,7 @@ export class SamlAccount extends BaseResource implements SamlAccountResource {
1220
firstName = '';
1321
lastName = '';
1422
verification: VerificationResource | null = null;
23+
samlConnection: SamlAccountConnectionResource | null = null;
1524

1625
public constructor(data: Partial<SamlAccountJSON>, pathRoot: string);
1726
public constructor(data: SamlAccountJSON, pathRoot: string) {
@@ -37,6 +46,46 @@ export class SamlAccount extends BaseResource implements SamlAccountResource {
3746
this.verification = new Verification(data.verification);
3847
}
3948

49+
if (data.saml_connection) {
50+
this.samlConnection = new SamlAccountConnection(data.saml_connection);
51+
}
52+
53+
return this;
54+
}
55+
}
56+
57+
export class SamlAccountConnection extends BaseResource implements SamlAccountConnectionResource {
58+
id!: string;
59+
name!: string;
60+
domain!: string;
61+
active!: boolean;
62+
provider!: string;
63+
syncUserAttributes!: boolean;
64+
allowSubdomains!: boolean;
65+
allowIdpInitiated!: boolean;
66+
disableAdditionalIdentifications!: boolean;
67+
createdAt!: Date;
68+
updatedAt!: Date;
69+
70+
constructor(data: SamlAccountConnectionJSON | null) {
71+
super();
72+
this.fromJSON(data);
73+
}
74+
protected fromJSON(data: SamlAccountConnectionJSON | null): this {
75+
if (data) {
76+
this.id = data.id;
77+
this.name = data.name;
78+
this.domain = data.domain;
79+
this.active = data.active;
80+
this.provider = data.provider;
81+
this.syncUserAttributes = data.sync_user_attributes;
82+
this.allowSubdomains = data.allow_subdomains;
83+
this.allowIdpInitiated = data.allow_idp_initiated;
84+
this.disableAdditionalIdentifications = data.disable_additional_identifications;
85+
this.createdAt = unixEpochToDate(data.created_at);
86+
this.updatedAt = unixEpochToDate(data.updated_at);
87+
}
88+
4089
return this;
4190
}
4291
}

packages/clerk-js/src/ui/components/UserProfile/AccountPage.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@ export const AccountPage = withCardStateProvider(() => {
1515
const { attributes, saml, social } = useEnvironment().userSettings;
1616
const card = useCardState();
1717
const { user } = useUser();
18+
1819
const showUsername = attributes.username.enabled;
1920
const showEmail = attributes.email_address.enabled;
2021
const showPhone = attributes.phone_number.enabled;
2122
const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0;
2223
const showSamlAccounts = saml && saml.enabled && user && user.samlAccounts.length > 0;
2324
const showWeb3 = attributes.web3_wallet.enabled;
2425

26+
const shouldAllowIdentificationCreation =
27+
!showSamlAccounts ||
28+
!user?.samlAccounts?.some(
29+
samlAccount => samlAccount.active && samlAccount.samlConnection?.disableAdditionalIdentifications,
30+
);
31+
2532
return (
2633
<Col
2734
elementDescriptor={descriptors.page}
@@ -43,11 +50,11 @@ export const AccountPage = withCardStateProvider(() => {
4350

4451
<UserProfileSection />
4552
{showUsername && <UsernameSection />}
46-
{showEmail && <EmailsSection />}
47-
{showPhone && <PhoneSection />}
48-
{showConnectedAccounts && <ConnectedAccountsSection />}
53+
{showEmail && <EmailsSection shouldAllowCreation={shouldAllowIdentificationCreation} />}
54+
{showPhone && <PhoneSection shouldAllowCreation={shouldAllowIdentificationCreation} />}
55+
{showConnectedAccounts && <ConnectedAccountsSection shouldAllowCreation={shouldAllowIdentificationCreation} />}
4956
{showSamlAccounts && <EnterpriseAccountsSection />}
50-
{showWeb3 && <Web3Section />}
57+
{showWeb3 && <Web3Section shouldAllowCreation={shouldAllowIdentificationCreation} />}
5158
</Col>
5259
</Col>
5360
);

packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsSection.tsx

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -46,41 +46,43 @@ const errorCodesForReconnect = [
4646
'external_account_email_address_verification_required',
4747
];
4848

49-
export const ConnectedAccountsSection = withCardStateProvider(() => {
50-
const { user } = useUser();
51-
const card = useCardState();
52-
53-
if (!user) {
54-
return null;
55-
}
56-
57-
const accounts = [
58-
...user.verifiedExternalAccounts,
59-
...user.unverifiedExternalAccounts.filter(a => a.verification?.error),
60-
];
49+
export const ConnectedAccountsSection = withCardStateProvider(
50+
({ shouldAllowCreation = true }: { shouldAllowCreation?: boolean }) => {
51+
const { user } = useUser();
52+
const card = useCardState();
53+
const hasExternalAccounts = Boolean(user?.externalAccounts?.length);
54+
55+
if (!user || (!shouldAllowCreation && !hasExternalAccounts)) {
56+
return null;
57+
}
6158

62-
return (
63-
<ProfileSection.Root
64-
title={localizationKeys('userProfile.start.connectedAccountsSection.title')}
65-
centered={false}
66-
id='connectedAccounts'
67-
>
68-
<Card.Alert>{card.error}</Card.Alert>
69-
<Action.Root>
70-
<ProfileSection.ItemList id='connectedAccounts'>
71-
{accounts.map(account => (
72-
<ConnectedAccount
73-
key={account.id}
74-
account={account}
75-
/>
76-
))}
77-
</ProfileSection.ItemList>
78-
79-
<AddConnectedAccount />
80-
</Action.Root>
81-
</ProfileSection.Root>
82-
);
83-
});
59+
const accounts = [
60+
...user.verifiedExternalAccounts,
61+
...user.unverifiedExternalAccounts.filter(a => a.verification?.error),
62+
];
63+
64+
return (
65+
<ProfileSection.Root
66+
title={localizationKeys('userProfile.start.connectedAccountsSection.title')}
67+
centered={false}
68+
id='connectedAccounts'
69+
>
70+
<Card.Alert>{card.error}</Card.Alert>
71+
<Action.Root>
72+
<ProfileSection.ItemList id='connectedAccounts'>
73+
{accounts.map(account => (
74+
<ConnectedAccount
75+
key={account.id}
76+
account={account}
77+
/>
78+
))}
79+
</ProfileSection.ItemList>
80+
{shouldAllowCreation && <AddConnectedAccount />}
81+
</Action.Root>
82+
</ProfileSection.Root>
83+
);
84+
},
85+
);
8486

8587
const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) => {
8688
const { additionalOAuthScopes, componentName, mode } = useUserProfileContext();

packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const EmailScreen = (props: EmailScreenProps) => {
3535
);
3636
};
3737

38-
export const EmailsSection = () => {
38+
export const EmailsSection = ({ shouldAllowCreation = true }) => {
3939
const { user } = useUser();
4040

4141
return (
@@ -79,19 +79,21 @@ export const EmailsSection = () => {
7979
</Action.Open>
8080
</Action.Root>
8181
))}
82-
83-
<Action.Trigger value='add'>
84-
<ProfileSection.ArrowButton
85-
id='emailAddresses'
86-
localizationKey={localizationKeys('userProfile.start.emailAddressesSection.primaryButton')}
87-
/>
88-
</Action.Trigger>
89-
90-
<Action.Open value='add'>
91-
<Action.Card>
92-
<EmailScreen />
93-
</Action.Card>
94-
</Action.Open>
82+
{shouldAllowCreation && (
83+
<>
84+
<Action.Trigger value='add'>
85+
<ProfileSection.ArrowButton
86+
id='emailAddresses'
87+
localizationKey={localizationKeys('userProfile.start.emailAddressesSection.primaryButton')}
88+
/>
89+
</Action.Trigger>
90+
<Action.Open value='add'>
91+
<Action.Card>
92+
<EmailScreen />
93+
</Action.Card>
94+
</Action.Open>
95+
</>
96+
)}
9597
</ProfileSection.ItemList>
9698
</Action.Root>
9799
</ProfileSection.Root>

packages/clerk-js/src/ui/components/UserProfile/PhoneSection.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@ const PhoneScreen = (props: PhoneScreenProps) => {
3535
);
3636
};
3737

38-
export const PhoneSection = () => {
38+
export const PhoneSection = ({ shouldAllowCreation = true }: { shouldAllowCreation?: boolean }) => {
3939
const { user } = useUser();
40+
const hasPhoneNumbers = Boolean(user?.phoneNumbers?.length);
41+
42+
if (!shouldAllowCreation && !hasPhoneNumbers) {
43+
return null;
44+
}
4045

4146
return (
4247
<ProfileSection.Root
@@ -84,19 +89,21 @@ export const PhoneSection = () => {
8489
</Action.Open>
8590
</Action.Root>
8691
))}
87-
88-
<Action.Trigger value='add'>
89-
<ProfileSection.ArrowButton
90-
id='phoneNumbers'
91-
localizationKey={localizationKeys('userProfile.start.phoneNumbersSection.primaryButton')}
92-
/>
93-
</Action.Trigger>
94-
95-
<Action.Open value='add'>
96-
<Action.Card>
97-
<PhoneScreen />
98-
</Action.Card>
99-
</Action.Open>
92+
{shouldAllowCreation && (
93+
<>
94+
<Action.Trigger value='add'>
95+
<ProfileSection.ArrowButton
96+
id='phoneNumbers'
97+
localizationKey={localizationKeys('userProfile.start.phoneNumbersSection.primaryButton')}
98+
/>
99+
</Action.Trigger>
100+
<Action.Open value='add'>
101+
<Action.Card>
102+
<PhoneScreen />
103+
</Action.Card>
104+
</Action.Open>
105+
</>
106+
)}
100107
</ProfileSection.ItemList>
101108
</Action.Root>
102109
</ProfileSection.Root>

0 commit comments

Comments
 (0)