Skip to content

Commit 2f2d8bb

Browse files
committed
fix(sso): correct SAML callback path and generate idpMetadata from cert+entryPoint
1 parent 1739a4e commit 2f2d8bb

File tree

4 files changed

+75
-24
lines changed

4 files changed

+75
-24
lines changed

apps/docs/content/docs/en/enterprise/sso.mdx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,18 @@ Go to **Settings → Enterprise → Single Sign-On** in your workspace.
6565

6666
The **Callback URL** shown in the form is the endpoint your identity provider must redirect users back to after authentication. Copy it and register it in your IdP before saving.
6767

68+
**OIDC providers** (Okta, Microsoft Entra ID, Google Workspace, Auth0):
6869
```
6970
https://simstudio.ai/api/auth/sso/callback/{provider-id}
7071
```
7172

72-
For self-hosted:
73+
**SAML providers** (ADFS, Shibboleth):
7374
```
74-
https://your-instance.com/api/auth/sso/callback/{provider-id}
75+
https://simstudio.ai/api/auth/sso/saml2/callback/{provider-id}
7576
```
7677

78+
For self-hosted, replace `simstudio.ai` with your instance hostname.
79+
7780
### 5. Save and test
7881

7982
Click **Save**. To test, sign out and use the **Sign in with SSO** button on the login page. Enter an email address at your configured domain — Sim will redirect you to your identity provider.
@@ -194,9 +197,9 @@ Click **Save**. To test, sign out and use the **Sign in with SSO** button on the
194197
For self-hosted, use your instance's base URL (e.g. `https://sim.company.com`)
195198
4. Add an endpoint: **SAML Assertion Consumer Service** (HTTP POST) with the URL:
196199
```
197-
https://simstudio.ai/api/auth/sso/callback/adfs
200+
https://simstudio.ai/api/auth/sso/saml2/callback/adfs
198201
```
199-
For self-hosted: `https://sim.company.com/api/auth/sso/callback/adfs`
202+
For self-hosted: `https://sim.company.com/api/auth/sso/saml2/callback/adfs`
200203
5. Export the **Token-signing certificate** from **Certificates**: right-click**View CertificateDetailsCopy to File**, choose **Base-64 encoded X.509 (.CER)**. The `.cer` file is PEM-encodedrename it to `.pem` before pasting its contents into Sim.
201204
6. Note the **ADFS Federation Service endpoint URL** (e.g. `https://adfs.company.com/adfs/ls`)
202205

