Skip to content

feat(ui): Add Google Workspace SAML to self-serve SSO#8690

Open
LauraBeatris wants to merge 11 commits into
mainfrom
laura/orgs-1533-sdk-add-support-for-google-workspace-saml
Open

feat(ui): Add Google Workspace SAML to self-serve SSO#8690
LauraBeatris wants to merge 11 commits into
mainfrom
laura/orgs-1533-sdk-add-support-for-google-workspace-saml

Conversation

@LauraBeatris
Copy link
Copy Markdown
Member

@LauraBeatris LauraBeatris commented May 28, 2026

Description

Introduces Google Workspace SAML provider with tailored configure steps

CleanShot.2026-05-28.at.14.28.17.mp4

Unrelated changes/fixes

  • Update stepper line-height to fix alignment issue
  • Update test runs query to not refetch on window focus, which was causing the 'Refresh logs' button to display a spinner on mount

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment May 28, 2026 5:31pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 28, 2026

🦋 Changeset detected

Latest commit: a1018c2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@clerk/localizations Patch
@clerk/shared Patch
@clerk/ui Patch
@clerk/react Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/chrome-extension Patch
@clerk/clerk-js Patch
@clerk/expo-passkeys Patch
@clerk/expo Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/hono Patch
@clerk/msw Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/vue Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8690

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8690

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8690

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8690

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@8690

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8690

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8690

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8690

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8690

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8690

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8690

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8690

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8690

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8690

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8690

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8690

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8690

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8690

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8690

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8690

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8690

commit: a1018c2

@LauraBeatris LauraBeatris force-pushed the laura/orgs-1533-sdk-add-support-for-google-workspace-saml branch from 3fe1741 to b407275 Compare May 28, 2026 15:45
@LauraBeatris LauraBeatris force-pushed the laura/orgs-1533-sdk-add-support-for-google-workspace-saml branch from b407275 to f6e8d22 Compare May 28, 2026 15:47
@LauraBeatris LauraBeatris force-pushed the laura/orgs-1533-sdk-add-support-for-google-workspace-saml branch from f6e8d22 to 20ffc64 Compare May 28, 2026 15:56
@LauraBeatris LauraBeatris force-pushed the laura/orgs-1533-sdk-add-support-for-google-workspace-saml branch from 20ffc64 to e22691d Compare May 28, 2026 16:10
@LauraBeatris LauraBeatris force-pushed the laura/orgs-1533-sdk-add-support-for-google-workspace-saml branch from e22691d to 1e68b4e Compare May 28, 2026 16:13
@LauraBeatris LauraBeatris force-pushed the laura/orgs-1533-sdk-add-support-for-google-workspace-saml branch from 0db7c4c to 10ce4d7 Compare May 28, 2026 17:03
Previously, the 'Refresh logs' button was triggering a loading state on mount, since the step gets focused and the query gets triggered.

We don't need to refetch on window focus, in order to rely on the refresh logs button only + the current internal polling mechanism.
@LauraBeatris LauraBeatris force-pushed the laura/orgs-1533-sdk-add-support-for-google-workspace-saml branch from f5f6f55 to a1018c2 Compare May 28, 2026 17:30
@LauraBeatris LauraBeatris marked this pull request as ready for review May 28, 2026 17:30
@LauraBeatris LauraBeatris requested review from a team and iagodahlem May 28, 2026 17:30
@github-actions
Copy link
Copy Markdown
Contributor

API Changes Report

Generated by snapi on 2026-05-28T17:33:02.523Z

Summary

Metric Count
Packages analyzed 6
Packages with changes 1
🔴 Breaking changes 0
🟡 Non-breaking changes 2
🟢 Additions 0

🤖 This report was reviewed by claude-sonnet-4-6.


@clerk/shared

Current version: 4.14.0
Recommended bump: MINOR → 4.15.0

Subpath ./types

🟡 Non-breaking Changes (2)

Modified: __internal_LocalizationResource
Diff (before: 1680 lines, after: 1783 lines). Click to expand.
// ... 1215 unchanged lines elided ...
          groupLabel: LocalizationValue;
          okta: LocalizationValue;
          customSaml: LocalizationValue;
