Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a5981f0
feat(ui): SAML metadata URL submission in ConfigureSSO Configure step
iagodahlem May 12, 2026
55b448f
fix(shared): add idpMetadataUrl to FieldId union
iagodahlem May 12, 2026
a8afad3
refactor(ui): hoist enterprise connection update to ConfigureSSO prov…
iagodahlem May 12, 2026
2cfba00
fix(ui): render Configure step description above input
iagodahlem May 12, 2026
d89fa61
refactor(ui): split Configure step into 4 inner Wizard sub-steps
iagodahlem May 12, 2026
5685553
fix(ui): make ConfigureSSO Configure sub-step sections fill the body
iagodahlem May 12, 2026
dd7a1bb
fix(clerk-js): flatten SAML/OIDC body in toMeEnterpriseConnectionBody
iagodahlem May 12, 2026
94369dd
refactor(ui): add fill prop to ConfigureSSO Step.Section
iagodahlem May 12, 2026
71aad26
feat(ui): build CreateAppSubStep content in ConfigureSSO Configure step
iagodahlem May 12, 2026
1fb77de
refactor(ui): tighten Configure step layout and form-wrap SP copy rows
iagodahlem May 12, 2026
979fd62
feat(ui): build ConfigureAttributesSubStep content in ConfigureSSO
iagodahlem May 13, 2026
44f1890
refactor(ui): adopt Text colorScheme=secondary and tighten attribute …
iagodahlem May 13, 2026
c232b44
refactor(ui): scope ConfigureSSO Configure locales by provider and ad…
iagodahlem May 13, 2026
2c8e2ed
refactor(ui): tweak ConfigureAttributes list spacing and bullet style
iagodahlem May 13, 2026
0161f53
feat(ui): build AssignUsersSubStep and switch ConfigureSSO instructio…
iagodahlem May 13, 2026
53a89f3
refactor(ui): tighten ConfigureAttributes badge and pairs list styling
iagodahlem May 13, 2026
43b002f
refactor(ui): call __internal_useUserEnterpriseConnections directly i…
iagodahlem May 13, 2026
efdc631
refactor(ui): flatten Configure step instruction locales to single ke…
iagodahlem May 13, 2026
5b02ff9
refactor(ui): polish ConfigureSSO step header, spacing, and scrollbar
iagodahlem May 13, 2026
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
7 changes: 7 additions & 0 deletions .changeset/configure-sso-configure-step-metadata-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Implement the Okta SAML metadata URL submission path in the Configure step of `<__experimental_ConfigureSSO />`. Adds a single text input for the IdP metadata URL; Continue posts `{ saml: { idpMetadataUrl } }` via `user.updateEnterpriseConnection` wrapped in `useReverification`, with `useCardState` driving the loading state and `handleError` routing backend errors inline to the field or to the card-level error surface. Locale keys added under `configureSSO.configureStep` in `en-US`. Manual entry, file upload, SP-side copy rows, and the Okta admin-console walkthrough ship in follow-up PRs.
5 changes: 5 additions & 0 deletions .changeset/fix-enterprise-connection-flat-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix `toMeEnterpriseConnectionBody` to produce the flat snake_case body shape the backend expects for `user.createEnterpriseConnection` and `user.updateEnterpriseConnection`. SAML and OIDC fields are now top-level prefixed (e.g., `saml_idp_metadata_url`) rather than nested under `saml` / `oidc` objects. Without this fix, IdP metadata submission in `<__experimental_ConfigureSSO />` silently fails on the backend.
62 changes: 50 additions & 12 deletions packages/clerk-js/src/core/resources/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import type {
VerifyTOTPParams,
Web3WalletResource,
} from '@clerk/shared/types';
import { deepCamelToSnake } from '@clerk/shared/underscore';

