Language note: this is a historical trust archive kept in its original Chinese. The 2026-08-07 OAuth/OIDC compliance-audit baseline (all 31 findings fixed, regression-tested) is preserved verbatim for third-party assessors. The current security design lives in Security Architecture.
本报告是 2026-08-07 对 fulla(时名 authforge)的完整 OAuth/OIDC 合规尽调 基线快照:逐条附 file:line 证据与规范章节引文。报告发布后所有发现均已在 后续版本中修复(含 F-002 Critical 凭据哈希不一致等),并沉淀为回归测试; 保留本档案作为第三方评估者可核查的信任资产。当前安全设计见 security-architecture.md。
OAuth/OIDC 规范性审查报告 — fulla
| 项目 | 内容 |
|---|---|
| 审查对象 | fulla (test/coverage-push 分支, 2026-08-07) |
| 审查范围 | RFC 6749 / 6750 / 7662 / 7009 / 7636 / 8252 / 8628 / 8414 / 7519/7517/7515 / 7591/7592 / 9068 / 9700 + OIDC Core 1.0 / Discovery 1.0 / RP-Initiated Logout / Back-Channel Logout |
| 审查方法 | 静态代码审查 + 规范条款逐条核验;所有判断附 file:line 证据 + 规范章节引文 |
| 不在范围 | 动态渗透测试、性能/可用 性评估、前端 SPA 实现审查 |
| 评级分档 | 核心规范按 MUST/SHOULD 分档;OIDC profile 按「核心 MUST」「扩展 SHOULD」分档 |
配套文档:审查计划(检查要点、检查方法、判定标准)见
oauth-oidc-compliance-audit-plan.md。本报告为评估结果(符合性评级、发现、整改)。
1. 执行摘要
1.1 总体符合度
fulla 在 **OAuth 2.0 核心(RFC 6749)的"快乐路径"**上基本合规:授权码与刷新令牌的生成、哈希存储、单次性、轮换、重用级联吊销都按规范实现;PKCE 的 S256 算法是规范正确的 base64url(raw digest);introspect/revoke 的客户端认证模型近期(commit 246db32)已修正为 RFC 7662/7009 要求的客户端凭证模型。
但存在 若干违反 MUST 的实质性偏差,集中在三类:
- 令牌端点客户端认证不完整 ——
refresh_tokengrant 完全跳过客户端认证(违反 RFC 6749 §3.2.1 MUST)。 - client_secret 哈希写入/校验算法不一致 —— 注册/管理路径写无盐大写 SHA-256,校验路径算有盐小写 SHA-256(违反 RFC 6749 §10.6 凭证保护 MUST,且事实上导致动态注册的客户端无法认证)。
- OIDC 扩展能力大面积缺失 ——
prompt/max_age/auth_time/acr/amr/azp/RP-Initiated Logout/nonce 防重放/id_token-on-refresh 均未实现,使本项目实际只能算"OAuth2 + 一个最小 id_token 签发",而非完整 OIDC Provider。
1.2 风险等级分布
| 等级 | 数量 | 代表项 |
|---|---|---|
| 严重 (Critical) | 1 | F-002 client_secret 哈希不一致导致动态注册客户端认证失败 |
| 高 (High) | 6 | refresh_token 无客户端认证、Redis 非常量时间比较、Redis refresh 存储空操作、authorization 端点错误未按 §4.1.2.1 重定向、device_authorization 未认证机密客户端、WWW-Authenticate Bearer 缺失 |
| 中 (Medium) | 9 | id_token 缺 auth_time/acr/amr/azp、refresh 不重发 id_token、PKCE 默认不强制、device slow_down 未发出、iss 硬编码、loopback 例外未实现、token 端点无限流、state 未 urlEncode、token_endpoint_auth_method 未持久化 |
| 低 (Low) | 7 | registration_endpoint 未广告、introspection 缺 jti/username、成功响应缺 Cache-Control、jti 不存在、claims_supported 与实际不符、OpenAPI grant_type 枚举缺 device_code、CORS 校验注释 |
1.3 最高优先级 5 项
| 编号 | 问题 | 风险 | 修复复杂度 |
|---|---|---|---|
| F-002 | client_secret 哈希写/读算法不一致 | Critical | 中(统一算法 + 数据迁移) |
| F-003 | refresh_token grant 跳过客户端认证 | High | 低(加 validateClient 调用) |
| F-004 | Redis 后端 client_secret 非常量时间比较 | High | 低 |
| F-005 | Redis 后端 refresh_token 存储为空操作 | High | 中(实现 Redis save/getRefreshToken) |
| F-007 | authorization 端点错误未按 §4.1.2.1 重定向 | High | 中(区分 redirectable vs client 错误) |
2. 评级尺度
| 评级 | 定义 |
|---|---|
| 符合 | 满足规范条款的 MUST 与 SHOULD,无功能/安全偏差 |
| 部分符合 | 实现核心要求但缺字段、缺边角 MUST 或不满足 SHOULD |
| 不符合 | 违反某条 MUST,或实现存在功能性/安全性错误 |
| 未实现 | 规范定义了能力但代码无对应实现(含 stub、advertised-but-missing) |
OIDC profile 分档(按用户要求):
- 核心 MUST 档:OIDC Core 中标 MUST 的条款(如 id_token 必备 claim、UserInfo 须校验 openid scope)
- 扩展 SHOULD 档:OIDC Core 的 SHOULD 条款,以及 Session Management / RP-Initiated Logout / Back-Channel Logout 等独立规范(这些虽非 OIDC Core 的 MUST,但是「可互操作的 OIDC Provider」的事实标配)
风险等级独立于评级:一个"未实现"的扩展 SHOULD 可能只是 Medium,而一个"不符合"的核心 MUST 通常是 High/Critical。
3. 逐规范符合性评估
3.1 RFC 6749 OAuth 2.0 Authorization Framework —— 部分符合
3.1.1 §1.6 / §3.1.1 协议须运行于 HTTPS;redirect_uri 须 https —— 不符合(Medium)
- 检查方法:Grep redirect_uri scheme 校验、loopback 例外。
- 证据:
libs/oauth2/include/fulla/oauth2/model/Client.h:86-90仅做std::find精确字符串匹配;libs/drogon/src/validation/RuleEngine.cc:120-133的 regex^https?://...同时接受 http 与 https;无任何代码强制 https 或实现 RFC 8252 §7.3 / RFC 6749 §3.1.2.1 的 loopback 端口通配。 - 偏差:redirect_uri 注册与匹配接受任意 scheme,未强制 https,未实现 loopback 例外。
- 依据:RFC 6749 §3.1.2.1 "the redirection endpoint SHOULD require the use of TLS";§1.6 明确 TLS 为 MUST 级别的部署前提。
- 见:F-014。
3.1.2 §3.1.2.3 redirect_uri 须精确匹配 —— 符合
- 证据:
Client.h:83-90isRegisteredRedirectUri用std::find(..., redirectUri) != ...end(),注释明示"RFC 6749 §3.1.2.3 requires exact match, not prefix/pattern matching"。authorize 端libs/oauth2/src/protocol/ClientService.cc:32-44调用之;exchange 端PostgresGrantRepository.cc:181-189复核之。 - 判定:精确匹配、无通配、无前缀。符合。
3.1.3 §4.1.2 授权码:TTL ≤10min、一次性、绑定 —— 符合
- TTL:
libs/oauth2/src/protocol/TokenService.cc:151authCode.expiresAt = nowSeconds() + authCodeTtl_;,默认 600s(OAuth2Plugin.cc:53),exchange 时校验(TokenService.cc:226-230)。 - 一次性:
PostgresGrantRepository.cc:165-176原子 CASUPDATE oauth2_codes SET used=true WHERE code=$1 AND used=false RETURNING *;Memory 后端MemoryGrantRepository.cc:66-95用锁 +used标志。 - 绑定:
TokenService.cc:144-151写入clientId/userId/scope/redirectUri/codeChallenge/codeChallengeMethod/nonce;exchange 时校验 client_id(:200-204)、redirect_uri(PostgresGrantRepository.cc:181-189)、PKCE(:206-219)。 - 判定:符合。
3.1.4 §4.1.2.1 错误重定向 vs 直接错误 —— 不符合(High)
- 检查方法:审
AuthorizationEndpointController.cc各错误分支的响应方式。 - 证据:
- 无效 client_id → 直接 400(
AuthorizationEndpointController.cc:233-237) - 无效 redirect_uri → 直接 400(
:257-261) - scope 失败 → 直接 JSON(
:362-371) - 仅 consent-deny 一处按规范
?error=access_denied&state=重定向(SessionController.cc:728-737)
- 无效 client_id → 直接 400(
- 偏差:RFC 6749 §4.1.2.1 规定——若请求的
redirect_uri缺失/无效或client_id未知,AS 应直接告知用户错误(不重定向);但若 redirect_uri 与 client_id 均有效,所有其他错误(access_denied、invalid_scope、unsupported_response_type、server_error 等)都必须以?error=&state=形式 302 重定向到 redirect_uri。本项目对所有非 consent-deny 错误一律直接 4xx,state 在这些情况下不回显。 - 依据:RFC 6749 §4.1.2.1。
- 见:F-007。
3.1.5 §4.1.3 authorization_code 兑换 —— 部分符合(Medium)
- redirect_uri 比较可被绕过:
PostgresGrantRepository.cc:181-189与MemoryGrantRepository.cc:80-87均以if (!redirectUri.empty() && redirectUri != stored)守卫比较。RFC 6749 §4.1.3 规定"if theredirect_uriwas included in the initial authorization request then it MUST be included in the token request"——即签发时带了 redirect_uri,兑换时就必须带且必须匹配。当前实现允许兑换时省略 redirect_uri 从而跳过比较。RuleSet::oauth2Token(RuleSet.cc:360-376)对code必填、client_id选填,未强制 redirect_uri 在该场景下的必填性。 - client 绑定 / PKCE / 过期:已实现(见 3.1.3)。
- 见:F-009。
3.1.6 §3.2.1 / §4.1.3 token 端点对所有机密客户端 MUST 认证 —— 不符合(High)
- 检查方法:跟踪 4 种 grant 在 controller 入口的
validateClient调用。 - 证据:
- authorization_code →
TokenService.cc:177-187先validateClient✅ - client_credentials →
TokenEndpointController.cc:733-775先validateClient✅ - device_code →
TokenEndpointController.cc:1227-1276先validateClient(按 client_type 分支)✅ - refresh_token →
TokenEndpointController.cc:679-711直接plugin->refreshAccessToken(refreshTokenStr, clientId, ...),无validateClient调用 ❌
- authorization_code →
- 偏差:refresh_token grant 仅在
TokenService.cc:387-391做storedRt->clientId != clientId字符串比较,未校验 client_secret。任何机密客户端只要持有一个他不该有的 refresh_token 字符串,配上任意 client_id 即可刷新(client_id 必须匹配,但 client_id 不保密,攻击者从被泄漏的 refresh_token 关联日志即可推得)。这违反 RFC 6749 §3.2.1("The authorization server MUST [...] authenticate the client if the client was issued credentials")。 - 依据:RFC 6749 §3.2.1。
- 见:F-003。
3.1.7 §4.4 client_credentials —— 符合
- 仅限机密客户端:
TokenEndpointController.cc:765-775PUBLIC →unauthorized_client。 - 不发 refresh_token:
:825注释明确,:853响应省略。 - scope 校验:
:784-823超集 →invalid_scope;缺省取客户端注册的 scope 全集;注册 scope 为空且请求省略 →invalid_scope。 - subject 为
client:<id>(:839),符合 RFC 6749 §4.4.3 的 M2M 语义。 - 判定:符合。
3.1.8 §6 refresh_token —— 部分符合(中,配合 3.1.6 看)
- 轮换:每次刷新都发新 access + 新 refresh(
TokenService.cc:400-417)。 - 重用检测 + 级联吊销:原子 CAS
atomicRevokeRefreshToken(PostgresTokenRepository.cc:404-446)+revokeTokenFamily(:448-492,按 family_id 批量吊销 refresh 与关联 access),审计refresh_token_reuse_detected(TokenService.cc:367-373)。符合 RFC 6749 §6 的推荐实践。 - 绑定 client_id 校验:
TokenService.cc:387-391有比较,但配合 3.1.6 的"无认证",仅是字符串比较,弱。 - Redis 后端失效:
RedisTokenRepository.cc:155-165saveRefreshToken/getRefreshToken是空操作(注释 line 153-154 明示),atomicRevokeRefreshToken永远返回 nullopt,故 Redis 部署下轮换/级联完全不工作。 - 见:F-005。
3.1.9 §5.1 成功响应 —— 部分符合(Low)
- token_type=Bearer / expires_in:齐备(
TokenService.cc:293,297,432,434;TokenEndpointController.cc:850-851,1147-1148)。 - scope 回显:client_credentials(
:852)与 device_code(:1150-1153,仅非空时)回显;authorization_code 与 refresh_token 不回显(TokenService.cc:291-299, 430-436)。RFC 6749 §5.1 规定 scope 若与请求不同则 MUST 回显、相同则 OPTIONAL,缺失属轻微偏差。 - Cache-Control: no-store:RFC 6749 §5.1 RECOMMENDS 此头于所有 token 响应;本项目仅错误响应加(
OAuth2ErrorHandler.cc:74-75),成功响应不加。 - 非标
roles字段:TokenService.cc:299在 authorization_code 响应额外加roles,非 RFC 6749 字段(可接受但应文档化)。 - 见:F-019。
3.1.10 §5.2 错误响 应 —— 部分符合(Medium)
- error 码集合:覆盖
invalid_request/invalid_client/invalid_grant/unauthorized_client/unsupported_grant_type/invalid_scope/server_error/access_denied/authorization_pending/expired_token(ErrorCatalog.cc:218-236)。 - HTTP 状态映射:
invalid_client→401、server_error→500、access_denied→403、其余→400(OAuth2ErrorHandler.cc:93-107、ErrorCatalog.cc:221-233)。基本符合 §5.2。 - WWW-Authenticate on invalid_client:协议端点(introspect/revoke)经
OAuth2ErrorHandler::sendErrorResponse在authScheme非空时正确加WWW-Authenticate(OAuth2ErrorHandler.cc:85-88)。但/oauth2/token的 inline grant 分支(client_credentials/device_code 的invalid_client)直接构造 JSON,不加 WWW-Authenticate(TokenEndpointController.cc:718-725, 736-743, 757-762, 1238-1244, 1264-1271)。违反 §5.2 "MUST include the WWW-Authenticate header"。 - validation gate 误用应用信封:
RuleSet::oauth2Token失败经HttpResponder返回应用信封VALIDATION_INVALID_INPUT(HttpResponder.cc:57-58, 86),不是 RFC 6749 §5.2 的error: "invalid_request"。客户端按 OAuth2 协议解析错误时会失败。 - 见:F-006、F-008。