+         google: LocalizationValue;
        };
        warning: LocalizationValue;
      };
      verifyEmailDomainStep: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        addEmailAddress: {
          formTitle: LocalizationValue;
          formSubtitle: LocalizationValue;
          inputPlaceholder: LocalizationValue;
          inputLabel: LocalizationValue;
        };
        emailCode: {
          formTitle: LocalizationValue;
          formSubtitle: LocalizationValue<'identifier'>;
          resendButton: LocalizationValue;
          verified: {
            title: LocalizationValue;
            subtitle: LocalizationValue;
            inputLabel: LocalizationValue;
          };
        };
        domainTaken: {
          title: LocalizationValue<'domain'>;
          subtitle: LocalizationValue;
        };
      };
      testConfigurationStep: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        error__noSuccessfulTestRun: LocalizationValue;
        testUrl: {
          actionLabel__open: LocalizationValue;
        };
        testResults: {
          title: LocalizationValue;
          actionLabel__refresh: LocalizationValue;
          polling: LocalizationValue;
          status__success: LocalizationValue;
          status__failed: LocalizationValue;
          status__pending: LocalizationValue;
          empty: {
            title: LocalizationValue;
            subtitle: LocalizationValue;
          };
        };
        testRunDetails: {
          title: LocalizationValue;
          runDetails: {
            sectionTitle: LocalizationValue;
            timestamp: LocalizationValue;
            status: LocalizationValue;
            errorCode: LocalizationValue;
            fullMessage: LocalizationValue;
            actionLabel__copy: LocalizationValue;
            actionLabel__copied: LocalizationValue;
          };
          parsedUserInfo: {
            sectionTitle: LocalizationValue;
            email: LocalizationValue;
            firstName: LocalizationValue;
          };
          howToFix: {
            sectionTitle: LocalizationValue;
            actionLabel__viewDocumentation: LocalizationValue;
            saml_user_attribute_missing: {
              intro: LocalizationValue;
              step1: LocalizationValue;
              step2: LocalizationValue;
              step3: LocalizationValue;
            };
            saml_response_relaystate_missing: {
              description: LocalizationValue;
            };
            saml_email_address_domain_mismatch: {
              description: LocalizationValue;
            };
            oauth_access_denied: {
              description: LocalizationValue;
            };
            oauth_token_exchange_error: {
              description: LocalizationValue;
            };
            oauth_fetch_user_error: {
              intro: LocalizationValue;
              step1: LocalizationValue;
              step2: LocalizationValue;
            };
          };
        };
      };
      configureStep: {
        attributeMappingTable: {
          badges: {
            required: LocalizationValue;
            optional: LocalizationValue;
          };
        };
        samlOkta: {
          mainHeaderTitle: LocalizationValue;
          createAppStep: {
            headerSubtitle: LocalizationValue;
            createAppInstructions: {
              title: LocalizationValue;
              step1: LocalizationValue;
              step2: LocalizationValue;
              step3: LocalizationValue;
              step4: LocalizationValue;
              step5: LocalizationValue;
            };
            serviceProviderInstructions: {
              title: LocalizationValue;
              paragraph1: LocalizationValue;
              paragraph2: LocalizationValue;
              serviceProviderFields: {
                acsUrl: {
                  label: LocalizationValue;
                };
                spEntityId: {
                  label: LocalizationValue;
                };
              };
            };
            completeSamlIntegrationInstructions: {
              title: LocalizationValue;
              step1: LocalizationValue;
              step2: LocalizationValue;
            };
          };
          attributeMappingStep: {
            headerSubtitle: LocalizationValue;
            paragraph: LocalizationValue;
            step1: LocalizationValue;
            step2: LocalizationValue;
            attributeMappingTable: {
              columns: {
                name: LocalizationValue;
                expression: LocalizationValue;
              };
              rows: {
                email: {
                  name: LocalizationValue;
                  expression: LocalizationValue;
                };
                firstName: {
                  name: LocalizationValue;
                  expression: LocalizationValue;
                };
                lastName: {
                  name: LocalizationValue;
                  expression: LocalizationValue;
                };
              };
            };
          };
          assignUsersStep: {
            headerSubtitle: LocalizationValue;
            assignUsersInstructions: {
              title: LocalizationValue;
              paragraph: LocalizationValue;
              step1: LocalizationValue;
              step2: LocalizationValue;
              step3: LocalizationValue;
              step4: LocalizationValue;
              step5: LocalizationValue;
            };
          };
          identityProviderMetadataStep: {
            headerSubtitle: LocalizationValue;
            modes: {
              title: LocalizationValue;
              ariaLabel: LocalizationValue;
              metadataUrl: LocalizationValue;
              manual: LocalizationValue;
            };
            metadataUrl: {
              label: LocalizationValue;
              placeholder: LocalizationValue;
              description: LocalizationValue;
            };
            manual: {
              description: LocalizationValue;
              signOnUrl: {
                label: LocalizationValue;
                placeholder: LocalizationValue;
              };
              issuer: {
                label: LocalizationValue;
                placeholder: LocalizationValue;
              };
              signingCertificate: {
                label: LocalizationValue;
                uploadFile: LocalizationValue;
                replaceFile: LocalizationValue;
                removeFile: LocalizationValue;
                fileUploaded: LocalizationValue;
              };
            };
          };
        };
        samlCustom: {
          mainHeaderTitle: LocalizationValue;
          createAppStep: {
            headerSubtitle: LocalizationValue;
            createAppInstructions: {
              title: LocalizationValue;
              paragraph: LocalizationValue;
            };
            serviceProviderFields: {
              acsUrl: {
                label: LocalizationValue;
              };
              spEntityId: {
                label: LocalizationValue;
              };
            };
          };
          attributeMappingStep: {
            headerSubtitle: LocalizationValue;
            paragraph: LocalizationValue;
            attributeMappingTable: {
              title: LocalizationValue;
              columns: {
                userProfile: LocalizationValue;
                attributeName: LocalizationValue;
              };
              rows: {
                email: {
                  userProfile: LocalizationValue;
                  attributeName: LocalizationValue;
                };
                firstName: {
                  userProfile: LocalizationValue;
                  attributeName: LocalizationValue;
                };
                lastName: {
                  userProfile: LocalizationValue;
                  attributeName: LocalizationValue;
                };
              };
            };
          };
          assignUsersStep: {
            headerSubtitle: LocalizationValue;
            title: LocalizationValue;
            paragraph: LocalizationValue;
          };
          identityProviderMetadataStep: {
            headerSubtitle: LocalizationValue;
            modes: {
              title: LocalizationValue;
              ariaLabel: LocalizationValue;
              metadataUrl: LocalizationValue;
              manual: LocalizationValue;
            };
            metadataUrl: {
              label: LocalizationValue;
              placeholder: LocalizationValue;
+             description: LocalizationValue;
+           };
+           manual: {
+             description: LocalizationValue;
+             signOnUrl: {
+               label: LocalizationValue;
+               placeholder: LocalizationValue;
+             };
+             issuer: {
+               label: LocalizationValue;
+               placeholder: LocalizationValue;
+             };
+             signingCertificate: {
+               label: LocalizationValue;
+               uploadFile: LocalizationValue;
+               replaceFile: LocalizationValue;
+               removeFile: LocalizationValue;
+               fileUploaded: LocalizationValue;
+             };
+           };
+         };
+       };
+       samlGoogle: {
+         mainHeaderTitle: LocalizationValue;
+         createAppStep: {
+           headerSubtitle: LocalizationValue;
+           createAppInstructions: {
+             title: LocalizationValue;
+             step1: LocalizationValue;
+             step2: LocalizationValue;
+             step3: LocalizationValue;
+             step4: LocalizationValue;
+             step5: LocalizationValue;
+           };
+         };
+         identityProviderMetadataStep: {
+           headerSubtitle: LocalizationValue;
+           modes: {
+             title: LocalizationValue;
+             ariaLabel: LocalizationValue;
+             metadataFile: LocalizationValue;
+             manual: LocalizationValue;
+           };
+           metadataFile: {
+             label: LocalizationValue;
              description: LocalizationValue;
+             uploadFile: LocalizationValue;
+             replaceFile: LocalizationValue;
+             removeFile: LocalizationValue;
+             fileUploaded: LocalizationValue;
            };
            manual: {
              description: LocalizationValue;
              signOnUrl: {
                label: LocalizationValue;
                placeholder: LocalizationValue;
              };
              issuer: {
                label: LocalizationValue;
                placeholder: LocalizationValue;
              };
              signingCertificate: {
                label: LocalizationValue;
                uploadFile: LocalizationValue;
                replaceFile: LocalizationValue;
                removeFile: LocalizationValue;
                fileUploaded: LocalizationValue;
+             };
+           };
+         };
+         serviceProviderStep: {
+           headerSubtitle: LocalizationValue;
+           title: LocalizationValue;
+           paragraph: LocalizationValue;
+           serviceProviderFields: {
+             acsUrl: {
+               label: LocalizationValue;
+             };
+             spEntityId: {
+               label: LocalizationValue;
+             };
+           };
+           nameIdInstructions: {
+             step1: LocalizationValue;
+             step2: LocalizationValue;
+           };
+         };
+         attributeMappingStep: {
+           headerSubtitle: LocalizationValue;
+           paragraph: LocalizationValue;
+           step1: LocalizationValue;
+           step2: LocalizationValue;
+           attributeMappingTable: {
+             columns: {
+               googleAttribute: LocalizationValue;
+               appAttribute: LocalizationValue;
+             };
+             rows: {
+               email: {
+                 googleAttribute: LocalizationValue;
+                 appAttribute: LocalizationValue;
+               };
+               firstName: {
+                 googleAttribute: LocalizationValue;
+                 appAttribute: LocalizationValue;
+               };
+               lastName: {
+                 googleAttribute: LocalizationValue;
+                 appAttribute: LocalizationValue;
+               };
              };
+           };
+         };
+         configureUserAccess: {
+           headerSubtitle: LocalizationValue;
+           assignUsersInstructions: {
+             paragraph1: LocalizationValue;
+             step1: LocalizationValue;
+             step2: LocalizationValue;
+             step3: LocalizationValue;
+             paragraph2: LocalizationValue;
            };
          };
        };
// ... 182 unchanged lines elided ...

Static analyzer: Breaking change in type alias __internal_LocalizationResource: Type changed: {locale:string;maintenanceMode:LocalizationValue;roles:{[r:string]:LocalizationValue;};socialButtonsBlockButton:Localiz…{locale:string;maintenanceMode:LocalizationValue;roles:{[r:string]:LocalizationValue;};socialButtonsBlockButton:Localiz…

🤖 AI review (reclassified as non-breaking) (85%): The __internal_LocalizationResource type had new fields added to it (e.g., configureSSO.selectProviderStep.saml.google, configureSSO.configureStep.samlGoogle, and other nested additions). This type is used as an input/source for LocalizationResource (via DeepPartial<DeepLocalizationWithoutObjects<__internal_LocalizationResource>>), meaning consumers providing localization objects only need to supply a subset. Adding new optional keys to the shape is non-breaking for existing consumers. However, the rule-based analyzer flagged it as breaking. Since LocalizationResource extends DeepPartial<...>, existing consumer code that provides a partial localization object would still compile fine with the additions.

Modified: FieldId
- type FieldId = 'firstName' | 'lastName' | 'name' | 'slug' | 'emailAddress' | 'phoneNumber' | 'currentPassword' | 'newPassword' | 'signOutOfOtherSessions' | 'passkeyName' | 'password' | 'confirmPassword' | 'identifier' | 'username' | 'code' | 'role' | 'deleteConfirmation' | 'deleteOrganizationConfirmation' | 'enrollmentMode' | 'affiliationEmailAddress' | 'deleteExistingInvitationsSuggestions' | 'legalAccepted' | 'apiKeyDescription' | 'apiKeyExpirationDate' | 'apiKeyRevokeConfirmation' | 'apiKeySecret' | 'idpCertificate' | 'idpEntityId' | 'idpMetadataUrl' | 'idpSsoUrl' | 'acsUrl' | 'spEntityId' | 'web3WalletName';
+ type FieldId = 'firstName' | 'lastName' | 'name' | 'slug' | 'emailAddress' | 'phoneNumber' | 'currentPassword' | 'newPassword' | 'signOutOfOtherSessions' | 'passkeyName' | 'password' | 'confirmPassword' | 'identifier' | 'username' | 'code' | 'role' | 'deleteConfirmation' | 'deleteOrganizationConfirmation' | 'enrollmentMode' | 'affiliationEmailAddress' | 'deleteExistingInvitationsSuggestions' | 'legalAccepted' | 'apiKeyDescription' | 'apiKeyExpirationDate' | 'apiKeyRevokeConfirmation' | 'apiKeySecret' | 'idpCertificate' | 'idpEntityId' | 'idpMetadata' | 'idpMetadataUrl' | 'idpSsoUrl' | 'acsUrl' | 'spEntityId' | 'web3WalletName';

Static analyzer: Breaking change in type alias FieldId: Type changed: 'firstName'|'lastName'|'name'|'slug'|'emailAddress'|'phoneNumber'|'currentPassword'|'newPassword'|'signOutOfOtherSessio…'firstName'|'lastName'|'name'|'slug'|'emailAddress'|'phoneNumber'|'currentPassword'|'newPassword'|'signOutOfOtherSessio…

🤖 AI review (reclassified as non-breaking) (80%): The FieldId type had 'idpMetadata' added as a new member to the union. FieldId is used internally as a field identifier type. Consumers who exhaustively switch over FieldId values (e.g., via a switch statement with no default) could be affected, but adding a new union member to a string union that is used as an identifier/label type is generally non-breaking for consumers that receive values of this type. Consumers who produce values typed as FieldId are unaffected since the new string is simply an additional valid value.


Report generated by snapi

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds Google Workspace as a new SAML provider option to the self-serve ConfigureSSO wizard. It introduces shared form infrastructure (IdentityProviderConfigurationForm, IdentityProviderConfigurationModes) to handle SAML configuration across multiple providers, refactors existing Okta and Custom SAML steps to use this infrastructure, and implements a new four-step Google SAML configuration flow. Type definitions, localization keys, and English localization strings are extended to support the new provider. The old controller-based metadata form and hook are removed in favor of local component state management.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • clerk/javascript#8535: Extends the same Okta ConfigureSSO flow and IdP metadata submission step that is refactored in this PR to use the new shared SAML configuration form logic.
  • clerk/javascript#8651: Builds on the same step-based per-IdP ConfigureSSO architecture by extending the configureStep schema for SAML and introducing the same shared IdentityProviderConfigurationForm/modes pattern.
  • clerk/javascript#8544: Directly connected through the useEnterpriseConnectionTestRuns hook that is modified here to disable refetch-on-focus behavior.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description is well-related to the changeset, explaining the introduction of Google Workspace SAML provider with tailored configure steps, plus mentioning unrelated fixes (stepper alignment and test runs query). It provides meaningful information about the PR's intent.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title accurately captures the main feature addition: Google Workspace SAML support for self-serve SSO configuration, which is the primary change reflected across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/ui/src/components/ConfigureSSO/steps/__tests__/SelectProviderStep.test.tsx (1)

105-132: ⚡ Quick win

Add a submit-path test for the new Google provider.

These assertions cover rendering/icons, but they don’t verify wiring. Please add a case that selects Google Workspace, clicks Continue, and asserts setProvider('saml_google'), createEnterpriseConnection('saml_google', ...), and navigation to configure.

As per coding guidelines: **/*.{test,spec}.{ts,tsx}: Unit tests are required for all new functionality.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/ui/src/components/ConfigureSSO/steps/__tests__/SelectProviderStep.test.tsx`
around lines 105 - 132, Add a new test in SelectProviderStep.test.tsx that
simulates selecting the "Google Workspace" radio, clicking the Continue button,
and asserting the wiring: mock and verify setProvider was called with
'saml_google', createEnterpriseConnection was invoked with 'saml_google' and the
expected args, and the app navigated to the "configure" route (assert the mocked
router/navigation push was called with 'configure'); use the existing
createFixtures()/renderStep(wrapper) helpers and the same mocking utilities used
elsewhere in this file to locate and click the Continue button and to spy on
setProvider, createEnterpriseConnection, and the navigation method.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx`:
- Around line 270-280: The initial mode logic currently sets mode to 'manual'
whenever any SAML config exists (see samlConnection and hasExistingConfig),
which overrides metadata-URL-only setups; change the initializer for the React
state in SamlCustomConfigureSteps (the useState for mode / IdpConfigurationMode)
to prefer 'metadataUrl' when samlConnection?.idpMetadataUrl is present,
otherwise fall back to 'manual' if any other config exists, or 'metadataUrl'
when no config exists per previous behavior; update references to
hasExistingConfig/idpMetadataUrl in the mode initializer accordingly so initial
UI reflects metadata URL setups.

In
`@packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlOktaConfigureSteps.tsx`:
- Around line 468-478: The initial mode logic currently sets mode to 'manual'
whenever any SAML config exists; change this to prefer 'metadataUrl' when the
saved config only contains idpMetadataUrl. Update the initialization of mode
(the React.useState<IdpConfigurationMode> call) to compute an initialMode that
checks samlConnection: if samlConnection?.idpMetadataUrl is present and
idpEntityId, idpSsoUrl, and idpCertificate are absent then use 'metadataUrl',
otherwise if any other fields or idpCertificate exist use 'manual', and fall
back to 'metadataUrl' when no config exists; keep references to samlConnection,
hasExistingConfig (or replace with the new computed initialMode) and
IdpConfigurationMode to locate the change.

In
`@packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx`:
- Around line 161-168: The certificate upload is currently allowing private key
files via the accept prop ('.key') on the FileUploadField inside
IdentityProviderConfigurationForm; remove '.key' from the accept list (keep
.pem, .crt, .cer, .cert) and add server/client-side validation in the
FileUploadField upload handler (used by form.onCertFileChange / form.certFile)
to reject files that contain private key markers (e.g. "-----BEGIN PRIVATE
KEY-----" or "-----BEGIN RSA PRIVATE KEY-----") and surface a clear validation
error to the user; this ensures the accept prop and the FileUploadField both
prevent private-key uploads.

---

Nitpick comments:
In
`@packages/ui/src/components/ConfigureSSO/steps/__tests__/SelectProviderStep.test.tsx`:
- Around line 105-132: Add a new test in SelectProviderStep.test.tsx that
simulates selecting the "Google Workspace" radio, clicking the Continue button,
and asserting the wiring: mock and verify setProvider was called with
'saml_google', createEnterpriseConnection was invoked with 'saml_google' and the
expected args, and the app navigated to the "configure" route (assert the mocked
router/navigation push was called with 'configure'); use the existing
createFixtures()/renderStep(wrapper) helpers and the same mocking utilities used
elsewhere in this file to locate and click the Continue button and to spy on
setProvider, createEnterpriseConnection, and the navigation method.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 48c9bbf8-15ce-459e-91a0-1010da42cdcd

📥 Commits

Reviewing files that changed from the base of the PR and between 37535f9 and a1018c2.

📒 Files selected for processing (18)
  • .changeset/beige-breads-bathe.md
  • packages/localizations/src/en-US.ts
  • packages/shared/src/react/hooks/useEnterpriseConnectionTestRuns.tsx
  • packages/shared/src/types/elementIds.ts
  • packages/shared/src/types/localization.ts
  • packages/ui/src/components/ConfigureSSO/elements/Stepper/Stepper.tsx
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/index.tsx
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlGoogleConfigureSteps.tsx
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlOktaConfigureSteps.tsx
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/index.tsx
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationModes.tsx
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderMetadataForm.tsx
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/useIdentityProviderMetadataForm.ts
  • packages/ui/src/components/ConfigureSSO/steps/SelectProviderStep.tsx
  • packages/ui/src/components/ConfigureSSO/steps/__tests__/SelectProviderStep.test.tsx
  • packages/ui/src/components/ConfigureSSO/types.ts
💤 Files with no reviewable changes (2)
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderMetadataForm.tsx
  • packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/useIdentityProviderMetadataForm.ts

Comment on lines +161 to +168
<FileUploadField
field={form.certificateField}
file={form.certFile}
onFileChange={form.onCertFileChange}
existingFilePresent={Boolean(form.existingCertPresent)}
labels={labels}
accept='.pem,.key,.crt,.cer,.cert'
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disallow private-key uploads in the certificate field

Line 167 includes .key in accepted extensions. That can lead users to upload private keys as “certificates,” risking secret leakage.

Suggested fix
-      accept='.pem,.key,.crt,.cer,.cert'
+      accept='.pem,.crt,.cer,.cert'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<FileUploadField
field={form.certificateField}
file={form.certFile}
onFileChange={form.onCertFileChange}
existingFilePresent={Boolean(form.existingCertPresent)}
labels={labels}
accept='.pem,.key,.crt,.cer,.cert'
/>
<FileUploadField
field={form.certificateField}
file={form.certFile}
onFileChange={form.onCertFileChange}
existingFilePresent={Boolean(form.existingCertPresent)}
labels={labels}
accept='.pem,.crt,.cer,.cert'
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx`
around lines 161 - 168, The certificate upload is currently allowing private key
files via the accept prop ('.key') on the FileUploadField inside
IdentityProviderConfigurationForm; remove '.key' from the accept list (keep
.pem, .crt, .cer, .cert) and add server/client-side validation in the
FileUploadField upload handler (used by form.onCertFileChange / form.certFile)
to reject files that contain private key markers (e.g. "-----BEGIN PRIVATE
KEY-----" or "-----BEGIN RSA PRIVATE KEY-----") and surface a clear validation
error to the user; this ensures the accept prop and the FileUploadField both
prevent private-key uploads.

@LauraBeatris LauraBeatris changed the title chore(ui): Add Google Workspace SAML to self-serve SSO feat(ui): Add Google Workspace SAML to self-serve SSO May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant