OIDC 提供者

Better Auth 的 Open ID Connect 插件,允许您拥有自己的 OIDC 提供者。

此插件即将被弃用,将由 OAuth Provider Plugin 取代。

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

主要功能

  • 客户端注册:注册客户端以使用您的 OIDC 提供者进行认证。
  • 动态客户端注册:允许客户端动态注册。
  • 受信任客户端:配置硬编码的受信任客户端,支持可选的跳过用户同意。
  • 授权码流程:支持授权码流程。
  • 公共客户端:支持 SPA、移动应用、CLI 工具等公共客户端。
  • JWKS 端点:发布 JWKS 端点供客户端验证令牌。(尚未完全实现)
  • 刷新令牌:签发刷新令牌并使用 refresh_token 授权刷新访问令牌。
  • OAuth 同意:实现 OAuth 同意页面供用户授权,受信任应用可选择跳过同意。
  • UserInfo 端点:提供 UserInfo 端点,客户端可获取用户详情。

此插件正在积极开发中,可能不适合生产环境使用。如发现问题或 Bug,请在 GitHub 报告。

安装

挂载插件

将 OIDC 插件添加到您的 auth 配置中。查看配置部分了解插件的配置方法。

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

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

数据库迁移

运行迁移或生成数据库结构以添加必要的字段和表。

npx auth migrate
npx auth generate

请参阅 Schema 部分了解如何手动添加字段。

添加客户端插件

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

auth-client.ts
import { createAuthClient } from "better-auth/client";
import { oidcClient } from "better-auth/client/plugins"

const authClient = createAuthClient({
    plugins: [
    oidcClient({ 
        // 您的 OIDC 配置
      }) 
    ]
})

使用

安装完成后,您可以利用 OIDC 提供者来管理应用内的认证流程。

注册新客户端

要注册新的 OIDC 客户端,可以在客户端调用 oauth2.register 方法,或在服务器调用 auth.api.registerOAuthApplication 接口。

POST/oauth2/register
Notes

默认情况下,客户端注册需认证。设置 allowDynamicClientRegistration: true 可允许公开注册。确保在 Auth 客户端配置中添加 oidcClient() 插件。

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,});
Parameters
redirect_urisstring[]required

重定向 URI 列表。

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_namestring

应用名称。

client_uristring

应用 URI。

logo_uristring

应用图标 URI。

scopestring

应用支持的作用域,空格分隔。

contactsstring[]

应用联系方式。

tos_uristring

应用服务条款 URI。

policy_uristring

应用隐私政策 URI。

jwks_uristring

应用 JWKS URI。

jwksRecord<string, any>

应用 JWKS 数据。

metadataRecord<string, any>

应用元数据。

software_idstring

应用软件 ID。

software_versionstring

应用软件版本。

software_statementstring

应用软件声明。

此接口支持符合 RFC7591 标准的客户端注册。

应用创建成功后,您将获得 client_idclient_secret,可展示给用户。

受信任客户端

针对第一方应用和内部服务,您可以直接在 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: "内部管理后台",
                type: "web",
                redirectUrls: ["https://dashboard.company.com/auth/callback"],
                disabled: false,
                skipConsent: true, // 此受信任客户端跳过同意页面
                metadata: { internal: true }
            },
            {
                clientId: "mobile-app",
                clientSecret: "mobile-secret", 
                name: "公司移动应用",
                type: "native",
                redirectUrls: ["com.company.app://auth"],
                disabled: false,
                skipConsent: false, // 需要同意时仍显示同意页面
                metadata: {}
            }
        ]
    })]
})

UserInfo 端点

OIDC 提供者包含 UserInfo 端点,允许客户端获取认证用户的信息。该端点路径为 /oauth2/userinfo,需提供有效的访问令牌。

GET
/oauth2/userinfo

服务器端用法

server.ts
import { auth } from "@/lib/auth";

const userInfo = await auth.api.oAuth2userInfo({
  headers: {
    authorization: "Bearer ACCESS_TOKEN"
  }
});
// userInfo 中包含基于授权作用域的用户详情

客户端用法(针对第三方 OAuth 客户端)

第三方 OAuth 客户端可通过标准 HTTP 请求调用 UserInfo 端点:

external-client.ts
const response = await fetch('https://your-domain.com/api/auth/oauth2/userinfo', {
  headers: {
    'Authorization': 'Bearer ACCESS_TOKEN'
  }
});

const userInfo = await response.json();

