OIDC 提供程序

OIDC 提供者插件 使您能够构建和管理自己的 OpenID Connect (OIDC) 提供者,从而完全掌控用户身份验证,无需依赖 Okta 或 Azure AD 等第三方服务。它还允许其他服务通过您的 OIDC 提供者进行用户身份验证。

主要功能

  • 客户端注册:注册客户端以通过您的 OIDC 提供者进行身份验证。
  • 动态客户端注册:允许客户端动态注册。
  • 受信任客户端:配置硬编码的受信任客户端,可选择绕过用户同意流程。
  • 授权码流程:支持授权码流程(Authorization Code Flow)。
  • 公共客户端:支持 SPA、移动应用、CLI 工具等的公共客户端。
  • JWKS 端点:发布 JWKS 端点,允许客户端验证令牌。(尚未完全实现)
  • 刷新令牌:签发刷新令牌,并使用 refresh_token 授权类型处理访问令牌的续期。
  • OAuth 同意界面:实现用于用户授权的 OAuth 同意界面,并为受信任应用提供绕过同意的选项。
  • 用户信息端点:提供用户信息端点(UserInfo endpoint),供客户端获取用户详细信息。

此插件正在积极开发中,可能不适合生产环境使用。请在 GitHub 上报告任何问题或错误。

安装

挂载插件

将 OIDC 插件添加到您的 auth 配置中。有关如何配置插件,请参阅 OIDC 配置

auth.ts
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";

const auth = betterAuth({
    plugins: [oidcProvider({
        loginPage: "/sign-in", // 登录页面的路径
        // ...其他选项
    })]
})

迁移数据库

运行迁移或生成模式,以向数据库添加必要的字段和表。

npx @better-auth/cli migrate
npx @better-auth/cli generate

有关手动添加字段,请参阅 Schema 部分。

添加客户端插件

将 OIDC 客户端插件添加到您的 auth 客户端配置中。

import { createAuthClient } from "better-auth/client";
import { oidcClient } from "better-auth/client/plugins"
const authClient = createAuthClient({
    plugins: [oidcClient({
        // 您的 OIDC 配置
    })]
})

使用

安装完成后,您可以使用 OIDC Provider 来管理应用程序内的认证流程。

注册新客户端

要注册新的 OIDC 客户端,请使用 oauth2.register 方法。

简单示例

const application = await client.oauth2.register({
    client_name: "My Client",
    redirect_uris: ["https://client.example.com/callback"],
});

完整方法

