') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(ui): Add Google Workspace SAML to self-serve SSO by LauraBeatris · Pull Request #8690 · clerk/javascript · GitHub
Skip to content
Merged
7 changes: 7 additions & 0 deletions .changeset/beige-breads-bathe.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/shared': patch
'@clerk/ui': patch
---

Add support for Google Workspace SAML provider to self-serve SSO
105 changes: 102 additions & 3 deletions packages/localizations/src/en-US.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -242,6 +242,7 @@ export const enUS: LocalizationResource = {
groupLabel: 'SAML',
okta: 'Okta Workforce',
customSaml: 'Custom SAML Provider',
google: 'Google Workspace',
},
warning: 'Once a provider is selected you cannot change again until the configuration is over',
},
Expand DownExpand Up@@ -359,11 +360,11 @@ export const enUS: LocalizationResource = {
step5: 'Click <bold>Next</bold> to complete creating the application.',
},
serviceProviderInstructions: {
title: 'Configure service provider',
title: 'Add service provider configuration to Okta',
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:',
'To configure your service provider, you must add these two fields to your Okta SAML application:',
serviceProviderFields: {
acsUrl: {
label: 'Single sign-on URL',
Expand All@@ -387,7 +388,7 @@ export const enUS: LocalizationResource = {
step2: 'Select <bold>Add Expression</bold> for each row below, then enter the matching name and value:',
attributeMappingTable: {
columns: {
name: 'Name',
name: 'Attribute name',
expression: 'Expression',
},
rows: {
Expand DownExpand Up@@ -516,6 +517,104 @@ export const enUS: LocalizationResource = {
},
},
},
samlGoogle: {
mainHeaderTitle: 'Configure Google Workspace',
createAppStep: {
headerSubtitle: 'Create a new enterprise application in your Google Workspace',
createAppInstructions: {
title: 'Create a new enterprise application in Google Workspace',
step1: 'Sign in to Google Admin Portal.',
step2: 'In the side navigation, under <bold>Apps</bold>, select <bold>Web and mobile apps.</bold>',
step3: 'Click on the <bold>Add</bold> app button, and select <bold>Add custom SAML app.</bold>',
step4: 'In the <bold>App details</bold> section, fill out the required <bold>App name</bold>.',
step5: 'Select the <bold>Continue</bold> button.',
},
},
identityProviderMetadataStep: {
headerSubtitle: 'Configure identity provider metadata',
modes: {
title: 'Fill in your Google Workspace application details',
ariaLabel: 'Configuration ',
metadataFile: 'Add via metadata',
manual: 'Configure manually',
},
metadataFile: {
label: 'IdP metadata',
description: 'In your Google Workspace application, download the IdP metadata and upload it below.',
uploadFile: 'Upload file',
replaceFile: 'Replace file',
removeFile: 'Remove file',
fileUploaded: 'File uploaded',
},
manual: {
description: 'In your Google Workspace application, retrieve these values.',
signOnUrl: {
label: 'SSO URL',
placeholder: 'Paste URL here...',
},
issuer: {
label: 'Entity ID',
placeholder: 'Paste URL here...',
},
signingCertificate: {
label: 'Signing certificate',
uploadFile: 'Upload file',
replaceFile: 'Replace file',
removeFile: 'Remove file',
fileUploaded: 'File uploaded',
},
},
},
serviceProviderStep: {
headerSubtitle: 'Configure service provider',
title: 'Configure service provider',
paragraph:
'To configure your service provider, you must add these two fields to your Google Workspace SAML application:',
serviceProviderFields: {
acsUrl: {
label: 'ACS URL',
},
spEntityId: {
label: 'Entity ID',
},
},
nameIdInstructions: {
step1:
'Under the <bold>Name ID</bold> section, select the <bold>Name ID</bold> format dropdown and select <bold>Email</bold>.',
step2: 'Select <bold>Continue</bold>',
},
},
attributeMappingStep: {
headerSubtitle: 'Map user attributes from Google Workspace to your application',
paragraph: 'We expect your SAML response to return the user’s email, first name and last name.',
step1: 'In the <bold>Google Admin Console</bold>, find the <bold>Attributes</bold> section.',
step2:
'Select <bold>Add mapping</bold> for each attribute, and enter the following Google and app attribute:',
attributeMappingTable: {
columns: {
googleAttribute: 'Google attribute',
appAttribute: 'App attribute',
},
rows: {
email: { googleAttribute: 'Primary email', appAttribute: 'email' },
firstName: { googleAttribute: 'First name', appAttribute: 'firstName' },
lastName: { googleAttribute: 'Last name', appAttribute: 'lastName' },
},
},
},
configureUserAccess: {
headerSubtitle: 'Enable your Google Workspace SAML application',
assignUsersInstructions: {
paragraph1:
"Once the configuration is complete in Google, you'll be redirected to the app's overview page.",
step1: 'Open the <bold>User access</bold> section.',
step2: 'Select <bold>ON for everyone.</bold>',
step3: 'Select <bold>Save</bold>.',
paragraph2:
'Google may take up to 24 hours to propagate these changes. The connection will remain inactive until they take effect.',
},
},
},
},
},
createOrganization: {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,7 @@ function useEnterpriseConnectionTestRuns(
},
enabled: queryEnabled,
refetchIntervalInBackground: false,
refetchOnWindowFocus: false,
});

const hasRows = (query.data?.data?.length ?? 0) > 0;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/types/elementIds.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ export type FieldId =
| 'apiKeySecret'
| 'idpCertificate'
| 'idpEntityId'
| 'idpMetadata'
| 'idpMetadataUrl'
| 'idpSsoUrl'
| 'acsUrl'
Expand Down
94 changes: 94 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1308,6 +1308,7 @@ export type __internal_LocalizationResource = {
groupLabel: LocalizationValue;
okta: LocalizationValue;
customSaml: LocalizationValue;
google: LocalizationValue;
};
warning: LocalizationValue;
};
Expand DownExpand Up@@ -1570,6 +1571,99 @@ export type __internal_LocalizationResource = {
};
};
};
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;
};
};
};
};
confirmation: {
statusSection: {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,6 +95,7 @@ const Item = ({
fontSize: theme.fontSizes.$xs,
fontWeight: theme.fontWeights.$medium,
color: theme.colors.$colorBackground,
lineHeight: '1rem',
})}
>
{bullet}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,12 +6,12 @@ import { useConfigureSSO } from '../../ConfigureSSOContext';
import { Step } from '../../elements/Step';
import { Wizard } from '../../elements/Wizard';
import type { ProviderType } from '../../types';
import { SamlCustomConfigureSteps } from './saml/SamlCustomConfigureSteps';
import { SamlOktaConfigureSteps } from './saml/SamlOktaConfigureSteps';
import { SamlCustomConfigureSteps, SamlGoogleConfigureSteps, SamlOktaConfigureSteps } from './saml';

const STEPS_BY_PROVIDER: Record<ProviderType, () => JSX.Element> = {
saml_custom: SamlCustomConfigureSteps,
saml_okta: SamlOktaConfigureSteps,
saml_google: SamlGoogleConfigureSteps,
};

export const ConfigureStep = (): JSX.Element | null => {
Expand Down
Loading
Loading