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 客户端
- 授权管理: 查看和撤销用户授权
- 密钥轮换: 支持客户端密钥轮换
- 应用访问控制: 用户/部门白名单、应用角色与自定义 claims
启用 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: `${env.CORS_ORIGIN}/oauth-login`, consentPage: `${env.CORS_ORIGIN}/consent`, scopes: ['openid', 'profile', 'email', 'offline_access'] }) ]})| 配置项 | 说明 | 示例 |
|---|---|---|
loginPage |
用户未登录时重定向的登录页面 | /oauth-login |
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,客户端使用同源 /api/auth。 // 生产公开 issuer 固定为 https://iam.cq-i.cn/api/auth。 plugins: [adminClient(), oauthProviderClient()]})Well-Known 端点
Section titled “Well-Known 端点”启用 OAuth Provider 后,系统会提供以下发现端点:
| 端点 | 说明 |
|---|---|
/.well-known/oauth-authorization-server/api/auth |
RFC 8414 授权服务器元数据(只在后端 origin) |
/api/auth/.well-known/openid-configuration |
OpenID Connect 发现文档 |
示例 URL:
https://iam-api.cq-i.cn/.well-known/oauth-authorization-server/api/authhttps://iam.cq-i.cn/api/auth/.well-known/openid-configuration
OIDC 客户端应使用前端域名下的 openid-configuration(经已有 /api/* 代理到 Hono)。RFC 8414 元数据只在后端 Worker 提供,不占用 iam.cq-i.cn 根路径。
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_basic', grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], scope: 'openid profile email', skip_consent: false, enable_end_session: true, require_pkce: 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 |
require_pkce |
boolean |
是否强制 Authorization Code 使用 PKCE |
post_logout_redirect_uris |
string[] |
登出后重定向 URI |
openid profile email 是当前默认 scope,机密客户端默认推荐 client_secret_basic。public client 使用 token_endpoint_auth_method=none,并应强制 require_pkce=true。邮箱相关 claims 用于下游系统的首次绑定;绑定后应改用 ID Token 的 sub。
管理端还可维护 metadata:description、logo、tokenLifetime、allowedDepartments、allowedUsers。metadata 的解析和表单 schema 位于 @IAM/contracts。
获取客户端列表
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. 用户选择企业微信,或跳转到 /login/email
B->>S: 3c. 登录成功后整页访问 /api/auth/oauth2/authorize?...
end
alt 首次企微登录未补邮箱或邮箱未验证
S->>B: 重定向到 /complete-profile
B->>B: 填写企业邮箱并完成邮件验证
B->>S: POST /api/auth/oauth2/continue
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-state &nonce=random-nonce &code_challenge=base64url-sha256 &code_challenge_method=S256IAM 授权服务器收到请求后,检查用户是否已登录(即浏览器是否携带有效的 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-login 主入口是企业微信。用户也可以点击「使用邮箱密码登录」,跳到 /login/email;跳转会保留授权参数,并在缺少 callbackUrl 时补成 /api/auth/oauth2/authorize?...。
邮箱登录使用 Better Auth 前端 SDK。成功后若回跳地址以 /api/ 开头,前端执行整页跳转,让浏览器带着新 Session Cookie 重新进入授权端点。
第四步:已登录会话恢复 OIDC 流程
Section titled “第四步:已登录会话恢复 OIDC 流程”登录成功后浏览器再次请求 /api/auth/oauth2/authorize:
- Session 已建立:登录接口通过
Set-Cookie种入会话 - 验签授权上下文:授权端点校验 URL 上的
exp与sig,确认参数未被篡改且未过期 - 继续授权决策:用户已登录,进入跳过确认或 consent 流程
第五步:授权决策
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&code_verifier=original-random-value第八步:返回令牌
Section titled “第八步:返回令牌”IAM 验证无误后,返回令牌:
{ "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "...", "id_token": "..."}IAM 提供了 OAuth 管理界面:
-
OAuth 客户端管理:
/admin/oauth-clients- 创建、编辑、删除客户端
- 查看客户端详情
- 轮换客户端密钥
-
OAuth 授权管理:
/admin/consents- 查看所有授权记录
- 编辑授权范围
- 撤销用户授权
启用 OAuth Provider 插件后,需要更新数据库结构:
# 生成 Schemapnpm dlx auth@1.7.1 generate --yes \ --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
- 强制 PKCE: public client 必须使用 S256;机密客户端也建议开启
- 校验 ID Token: 接入方必须校验签名、
iss、aud、exp、iat和nonce - 角色边界:
app_role是应用角色提示,接入应用仍需落实自己的授权策略