@@ -270,7 +273,7 @@ Users who sign in via SSO for the first time are automatically provisioned and a
270273
},
271274
{
272275
question: "What is the Callback URL?",
273-
answer: "The Callback URL (also called Redirect URI or ACS URL) is the endpoint in Sim that receives the authentication response from your identity provider. It follows the format: https://simstudio.ai/api/auth/sso/callback/{provider-id}. You must register this URL in your identity provider before SSO will work."
276+
answer: "The Callback URL (also called Redirect URI or ACS URL) is the endpoint in Sim that receives the authentication response from your identity provider. For OIDC providers it follows the format: https://simstudio.ai/api/auth/sso/callback/{provider-id}. For SAML providers it is: https://simstudio.ai/api/auth/sso/saml2/callback/{provider-id}. You must register this URL in your identity provider before SSO will work."
274277
},
275278
{
276279
question: "How do I update or replace an existing SSO configuration?",

apps/sim/app/api/auth/sso/register/route.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ export async function POST(request: NextRequest) {
344344
} = body
345345

346346
const computedCallbackUrl =
347-
callbackUrl || `${getBaseUrl()}/api/auth/sso/callback/${providerId}`
347+
callbackUrl || `${getBaseUrl()}/api/auth/sso/saml2/callback/${providerId}`
348348

349349
const escapeXml = (str: string) =>
350350
str.replace(/[<>&"']/g, (c) => {
@@ -371,13 +371,38 @@ export async function POST(request: NextRequest) {
371371
</md:SPSSODescriptor>
372372
</md:EntityDescriptor>`
373373

374+
const certBase64 = cert
375+
.replace(/-----BEGIN CERTIFICATE-----/g, '')
376+
.replace(/-----END CERTIFICATE-----/g, '')
377+
.replace(/\s/g, '')
378+
379+
const computedIdpMetadataXml =
380+
idpMetadata ||
381+
`<?xml version="1.0"?>
382+
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${escapeXml(entryPoint)}">
383+
<IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
384+
<KeyDescriptor use="signing">
385+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
386+
<ds:X509Data>
387+
<ds:X509Certificate>${certBase64}</ds:X509Certificate>
388+
</ds:X509Data>
389+
</ds:KeyInfo>
390+
</KeyDescriptor>
391+
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${escapeXml(entryPoint)}"/>
392+
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="${escapeXml(entryPoint)}"/>
393+
</IDPSSODescriptor>
394+
</EntityDescriptor>`
395+
374396
const samlConfig: any = {
375397
entryPoint,
376398
cert,
377399
callbackUrl: computedCallbackUrl,
378400
spMetadata: {
379401
metadata: spMetadataXml,
380402
},
403+
idpMetadata: {
404+
metadata: computedIdpMetadataXml,
405+
},
381406
mapping,
382407
}
383408

@@ -386,11 +411,6 @@ export async function POST(request: NextRequest) {
386411
if (signatureAlgorithm) samlConfig.signatureAlgorithm = signatureAlgorithm
387412
if (digestAlgorithm) samlConfig.digestAlgorithm = digestAlgorithm
388413
if (identifierFormat) samlConfig.identifierFormat = identifierFormat
389-
if (idpMetadata) {
390-
samlConfig.idpMetadata = {
391-
metadata: idpMetadata,
392-
}
393-
}
394414

395415
providerConfig.samlConfig = samlConfig
396416
providerConfig.mapping = undefined

apps/sim/ee/sso/components/sso-settings.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ export function SSO() {
320320
})
321321
}
322322

323-
const callbackUrl = `${getBaseUrl()}/api/auth/sso/callback/${formData.providerId || existingProvider?.providerId || 'provider-id'}`
323+
const isSaml = formData.providerType === 'saml'
324+
const callbackUrl = `${getBaseUrl()}/api/auth/${isSaml ? 'sso/saml2/callback' : 'sso/callback'}/${formData.providerId || existingProvider?.providerId || 'provider-id'}`
324325

325326
const copyToClipboard = async (url: string) => {
326327
try {
@@ -390,7 +391,7 @@ export function SSO() {
390391
}
391392

392393
if (existingProvider && !isEditing) {
393-
const providerCallbackUrl = `${getBaseUrl()}/api/auth/sso/callback/${existingProvider.providerId}`
394+
const providerCallbackUrl = `${getBaseUrl()}/api/auth/${existingProvider.providerType === 'saml' ? 'sso/saml2/callback' : 'sso/callback'}/${existingProvider.providerId}`
394395

395396
return (
396397
<div className='flex h-full flex-col gap-4.5'>
@@ -765,7 +766,7 @@ export function SSO() {
765766
<FormField label='Callback URL Override' optional>
766767
<Input
767768
type='url'
768-
placeholder={`${getBaseUrl()}/api/auth/sso/callback/provider-id`}
769+
placeholder={`${getBaseUrl()}/api/auth/sso/saml2/callback/provider-id`}
769770
value={formData.callbackUrl}
770771
autoComplete='off'
771772
autoCapitalize='none'

packages/db/scripts/register-sso-provider.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* SAML Providers:
2727
* SSO_SAML_ENTRY_POINT=https://your-idp/sso
2828
* SSO_SAML_CERT=your-certificate-pem-string
29-
* SSO_SAML_CALLBACK_URL=https://yourdomain.com/api/auth/sso/callback/provider-id
29+
* SSO_SAML_CALLBACK_URL=https://yourdomain.com/api/auth/sso/saml2/callback/provider-id
3030
* SSO_SAML_SP_METADATA=<custom-sp-metadata-xml> (optional, auto-generated if not provided)
3131
* SSO_SAML_IDP_METADATA=<idp-metadata-xml> (optional)
3232
* SSO_SAML_AUDIENCE=https://yourdomain.com (optional, defaults to SSO_ISSUER)
@@ -242,7 +242,7 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
242242
).replace(/\/$/, '')
243243

244244
const callbackUrl =
245-
process.env.SSO_SAML_CALLBACK_URL || `${appBaseUrl}/api/auth/sso/callback/${providerId}`
245+
process.env.SSO_SAML_CALLBACK_URL || `${appBaseUrl}/api/auth/sso/saml2/callback/${providerId}`
246246

247247
let spMetadata = process.env.SSO_SAML_SP_METADATA
248248
if (!spMetadata) {
@@ -254,6 +254,31 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
254254
</md:EntityDescriptor>`
255255
}
256256

257+
const idpMetadataXml = process.env.SSO_SAML_IDP_METADATA
258+
let computedIdpMetadata: string
259+
if (idpMetadataXml) {
260+
computedIdpMetadata = idpMetadataXml
261+
} else {
262+
const certBase64 = cert
263+
.replace(/-----BEGIN CERTIFICATE-----/g, '')
264+
.replace(/-----END CERTIFICATE-----/g, '')
265+
.replace(/\s/g, '')
266+
computedIdpMetadata = `<?xml version="1.0"?>
267+
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${entryPoint}">
268+
<IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
269+
<KeyDescriptor use="signing">
270+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
271+
<ds:X509Data>
272+
<ds:X509Certificate>${certBase64}</ds:X509Certificate>
273+
</ds:X509Data>
274+
</ds:KeyInfo>
275+
</KeyDescriptor>
276+
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${entryPoint}"/>
277+
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="${entryPoint}"/>
278+
</IDPSSODescriptor>
279+
</EntityDescriptor>`
280+
}
281+
257282
config.samlConfig = {
258283
issuer,
259284
entryPoint,
@@ -268,12 +293,9 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null {
268293
metadata: spMetadata,
269294
entityID: appBaseUrl,
270295
},
271-
}
272-
const idpMetadata = process.env.SSO_SAML_IDP_METADATA
273-
if (idpMetadata) {
274-
config.samlConfig.idpMetadata = {
275-
metadata: idpMetadata,
276-
}
296+
idpMetadata: {
297+
metadata: computedIdpMetadata,
298+
},
277299
}
278300
}
279301

@@ -598,8 +620,13 @@ async function registerSSOProvider(): Promise<boolean> {
598620

599621
const baseUrl =
600622
process.env.NEXT_PUBLIC_APP_URL || process.env.BETTER_AUTH_URL || 'https://your-domain.com'
601-
const callbackUrl = `${baseUrl}/api/auth/sso/callback/${ssoConfig.providerId}`
602-
logger.info(`📋 Callback URL (configure this in your identity provider): ${callbackUrl}`)
623+
const callbackPath =
624+
ssoConfig.providerType === 'saml'
625+
? `api/auth/sso/saml2/callback/${ssoConfig.providerId}`
626+
: `api/auth/sso/callback/${ssoConfig.providerId}`
627+
logger.info(
628+
`📋 Callback URL (configure this in your identity provider): ${baseUrl}/${callbackPath}`
629+
)
603630

604631
return true
605632
} catch (error) {

0 commit comments

Comments
 (0)