OAuth 2.1 提供者

一个更好的认证插件,使您的认证服务器能够作为 OAuth 2.1 提供者运行。

一个OAuth 2.1 提供者插件,允许您将认证服务器转变为兼容 OIDC 的 OAuth 提供者,支持用户和其他服务通过您的 API 进行认证。

该插件默认具有安全配置,使不熟悉 OAuth 细节的用户也能轻松使用。

主要特性

  • OAuth 2.1:限制使用符合 OAuth 2.1 的安全实践
  • 发行者验证:授权响应包含 iss 参数以防止 混淆攻击
  • 启用 MCP:支持 MCP 认证
  • 兼容 OIDC:遵循 OIDC 标准,支持 openid 范围
    • UserInfo:提供当前用户详情的端点
    • id_token:JWT 签名的用户信息
    • OIDC 注销RP发起的注销支持
  • 动态客户端注册:允许客户端动态注册
    • 公有客户端:支持原生移动客户端和用户代理客户端(如 AI)
    • 机密客户端:支持 Web 客户端
    • 受信任客户端:支持硬编码受信任客户端,并可选择绕过同意
  • 兼容 JWT 插件:默认必需,可选禁用
    • JWT 签名:请求 resource 时签发 JWT 令牌
    • JWKS 校验:在 /jwks 端点远程验证令牌
  • 授权提示:启动特定登录流程的提示
    • 同意:确保为每个范围授权同意。使用 prompt=consent 强制。
    • 选择账户:确保在授权特定范围前选择账户。使用 prompt=select_account 强制。
  • 资源端点:读取和管理令牌。
    • 令牌核查:符合 RFC7662 的核查端点。
    • 撤销:符合 RFC7009 的撤销端点。

支持的授权方式

  • authorization_code:支持使用 PKCE 和 S256 要求的用户令牌交换
  • refresh_token:支持刷新令牌及通过 offline_access 范围的访问令牌续期
  • client_credentials:机器对机器的 API 通信令牌

安装

挂载插件

将 OIDC 插件添加到您的认证配置中。详情参见 配置章节,了解如何配置插件。

auth.ts
import { betterAuth } from "better-auth";
import { jwt } from "better-auth/plugins";
import { oauthProvider } from "@better-auth/oauth-provider"; 

const auth = betterAuth({
  disabledPaths: [
    "/token",
  ],
  plugins: [
    jwt(),
    oauthProvider({ 
      loginPage: "/sign-in", 
      consentPage: "/consent", 
      // ...其他选项
    }) 
  ],
});

迁移数据库

运行迁移或生成 schema 以新增数据库所需的字段和表。

npx auth migrate
npx auth generate

详情请参见 Schema 部分,可手动添加字段。

添加 ./well-known 端点

请添加所有 Well-Known 端点 到您的项目。如果不确定,系统会通过警告提示位置。

  • 必须在发行者路径(无路径则为根路径)添加 OAuth 授权服务器元数据端点。
  • 如果您使用 openid 范围,必须在发行者路径(无路径则为根路径)添加 OpenID 配置。
  • 如果使用资源服务器(如 MCP),必须将资源服务器元数据添加至您的 API,并在发行者路径后附加。

创建您的第一个 OAuth 客户端

创建一个机密 OAuth 客户端。

const client = await auth.api.createOAuthClient({
		headers,
		body: {
			redirect_uris: [redirectUri],
		}
	});
console.log(client); // 如需要,您可以将 `client_id` 添加至 `cachedTrustedClients`

若要创建公有客户端(即无客户端密钥),请设置 token_endpoint_auth_method: "none"

客户端插件

存在两种客户端,您可根据需求添加一种或两种。

OAuth 客户端

OAuth 客户端是连接的 oauthClient,如移动或 Web 应用。

auth-client.ts
import { createAuthClient } from "better-auth/client";
import { oauthProviderClient } from "@better-auth/oauth-provider/client"

export const authClient = createAuthClient({
  plugins: [
    oauthProviderClient(), 
  ],
});

资源客户端

资源服务器是运行在您的 API 服务器上的客户端,用于执行令牌验证和提供元数据等操作。

server-client.ts
import { auth } from "@/lib/auth";
import { createAuthClient } from "better-auth/client";
import { oauthProviderResourceClient } from "@better-auth/oauth-provider/resource-client"

export const serverClient = createAuthClient({
  plugins: [
    oauthProviderResourceClient(auth) // auth 可选
  ],
});

使用说明

该插件作为 OAuth 2.1 服务器运行,具有 OIDC 兼容端点和 JWT 可验证的访问令牌。以下为各端点的详细说明。

OAuth 客户端

OAuth 中有两种类型的客户端:

  • 公有客户端:无法存储客户端密钥,如原生移动端和用户代理(如 AI)客户端
  • 机密客户端:可存储客户端密钥,如 Web 客户端

获取客户端信息

获取特定用户或组织所有的客户端信息,使用以下端点:

GET/oauth2/get-client
const { data, error } = await authClient.oauth2.getClient({    query: {        client_id, // required    },});
Parameters
client_idstring,required

OAuth 客户端的 client_id

获取公有客户端信息

获取公有客户端字段以在登录流程页面显示(如同意页面),使用以下端点。注意:需用户登录后方可使用。

GET/oauth2/public-client
const { data, error } = await authClient.oauth2.publicClient({    query: {        client_id, // required    },});
Parameters
client_idstring,required

OAuth 客户端的 client_id

列出客户端

获取特定用户或组织所拥有的所有客户端列表,使用以下端点:

GET/oauth2/get-clients
const { data, error } = await authClient.oauth2.getClients();

创建客户端

创建与指定用户或组织关联的 OAuth 客户端,使用 /oauth2/create-client 端点(如 createOAuthClient)。参数与 RFC7591 描述的注册端点相同。

数据库中的以下字段属于受限字段,仅管理员用户可编辑:

  • client_secret_expires_at:机密客户端密钥过期时间
  • skip_consent:允许跳过用户同意流程。适用于受信任客户端。
  • enable_end_session:允许用户通过其 id_token/oauth2/end-session 端点从客户端注销。用于 OIDC 配置和受信任客户端。
  • metadata:附加到客户端的私有元数据。

部分场景下,您可能希望通过自定义 API、公司管理员门户或服务器初始化逻辑创建带有受限字段的客户端,可使用以下仅限服务器端的端点:

admin-create-oauth.ts
import { auth } from "@/lib/auth"

await auth.api.adminCreateOAuthClient({
  headers,
  body: {
    redirect_uris: [redirectUri],
    client_secret_expires_at: 0, 
    skip_consent: true, 
    enable_end_session: true, 
  }
});

更新客户端

更新与指定用户或组织关联的 OAuth 客户端,使用 /oauth2/update-client 端点(如 updateOAuthClient)。参数与 RFC7591 描述的注册端点相同。

POST/oauth2/update-client
const { data, error } = await authClient.oauth2.updateClient({    client_id, // required    update, // required});
Parameters
client_idstring,required

OAuth 客户端的 client_id

updateOAuthClient,required

需要更新的字段

本端点限制:

  • 不支持在机密与公有客户端间切换。客户端类型创建时确定。
  • 不支持更新客户端密钥。旋转客户端密钥请使用专门的端点。

部分场景下,您可能希望通过自定义 API、公司管理员门户或服务器初始化逻辑更新带有受限字段的客户端,可使用以下仅限服务器端端点。字段同创建部分描述。

admin-update-oauth.ts
import { auth } from "@/lib/auth"

await auth.api.adminUpdateOAuthClient({
  headers,
  body: {
    redirect_uris: [redirectUri],
    client_secret_expires_at: 0, 
    skip_consent: true, 
    enable_end_session: true, 
  }
});

旋转客户端密钥

当前实现会立即生效,旧密钥即时失效。

旋转客户端密钥,请使用以下端点:

POST/oauth2/client/rotate-secret
const { data, error } = await authClient.oauth2.client.rotateSecret({    client_id, // required});
Parameters
client_idstring,required

OAuth 客户端的 client_id

删除客户端

删除用户或组织的客户端,请使用以下端点:

POST/oauth2/delete-client
const { data, error } = await authClient.oauth2.deleteClient({    client_id, // required});
Parameters
client_idstring,required

OAuth 客户端的 client_id

OAuth 同意管理

对于所有非受信任客户端(尤其是无 skip_consent 的),都需要用户同意。以下端点允许用户或 reference_id 管理其已授权的同意。

获取同意详情

获取特定同意详情,使用以下端点:

GET/oauth2/get-consent
const { data, error } = await authClient.oauth2.getConsent({    query: {        id, // required    },});
Parameters
idstring,required

同意条目 ID

列出同意

获取用户的所有同意列表,使用以下端点:

GET/oauth2/get-consents
const { data, error } = await authClient.oauth2.getConsents();

更新同意

更新特定同意条目,使用以下端点:

POST/oauth2/update-consent
const { data, error } = await authClient.oauth2.updateConsent({    id, // required    update, // required});
Parameters
idstring,required

同意条目 ID

updateOAuthConsent,required

需要更新的字段

删除同意

撤销用户对某个客户端的同意。

POST/oauth2/delete-consent
const { data, error } = await authClient.oauth2.deleteConsent({    id, // required});
Parameters
idstring,required

同意条目 ID

动态注册端点

该端点支持符合 RFC7591 的客户端注册。

安装后,您可以使用 OAuth 提供者管理应用内的认证流程。

创建客户端后,将获得 client_idclient_secret,可显示给用户。client_secret 只提供一次,务必提示用户保存。

配置

启用客户端注册请在 BetterAuth 配置中设置 allowDynamicClientRegistration: true

auth.ts
oauthProvider({
  allowDynamicClientRegistration: true,
  // ... 其他选项
})

若要启用无需认证的客户端注册(允许动态注册公有客户端),额外设置 allowUnauthenticatedClientRegistration: true

当 MCP 协议标准化无认证动态客户端注册时,allowUnauthenticatedClientRegistration 将被弃用。当前,客户端 ID 元数据文档software_statementjwks_uri仍在讨论中。

auth.ts
oauthProvider({
  allowDynamicClientRegistration: true,
  allowUnauthenticatedClientRegistration: true,
  // ... 其他选项
})

基本示例

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

import { authClient } from "@/lib/auth-client"

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

所有参数详情请参见 RFC 7591 注册部分

当前不支持以下参数:

  • jwks
  • jwks_uri

授权端点

一个符合 OAuth 2.1 授权端点规范 的端点。由于某些细节尚未完全规范,部分内容参照旧版 OAuth 2.0 授权端点 但始终实现了 OAuth 2.1 与 OAuth 2.0 的差异

授权端点为启动 OAuth 2.1 授权流程的入口。

重要说明:

  • OAuth 2.1 中仅支持 response_type: "code"
  • 不支持 code_challenge_method: "plain",因这存在安全漏洞。
  • 所有授权响应(成功和错误)均包含用于发行者验证的 iss 参数(RFC 9207)。

状态(State)

需发送状态参数以防止跨站请求伪造(CSRF)攻击。客户端通过该方式确保只响应其发起的请求。

客户端生成状态值,并存储在安全、HTTP-only Cookie 或数据库等处。

代码挑战

代码挑战用于保护授权端点返回的授权码。

其通过从代码验证器派生代码挑战并通过 PKCE(Proof Key for Code Exchange) 发送至授权服务器。

redirect_uri 回调时,客户端比对返回状态与初始状态是否匹配,然后使用 authorization_code 授权方式和原始代码验证器在令牌端点交换令牌。

令牌端点

默认情况下,令牌端点支持以下授权:

  • "authorization_code"
  • "client_credentials"
  • "refresh_token"

授权码授权方式

该方式允许客户端获取用户访问令牌,及可选的刷新令牌(包含 "offline_access" 范围)。

客户端凭证授权方式

该方式允许客户端获取机器间访问令牌。

刷新令牌授权方式

该方式允许客户端刷新访问令牌,无需用户再次登录。

当前实现为每次刷新请求发放新的刷新令牌。

同意端点

接受或拒绝用户对某组权限的同意。注意拒绝某些权限时,会取消本次授权的同意,之前已有的其他同意依然有效。要移除同意,请删除该用户对应客户端的 "oauthConsent"。

POST/oauth2/consent
const { data, error } = await authClient.oauth2.consent({    accept, // required    scope,});
Parameters
acceptboolean,required

接受或拒绝用户同意

scopestring,

空格分隔的接收权限列表。不提供则默认为原始请求权限。

Continue 端点

注册页面必须先 配置 以进行注册步骤。 账户选择页面必须先 配置 用于账户选择。 登录后页面必须先 配置 用于登录后操作。

POST/oauth2/continue
const { data, error } = await authClient.oauth2.continue({    selected,    created,    postLogin,});
Parameters
selectedboolean,

确认已选择账户

createdboolean,

确认已完成账户注册

postLoginboolean,

确认已完成登录后操作

令牌核查端点

符合 RFC7662 规范的令牌核查端点。

此端点提供所给令牌的详细信息。如令牌绑定于会话,确保该会话为 active

通过 customAccessTokenClaims 可向机密客户端的 resources 字段存储允许其访问的资源,以提供资源特定声明。

撤销端点

符合 RFC7009 规范的撤销端点。

此端点撤销所给令牌。

  • 不透明访问令牌(opaque access_token):立即从数据库删除该令牌,刷新令牌依然有效
  • JWT 访问令牌:验证令牌是否允许从客户端存储中删除
  • 刷新令牌(refresh_token):删除所有使用该刷新令牌发放的访问令牌,且撤销该刷新令牌,禁止其再次发放令牌。

针对 access_token 类型,

会话结束端点

符合 RP-发起注销规范。

此端点允许指定受信任客户端远程执行注销操作。

允许 RP 发起注销,需专门创建开启注销功能的受信任客户端:

admin-create-oauth.ts
import { auth } from "@/lib/auth"

await auth.api.adminCreateOAuthClient({
  headers,
  body: {
    redirect_uris: [redirectUri],
    enable_end_session: true, 
  }
});

如设置 disableJwtPlugin: true,公有客户端将无法通过此端点注销,因为不会发送 id_token

UserInfo 端点

UserInfo 端点提供符合 OIDC 标准的用户信息。端点为 /oauth2/userinfo,需带有效访问令牌且至少带有 openid 权限。

// 客户端调用 UserInfo 端点示例
const response = await fetch('https://your-domain.com/api/auth/oauth2/userinfo', {
  headers: {
    'Authorization': 'Bearer ACCESS_TOKEN'
  }
});

const userInfo = await response.json();
// userInfo 根据授予的权限返回用户信息

UserInfo 端点根据授权时授予的范围返回不同的声明:

  • openid:返回用户 ID (sub 声明)
  • profile:返回 name, picture, given_name, family_name
  • email:返回 emailemail_verified

customUserInfoClaims 函数接收用户对象、请求的 scopes 数组以及传入的访问令牌,允许添加额外响应信息。

Well known

Openid 配置

提供 OpenID Connect 发现元数据,位于 /.well-known/openid-configuration

要求带有 openid 范围。

必须将该配置添加至发行者路径。如未设置发行者,则为基础路径 /api/auth

若发行者路径非根路径,且根路径未提供 openid-configuration,我们推荐在根添加以防客户端硬编码访问 /.well-known/openid-configuration(忽视了规范中的发行者路径)。

注意:对于带路径的发行者,OpenID 使用路径拼接,应在发行者路径后附加 /.well-known/openid-configuration。无发行者路径则应从根开始。

[issuer-path]/.well-known/openid-configuration/route.ts
import { oauthProviderOpenIdConfigMetadata } from "@better-auth/oauth-provider";
import { auth } from "@/lib/auth";

export const GET = oauthProviderOpenIdConfigMetadata(auth);

本地测试时,如遭遇跨域问题(例如使用 MCP Inspector),因前端调用端点而非后端。可添加 Access-Control-Allow-Methods": "GET""Access-Control-Allow-Origin": "*" 进行测试。

OAuth 授权服务器

提供 RFC8414 兼容元数据,位于 /.well-known/oauth-authorization-server

必须将该配置添加至发行者路径。未设置发行者,则为基础路径 /api/auth

注意:带路径的发行者,OAuth 2.1 授权服务器采用路径插入,故在发行者路径后追加 /.well-known/oauth-authorization-server。无发行者路径则从根开始添加。

/.well-known/oauth-authorization-server/[issuer-path]/route.ts
import { oauthProviderAuthServerMetadata } from "@better-auth/oauth-provider";
import { auth } from "@/lib/auth";

export const GET = oauthProviderAuthServerMetadata(auth);

本地测试时,如遇跨域问题(例如使用 MCP Inspector),因前端调用端点而非后端。可添加 Access-Control-Allow-Methods": "GET""Access-Control-Allow-Origin": "*" 进行测试。

API 服务器

本节展示如何让您的 API 验证来自客户端的令牌。

验证

可通过 oauthProviderResourceClient 插件或 better-auth/oauth2 包里的 verifyAccessToken 进行验证。

使用 better-auth 包:

api/[endpoint].ts
import { verifyAccessToken } from "better-auth/oauth2";

export const GET = async (req: Request) => {
  const authorization = req.headers?.get("authorization") ?? undefined;
  const accessToken = authorization?.startsWith("Bearer ")
    ? authorization.replace("Bearer ", "")
    : authorization;
  const payload = await verifyAccessToken(
    accessToken, {
      verifyOptions: {
        issuer: "https://auth.example.com",
        audience: "https://api.example.com",
      },
      scopes: ["read:post"], // 可选
    }
  );
  // ...后续操作
}

使用 oauthProviderResourceClient 插件:

api/[endpoint].ts
import { serverClient } from "@/lib/server-client";

export const POST = async (req: Request) => {
  const authorization = req.headers?.get("authorization") ?? undefined;
  const accessToken = authorization?.startsWith("Bearer ")
    ? authorization.replace("Bearer ", "")
    : authorization;
  const payload = await serverClient.verifyAccessToken(
    accessToken, {
      verifyOptions: {
        issuer: "https://auth.example.com",
        audience: "https://api.example.com",
      },
      scopes: ["write:post"], // 可选
    }
  );
  // ...后续操作
}

JWT 验证

  • 验证令牌有效性:
    • 使用 JWKS 验证签名
    • 校验 iss(发行者)和 aud(受众)声明
    • 验证 exp(过期)和(若有)nbf 声明
  • 校验端点所需的 scope

不透明访问令牌(opaque)

  • 发送令牌至 /oauth2/introspect 并验证返回 active: true
  • 校验端点所需的 scope

建议

最简单的方式是仅接受 JWT 格式的访问令牌,拒绝不透明令牌。

优点

  • 快速:可本地验证,无需网络请求
  • 前瞻:令牌签发后,不依赖授权服务器
  • 无需客户端密钥:API 验证令牌时不需机密客户端凭据

附带支持不透明访问令牌也可实现,但有利弊。

优点

  • 令牌和客户端即时验证
  • 客户端可无需传递 resource 参数(取决授权服务器配置)

缺点

  • DOS 风险:若客户端为外部(如外部 API、MCP 代理),大量不透明令牌核查可能使授权服务器过载
  • 性能影响:每个令牌核查需网络请求 introspection 端点
  • 需密钥:通常 introspection 需要 client_secret,公有客户端难以安全提供
    • 注意:目前尚未实现 introspection 的承载令牌和私钥 JWT 方法

范围与权限

  • Scopes(范围) 定义客户端代表用户请求的权限。通常为包含在访问令牌中的粗粒度标签。
  • Permissions(权限) 定义用户(或服务)对资源实际被授权执行的操作,通常由资源服务器强制执行。

实际应用中可结合两种方式,视系统复杂度及资源服务器授权处理方式而定。

Scopes 与 Permissions 相同

每个范围直接对应一个权限。

  • 例如:read:post 范围即权限 read:post

优点:

  • 简单易实现和理解
  • 无需额外映射逻辑

缺点:

  • 访问令牌可能因过多权限而过大,尤其 JWT
  • 灵活性有限,不易扩展更细粒度权限

Scopes 与 Permissions 区分

Scopes 表示高层访问类别,每个范围映射至一个或多个底层权限。

  • 例如:view:post 范围可能映射到
    • read:post:content
    • read:post:metadata(且仅限用户拥有的帖子)

优点:

  • 灵活可扩展,适合复杂系统
  • 令牌保持简洁,仅携带 scopes,无需全部权限

缺点:

  • 资源服务需为每次请求解析范围映射权限
  • 增加实现复杂度和授权检查开销

配置

重定向页面

OAuth 流程中,用户可能被多次重定向。例如,用户可能先到登录页面,再跳转至同意页面,最后返回应用。以下说明常见的登录流程及所需配置。

流程中检测每个重定向步骤时,会验证在初始 /oauth2/authorize 重定向时签名的查询参数。包含所有参数(包括自定义参数)均被签名和验证。

若登录页有自定义查询参数,可将其附加在签名查询(即 sig 字段)结尾。

若使用客户端插件 oauthProviderClient,则 oauth_query 参数会自动发送至所有需要的端点。若是自定义登录端点,则需手动在请求体中的 oauth_query 字段添加带签名的查询,内容仅包括签名查询参数。

登录页面

用户跳转到 OIDC 提供者进行认证时,若未登录,会跳转至登录页面。可通过初始化时提供 loginPage 选项自定义登录页。

auth.ts
oauthProvider({
  loginPage: "/sign-in"
})

无需额外处理,插件会在新会话创建后自动继续授权流程。

同意页面

用户跳转到 OIDC 提供者认证时,可能需授权应用访问数据。

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

auth.ts
oauthProvider({
  consentPage: "/consent"
})

插件会带着 client_idscope 参数重定向至指定路径,您可根据该信息展示自定义同意页。用户同意后调用 oauth2.consent 完成授权。

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

const res = await authClient.oauth2.consent({
	accept: true,
  // 可选接受的范围,未指定时接受所有原始请求范围
  scope: "openid profile email"
});

注册页面

客户端通过 prompt: create 跳转用户到注册页时,配置如下:

auth.ts
oauthProvider({
  signUp: {
    page: "/sign-up", 
  }
})

欲在注册步骤中阻断登录流程,使用 shouldRedirect 函数:

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

oauthProvider({
  signUp: {
    page: "/sign-up",
    shouldRedirect: async ({ headers }) => { 
      const isUserRegistered = await userRegistered(headers);
      return isUserRegistered ? false : "/setup";
    },
  }
})

选择账户页面

用户认证时被重定向至选择账户页,需先启用选择账户配置。

下面示例使用多会话插件,若登录多个会话,则自动跳转选择账户页:

auth.ts
oauthProvider({
  selectAccount: {
    page: "/select-account", 
    shouldRedirect: async ({ headers }) => { 
      const allSessions = await auth.api.listDeviceSessions({
        headers,
      })
      return allSessions?.length >= 1;
    },
  }
})

插件会跳转至 selectAccount.page,该页面应提示用户选择账户,选择完成后调用 oauth2Continue

select-account.ts
import { authClient } from "@/lib/auth-client"

await authClient.multiSession.setActive({
  sessionToken,
});
await client.oauth2.oauth2Continue({
  selected: true,
});

登录后页面

如果某个范围要求指定组织,需在登录后流程中配置所有以下选项,将 reference_id (如组织 ID、团队 ID)绑定至流程。

下面示例使用组织插件,自动在登录后跳转选择组织页:

auth.ts
oauthProvider({
  scopes: ["openid", "profile", "email", "read:organization"]
  postLogin: {
    page: "/select-organization", 
    shouldRedirect: async ({ session, scopes, headers }) => { 
      const userOnlyScopes = ["openid", "profile", "email", "offline_access"];
      if (scopes.every((sc) => userOnlyScopes.includes(sc))) {
        return false;
      }
      const organizations = await auth.api.listOrganizations({
        headers,
      });
      return organizations.length > 1 || !(
        organizations.length === 1 && organizations.at(0)?.id === session.activeOrganizationId
      )
    },
    consentReferenceId: ({ session, scopes }) => { 
      if (scopes.includes("read:organization")) {
        const activeOrganizationId = (session?.activeOrganizationId ?? undefined) as string | undefined;
        if (!activeOrganizationId) {
          throw new APIError("BAD_REQUEST", {
            error: "set_organization",
            error_description: "must set organization for these scopes",
          })
        }
        return activeOrganizationId;
      } else {
        return undefined;
      }
    },
  }
})

插件会将用户重定向至 postLogin.page 以完成选择。选择完成后调用 oauth2Continue

select-organization.ts
import { authClient } from "@/lib/auth-client"

await authClient.organization.setActive({
  organizationId,
});
await client.oauth2.oauth2Continue({
  postLogin: true,
});

缓存的受信任客户端

针对第一方应用和内部服务,可缓存受信任客户端提升性能。值以内存缓存,并阻止 CRUD 接口修改。

auth.ts
oauthProvider({
  // 受信任客户端 clientId 列表
  cachedTrustedClients: new Set([
    "internal-dashboard",
    "mobile-app",
  ]),
})

有效受众(Audiences)

本 OAuth 服务器允许的一组有效受众(资源)。若未指定,则默认发送基础 URL。推荐指定除基础 URL 之外的受众,如您的 API。

auth.ts
oauthProvider({
  validAudiences: [
    "https://api.example.com",
    "https://api.example.com/mcp",
  ]
})

Scopes

Scopes 授权客户端访问指定资源。目前默认支持:

  • openid:返回用户 ID (sub 声明)
  • profile:返回姓名、头像、名、姓
  • email:返回邮箱及邮箱验证状态
  • offline_access:返回刷新令牌

您可自由定义所支持的 scopes!注:需包含 openid 以满足 OIDC 服务器,否则为标准 OAuth 2.1 服务器。所有支持的 scopes 必须包含于此数组。

auth.ts
oauthProvider({
  scopes: [ "openid", "profile", "offline_access", "read:post", "write:post" ],
})

Claims

内置支持的声明包括:["sub", "iss", "aud", "exp", "iat", "sid", "scope", "azp"]。

建议给 id token 和 userinfo 中附加的声明使用命名空间,防止未来冲突。

customIdTokenClaimscustomUserInfoClaims 中添加的声明,应在 advertisedMetadata.claims_supported 中声明,方便客户端验证。例如,以下演示了基础声明加上 "locale" 和 "https://example.com/org"。

技巧:这两个函数也可抛出错误,例如用户已不再是组织成员或无请求权限。

auth.ts
oauthProvider({
  // 附加声明到 id tokens
  customIdTokenClaims: ({ user, scopes, metadata }) => {
    return {
      locale: "en-GB",
    };
  },
  // 附加声明到访问令牌
  customAccessTokenClaims: ({ user, scopes, referenceId, resource, metadata }) => {
    return {
      "https://example.com/org": referenceId,
      "https://example.com/roles": ["editor"],
    };
  },
  // 附加额外 userinfo 声明
  customUserInfoClaims: ({ user, scopes, jwt }) => {
    return {
      locale: "en-GB",
    };
  },
})

过期时间配置

每种令牌类型和授权类型均可独立设置默认过期时间。

  • accessTokenExpiresIn 默认 1 小时
  • m2mAccessTokenExpiresIn 默认 1 小时
  • idTokenExpiresIn 默认 10 小时
  • refreshTokenExpiresIn 默认 30 天
  • codeExpiresIn 默认 10 分钟

访问令牌还支持基于 scopes 单独设置更短过期,适用于高权限的授权。以最早过期时间为准,未设置的使用默认。注意:该时间应低于默认 accessTokenExpiresInm2mAccessTokenExpiresIn

auth.ts
oauthProvider({
  scopeExpirations: {
    "write:payments": "5m",
    "read:payments": "30m",
  },
})

注册

动态客户端注册

动态注册允许授权注册公有和机密客户端。

auth.ts
oauthProvider({
  allowDynamicClientRegistration: true, 
})

无认证客户端注册允许公有客户端(非机密)无授权头动态注册,适合 MCP 自动注册公有客户端。

auth.ts
oauthProvider({
  allowDynamicClientRegistration: true,
  allowUnauthenticatedClientRegistration: true, 
})

当 MCP 协议标准化无认证动态客户端注册时,allowUnauthenticatedClientRegistration 将被弃用。当前,客户端 ID 元数据文档software_statementjwks_uri仍在讨论中。

动态客户端注册过期时间

您可设置动态注册机密客户端的过期时间。默认动态注册机密客户端无过期。

auth.ts
oauthProvider({
  allowDynamicClientRegistration: true,
  clientRegistrationClientSecretExpiration: "30d", 
})

动态客户端注册默认范围

若客户端注册时未提交 scopes,可设置默认 scopes。所有默认 scopes 必须在 scopes 中定义。

auth.ts
oauthProvider({
  scopes: ["reader", "editor"],
  clientRegistrationDefaultScopes: ["reader"], 
})

若还需设置允许的额外 scopes,设置 clientRegistrationAllowedScopes。该集合包含默认 scopes。

auth.ts
oauthProvider({
  scopes: ["reader", "editor"],
  clientRegistrationDefaultScopes: ["reader"],
  clientRegistrationAllowedScopes: ["editor"], 
})

PKCE 配置

PKCE 是防止授权码被截获的一种安全机制。插件遵循 OAuth 2.1 规范,默认对所有授权码流程要求 PKCE。

默认行为

默认要求所有客户端使用 PKCE,最大安全、符合 OAuth 2.1 最佳实践。

总是要求 PKCE:

  • 公有客户端(原生/用户代理应用)
  • 包含 offline_access 范围的授权请求(刷新令牌)

每客户端 PKCE 配置

客户端可在注册时选择关闭 PKCE(适配兼容性):

register-client.ts
// 注册不支持 PKCE 的机密客户端
const response = await auth.api.createOAuthClient({
  headers,
  body: {
    client_name: 'Legacy Backend Service',
    redirect_uris: ['https://app.example.com/callback'],
    token_endpoint_auth_method: 'client_secret_post',
    grant_types: ['authorization_code'],
    require_pkce: false, // 选择关闭 PKCE
  }
});

require_pkce 字段说明:

  • 默认 true(要求 PKCE)
  • 仅适用于机密客户端
  • 公有客户端忽略(始终强制 PKCE)
  • offline_access 范围忽略(始终强制 PKCE)

使用 require_pkce: false 场景:

  • 兼容不支持 PKCE 的遗留机密客户端
  • 后端到后端集成,无法更新客户端
  • 迁移期间短期兼容

建议: 尽可能保持启用 PKCE(默认),即使机密客户端,也增加安全防护。

从 oidc-provider 迁移

若从已废弃的 oidc-provider 插件迁移,且存在不支持 PKCE 的机密客户端:

  1. 旧客户端可针对单个客户端设置 require_pkce: false
  2. 新客户端注册应始终开启 PKCE(默认)。
  3. 逐步淘汰不支持 PKCE 的客户端。
  4. 监控使用了 require_pkce: false 的客户端以便规划迁移。

安全考虑

PKCE 防止授权码截获。即使机密客户端使用 client_secret 认证,PKCE 依旧提供额外安全保障:

  • 防御深度:多层防护
  • 防止误配置:密钥泄露风险降低
  • 符合未来标准

仅在遗留兼容绝对必要时关闭机密客户端的 PKCE。

组织

OAuth 客户端注册时绑定用户或 reference_id,且不可变。

若使用 组织插件,请确保在新建客户端时,激活会话中的 activeOrganizationId 被设置。

auth.ts
oauthProvider({
  clientReference: ({ session }) => {
    return (session?.activeOrganizationId as string | undefined) ?? undefined;
  },
})

具体设置用户权限和角色见 Claims

客户端 CRUD 权限

确定登录用户具备客户端创建、读取、更新、删除权限,可使用 clientPrivileges 配置。默认允许拥有匹配 userIdclientReference 的用户操作。

示例仅允许组织管理员对 OAuth 客户端执行 CRUD 操作,假设普通用户不可创建客户端:

auth.ts
oauthProvider({
  clientPrivileges: async ({ action, headers, user, session }) => {
    if (!session?.activeOrganizationId) return false;
    const { data: member } = await auth.api.getActiveMember({
      headers,
    });
    return member.role === 'owner';
  },
})

存储

默认所有密钥在数据库中以 hashed 形式存储,防止泄露时暴露 client_secret

  • storeClientSecret:应用客户端密钥的存储方式。仅当 disableJwtPlugin: true 时,客户端密钥以 encrypted 方式存储。
  • storeTokens:令牌值的存储方式,主要指会话刷新令牌和不透明访问令牌。

限流

OAuth 提供者内置所有 OAuth 端点限流,防止滥用和拒绝服务攻击。

限流规则为按 IP 及端点。每个客户端 IP 对每个端点均有独立的限流计数器,重置窗口时间后复位。

此限流仅在 Better Auth 全局限流开启时生效。默认限流仅生产环境生效。详见 限流

默认限制:

端点时间窗口最大请求数
/oauth2/token60秒20 次
/oauth2/authorize60秒30 次
/oauth2/introspect60秒100 次
/oauth2/revoke60秒30 次
/oauth2/register60秒5 次
/oauth2/userinfo60秒60 次

您可自定义各端点的限流参数:

auth.ts
oauthProvider({
  rateLimit: {
    token: { window: 60, max: 20 },        // 每分钟 20 次请求
    authorize: { window: 60, max: 30 },    // 每分钟 30 次请求
    introspect: { window: 60, max: 100 },  // 每分钟 100 次请求
    revoke: { window: 60, max: 30 },       // 每分钟 30 次请求
    register: { window: 60, max: 5 },      // 每分钟 5 次请求
    userinfo: { window: 60, max: 60 },     // 每分钟 60 次请求
  },
})

如需关闭某个端点的自定义限流,回退到全局限流,设置该端点为 false

auth.ts
oauthProvider({
  rateLimit: {
    introspect: false, // 使用全局限流替代此端点限流
  },
})

设置端点为 false,将移除 OAuth 提供者更严格的限流,但依然受 Better Auth 全局限流限制(若启用)。

刷新令牌自定义

您可使用 formatRefreshToken 自定义会话刷新令牌的字符串格式。

此函数可为刷新令牌增加功能,如加密。

示例如更改刷新令牌格式,同时兼容原有简单格式:

auth.ts
oauthProvider({
  formatRefreshToken: {
    encrypt: (token, sessionId) => {
      const res = sessionId ? `1.${token}.${sessionId}` : token;
      return res;
    },
    decrypt: (token) => {
      const tokenSplit = token.split('.');
      if (tokenSplit.length === 3 && tokenSplit.at(0) === '1') {
        return {
          token: tokenSplit.at(1),
          sessionId: tokenSplit.at(2),
        };
      }
      return { token };
    },
  }
})

加密伪代码示例:

auth.ts
import { betterAuth } from "better-auth";
import { CompactEncrypt, compactDecrypt } from 'jose'
import { oauthProvider } from "@better-auth/oauth-provider"; 

const secret = "SOME_SECRET_OR_KEY"
const alg = "A256KW"
const enc = "A256GCM"

const auth = betterAuth({
  plugins: [
    oauthProvider({
    formatRefreshToken: {
      encrypt: (token, sessionId) {
        const value = JSON.stringify({
          sessionId,
          token,
        });
        const jwe = await new CompactEncrypt(Buffer.from(value))
          .setProtectedHeader({ alg, enc })
          .encrypt(secret);
        return jwe;
      },
      decrypt: (token) {
        const { plaintext } = await compactDecrypt(token, secret);
        const payload = new TextDecoder().decode(plaintext);
        return JSON.parse(payload);
      },
    }
  })
]
})

广告元数据

可自定义元数据端点,实现对外展示的 scopes 和 claims 与实际支持的不同,避免暴露所有支持的权限。

所有出现在 advertisedMetadata 中的 scopes 必须scopes 中声明,否则初始化失败。

Scopes

auth.ts
oauthProvider({
  scopes: ["openid", "profile", "email", "offline_access", "read:post"],
  advertisedMetadata: {
    scopes_supported: ["openid", "profile", "read:post"],
  },
})

Claims

声明为额外声明,除默认支持的以外。仅对 OIDC(即 openid 范围)适用。

auth.ts
oauthProvider({
  advertisedMetadata: {
    claims_supported: ["https://example.com/roles"],
  },
})

禁用 JWT 插件

默认情况下,访问和 ID 令牌可通过 JWT 插件签发与验证。

可禁用 JWT 要求,此时访问令牌总是以不透明格式且 ID 令牌使用 HS256 对称签名(使用 client_secret)。该选项仍符合 OIDC,/userinfo 依旧可用,签名的 id_token 依旧提供。

关键差异:

  • 提供有效 resource 时,访问令牌总是返回不透明格式,非 JWT 格式
  • 公有客户端不返回 id_token,但返回的访问令牌依然可通过 /oauth2/userinfo 获取用户数据
  • 机密客户端的 id_token 使用其 client_secret 签名
auth.ts
oauthProvider({
  disableJwtPlugin: true, 
})

逐对(Pairwise)主题标识符

默认情况下,令牌中的 sub(主体)声明使用用户的内部 ID,是所有客户端通用的公开主体类型,符合 OIDC 核心规范 8节

您可启用 逐对(pairwise) 主题标识符,使每个客户端为同一用户生成唯一且不可关联的 sub,防止关联分析。

auth.ts
oauthProvider({
  pairwiseSecret: "your-256-bit-secret", 
})

当配置了 pairwiseSecret,服务器在发现端点的 subject_types_supported 同时声明 "public""pairwise"。客户端通过注册时设置 subject_type: "pairwise" 选择逐对。

每客户端配置

register-client.ts
const response = await auth.api.createOAuthClient({
  headers,
  body: {
    client_name: 'Privacy-Sensitive App',
    redirect_uris: ['https://app.example.com/callback'],
    token_endpoint_auth_method: 'client_secret_post',
    subject_type: 'pairwise', // 开启逐对 sub
  }
});

工作原理

逐对标识符通过基于客户端第一个重定向 URI 的主机(sector identifier)和用户 ID,使用 pairwiseSecret 做 HMAC-SHA256 生成。

因此:

  • 两个不同 host 的客户端对同一用户给出不同的 sub
  • 相同 host 的两个客户端对同一用户给出相同的逐对 sub(遵循 OIDC 核心 8.1 节)
  • 同一个客户端对某用户总是给出同样的 sub(确定性)

逐对 sub 出现在:

  • id_token
  • /oauth2/userinfo 响应
  • 令牌核查 (/oauth2/introspect)

JWT 访问令牌始终使用真实用户 ID 作为 sub,因资源服务器可能需要直接查询用户信息。

限制:

  • 不支持 sector_identifier_uri。逐对客户端所有 redirect_uris 必须共享同一主机。跨主机注册会被拒绝。
  • pairwiseSecret 必须至少 32 字符。
  • 旋转 pairwiseSecret 会改变所有逐对 sub,破坏已存在 RP 会话。配置后应固定该秘密。

MCP

仅需添加资源服务器指向此 OAuth 2.1 授权服务器,轻松让 API 兼容 MCP 协议

若使用 "openid" 和机密 MCP 客户端,禁止禁用 JWT 插件,因为 id_token 验证可能不支持仅用 client_secret

安装

添加资源服务器客户端

(可选)如果本地有 auth 配置,可作为参数提供给客户端,便于配置校验。亦可直接在调用时覆盖。若无,则由 TypeScript 指导最低配置要求。

server-client.ts
import { auth } from "@/lib/auth";
import { createAuthClient } from "better-auth/client";
import { oauthProviderResourceClient } from "@better-auth/oauth-provider/resource-client"

export const serverClient = createAuthClient({
  plugins: [oauthProviderResourceClient(auth)], // auth 可选
});

向 API 添加 OAuth 受保护资源元数据

/.well-known/oauth-protected-resource/[resource-path]/route.ts
import { serverClient } from "@/lib/server-client";

export const GET = async () => {
  const metadata = await serverClient.getProtectedResourceMetadata({
    resource: "https://api.example.com", // `aud` 声明
    authorization_servers: ["https://auth.example.com"],
  })

  return new Response(JSON.stringify(metadata), {
    headers: {
      "Content-Type": "application/json",
      "Cache-Control":
        "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
    },
  });
};

若使用 allowUnauthenticatedClientRegistration,确保 API 服务器本身也是机密客户端:

await auth.api.createOAuthClient({
  headers,
  body: {
    redirect_uris: [redirectUri],
  }
});

以上信息应传入 remoteVerify.clientIdremoteVerify.clientSecret。此外,remoteVerify.introspectUrl 类似 ${BASE_URL}/${AUTH_PATH}/oauth2/introspect

若您选择不支持 allowUnauthenticatedClientRegistration(仅支持 allowDynamicClientRegistration),则 MCP 客户端(如 ChatGPT、Anthropic、Gemini)需要能够让您在其 UI 或对话时动态输入公有 client_id。

处理 MCP 错误

必须指定特定 audience 进行验证,默认为所有 validAudiencesbaseUrl

  • 使用客户端 verifyAccessToken 函数

参考 验证 示例。

  • 若有 auth 配置,可用客户端 verifyAccessToken 自动决定端点
api/[endpoint].ts
import { auth } from "@/lib/auth";
import { serverClient } from "@/lib/server-client";

export const GET = async (req: Request) => {
  const authorization = req.headers?.get("authorization") ?? undefined;
  const accessToken = authorization?.startsWith("Bearer ")
    ? authorization.replace("Bearer ", "")
    : authorization;
  const payload = await serverClient.verifyAccessToken(
    accessToken, {
      verifyOptions: {
        audience: "https://api.example.com",
      }
    }
  );
  // ...后续操作
}
  • 使用 mcpHandler 辅助函数
api/[transport]/route.ts
import { createMcpHandler } from "mcp-handler";
import { mcpHandler } from "@better-auth/oauth-provider";
import { z } from "zod";

const handler = mcpHandler({
  jwksUrl: "https://auth.example.com/api/auth/jwks",
  verifyOptions: {
    issuer: "https://auth.example.com",
    audience: "https://api.example.com",
  },
}, (req, jwt) => {
  return createMcpHandler(
    (server) => {
      server.registerTool(
        "echo", {
          description: "Echo a message",
          inputSchema: {
            message: z.string(),
          },
        },
        async ({ message }) => {
          return {
            content: [
              {
                type: "text",
                text: `Echo: ${message}${
                  jwt?.sub
                    ? ` for user ${jwt.sub}`
                    : ""
                }`,
              },
            ],
          };
        }
      );
    }, {
      serverInfo: {
        name: "demo-better-auth",
        version: "1.0.0",
      }
    }, {
      basePath: "/api",
      maxDuration: 60,
      verboseLogs: true,
    }
  )(req);
});

export { handler as GET, handler as POST, handler as DELETE };

数据库 Schema

OAuth 提供者插件新增以下表格:

OAuth 客户端表

表名:oauthClient

Table
字段
类型
描述
id
string
pk
OAuth 客户端数据库 ID
clientId
string
pk
OAuth 客户端唯一标识
clientSecret
string
?
OAuth 客户端密钥。公有客户端使用 PKCE 时可选。
disabled
boolean
?
是否禁用该应用
skipConsent
boolean
?
是否允许跳过同意页。适用于受信任应用。
enableEndSession
boolean
?
是否允许通过 id_token 登出。适用于受信任应用。
subjectType
string
?
客户端主题标识类型。设为 "pairwise" 时,为每用户提供唯一、不可关联的 sub。需配置服务端 `pairwiseSecret`。
scopes
string[]
?
客户端允许使用的权限范围
userId
string
fk?
客户端所属用户 ID(可选)
referenceId
string
fk?
客户端所属引用 ID(非用户)(可选)
createdAt
Date
创建时间
updatedAt
Date
更新时间
name
string
?
OAuth 客户端名称
uri
string
?
在 UI 展示的网站 URI
icon
string
?
在 UI 展示的网站图标
contacts
string[]
?
客户端联系方式列表(如客服邮箱、电话),展示于 UI
tos
string
?
客户端服务条款,展示于 UI
policy
string
?
客户端隐私政策,展示于 UI
softwareId
string
?
客户端定义的软件标识符,不同版本应保持一致。
softwareVersion
string
?
客户端定义的软件版本号。
softwareStatement
string
?
包含软件元数据的签名 JWT 声明。
redirectUris
string[]
重定向 URI 数组
tokenEndpointAuthMethod
string
?
指定 Token 端点认证方法。支持:['none', 'client_secret_basic', 'client_secret_post']
grantTypes
string[]
?
支持的授权类型数组。支持:['authorization_code', 'client_credentials', 'refresh_token']
responseTypes
string[]
?
支持的响应类型数组。支持:['code']
public
boolean
?
客户端是否为公有客户端
type
string
?
OAuth 客户端类型。支持:['web', 'native', 'user-agent-based']
metadata
json
?
客户端附加元数据

OAuth 刷新令牌表

表名:oauthRefreshToken

Table
字段
类型
描述
id
string
pk
刷新令牌数据库 ID
token
string
刷新令牌的哈希或加密后的值
clientId
string
fk
所属 OAuth 客户端 ID
sessionId
string
fk
发放令牌时使用的会话 ID(且会话仍有效)
userId
string
fk
所属用户 ID
referenceId
string
fk?
同意的引用 ID
scopes
string[]
授权的 scopes 数组
revoked
Date
?
撤销时间戳
authTime
Date
?
原始认证时间。令牌旋转时保留,该时间用于 OIDC Core 1.0 12.2节中正确的 auth_time 声明。
createdAt
Date
创建时间戳
expiresAt
Date
过期时间戳

OAuth 访问令牌表

表名:oauthAccessToken

Table
字段
类型
描述
id
string
pk
不透明访问令牌数据库 ID
token
string
访问令牌哈希或加密值
clientId
string
fk
所属 OAuth 客户端 ID
sessionId
string
fk?
发放令牌使用的会话 ID(且会话仍有效)
refreshId
string
fk?
关联的刷新令牌 ID
userId
string
fk?
所属用户 ID
referenceId
string
fk?
同意的引用 ID
scopes
string[]
授权的 scopes 数组
createdAt
Date
创建时间
expiresAt
Date
过期时间

OAuth 同意表

表名:oauthConsent

Table
字段
类型
描述
id
string
pk
同意记录数据库 ID
userId
string
fk
用户 ID
clientId
string
fk
OAuth 客户端 ID
referenceId
string
fk?
同意引用 ID
scopes
string
以逗号分隔的已同意 scopes
createdAt
Date
同意时间戳
updatedAt
Date
同意更新时间戳

选项

前缀

可为不透明访问令牌、刷新令牌和客户端密钥添加前缀,有助于秘密扫描工具(如 GitHub Secret ScannersGitGuardianTrufflehog)识别令牌格式。

推荐在部署前先添加前缀,部署后视为不可变,否则应使用对应的生成函数。

以下配置项均可在 prefix 中设置:

  • opaqueAccessToken:不透明访问令牌前缀。若已部署,使用 generateOpaqueAccessToken 实现。
  • refreshToken:刷新令牌前缀。若已部署,使用 generateRefreshToken 实现。
  • clientSecret:客户端密钥前缀。若已部署,使用 generateClientSecret 实现。

优化提示

为提升查询性能,数据库适配器可将 oauthClient 表中的 client_id 字段映射为 id。注意 id 应支持 UUID 和 URL 格式的字符串。

迁移

OIDC Provider 插件 迁移

配置变更

  • idTokenExpiresIn 默认为 10 小时(之前通过 accessTokenExpiresIn 为 1 小时)
  • refreshTokenExpiresIn 默认为 30 天(之前为 7 天)
  • advertisedMetadata(之前为 metadata)不再支持修改元数据字段,防止误配置。
  • clientRegistrationDefaultScopes(之前 defaultScope)改为数组格式,非空格分隔字符串
  • consentPage 现在必须设置
  • getConsentHTML 移除,改用 consentPage,因原始 HTML 不被授权端点支持
  • requirePKCE(全局配置)移除。PKCE 由 OAuth 2.1 默认要求,个别客户端可通过 require_pkce: false 设为不强制。
  • allowPlainCodeChallengeMethod 移除。plain 改用更安全的默认 S256 方法。
  • customUserInfoClaims(之前 getAdditionalUserInfoClaim)参数改为传入访问令牌 JWT 负载。
  • storeClientSecret 默认为 hashed,若设置 disableJwtPlugin: true,默认为 encrypted(之前为明文 plain)。
  • JWT 插件默认启用。禁用请设置 disableJwtPlugin: true
  • 授权请求 code_challenge_method 必须大写 "S256",符合 OAuth 2.1。

数据库变更

表:oauthClient

旧名为 oauthApplication

  • storeClientSecret 未设置或为 plain,需将所有存储的 clientSecret 转为 SHA-256 哈希并编码为 base64Url 或按新设置方式存储。示例函数:
import { createHash } from "@better-auth/utils/hash";
import { base64Url } from "@better-auth/utils/base64";

const defaultHasher = async (value: string) => {
	const hash = await createHash("SHA-256").digest(
		new TextEncoder().encode(value),
	);
	const hashed = base64Url.encode(new Uint8Array(hash), {
		padding: false,
	});
	return hashed;
};
  • type 不再必需,改为新增 public(boolean)字段。迁移规则:
    • type: "public":设置 type: undefinedpublic: trueclientSecret: undefined
    • type: "native"public: trueclientSecret: undefined
    • type: "user-agent-based"public: trueclientSecret: undefined
    • clientSecret: undefinedpublic: true
  • 重命名 redirectURLsredirectUris
  • 新增 requirePkce 字段(可选,默认 true),兼容旧机密客户端需设置为 false
  • metadata 字段拆分存储,OIDC 插件未用,OAuth 插件可能会用
表:oauthAccessToken

方案一(简单):

可选择不转移此表,影响较小。用户需重新登录。删除现有 oauthAccessToken 表即可。

方案二(复杂):

全表迁移(可能需先克隆一份 oauthAccessTokenoauthRefreshToken)。

  • 将带有 refreshToken 字段的条目转换为新建 oauthRefreshToken 条目:
{
  token: defaultHasher(refreshToken),
  expiresAt: refreshTokenExpiresAt,
  clientId: clientId,
  scopes: scopes,
  userId: userId,
  createdAt: createdAt,
  updatedAt: updatedAt,
}
  • 保留 oauthAccessToken 并关联新建 oauthRefreshToken
{
  token: defaultHasher(accessToken),
  expiresAt: accessTokenExpiresAt,
  clientId: clientId,
  scopes: scopes,
  refreshId: oauthRefreshToken.id, // 无刷新令牌时为 `undefined`
  createdAt: createdAt,
  updatedAt: updatedAt,
}

MCP 插件 迁移

MCP 端点由 /mcp 迁移至 /oauth2

  • /oauth2/authorize(之前 /mcp/authorize
  • /oauth2/token(之前 /mcp/token
  • /oauth2/register(之前 /mcp/register
  • /mcp/get-session 删除,改用 /oauth2/introspect
  • /.well-known/oauth-protected-resource 删除,使用 mcpHandler 辅助或手动调用服务器 api.oAuth2introspectVerify 或资源客户端 verifyAccessToken
  • 数据库变更同 From OIDC Provider Plugin 部分。

本页目录

安装
挂载插件
迁移数据库
添加 ./well-known 端点
创建您的第一个 OAuth 客户端
客户端插件
OAuth 客户端
资源客户端
使用说明
OAuth 客户端
获取客户端信息
获取公有客户端信息
列出客户端
创建客户端
更新客户端
旋转客户端密钥
删除客户端
OAuth 同意管理
获取同意详情
列出同意
更新同意
删除同意
动态注册端点
配置
基本示例
授权端点
令牌端点
授权码授权方式
客户端凭证授权方式
刷新令牌授权方式
同意端点
Continue 端点
令牌核查端点
撤销端点
会话结束端点
UserInfo 端点
Well known
Openid 配置
OAuth 授权服务器
API 服务器
验证
JWT 验证
不透明访问令牌(opaque)
建议
范围与权限
配置
重定向页面
登录页面
同意页面
注册页面
选择账户页面
登录后页面
缓存的受信任客户端
有效受众(Audiences)
Scopes
Claims
过期时间配置
注册
动态客户端注册
动态客户端注册过期时间
动态客户端注册默认范围
PKCE 配置
默认行为
每客户端 PKCE 配置
从 oidc-provider 迁移
安全考虑
组织
客户端 CRUD 权限
存储
限流
刷新令牌自定义
广告元数据
Scopes
Claims
禁用 JWT 插件
逐对(Pairwise)主题标识符
每客户端配置
工作原理
MCP
安装
确保 Well Known 路径正确
添加资源服务器客户端
向 API 添加 OAuth 受保护资源元数据
处理 MCP 错误
数据库 Schema
OAuth 客户端表
OAuth 刷新令牌表
OAuth 访问令牌表
OAuth 同意表
选项
前缀
优化提示
迁移
OIDC Provider 插件 迁移
配置变更
数据库变更
表:oauthClient
表:oauthAccessToken
MCP 插件 迁移