Skip to content

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 客户端
  • 授权管理: 查看和撤销用户授权
  • 密钥轮换: 支持客户端密钥轮换

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

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()]
})

启用 OAuth Provider 后,系统会自动提供以下发现端点:

端点说明
/.well-known/oauth-authorization-serverOAuth 2.0 授权服务器元数据
/.well-known/openid-configurationOpenID Connect 发现文档

示例 URL:

  • https://your-domain.com/api/auth/.well-known/oauth-authorization-server
  • https://your-domain.com/api/auth/.well-known/openid-configuration

使用 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_secret
console.log(data.client_id, data.client_secret)
参数类型说明
redirect_urisstring[]授权回调 URI 列表(必填)
client_namestring客户端名称
token_endpoint_auth_methodstring认证方式:client_secret_postclient_secret_basicnone
grant_typesstring[]授权类型:authorization_coderefresh_tokenclient_credentials
response_typesstring[]响应类型,通常为 ['code']
scopestring请求的权限范围
skip_consentboolean是否跳过用户授权确认
enable_end_sessionboolean是否启用 OIDC end_session
post_logout_redirect_urisstring[]登出后重定向 URI
const { data, error } = await authClient.oauth2.getClients()
// data: OAuthClient[]
const { data, error } = await authClient.oauth2.updateClient({
client_id: 'xxx',
update: {
client_name: 'New Name'
}
})
const { data, error } = await authClient.oauth2.client.rotateSecret({
client_id: 'xxx'
})
// 返回新的 client_secret
const { error } = await authClient.oauth2.deleteClient({
client_id: 'xxx'
})
const { data, error } = await authClient.oauth2.getConsents()
// data: OAuthConsent[]
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)存放用户数据的 APIIAM 后端 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

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-state

IAM 授权服务器收到请求后,检查用户是否已登录(即浏览器是否携带有效的 Session Cookie)。

第二步:用户未登录 → 跳转登录页

Section titled “第二步:用户未登录 → 跳转登录页”

如果用户尚未登录,授权服务器会将当前的 OAuth query 参数加上 exp(过期时间)和 sig(签名),然后 302 重定向 到配置的登录页:

302 → /oauth-login?client_id=xxx&redirect_uri=...&exp=...&sig=...

关键点:页面 URL 上的完整 query string 就是后续恢复 OIDC 流程的”凭据”,其中 expsig 确保参数不被篡改且有时效性。

第三步:用户在登录页提交凭据

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 端点完成以下操作:

  1. 验证凭据:校验邮箱和密码
  2. 创建 Session:生成 Session 并通过 Set-Cookie 种入浏览器
  3. 恢复 OIDC 流程:因为请求中携带了 oauth_query,OAuth Provider 插件的 after hook 会自动:
    • oauth_query 中的 expsig 进行验签,确保参数未被篡改且未过期
    • 将原始 OIDC query 暂存
    • 在登录成功、Cookie 已设置后,自动恢复原始 OIDC 授权流程

恢复后的请求重新进入 /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/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=xyz123
&redirect_uri=https://app.com/callback
&client_id=xxx
&client_secret=xxx

IAM 验证无误后,返回令牌:

{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "...",
"id_token": "..."
}

IAM 提供了 OAuth 管理界面:

  • OAuth 客户端管理: /oauth-clients

    • 创建、编辑、删除客户端
    • 查看客户端详情
    • 轮换客户端密钥
  • OAuth 授权管理: /consents

    • 查看所有授权记录
    • 编辑授权范围
    • 撤销用户授权

启用 OAuth Provider 插件后,需要更新数据库结构:

Terminal window
# 生成 Schema
pnpm 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
  1. 保护客户端密钥: client_secret 仅在创建和轮换时返回一次
  2. 使用 HTTPS: 生产环境必须使用 HTTPS
  3. 验证 redirect_uri: 确保回调 URI 与注册的一致
  4. 定期轮换密钥: 建议定期轮换客户端密钥
  5. 最小权限原则: 只请求必要的 scope