feat(sso): defaultSSO options and ACS endpoint (#3660)

Co-authored-by: Bereket Engida <Bekacru@gmail.com>
Co-authored-by: Bereket Engida <86073083+Bekacru@users.noreply.github.com>
This commit is contained in:
KinfeMichael Tariku
2025-09-19 20:26:53 +03:00
committed by GitHub
parent a208c09894
commit b3ead859e6
10 changed files with 1351 additions and 225 deletions

View File

@@ -0,0 +1,174 @@
---
title: SAML SSO with Okta
description: A guide to integrating SAML Single Sign-On (SSO) with Better Auth, featuring Okta
---
This guide walks you through setting up SAML Single Sign-On (SSO) with your Identity Provider (IdP), using Okta as an example. For advanced configuration details and the full API reference, check out the [SSO Plugin Documentation](/docs/plugins/sso).
## What is SAML?
SAML (Security Assertion Markup Language) is an XML-based standard for exchanging authentication and authorization data between an Identity Provider (IdP) (e.g., Okta, Azure AD, OneLogin) and a Service Provider (SP) (in this case, Better Auth).
In this setup:
- **IdP (Okta)**: Authenticates users and sends assertions about their identity.
- **SP (Better Auth)**: Validates assertions and logs the user in.up.
### Step 1: Create a SAML Application in Okta
1. Log in to your Okta Admin Console
2. Navigate to Applications > Applications
3. Click "Create App Integration"
4. Select "SAML 2.0" as the Sign-in method
5. Configure the following settings:
- **Single Sign-on URL**: Your Better Auth ACS endpoint (e.g., `http://localhost:3000/api/auth/sso/saml2/sp/acs/sso`). while `sso` being your providerId
- **Audience URI (SP Entity ID)**: Your Better Auth metadata URL (e.g., `http://localhost:3000/api/auth/sso/saml2/sp/metadata`)
- **Name ID format**: Email Address or any of your choice.
6. Download the IdP metadata XML file and certificate
### Step 2: Configure Better Auth
Heres an example configuration for Okta in a dev environment:
```typescript
const ssoConfig = {
defaultSSO: [{
domain: "localhost:3000", // Your domain
providerId: "sso",
samlConfig: {
// SP Configuration
issuer: "http://localhost:3000/api/auth/sso/saml2/sp/metadata",
entryPoint: "https://trial-1076874.okta.com/app/trial-1076874_samltest_1/exktofb0a62hqLAUL697/sso/saml",
callbackUrl: "/dashboard", // Redirect after successful authentication
// IdP Configuration
idpMetadata: {
entityID: "https://trial-1076874.okta.com/app/exktofb0a62hqLAUL697/sso/saml/metadata",
singleSignOnService: [{
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
Location: "https://trial-1076874.okta.com/app/trial-1076874_samltest_1/exktofb0a62hqLAUL697/sso/saml"
}],
cert: `-----BEGIN CERTIFICATE-----
MIIDqjCCApKgAwIBAgIGAZhVGMeUMA0GCSqGSIb3DQEBCwUAMIGVMQswCQYDVQQGEwJVUzETMBEG
...
[Your Okta Certificate]
...
-----END CERTIFICATE-----`
},
// SP Metadata
spMetadata: {
metadata: `<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
entityID="http://localhost:3000/api/sso/saml2/sp/metadata">
...
[Your SP Metadata XML]
...
</md:EntityDescriptor>`
}
}
}]
}
```
### Step 3: Multiple Default Providers (Optional)
You can configure multiple SAML providers for different domains:
```typescript
const ssoConfig = {
defaultSSO: [
{
domain: "company.com",
providerId: "company-okta",
samlConfig: {
// Okta SAML configuration for company.com
}
},
{
domain: "partner.com",
providerId: "partner-adfs",
samlConfig: {
// ADFS SAML configuration for partner.com
}
},
{
domain: "contractor.org",
providerId: "contractor-azure",
samlConfig: {
// Azure AD SAML configuration for contractor.org
}
}
]
}
```
<Callout type="info">
**Explicit**: Pass providerId directly when signing in.
**Domain fallback:** Matches based on the users email domain. e.g. user@company.com → matches `company-okta` provider.
</Callout>
### Step 4: Initiating Sign-In
You can start an SSO flow in three ways:
**1. Explicitly by `providerId` (recommended):**
```typescript
// Explicitly specify which provider to use
await authClient.signIn.sso({
providerId: "company-okta",
callbackURL: "/dashboard"
});
```
**2. By email domain matching:**
```typescript
// Automatically matches provider based on email domain
await authClient.signIn.sso({
email: "user@company.com",
callbackURL: "/dashboard"
});
```
**3. By specifying domain:**
```typescript
// Explicitly specify domain for matching
await authClient.signIn.sso({
domain: "partner.com",
callbackURL: "/dashboard"
});
```
**Important Notes**:
- DummyIDP should ONLY be used for development and testing
- Never use these certificates in production
- The example uses `localhost:3000` - adjust URLs for your environment
- For production, always use proper IdP providers like Okta, Azure AD, or OneLogin
### Step 5: Dynamically Registering SAML Providers
For dynamic registration, you should register SAML providers using the API. See the [SSO Plugin Documentation](/docs/plugins/sso#register-a-saml-provider) for detailed registration instructions.
Example registration:
```typescript
await authClient.sso.register({
providerId: "okta-prod",
issuer: "https://your-domain.com",
domain: "your-domain.com",
samlConfig: {
// Your production SAML configuration
}
});
```
## Additional Resources
- [SSO Plugin Documentation](/docs/plugins/sso)
- [Okta SAML Documentation](https://developer.okta.com/docs/concepts/saml/)
- [SAML 2.0 Specification](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf)

View File

@@ -7,11 +7,6 @@ description: Integrate Single Sign-On (SSO) with your application.
Single Sign-On (SSO) allows users to authenticate with multiple applications using a single set of credentials. This plugin supports OpenID Connect (OIDC), OAuth2 providers, and SAML 2.0.
<Callout type="warn">
This plugin is in active development and may not be suitable for production use. Please report any issues or bugs on [GitHub](https://github.com/better-auth/better-auth) and any security concerns on [security@better-auth.com](mailto:security@better-auth.com).
</Callout>
## Installation
<Steps>
@@ -100,14 +95,18 @@ await authClient.sso.register({
discoveryEndpoint: "https://idp.example.com/.well-known/openid-configuration",
scopes: ["openid", "email", "profile"],
pkce: true,
},
mapping: {
id: "sub",
email: "email",
emailVerified: "email_verified",
name: "name",
image: "picture",
},
mapping: {
id: "sub",
email: "email",
emailVerified: "email_verified",
name: "name",
image: "picture",
extraFields: {
department: "department",
role: "role"
}
}
}
});
```
</Tab>
@@ -129,14 +128,18 @@ await auth.api.registerSSOProvider({
discoveryEndpoint: "https://idp.example.com/.well-known/openid-configuration",
scopes: ["openid", "email", "profile"],
pkce: true,
},
mapping: {
id: "sub",
email: "email",
emailVerified: "email_verified",
name: "name",
image: "picture",
},
mapping: {
id: "sub",
email: "email",
emailVerified: "email_verified",
name: "name",
image: "picture",
extraFields: {
department: "department",
role: "role"
}
}
}
},
headers,
});
@@ -183,19 +186,20 @@ await authClient.sso.register({
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-sp-encryption-key-password"
},
mapping: {
id: "nameID",
email: "email",
name: "displayName",
firstName: "givenName",
lastName: "surname",
emailVerified: "email_verified",
extraFields: {
department: "department",
role: "role"
}
}
},
mapping: {
id: "nameID",
email: "email",
name: "displayName",
firstName: "givenName",
lastName: "surname",
extraFields: {
department: "department",
role: "role"
}
},
}
});
```
</Tab>
@@ -233,19 +237,20 @@ await auth.api.registerSSOProvider({
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-sp-encryption-key-password"
},
mapping: {
id: "nameID",
email: "email",
name: "displayName",
firstName: "givenName",
lastName: "surname",
emailVerified: "email_verified",
extraFields: {
department: "department",
role: "role"
}
}
},
mapping: {
id: "nameID",
email: "email",
name: "displayName",
firstName: "givenName",
lastName: "surname",
extraFields: {
department: "department",
role: "role"
}
},
}
},
headers,
});
@@ -611,6 +616,36 @@ organizationProvisioning: {
## SAML Configuration
### Default SSO Provider
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
sso({
defaultSSO: {
providerId: "default-saml", // Provider ID for the default provider
samlConfig: {
issuer: "https://your-app.com",
entryPoint: "https://idp.example.com/sso",
cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
callbackUrl: "http://localhost:3000/api/auth/sso/saml2/sp/acs",
spMetadata: {
entityID: "http://localhost:3000/api/auth/sso/saml2/sp/metadata",
metadata: "<!-- Your SP Metadata XML -->",
}
}
}
})
]
});
```
The defaultSSO provider will be used when:
1. No matching provider is found in the database
This allows you to test SAML authentication without setting up providers in the database. The defaultSSO provider supports all the same configuration options as regular SAML providers.
### Service Provider Configuration
When registering a SAML provider, you need to provide Service Provider (SP) metadata configuration:
@@ -679,6 +714,8 @@ The plugin requires additional fields in the `ssoProvider` table to store the pr
]}
/>
For a detailed guide on setting up SAML SSO with examples for Okta and testing with DummyIDP, see our [SAML SSO Setup Guide](/docs/guides/sso-saml-guide).
## Options
### Server
@@ -735,5 +772,31 @@ The plugin requires additional fields in the `ssoProvider` table to store the pr
type: "number | function",
default: 10,
},
defaultSSO: {
description: "Configure a default SSO provider for testing and development. This provider will be used when no matching provider is found in the database.",
type: "object",
properties: {
domain: {
description: "The domain to match for this default provider.",
type: "string",
required: true,
},
providerId: {
description: "The provider ID to use for the default provider.",
type: "string",
required: true,
},
samlConfig: {
description: "SAML configuration for the default provider.",
type: "SAMLConfig",
required: false,
},
oidcConfig: {
description: "OIDC configuration for the default provider.",
type: "OIDCConfig",
required: false,
},
},
},
}}
/>