POST
/oauth2/register
const { data, error } = await authClient.oauth2.register({    redirect_uris: ["https://client.example.com/callback"], // required    token_endpoint_auth_method: "client_secret_basic",    grant_types: ["authorization_code"],    response_types: ["code"],    client_name: "My App",    client_uri: "https://client.example.com",    logo_uri: "https://client.example.com/logo.png",    scope: "profile email",    contacts: ["admin@example.com"],    tos_uri: "https://client.example.com/tos",    policy_uri: "https://client.example.com/policy",    jwks_uri: "https://client.example.com/jwks",    jwks: {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."}]},    metadata: {"key": "value"},    software_id: "my-software",    software_version: "1.0.0",    software_statement,});
属性描述类型
redirect_uris
重定向 URI 列表
string[]
token_endpoint_auth_method?
令牌端点的认证方法
"none" | "client_secret_basic" | "client_secret_post"
grant_types?
应用支持的授权类型
("authorization_code" | "implicit" | "password" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:jwt-bearer" | "urn:ietf:params:oauth:grant-type:saml2-bearer")[]
response_types?
应用支持的响应类型
("code" | "token")[]
client_name?
应用名称
string
client_uri?
应用 URI
string
logo_uri?
应用 Logo URI
string
scope?
应用支持的作用域。使用空格分隔
string
contacts?
应用的联系信息
string[]
tos_uri?
应用服务条款 URI
string
policy_uri?
应用隐私政策 URI
string
jwks_uri?
应用 JWKS URI
string
jwks?
应用的 JWKS
Record<string, any>
metadata?
应用的元数据
Record<string, any>
software_id?
应用的软件 ID
string
software_version?
应用的软件版本
string
software_statement?
应用的软件声明
string

此端点支持符合 RFC7591 规范的客户端注册。

应用创建后,您将收到可以展示给用户的 client_idclient_secret

此端点支持符合 RFC7591 规范的客户端注册。

可信客户端

对于第一方应用程序和内部服务,您可以直接在 OIDC 提供者配置中配置可信客户端。可信客户端绕过数据库查询以获得更好的性能,并且可以选择跳过同意屏幕以改善用户体验。

auth.ts
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";

const auth = betterAuth({
    plugins: [
      oidcProvider({
        loginPage: "/sign-in",
        trustedClients: [
            {
                clientId: "internal-dashboard",
                clientSecret: "secure-secret-here",
                name: "Internal Dashboard",
                type: "web",
                redirectURLs: ["https://dashboard.company.com/auth/callback"],
                disabled: false,
                skipConsent: true, // 为此可信客户端跳过同意步骤
                metadata: { internal: true }
            },
            {
                clientId: "mobile-app",
                clientSecret: "mobile-secret", 
                name: "Company Mobile App",
                type: "native",
                redirectURLs: ["com.company.app://auth"],
                disabled: false,
                skipConsent: false, // 如有需要仍要求同意
                metadata: {}
            }
        ]
    })]
})

用户信息端点

OIDC 提供程序包含一个用户信息端点,允许客户端检索已认证用户的信息。该端点位于 /oauth2/userinfo,需要有效的访问令牌。

GET
/oauth2/userinfo
client-app.ts
// 客户端使用用户信息端点的示例
const response = await fetch('https://your-domain.com/api/auth/oauth2/userinfo', {
  headers: {
    'Authorization': 'Bearer ACCESS_TOKEN'
  }
});

const userInfo = await response.json();
// userInfo 包含基于授权范围的用户详细信息

用户信息端点根据授权期间授予的范围返回不同的声明:

  • 使用 openid 范围:返回用户 ID(sub 声明)
  • 使用 profile 范围:返回姓名、图片、名、姓
  • 使用 email 范围:返回邮箱和邮箱验证状态

getAdditionalUserInfoClaim 函数接收用户对象、请求的范围数组和客户端,允许您根据授权期间授予的范围有条件地包含声明。这些附加声明将包含在用户信息端点响应和 ID 令牌中。

同意屏幕

当用户被重定向到 OIDC 提供程序进行身份验证时,可能会提示他们授权应用程序访问其数据。这被称为同意屏幕。默认情况下,Better Auth 会显示一个示例同意屏幕。您可以在初始化时提供 consentPage 选项来自定义同意屏幕。

注意:对于设置了 skipConsent: true 的可信客户端,将完全跳过同意页面,为第一方应用提供无缝体验。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    plugins: [oidcProvider({
        consentPage: "/path/to/consent/page"
    })]
})

该插件会将用户重定向到指定路径,并在 URL 中附带 consent_codeclient_idscope 查询参数。您可以使用这些信息展示自定义的同意页面。一旦用户同意授权,您可以调用 oauth2.consent 来完成授权流程。

POST
/oauth2/consent

同意端点支持两种传递授权码的方式:

方法一:URL 参数

consent-page.ts
// 从 URL 中获取授权码
const params = new URLSearchParams(window.location.search);

// 在请求体中提交授权码以完成同意操作
const consentCode = params.get('consent_code');
if (!consentCode) {
	throw new Error('URL 参数中未找到授权码');
}

const res = await client.oauth2.consent({
	accept: true, // 若拒绝则设为 false
	consent_code: consentCode,
});

方法二:基于 Cookie

consent-page.ts
// 授权码会自动存储在已签名的 Cookie 中
// 只需提交同意决定即可
const res = await client.oauth2.consent({
	accept: true, // 若拒绝则设为 false
	// 使用基于 Cookie 的流程时无需提供 consent_code
});

两种方法都得到完整支持。URL 参数方式适用于移动应用和第三方场景,而基于 Cookie 的方式为 Web 应用提供了更简洁的实现。

处理登录流程

当用户被重定向至 OIDC 提供商进行身份验证时,若尚未登录,系统将自动跳转至登录页面。您可以在初始化时通过配置 loginPage 选项来自定义登录页面的路径。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    plugins: [oidcProvider({
        loginPage: "/sign-in"
    })]
})

您无需进行额外处理;当新会话创建时,插件将自动接管并继续完成授权流程。

配置选项

OIDC 元数据