根据作用域返回的声明包括:

  • 包含 openid:返回用户的 ID (sub 声明)
  • 包含 profile:返回 namepicturegiven_namefamily_name
  • 包含 email:返回 emailemail_verified

自定义声明

getAdditionalUserInfoClaim 函数接收用户对象、请求的作用域数组以及客户端,可以基于授权时赋予的作用域有条件地加入自定义声明,这些额外声明将包含在 UserInfo 端点响应和 ID 令牌中。

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

export const auth = betterAuth({
    plugins: [
        oidcProvider({
            loginPage: "/sign-in",
            getAdditionalUserInfoClaim: async (user, scopes, client) => {
                const claims: Record<string, any> = {};
                
                // 根据作用域添加自定义声明
                if (scopes.includes("profile")) {
                    claims.department = user.department;
                    claims.job_title = user.jobTitle;
                }
                
                // 基于客户端元数据添加声明
                if (client.metadata?.includeRoles) {
                    claims.roles = user.roles;
                }
                
                return claims;
            }
        })
    ]
});

同意页面

当用户被重定向到 OIDC 提供者进行认证时,可能会被提示授权应用访问其数据,即同意页面。默认情况下,Better Auth 会显示示例同意页面。你也可以通过初始化时传入 consentPage 选项来自定义同意页面。

注意skipConsent: true 的受信任客户端将直接跳过同意页面,提供无缝体验。

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

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

插件会将用户重定向到指定路径,并附带 consent_codeclient_idscope 查询参数。您可以使用这些信息显示自定义同意页面。用户同意后,可以调用 oauth2.consent 完成授权。

POST
/oauth2/consent

同意端点支持两种传递同意码的方法:

方法一:URL 参数

consent-page.ts
import { authClient } from "@/lib/auth-client"

// 从 URL 获取同意码
const params = new URLSearchParams(window.location.search);

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

const res = await authClient.oauth2.consent({
	accept: true, // 或 false 拒绝
	consent_code: consentCode,
});

方法二:基于 Cookie

consent-page.ts
import { authClient } from "@/lib/auth-client"

// 同意码会自动存储在签名 Cookie 中
// 直接提交同意结果即可
const res = await authClient.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 提供者插件可以与 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 令牌使用应用密钥以 HMAC-SHA256 签名。

动态客户端注册

若允许客户端动态注册,可开启该功能,设置 allowDynamicClientRegistrationtrue

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

此时,客户端可通过 /register 端点公开注册。

数据库结构

OIDC 提供者插件在数据库中添加以下表:

OAuth 应用

表名:oauthApplication

Table
字段
类型
描述
id
string
pk
OAuth 客户端的数据库 ID
clientId
string
pk
每个 OAuth 客户端唯一标识
clientSecret
string
?
OAuth 客户端密钥,公共客户端使用 PKCE 可选
name
string
OAuth 客户端名称
redirectUrls
string
逗号分隔的重定向 URL 列表
metadata
string
?
OAuth 客户端的附加元数据
type
string
OAuth 客户端类型(例如 web、mobile)
disabled
boolean
客户端是否被禁用
userId
string
?
拥有该客户端的用户 ID(可选)
createdAt
Date
OAuth 客户端创建时间
updatedAt
Date
OAuth 客户端更新时间

OAuth 访问令牌

表名:oauthAccessToken

Table
字段
类型
描述
id
string
pk
访问令牌的数据库 ID
accessToken
string
签发给客户端的访问令牌
refreshToken
string
签发给客户端的刷新令牌
accessTokenExpiresAt
Date
访问令牌过期时间
refreshTokenExpiresAt
Date
刷新令牌过期时间
clientId
string
fk
OAuth 客户端 ID
userId
string
fk
与令牌关联的用户 ID
scopes
string
逗号分隔的授权作用域列表
createdAt
Date
访问令牌创建时间
updatedAt
Date
访问令牌更新时间

OAuth 同意

表名:oauthConsent

Table
字段
类型
描述
id
string
pk
同意记录的数据库 ID
userId
string
fk
授权用户 ID
clientId
string
fk
OAuth 客户端 ID
scopes
string
同意的作用域列表,逗号分隔
consentGiven
boolean
是否已同意
createdAt
Date
同意时间
updatedAt
Date
同意更新时间

选项

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

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

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

consentPage: string - 自定义同意页面路径。

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

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

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

schema: AuthPluginSchema - 定制 OIDC 提供者的数据库结构。