单点登录 (SSO)
OIDC OAuth2 SSO SAML
单点登录 (SSO) 允许用户使用一套凭据对多个应用程序进行身份验证。此插件支持 OpenID Connect (OIDC)、OAuth2 提供程序和 SAML 2.0。
此插件处于积极开发中,可能不适合生产环境使用。请在 GitHub 上报告任何问题或 bug,并在 security@better-auth.com 上报告任何安全问题。
安装
安装插件
npm install @better-auth/sso将插件添加到服务器
import { betterAuth } from "better-auth"
import { sso } from "@better-auth/sso";
const auth = betterAuth({
plugins: [
sso()
]
})迁移数据库
运行迁移或生成架构,以向数据库添加必要的字段和表。
npx @better-auth/cli migratenpx @better-auth/cli generate请参阅 架构 部分以手动添加字段。
添加客户端插件
import { createAuthClient } from "better-auth/client"
import { ssoClient } from "@better-auth/sso/client"
const authClient = createAuthClient({
plugins: [
ssoClient()
]
})使用
注册 OIDC 提供程序
要注册 OIDC 提供程序,请使用 registerSSOProvider 端点并提供提供程序的必要配置细节。
将使用提供程序 ID 自动生成重定向 URL。例如,如果提供程序 ID 是 hydra,则重定向 URL 将是 {baseURL}/api/auth/sso/callback/hydra。请注意,/api/auth 可能会根据您的基本路径配置而有所不同。
示例
import { authClient } from "@/lib/auth-client";
// 使用 OIDC 配置注册
await authClient.sso.register({
providerId: "example-provider",
issuer: "https://idp.example.com",
domain: "example.com",
oidcConfig: {
clientId: "client-id",
clientSecret: "client-secret",
authorizationEndpoint: "https://idp.example.com/authorize",
tokenEndpoint: "https://idp.example.com/token",
jwksEndpoint: "https://idp.example.com/jwks",
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",
},
});const { headers } = await signInWithTestUser();
await auth.api.registerSSOProvider({
body: {
providerId: "example-provider",
issuer: "https://idp.example.com",
domain: "example.com",
oidcConfig: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
authorizationEndpoint: "https://idp.example.com/authorize",
tokenEndpoint: "https://idp.example.com/token",
jwksEndpoint: "https://idp.example.com/jwks",
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",
},
},
headers,
});注册 SAML 提供程序
要注册 SAML 提供程序,请使用带有 SAML 配置细节的 registerSSOProvider 端点。提供程序将充当服务提供程序 (SP) 并与您的身份提供程序 (IdP) 集成。
import { authClient } from "@/lib/auth-client";
await authClient.sso.register({
providerId: "saml-provider",
issuer: "https://idp.example.com",
domain: "example.com",
samlConfig: {
entryPoint: "https://idp.example.com/sso",
cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
callbackUrl: "https://yourapp.com/api/auth/sso/saml2/callback/saml-provider",
audience: "https://yourapp.com",
wantAssertionsSigned: true,
signatureAlgorithm: "sha256",
digestAlgorithm: "sha256",
identifierFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
idpMetadata: {
metadata: "<!-- IdP Metadata XML -->",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-private-key-password",
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-encryption-key-password"
},
spMetadata: {
metadata: "<!-- SP Metadata XML -->",
binding: "post",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-sp-private-key-password",
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",
extraFields: {
department: "department",
role: "role"
}
},
});const { headers } = await signInWithTestUser();
await auth.api.registerSSOProvider({
body: {
providerId: "saml-provider",
issuer: "https://idp.example.com",
domain: "example.com",
samlConfig: {
entryPoint: "https://idp.example.com/sso",
cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
callbackUrl: "https://yourapp.com/api/auth/sso/saml2/callback/saml-provider",
audience: "https://yourapp.com",
wantAssertionsSigned: true,
signatureAlgorithm: "sha256",
digestAlgorithm: "sha256",
identifierFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
idpMetadata: {
metadata: "<!-- IdP Metadata XML -->",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-private-key-password",
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-encryption-key-password"
},
spMetadata: {
metadata: "<!-- SP Metadata XML -->",
binding: "post",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-sp-private-key-password",
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",
extraFields: {
department: "department",
role: "role"
}
},
},
headers,
});获取服务提供程序元数据
对于 SAML 提供程序,您可以检索需要在身份提供程序中配置的服务提供程序元数据 XML:
const response = await auth.api.spMetadata({
query: {
providerId: "saml-provider",
format: "xml" // 或 "json"
}
});
const metadataXML = await response.text();
console.log(metadataXML);使用 SSO 登录
要使用 SSO 提供程序登录,您可以调用 signIn.sso
您可以使用匹配域名的电子邮件登录:
const res = await authClient.signIn.sso({
email: "user@example.com",
callbackURL: "/dashboard",
});或者您可以指定域名:
const res = await authClient.signIn.sso({
domain: "example.com",
callbackURL: "/dashboard",
});如果提供程序与组织关联,您也可以使用组织 slug 登录:
const res = await authClient.signIn.sso({
organizationSlug: "example-org",
callbackURL: "/dashboard",
});或者,您可以使用提供程序的 ID 登录:
const res = await authClient.signIn.sso({
providerId: "example-provider-id",
callbackURL: "/dashboard",
});要使用服务器 API,您可以使用 signInSSO
const res = await auth.api.signInSSO({
body: {
organizationSlug: "example-org",
callbackURL: "/dashboard",
}
});完整方法
const { data, error } = await authClient.signIn.sso({ email: "john@example.com", organizationSlug: "example-org", providerId: "example-provider", domain: "example.com", callbackURL: "https://example.com/callback", // required errorCallbackURL: "https://example.com/callback", newUserCallbackURL: "https://example.com/new-user", scopes: ["openid", "email", "profile", "offline_access"], requestSignUp: true,});| Prop | Description | Type |
|---|---|---|
email? | 用于登录的电子邮件地址。此用于标识用于登录的颁发者。如果提供了颁发者,则为可选。 | string |
organizationSlug? | 用于登录的组织 slug。 | string |
providerId? | 用于登录的提供程序 ID。这可以代替电子邮件或颁发者提供。 | string |
domain? | 提供程序的域名。 | string |
callbackURL | 登录后重定向到的 URL。 | string |
errorCallbackURL? | 登录后重定向到的 URL。 | string |
newUserCallbackURL? | 如果用户是新用户,登录后重定向到的 URL。 | string |
scopes? | 从提供程序请求的范围。 | string[] |
requestSignUp? | 明确请求注册。当此提供程序的 disableImplicitSignUp 为 true 时有用。 | boolean |
当用户通过身份验证时,如果用户不存在,将使用 provisionUser 函数来配置用户。如果启用了组织配置并且提供程序与组织关联,用户将被添加到组织中。
const auth = betterAuth({
plugins: [
sso({
provisionUser: async (user) => {
// 配置用户
},
organizationProvisioning: {
disabled: false,
defaultRole: "member",
getRole: async (user) => {
// 如果需要,获取角色
},
},
}),
],
});配置
SSO 插件提供了强大的配置功能,当用户通过 SSO 提供程序登录时,可以自动设置用户并管理他们的组织成员资格。
用户配置
用户配置允许您在用户通过 SSO 提供程序登录时运行自定义逻辑。这对于以下情况很有用:
- 使用来自 SSO 提供程序的附加数据设置用户配置文件
- 将用户属性与外部系统同步
- 创建用户特定资源
- 记录 SSO 登录
- 从 SSO 提供程序更新用户信息
const auth = betterAuth({
plugins: [
sso({
provisionUser: async ({ user, userInfo, token, provider }) => {
// 使用 SSO 数据更新用户配置文件
await updateUserProfile(user.id, {
department: userInfo.attributes?.department,
jobTitle: userInfo.attributes?.jobTitle,
manager: userInfo.attributes?.manager,
lastSSOLogin: new Date(),
});
// 创建用户特定资源
await createUserWorkspace(user.id);
// 与外部系统同步
await syncUserWithCRM(user.id, userInfo);
// 记录 SSO 登录
await auditLog.create({
userId: user.id,
action: 'sso_signin',
provider: provider.providerId,
metadata: {
email: userInfo.email,
ssoProvider: provider.issuer,
},
});
},
}),
],
});provisionUser 函数接收:
- user:来自数据库的用户对象
- userInfo:来自 SSO 提供程序的用户信息(包括属性、电子邮件、姓名等)
- token:OAuth2 令牌(针对 OIDC 提供程序) - 对于 SAML 可能为 undefined
- provider:SSO 提供程序配置
组织配置
组织配置在 SSO 提供程序链接到特定组织时自动管理用户在组织中的成员资格。这对于以下情况特别有用:
- 企业 SSO,其中每个公司/域映射到组织
- 基于 SSO 属性的自动角色分配
- 通过 SSO 管理团队成员资格
基本组织配置
const auth = betterAuth({
plugins: [
sso({
organizationProvisioning: {
disabled: false, // 启用组织配置
defaultRole: "member", // 新成员的默认角色
},
}),
],
});带有自定义角色的高级组织配置
const auth = betterAuth({
plugins: [
sso({
organizationProvisioning: {
disabled: false,
defaultRole: "member",
getRole: async ({ user, userInfo, provider }) => {
// 基于 SSO 属性分配角色
const department = userInfo.attributes?.department;
const jobTitle = userInfo.attributes?.jobTitle;
// 基于职位标题的管理员
if (jobTitle?.toLowerCase().includes('manager') ||
jobTitle?.toLowerCase().includes('director') ||
jobTitle?.toLowerCase().includes('vp')) {
return "admin";
}
// IT 部门的特殊角色
if (department?.toLowerCase() === 'it') {
return "admin";
}
// 默认成员角色
return "member";
},
},
}),
],
});将 SSO 提供程序链接到组织
在注册 SSO 提供程序时,您可以将它链接到特定组织:
await auth.api.registerSSOProvider({
body: {
providerId: "acme-corp-saml",
issuer: "https://acme-corp.okta.com",
domain: "acmecorp.com",
organizationId: "org_acme_corp_id", // 链接到组织
samlConfig: {
// SAML 配置...
},
},
headers,
});现在,当来自 acmecorp.com 的用户通过此提供程序登录时,他们将自动被添加到“Acme Corp”组织中,并获得适当的角色。
多组织示例
您可以为不同组织设置多个 SSO 提供程序:
// Acme Corp SAML 提供程序
await auth.api.registerSSOProvider({
body: {
providerId: "acme-corp",
issuer: "https://acme.okta.com",
domain: "acmecorp.com",
organizationId: "org_acme_id",
samlConfig: { /* ... */ },
},
headers,
});
// TechStart OIDC 提供程序
await auth.api.registerSSOProvider({
body: {
providerId: "techstart-google",
issuer: "https://accounts.google.com",
domain: "techstart.io",
organizationId: "org_techstart_id",
oidcConfig: { /* ... */ },
},
headers,
});组织配置流程
- 用户通过链接到组织的 SSO 提供程序登录
- 用户通过身份验证 并在数据库中找到或创建
- 检查组织成员资格 - 如果用户尚未成为链接组织的成员
- 确定角色 使用
defaultRole或getRole函数 - 将用户添加到 组织中,并使用确定的角色
- 运行用户配置 (如果配置)以进行额外设置
配置最佳实践
1. 幂等操作
确保您的配置函数可以安全地多次运行:
provisionUser: async ({ user, userInfo }) => {
// 检查是否已配置
const existingProfile = await getUserProfile(user.id);
if (!existingProfile.ssoProvisioned) {
await createUserResources(user.id);
await markAsProvisioned(user.id);
}
// 始终更新属性(它们可能发生变化)
await updateUserAttributes(user.id, userInfo.attributes);
},2. 错误处理
优雅地处理错误以避免阻塞用户登录:
provisionUser: async ({ user, userInfo }) => {
try {
await syncWithExternalSystem(user, userInfo);
} catch (error) {
// 记录错误但不抛出 - 用户仍可以登录
console.error('Failed to sync user with external system:', error);
await logProvisioningError(user.id, error);
}
},3. 条件配置
仅在需要时运行某些配置步骤:
organizationProvisioning: {
disabled: false,
getRole: async ({ user, userInfo, provider }) => {
// 仅为某些提供程序处理角色分配
if (provider.providerId.includes('enterprise')) {
return determineEnterpriseRole(userInfo);
}
return "member";
},
},SAML 配置
服务提供程序配置
在注册 SAML 提供程序时,您需要提供服务提供程序 (SP) 元数据配置:
- metadata:服务提供程序的 XML 元数据
- binding:绑定方法,通常为 "post" 或 "redirect"
- privateKey:用于签名的私钥(可选)
- privateKeyPass:私钥密码(如果加密)
- isAssertionEncrypted:断言是否应加密
- encPrivateKey:解密私钥(如果启用加密)
- encPrivateKeyPass:加密私钥密码
身份提供程序配置
您还需要提供身份提供程序 (IdP) 配置:
- metadata:来自您的身份提供程序的 XML 元数据
- privateKey:IdP 通信的私钥(可选)
- privateKeyPass:IdP 私钥密码(如果加密)
- isAssertionEncrypted:来自 IdP 的断言是否加密
- encPrivateKey:IdP 断言解密的私钥
- encPrivateKeyPass:IdP 解密密钥密码
SAML 属性映射
配置 SAML 属性如何映射到用户字段:
mapping: {
id: "nameID", // 默认:"nameID"
email: "email", // 默认:"email" 或 "nameID"
name: "displayName", // 默认:"displayName"
firstName: "givenName", // 默认:"givenName"
lastName: "surname", // 默认:"surname"
extraFields: {
department: "department",
role: "jobTitle",
phone: "telephoneNumber"
}
}SAML 端点
插件会自动创建以下 SAML 端点:
- SP 元数据:
/api/auth/sso/saml2/sp/metadata?providerId={providerId} - SAML 回调:
/api/auth/sso/saml2/callback/{providerId}
架构
插件需要在 ssoProvider 表中添加额外字段来存储提供程序的配置。
| Field Name | Type | Key | Description |
|---|---|---|---|
| id | string | 数据库标识符 | |
| issuer | string | - | 颁发者标识符 |
| domain | string | - | 提供程序的域名 |
| oidcConfig | string | - | OIDC 配置 (JSON 字符串) |
| samlConfig | string | - | SAML 配置 (JSON 字符串) |
| userId | string | - | 用户 ID |
| providerId | string | - | 提供程序 ID。用于标识提供程序并生成重定向 URL。 |
| organizationId | string | - | 组织 ID。如果提供程序链接到组织。 |
选项
服务器
provisionUser:当用户使用 SSO 提供程序登录时,用于配置用户的自定义函数。
organizationProvisioning:将用户配置到组织的选项。
defaultOverrideUserInfo:默认使用提供程序信息覆盖用户信息。
disableImplicitSignUp:禁用新用户的隐式注册。
trustEmailVerified:信任来自提供程序的电子邮件验证标志。
| Prop | Type | Default |
|---|---|---|
provisionUser? | function | - |
organizationProvisioning? | object | - |
defaultOverrideUserInfo? | boolean | - |
disableImplicitSignUp? | boolean | - |
providersLimit? | number | function | 10 |