通过在初始化时提供配置对象,您可以自定义 OIDC 元数据。

auth.ts
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";

export const auth = betterAuth({
    plugins: [oidcProvider({
        metadata: {
            issuer: "https://your-domain.com",
            authorization_endpoint: "/custom/oauth2/authorize",
            token_endpoint: "/custom/oauth2/token",
            // ...其他自定义元数据项
        }
    })]
})

JWKS 端点

OIDC Provider 插件可以与 JWT 插件集成,为 ID 令牌提供非对称密钥签名,这些令牌可在 JWKS 端点上进行验证。

要使您的插件符合 OIDC 标准,您必须禁用 /token 端点,因为 OAuth 的等效端点位于 /oauth2/token

auth.ts
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";
import { jwt } from "better-auth/plugins";

export const auth = betterAuth({
    disabledPaths: [
        "/token",
    ],
    plugins: [
        jwt(), // 确保添加 JWT 插件
        oidcProvider({
            useJWTPlugin: true, // 启用 JWT 插件集成
            loginPage: "/sign-in",
            // ... 其他选项
        })
    ]
})

useJWTPlugin: false(默认值)时,ID 令牌将使用应用程序密钥进行签名。

动态客户端注册

如果您希望允许客户端动态注册,可以通过将 allowDynamicClientRegistration 选项设置为 true 来启用此功能。

auth.ts
const auth = betterAuth({
    plugins: [oidcProvider({
        allowDynamicClientRegistration: true,
    })]
})

这将允许客户端通过公开可用的 /register 端点进行注册。

数据库模式

OIDC Provider 插件向数据库添加以下表:

OAuth 应用

表名:oauthApplication

字段名称类型Key描述
idstringOAuth 客户端的数据库 ID
clientIdstring每个 OAuth 客户端的唯一标识符
clientSecretstringOAuth 客户端的密钥。对于使用 PKCE 的公共客户端是可选的。
namestring-OAuth 客户端的名称
redirectURLsstring-重定向 URL 的逗号分隔列表
metadatastringOAuth 客户端的附加元数据
typestring-OAuth 客户端类型(例如:web、mobile)
disabledboolean-指示客户端是否被禁用
userIdstring拥有该客户端的用户 ID(可选)
createdAtDate-OAuth 客户端创建时的时间戳
updatedAtDate-OAuth 客户端最后更新时间的时间戳

OAuth 访问令牌

表名:oauthAccessToken

字段名称类型Key描述
idstring访问令牌的数据库 ID
accessTokenstring-颁发给客户端的访问令牌
refreshTokenstring-颁发给客户端的刷新令牌
accessTokenExpiresAtDate-访问令牌的过期时间
refreshTokenExpiresAtDate-刷新令牌的过期时间
clientIdstringOAuth 客户端的 ID
userIdstring与令牌关联的用户 ID
scopesstring-授予的权限范围列表(逗号分隔)
createdAtDate-访问令牌创建时间戳
updatedAtDate-访问令牌最后更新时间戳

OAuth 授权同意

表名: oauthConsent

字段名称类型Key描述
idstring授权同意的数据库 ID
userIdstring给予授权的用户 ID
clientIdstringOAuth 客户端 ID
scopesstring-已授权范围的逗号分隔列表
consentGivenboolean-指示是否已给予授权
createdAtDate-授权给予的时间戳
updatedAtDate-授权最后更新的时间戳

选项

allowDynamicClientRegistration: boolean - 启用或禁用动态客户端注册。

metadata: OIDCMetadata - 自定义 OIDC 提供者元数据。

loginPage: string - 自定义登录页面的路径。

consentPage: string - 自定义授权页面的路径。

trustedClients: (Client & { skipConsent?: boolean })[] - 直接在提供者选项中配置的可信客户端数组。这些客户端绕过数据库查询,并可选择跳过授权页面。

getAdditionalUserInfoClaim: (user: User, scopes: string[], client: Client) => Record<string, any> - 用于获取额外用户信息声明(claims)的函数。

useJWTPlugin: boolean - 当设为 true 时,ID 令牌将使用 JWT 插件的非对称密钥进行签名。当设为 false(默认值)时,ID 令牌将使用应用密钥通过 HMAC-SHA256 进行签名。

schema: AuthPluginSchema - 自定义 OIDC 提供者的架构。