OIDC 提供者

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

此插件即将被弃用,转而使用 OAuth 提供者插件

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

主要功能

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

此插件处于积极开发中,可能不适合生产使用。如有问题或错误,请在 GitHub 上报告。

安装

挂载插件

将 OIDC 插件添加到您的认证配置中。参见 配置部分 了解如何配置插件。

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

请参见 模式 部分以手动添加字段。

添加客户端插件

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

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 以允许公开注册。确保向您的认证客户端配置中添加 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

应用 Logo 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

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

方法 1: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,
});

方法 2:基于 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 令牌使用应用程序密钥签名。

动态客户端注册

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

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

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

数据库结构

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

OAuth 应用

表名:oauthApplication

Table
字段
类型
描述
id
string
PK
Database ID of the OAuth client
clientId
string
-
Unique identifier for each OAuth client
clientSecret ?
string
-
Secret key for the OAuth client. Optional for public clients using PKCE.
icon ?
string
-
Icon of the OAuth client
name
string
-
Name of the OAuth client
redirectUrls
string
-
Comma-separated list of redirect URLs
metadata ?
string
-
Additional metadata for the OAuth client
type
string
-
Type of OAuth client (e.g., web, mobile)
disabled ?
boolean
-
Indicates if the client is disabled
userId ?
string
FK
ID of the user who owns the client. (optional)
createdAt
Date
-
Timestamp of when the OAuth client was created
updatedAt
Date
-
Timestamp of when the OAuth client was last updated

OAuth 访问令牌

表名:oauthAccessToken

Table
字段
类型
描述
id
string
PK
Database ID of the access token
accessToken
string
-
Access token issued to the client
refreshToken
string
-
Refresh token issued to the client
accessTokenExpiresAt
Date
-
Expiration date of the access token
refreshTokenExpiresAt
Date
-
Expiration date of the refresh token
clientId
string
FK
ID of the OAuth client
userId ?
string
FK
ID of the user associated with the token
scopes
string
-
Comma-separated list of scopes granted
createdAt
Date
-
Timestamp of when the access token was created
updatedAt
Date
-
Timestamp of when the access token was last updated

OAuth 同意

表名:oauthConsent

Table
字段
类型
描述
id
string
PK
Database ID of the consent
userId
string
FK
ID of the user who gave consent
clientId
string
FK
ID of the OAuth client
scopes
string
-
Comma-separated list of scopes consented to
consentGiven
boolean
-
Indicates if consent was given
createdAt
Date
-
Timestamp of when the consent was given
updatedAt
Date
-
Timestamp of when the consent was last updated

选项

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 提供者的数据库结构。