import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams';
import { unixEpochToDate } from '../../utils/date';
Expand Down Expand Up @@ -559,25 +558,64 @@ export class User extends BaseResource implements UserResource {
* Serializes `CreateMeEnterpriseConnectionParams` / `UpdateMeEnterpriseConnectionParams`
* for the `/me/enterprise_connections` FAPI endpoints.
*
* Uses `deepCamelToSnake` but preserves `saml.attributeMapping` and `customAttributes` as-is. Their keys are
* The handler expects a flat form body where SAML and OIDC fields are
* prefixed (e.g. `saml_idp_metadata_url`, `oidc_client_id`) rather
* than nested under `saml`/`oidc` objects. `attribute_mapping` and
* `custom_attributes` stay as object values and are JSON-stringified
* by the form serializer downstream — their inner keys are
* user-supplied data and must not be camel→snake transformed.
*/
function toMeEnterpriseConnectionBody(
params: CreateMeEnterpriseConnectionParams | UpdateMeEnterpriseConnectionParams,
): Record<string, unknown> {
const originalAttributeMapping =
params.saml && typeof params.saml === 'object' ? params.saml.attributeMapping : undefined;
const originalCustomAttributes = 'customAttributes' in params ? params.customAttributes : undefined;

const body = deepCamelToSnake(params) as Record<string, any>;

if (originalAttributeMapping !== undefined && body.saml && typeof body.saml === 'object') {
body.saml.attribute_mapping = originalAttributeMapping;
const body: Record<string, unknown> = {};

// Top-level fields. `provider` is only on Create, the rest are shared
setIfDefined(body, 'provider', (params as CreateMeEnterpriseConnectionParams).provider);
setIfDefined(body, 'name', params.name);
setIfDefined(body, 'organization_id', params.organizationId);
setIfDefined(body, 'active', (params as UpdateMeEnterpriseConnectionParams).active);
setIfDefined(body, 'sync_user_attributes', (params as UpdateMeEnterpriseConnectionParams).syncUserAttributes);
setIfDefined(
body,
'disable_additional_identifications',
(params as UpdateMeEnterpriseConnectionParams).disableAdditionalIdentifications,
);
setIfDefined(body, 'custom_attributes', (params as UpdateMeEnterpriseConnectionParams).customAttributes);

if (params.saml) {
setIfDefined(body, 'saml_idp_entity_id', params.saml.idpEntityId);
setIfDefined(body, 'saml_idp_sso_url', params.saml.idpSsoUrl);
setIfDefined(body, 'saml_idp_certificate', params.saml.idpCertificate);
setIfDefined(body, 'saml_idp_metadata_url', params.saml.idpMetadataUrl);
setIfDefined(body, 'saml_idp_metadata', params.saml.idpMetadata);
setIfDefined(body, 'saml_attribute_mapping', params.saml.attributeMapping);
setIfDefined(body, 'saml_allow_subdomains', params.saml.allowSubdomains);
setIfDefined(body, 'saml_allow_idp_initiated', params.saml.allowIdpInitiated);
setIfDefined(body, 'saml_force_authn', params.saml.forceAuthn);
}

if (originalCustomAttributes !== undefined) {
body.custom_attributes = originalCustomAttributes;
if (params.oidc) {
setIfDefined(body, 'oidc_client_id', params.oidc.clientId);
setIfDefined(body, 'oidc_client_secret', params.oidc.clientSecret);
setIfDefined(body, 'oidc_discovery_url', params.oidc.discoveryUrl);
setIfDefined(body, 'oidc_auth_url', params.oidc.authUrl);
setIfDefined(body, 'oidc_token_url', params.oidc.tokenUrl);
setIfDefined(body, 'oidc_user_info_url', params.oidc.userInfoUrl);
setIfDefined(body, 'oidc_requires_pkce', params.oidc.requiresPkce);
}

return body;
}

/**
* Adds `value` under `key` only when the caller actually provided it.
* Mirrors the SDK's existing semantics: `undefined` means "don't send
* this field"; `null` is forwarded so users can explicitly clear a
* value via the form-encoded body
*/
function setIfDefined(target: Record<string, unknown>, key: string, value: unknown): void {
if (value !== undefined) {
target[key] = value;
}
}
22 changes: 9 additions & 13 deletions packages/clerk-js/src/core/resources/__tests__/User.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ describe('User', () => {
provider: 'saml_okta',
name: 'New SSO',
organization_id: 'org_1',
saml: { idp_entity_id: 'https://idp.example.com' },
saml_idp_entity_id: 'https://idp.example.com',
},
});

Expand Down Expand Up @@ -291,13 +291,11 @@ describe('User', () => {
body: {
provider: 'saml_okta',
name: 'New SSO',
saml: {
idp_entity_id: 'https://idp.example.com',
attribute_mapping: {
emailAddress: 'mail',
firstName: 'givenName',
'custom:role': 'role',
},
saml_idp_entity_id: 'https://idp.example.com',
saml_attribute_mapping: {
emailAddress: 'mail',
firstName: 'givenName',
'custom:role': 'role',
},
},
});
Expand Down Expand Up @@ -359,11 +357,9 @@ describe('User', () => {
CustomValue: 'y',
nestedCamelKey: { innerCamelKey: 'z' },
},
saml: {
attribute_mapping: {
emailAddress: 'mail',
firstName: 'givenName',
},
saml_attribute_mapping: {
emailAddress: 'mail',
firstName: 'givenName',
},
},
});
Expand Down
108 changes: 89 additions & 19 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,28 +223,98 @@ export const enUS: LocalizationResource = {
},
warning: 'Once a provider is selected you cannot change again until the configuration is over',
},
verifyEmailDomainStep: {
title: 'Verify email address',
subtitle: 'Verify the email address you want to enable the enterprise connection on.',
addEmailAddress: {
formTitle: 'We need your email',
formSubtitle: 'In order to start we will need your email address',
inputPlaceholder: 'name@company.com',
inputLabel: 'Email address',
configureStep: {
spFields: {
acsUrl: {
label: 'Single sign-on URL',
},
spEntityId: {
label: 'Audience URI',
},
},
emailCode: {
formTitle: 'Verify your email address',
formSubtitle: 'Enter the verification code sent to {{identifier}}',
resendButton: "Didn't receive a code? Resend",
verified: {
title: 'We got your email',
subtitle: "You've verified your email address with the following email",
inputLabel: 'Verified email address',
attributeMapping: {
title: 'We expect your SAML responses to have the following specific attributes:',
paragraph:
"These are the defaults and probably won't need you to change them. However, many SAML configuration errors are due to incorrect attribute mappings, so it's worth double-checking. Here's how:",
columns: {
attribute: 'Attribute',
claimName: 'Claim Name',
},
badges: {
required: 'Required',
optional: 'Optional',
},
rows: {
email: {
attribute: 'Email address',
claim: 'user.email',
},
firstName: {
attribute: 'First Name',
claim: 'user.firstName',
},
lastName: {
attribute: 'Last Name',
claim: 'user.lastName',
},
},
},
domainTaken: {
title: 'This domain ({{domain}}) already has an SSO connection',
subtitle: "Contact the application's administrator to get access through the existing connection.",
samlOkta: {
title: 'Configure Okta Workforce',
subtitle: 'Create a new enterprise application in your Okta Dashboard',
createApp: {
title: 'Create a new enterprise application in Okta',
step1: 'Sign in to Okta and go to Admin → Applications.',
step2: 'Click Create App Integration.',
step3: 'Select SAML 2.0.',
step4: 'Fill in the General Settings (App name is required).',
step5: 'Click Next to complete creating the application.',
},
serviceProvider: {
title: 'Configure service provider',
paragraph1:
'Once you have moved forward from the General Settings instructions, you will be presented with the Configure SAML page.',
paragraph2:
'To configure your service provider (Clerk), you must add these two fields to your Okta application:',
},
completeSamlIntegration: {
title: 'Complete SAML integration',
step1: 'Select This is an internal app that we have created from the options menu.',
step2: 'Complete the form with any comments and select "Finish".',
},
configureAttributes: {
step1: 'In the Okta dashboard, find the Attribute Statements section.',
step2: 'Select Add Expression for each attribute, and enter the following name and expression pairs:',
pairs: {
conjunction: ' and ',
email: {
name: 'mail',
expression: 'user.profile.mail',
},
firstName: {
name: 'firstName',
expression: 'user.profile.firstName',
},
lastName: {
name: 'lastName',
expression: 'user.profile.lastName',
},
},
},
assignUsers: {
title: 'Assign selected user or group in Okta',
paragraph: 'You need to assign users or groups to your enterprise app before they can use it to sign in.',
step1: 'In the Okta dashboard, select the Assignments tab.',
step2: 'Select the Assign dropdown. You can either select Assign to people or Assign to groups.',
step3: 'In the search field, enter the user or group of users that you want to assign to the application.',
step4: 'Select the Assign button next to the user or group that you want to assign.',
step5: 'Select the Done button to complete the assignment.',
},
metadataUrl: {
label: 'Metadata URL',
placeholder: 'Paste URL here...',
description: 'In your Okta SAML app, go to the Sign On tab and retrieve the metadata URL. Paste it below.',
},
},
},
},
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/types/elementIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export type FieldId =
| 'apiKeyExpirationDate'
| 'apiKeyRevokeConfirmation'
| 'apiKeySecret'
| 'idpMetadataUrl'
| 'acsUrl'
| 'web3WalletName';
export type ProfileSectionId =
| 'profile'
Expand Down
93 changes: 92 additions & 1 deletion packages/shared/src/types/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,7 @@ export type __internal_LocalizationResource = {
customSaml: LocalizationValue;
};
warning: LocalizationValue;
},
};
verifyEmailDomainStep: {
title: LocalizationValue;
subtitle: LocalizationValue;
Expand All @@ -1338,6 +1338,97 @@ export type __internal_LocalizationResource = {
subtitle: LocalizationValue;
};
};
configureStep: {
spFields: {
acsUrl: {
label: LocalizationValue;
};
spEntityId: {
label: LocalizationValue;
};
};
attributeMapping: {
title: LocalizationValue;
paragraph: LocalizationValue;
columns: {
attribute: LocalizationValue;
claimName: LocalizationValue;
};
badges: {
required: LocalizationValue;
optional: LocalizationValue;
};
rows: {
email: {
attribute: LocalizationValue;
claim: LocalizationValue;
};
firstName: {
attribute: LocalizationValue;
claim: LocalizationValue;
};
lastName: {
attribute: LocalizationValue;
claim: LocalizationValue;
};
};
};
samlOkta: {
title: LocalizationValue;
subtitle: LocalizationValue;
createApp: {
title: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
step3: LocalizationValue;
step4: LocalizationValue;
step5: LocalizationValue;
};
serviceProvider: {
title: LocalizationValue;
paragraph1: LocalizationValue;
paragraph2: LocalizationValue;
};
completeSamlIntegration: {
title: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
};
configureAttributes: {
step1: LocalizationValue;
step2: LocalizationValue;
pairs: {
conjunction: LocalizationValue;
email: {
name: LocalizationValue;
expression: LocalizationValue;
};
firstName: {
name: LocalizationValue;
expression: LocalizationValue;
};
lastName: {
name: LocalizationValue;
expression: LocalizationValue;
};
};
};
assignUsers: {
title: LocalizationValue;
paragraph: LocalizationValue;
step1: LocalizationValue;
step2: LocalizationValue;
step3: LocalizationValue;
step4: LocalizationValue;
step5: LocalizationValue;
};
metadataUrl: {
label: LocalizationValue;
placeholder: LocalizationValue;
description: LocalizationValue;
};
};
};
};
apiKeys: {
formTitle: LocalizationValue;
Expand Down
Loading
Loading