OAuth 2.1 Provider
IAM 系统内置 OAuth 2.1 Provider 功能,可以作为身份提供者(IdP)为第三方应用提供认证服务。
- OAuth 2.1 协议支持: Authorization Code、Refresh Token、Client Credentials
- OpenID Connect: 支持 OIDC 标准,提供 ID Token
- 客户端管理: 创建、编辑、删除 OAuth 客户端
- 授权管理: 查看和撤销用户授权
- 密钥轮换: 支持客户端密钥轮换
启用 OAuth Provider 插件
Section titled “启用 OAuth Provider 插件”在 packages/auth/src/index.ts 中配置:
import { betterAuth } from 'better-auth'import { oauthProvider } from '@better-auth/oauth-provider'
export const auth = betterAuth({ // ... 其他配置 plugins: [ admin(), jwt(), oauthProvider({ loginPage: '/sign-in', // 登录页面路径 consentPage: '/consent' // 授权同意页面路径 }) ]})| 配置项 | 说明 | 示例 |
|---|---|---|
loginPage | 用户未登录时重定向的登录页面 | /sign-in |
consentPage | 用户授权同意页面 | /consent |
前端 Auth Client
Section titled “前端 Auth Client”在 apps/web/src/lib/auth-client.ts 中添加 OAuth Provider 客户端插件:
import { oauthProviderClient } from '@better-auth/oauth-provider/client'import { createAuthClient } from 'better-auth/react'
export const authClient = createAuthClient({ baseURL: env.NEXT_PUBLIC_SERVER_URL, plugins: [adminClient(), oauthProviderClient()]})Well-Known 端点
Section titled “Well-Known 端点”启用 OAuth Provider 后,系统会自动提供以下发现端点:
| 端点 | 说明 |
|---|---|
/.well-known/oauth-authorization-server | OAuth 2.0 授权服务器元数据 |
/.well-known/openid-configuration | OpenID Connect 发现文档 |
示例 URL:
https://your-domain.com/api/auth/.well-known/oauth-authorization-serverhttps://your-domain.com/api/auth/.well-known/openid-configuration
OAuth 客户端管理
Section titled “OAuth 客户端管理”使用 authClient.oauth2.createClient() API:
const { data, error } = await authClient.oauth2.createClient({ redirect_uris: ['https://your-app.com/callback'], client_name: 'My Application', token_endpoint_auth_method: 'client_secret_post', grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], scope: 'openid profile email', skip_consent: false, enable_end_session: true})
// 返回 client_id 和 client_secretconsole.log(data.client_id, data.client_secret)| 参数 | 类型 | 说明 |
|---|---|---|
redirect_uris | string[] | 授权回调 URI 列表(必填) |
client_name | string | 客户端名称 |
token_endpoint_auth_method | string | 认证方式:client_secret_post、client_secret_basic、none |
grant_types | string[] | 授权类型:authorization_code、refresh_token、client_credentials |
response_types | string[] | 响应类型,通常为 ['code'] |
scope | string | 请求的权限范围 |
skip_consent | boolean | 是否跳过用户授权确认 |
enable_end_session | boolean | 是否启用 OIDC end_session |
post_logout_redirect_uris | string[] | 登出后重定向 URI |
获取客户端列表
Section titled “获取客户端列表”const { data, error } = await authClient.oauth2.getClients()// data: OAuthClient[]const { data, error } = await authClient.oauth2.updateClient({ client_id: 'xxx', update: { client_name: 'New Name' }})轮换客户端密钥
Section titled “轮换客户端密钥”const { data, error } = await authClient.oauth2.client.rotateSecret({ client_id: 'xxx'})// 返回新的 client_secretconst { error } = await authClient.oauth2.deleteClient({ client_id: 'xxx'})获取授权列表
Section titled “获取授权列表”const { data, error } = await authClient.oauth2.getConsents()// data: OAuthConsent[]获取单个授权
Section titled “获取单个授权”const { data, error } = await authClient.oauth2.getConsent({ query: { id: 'consent-id' }})const { data, error } = await authClient.oauth2.updateConsent({ id: 'consent-id', update: { scopes: ['openid', 'profile'] }})const { error } = await authClient.oauth2.deleteConsent({ id: 'consent-id'})在 OAuth 2.1 / OIDC 流程中涉及以下角色:
| 角色 | 说明 | 对应实体 |
|---|---|---|
| 用户 (Resource Owner) | 坐在浏览器前的终端用户 | 浏览器 |
| 客户端 (Client) | 请求访问用户资源的第三方应用 | CRM、OAuth Debugger 等 |
| 授权服务器 (Authorization Server) | 负责登录、发放授权码和令牌 | IAM 系统 |
| 资源服务器 (Resource Server) | 存放用户数据的 API | IAM 后端 API |
sequenceDiagram
participant C as 第三方客户端
participant B as 用户浏览器
participant S as IAM 授权服务器
C->>B: 1. 重定向到 /api/auth/oauth2/authorize?...
B->>S: 2. 请求授权端点
alt 用户未登录
S->>B: 3a. 302 重定向到 /oauth-login?...&exp=...&sig=...
B->>B: 3b. 用户输入邮箱密码
B->>S: 3c. POST /api/auth/sign-in/email(携带 oauth_query)
S->>S: 3d. 验签 + 创建 Session + 恢复原始 OIDC query
end
alt 需要用户授权确认
S->>B: 4a. 302 重定向到 /consent?...&exp=...&sig=...
B->>B: 4b. 用户点击"同意"
B->>S: 4c. POST /api/auth/oauth2/consent(携带 oauth_query)
end
S->>B: 5. 302 重定向到 redirect_uri?code=...&state=...
B->>C: 6. 携带 code 到回调地址
C->>S: 7. POST /api/auth/oauth2/token 换取令牌
S->>C: 8. 返回 access_token, id_token, refresh_token
Authorization Code 流程详解
Section titled “Authorization Code 流程详解”OIDC 在”用户未登录”时的流程,本质上是两段流程的拼接:先走一段本地登录流程,登录成功后再恢复原来的 OIDC 授权流程继续执行。
第一步:第三方客户端发起授权请求
Section titled “第一步:第三方客户端发起授权请求”客户端将用户重定向到 IAM 的授权端点:
GET /api/auth/oauth2/authorize ?client_id=xxx &redirect_uri=https://app.com/callback &response_type=code &scope=openid profile email &state=random-stateIAM 授权服务器收到请求后,检查用户是否已登录(即浏览器是否携带有效的 Session Cookie)。
第二步:用户未登录 → 跳转登录页
Section titled “第二步:用户未登录 → 跳转登录页”如果用户尚未登录,授权服务器会将当前的 OAuth query 参数加上 exp(过期时间)和 sig(签名),然后 302 重定向 到配置的登录页:
302 → /oauth-login?client_id=xxx&redirect_uri=...&exp=...&sig=...关键点:页面 URL 上的完整 query string 就是后续恢复 OIDC 流程的”凭据”,其中
exp和sig确保参数不被篡改且有时效性。
第三步:用户在登录页提交凭据
Section titled “第三步:用户在登录页提交凭据”用户在 OAuth 登录页输入邮箱密码后,前端通过原生表单提交将凭据连同当前页面的 oauth_query 一起发送到认证端点:
POST /api/auth/sign-in/email
email=user@example.com &password=xxx &oauth_query=client_id%3Dxxx%26redirect_uri%3D...%26exp%3D...%26sig%3D...为什么使用原生表单提交而非 AJAX? 因为登录成功后需要通过服务端 302 重定向来恢复 OIDC 流程,原生表单提交可以让浏览器自动跟随重定向链,无需前端手动处理跳转。
第四步:服务端创建 Session 并恢复 OIDC 流程
Section titled “第四步:服务端创建 Session 并恢复 OIDC 流程”/sign-in/email 端点完成以下操作:
- 验证凭据:校验邮箱和密码
- 创建 Session:生成 Session 并通过
Set-Cookie种入浏览器 - 恢复 OIDC 流程:因为请求中携带了
oauth_query,OAuth Provider 插件的 after hook 会自动:- 对
oauth_query中的exp和sig进行验签,确保参数未被篡改且未过期 - 将原始 OIDC query 暂存
- 在登录成功、Cookie 已设置后,自动恢复原始 OIDC 授权流程
- 对
第五步:授权决策
Section titled “第五步:授权决策”恢复后的请求重新进入 /oauth2/authorize 的判断逻辑,此时用户已登录:
- 跳过授权确认:如果客户端配置了
skip_consent: true,或用户已经同意过相同 scope,则直接签发authorization_code并 302 到第三方redirect_uri - 需要授权确认:跳转到 consent 页面
第六步:用户授权确认(可选)
Section titled “第六步:用户授权确认(可选)”如果需要用户确认,浏览器进入 consent 页面:
/consent?client_id=xxx&scope=openid+profile+email&exp=...&sig=...用户点击”同意”后,前端将当前 query 作为 oauth_query 提交到 /api/auth/oauth2/consent。服务端验签通过后签发 authorization_code,302 重定向到第三方 redirect_uri:
302 → https://app.com/callback?code=xyz123&state=random-state第七步:第三方客户端换取令牌
Section titled “第七步:第三方客户端换取令牌”客户端在回调接口收到 code 后,在后端直接请求 IAM 的 Token 端点:
POST /api/auth/oauth2/tokenContent-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=xyz123&redirect_uri=https://app.com/callback&client_id=xxx&client_secret=xxx第八步:返回令牌
Section titled “第八步:返回令牌”IAM 验证无误后,返回令牌:
{ "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "...", "id_token": "..."}IAM 提供了 OAuth 管理界面:
-
OAuth 客户端管理:
/oauth-clients- 创建、编辑、删除客户端
- 查看客户端详情
- 轮换客户端密钥
-
OAuth 授权管理:
/consents- 查看所有授权记录
- 编辑授权范围
- 撤销用户授权
启用 OAuth Provider 插件后,需要更新数据库结构:
# 生成 Schemapnpm dlx @better-auth/cli@latest generate \ --config packages/auth/src/index.ts \ --output packages/db/src/schema/auth.ts
# 生成迁移pnpm db:generate
# 应用迁移pnpm db:migrate- 保护客户端密钥:
client_secret仅在创建和轮换时返回一次 - 使用 HTTPS: 生产环境必须使用 HTTPS
- 验证 redirect_uri: 确保回调 URI 与注册的一致
- 定期轮换密钥: 建议定期轮换客户端密钥
- 最小权限原则: 只请求必要的 scope