本报告是 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。
3.1.11 §10.4 / §10.6 凭证保护 —— 不符合(Critical,仅动态注册路径)
- access/refresh token 哈希存储:✅ UPPER-hex SHA-256(
TokenCrypto.cc:26-37),原值不入库。 - client_secret 哈希:❌ 见 3.6.2。写入路径用无盐大写 SHA-256,校验路径算有盐小写 SHA-256。两套算法永远不会匹配。
- 常量时间比较:Postgres ✅(
PostgresClientRepository.cc:14-27, 245-249),Memory ✅(MemoryClientRepository.cc:11-24, 163,但比较的是明文),Redis ❌(RedisClientRepository.cc:174-175==)。 - 见:F-002、F-004。
3.1.12 §10.9 维护撤销集 —— 符合
- access/refresh token 持
revoked标志,validateAccessToken(TokenService.cc:443-479)查 revoked + expiry。 - 判定:撤销即时生效。符合。
3.2 RFC 6750 Bearer Token Usage —— 部分符合
3.2.1 §2.1 Bearer 头解析 —— 符合
OAuth2AuthFilter.cc:39-52 校验 Authorization: Bearer 前缀, 缺失/格式错→401。
3.2.2 §2.3 query 传 token —— 部分符合(Low)
AuthorizationFilter.cc:99-107 接受 ?access_token=,属 RFC 6750 §2.3 已废弃方式。允许但不推荐。
3.2.3 §3 WWW-Authenticate: Bearer challenge —— 不符合(Medium)
- 证据:资源端点(
/api/me、/api/admin/*)401 经OAuth2AuthFilter/AuthorizationFilter返回应用信封AUTH_TOKEN_INVALID,不按 RFC 6750 §3 发WWW-Authenticate: Bearer realm="...", error="invalid_token", error_description="..."。 - 依据:RFC 6750 §3 "If the request lacks any authentication information [...], the resource server SHOULD NOT respond with the WWW-Authenticate header. [...] otherwise [...] MUST include the WWW-Authenticate header"。
- 见:F-006。
3.2.4 §3.1 insufficient_scope —— 不符合(High)
- 证据:
OAuth2AuthFilter.cc:73-75把scope写入 attributes 但从不校验。Grep 全libs/、apps/无任何资源端点按 scope 拒绝。/api/admin/*的 RBAC 按用户角色而非 OAuth scope。 - 依据:RFC 6750 §3.1 "If the access token [...] has insufficient scope",应返回
insufficient_scope。 - 影响:scope 退化成"签发时校验客户端白名单"的元数据,丧失细粒度资源授权能力。
- 见:F-010。
3.3 RFC 7662 Token Introspection —— 部分符合
3.3.1 §2.1 客户端认证 MUST —— 符合
TokenEndpointController.cc:279-345:extractClientCredentials 取 Basic 或 POST client_id/client_secret,缺失→invalid_client+WWW-Authenticate,validateClient 校验。符合。
3.3.2 §2.2 响应字 段 —— 部分符合(Low)
- 已发:
active/client_id/token_type/exp/iat/nbf/sub/aud/iss/scope(:377-409)。 - 缺失:
username、jti。jti全代码库不存在(Grep 无结果)。 - iss 不一致:refresh_token 分支硬编码
iss = "https://oauth.example.com"(PostgresTokenRepository.cc:567),与配置 issuer 不符。 - 见:F-016。
3.3.3 §2.2 无效/过期/撤销令牌须 active=false(非 4xx)—— 符合
:353-368 repo 返回 nullopt → 200 {active:false};repo 层 revoked/expired 返回 active=false 对象(:523-529, 553-558, 574-578),controller 序列化为完整字段(含 active=false)。符合。
3.3.4 §2.3 token_type_hint —— 符合
RuleSet.cc:573 接受但忽略。合规。
3.4 RFC 7009 Token Revocation —— 符合
3.4.1 §2.1 客户端认证 + 所有权 —— 符合
TokenEndpointController.cc:420-523:认证 + introspection->clientId != clientId → unauthorized_client(:508-522)。
3.4.2 §2.1 支持 access 与 refresh —— 符合
revokeAccessToken 同时回退尝试 refresh 表(PostgresTokenRepository.cc:652-677),命名误导但行为正确。
3.4.3 §2.2.1 成功/未知均返回 200;no-store —— 部分符合(Low)
:494-544 成功/未知均 200。但 createSuccessResponse(:235-240)不加 Cache-Control: no-store。RFC 7009 §2.2.1 RECOMMENDS 此头。见:F-019。
3.5 RFC 7636 PKCE —— 部分符合
3.5.1 §4.3 code_challenge_method 仅 plain/S256 —— 部分符合(Medium)
- authorize 端不校验方法集合(
AuthorizationEndpointController.cc:108-109透传),任意字符串被接受并存储。仅在 exchange 端Pkce.cc:17-26拒绝非plain/S256。 - 依据:RFC 7636 §4.3 "If the server supports PKCE [...] the server SHOULD reject authorization requests that do not support the requested transformation"。
- 见:F-013。
3.5.2 §4.4/§4.6 S256 算法 —— 符合
Pkce.cc:17-21 computeCodeChallenge 对 S256 = base64url(sha256(verifier)),对原始字节而非 hex 字符串编码。这是规范正确的实现(TokenService.h 顶部注释明确这是修复了旧 generateSha256Hash 的 base64(hex-string) bug 的新实现)。OAuth2Plugin.cc:601-628 的静态方法也委托到正确实现。
3.5.3 §4.6 exchange 时校验 —— 符合
TokenService.cc:206-219:存了 codeChallenge 则必须带 codeVerifier 且通过 validatePkceCodeVerifier→verifyCodeVerifier。
3.5.4 BCP §2.1.1 / RFC 9700 强制 PKCE —— 不符合(Medium)
- 证据:
auth.require_pkce_for_public默认 OFF(OAuth2Plugin.cc配置读取,AuthorizationEndpointController.cc:416-441、SessionController.cc:555-572仅在 flag on 时强制)。且检查名"for public"但实际对任意 client 触发,未区分 client_type。 - 依据:RFC 9700 §2.1.1 强烈建议对所有 authorization_code 客户端强制 PKCE。
- 见:F-011。
3.5.5 §4.1 code_verifier 格式校验 —— 部分符合(Low)
isValidCodeVerifierFormat(Pkce.cc:40-63)已实现 43-128/[A-Za-z0-9-._~],但未在 exchange 路径调用(仅重算比较)。RFC 7636 §4.6 不要求单独格式校验,故不算硬偏差。
3.6 RFC 8252 Native Apps —— 未实现(按适用性,Medium)
- loopback redirect 端口通配(§7.3):未实现。redirect_uri 精确匹配,native app 每次新端口都需重新注册。
- public client 必须用 PKCE(§8.1):见 3.5.4,默认不强制。
- 判定:未实现(按适用性评级 Medium;若项目不面向 native app,可标"不适用")。
- 见:F-014。
3.7 RFC 8628 Device Authorization Grant —— 部分符合
3.7.1 §3.1.1/§3.4 device_authorization 端点机密客户端认证 —— 不符合(High)
- 证据:
DeviceAuthController.cc:156plugin->validateClient(clientId, "", ...)用空 secret 调用。MemoryClientRepository.cc:150-157/PostgresClientRepository.cc:218-225对机密客户端空 secret 返回 false,故机密客户端根本无法发起 device flow;公开客户端可发起。 - 依据:RFC 8628 §3.1.1 要求机密客户端在 device_authorization 端点认证。
- 影响:机密客户端无法用 device flow;公开客户端体验正常但未做 PKCE 关联。
- 见:F-015。
3.7.2 §3.2 响应字段 —— 部分符合(Low)
device_code/user_code/verification_uri/expires_in/interval齐(DeviceAuthController.cc:202-213)。verification_uri_complete缺失(§3.3.1 可选)。- user_code 字符集:
"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"(:75)剔除歧义字符,符合 §5.2。 - token 端点 client_type 分支:
TokenEndpointController.cc:1227-1276正确分支(PUBLIC 仅 client_id;CONFIDENTIAL 需 secret)。✅
3.7.3 §3.5 polling 错误码 —— 不符合(Medium)
- 已发:
authorization_pending/expired_token/access_denied/invalid_grant/invalid_request(:935-1086,HTTP 400)✅。 - slow_down 未发:
ErrorCatalog.cc:232,483定义了slow_down,但 polling 逻辑从不比较轮询频率,永远不返回slow_down。Grep 确认无任何 emission 点。 - 依据:RFC 8628 §3.5 "If the client is polling too quickly, the authorization server SHOULD return the
slow_downerror"。 - 见:F-012。
3.8 RFC 8414 + OIDC Discovery 1.0 —— 部分符合
3.8.1 OIDC Discovery §4 字段完备性 —— 部分符合(Medium)
DiscoveryController.cc::oidcDiscovery(:152-222)已发:issuer/authorization_endpoint/token_endpoint/userinfo_endpoint/device_authorization_endpoint/jwks_uri/introspection_endpoint/revocation_endpoint/response_types_supported/grant_types_supported/subject_types_supported/id_token_signing_alg_values_supported/scopes_supported/token_endpoint_auth_methods_supported/claims_supported/code_challenge_methods_supported。
缺失:
end_session_endpoint(OIDC RP-Initiated Logout 必备)❌registration_endpoint(实际有/oauth2/register但未广告)❌introspection_endpoint_auth_methods_supported/revocation_endpoint_auth_methods_supported(仅 RFC 8414metadata()有,OIDC discovery 无)❌response_modes_supported(仅 metadata 有)❌
3.8.2 OIDC Discovery §3 issuer 精确一致 —— 不符合(High)
- 配置 issuer:
baseUrl来自customConfig["metadata"]["issuer"],默认http://localhost:5555(DiscoveryController.cc:160-167)。默认是 http,OIDC Discovery §3 规定 issuer 须使用 https。 - iss claim 来源不一致:access token 的
iss取自 DB 列oauth2_access_tokens.issuer(PostgresTokenRepository.cc:537);refresh token 内省 iss 硬编码"https://oauth.example.com"(:567)。签发时 issuer 写入何处未在本次审计深追,但 introspect 的 refresh 分支与配置 issuer 显然不符。 - 无尾斜杠归一化:
baseUrl + "/oauth2/..."直接拼接(:171-177),若 operator 配置带尾斜杠会产生//oauth2/...。 - 依据:OIDC Discovery §3 "The issuer value MUST be exactly identical to the Issuer URL [...] The issuer value is a URL [...] using https"。
- 见:F-016。
3.8.3 RFC 8414 oauth-authorization-server —— 符合
metadata()(:57-151)字段齐全。
3.9 OIDC Core 1.0 —— 部分符合(核心 MUST 大体满足,扩展 SHOULD 大面积缺失)
3.9.1 §2 id_token claims(核心 MUST 档)—— 部分符合 (Medium)
- 已发:
iss/sub/aud/exp/iat/nonce(TokenService.cc:306-317,nonce 仅在非空时)。 - 缺失:
auth_time、acr、amr、azp。 - 依据:
- OIDC Core §2
auth_time:REQUIRED whenmax_age请求、否则 OPTIONAL。当前 max_age 不支持故可缺,但 §3.1.2.1 又把 max_age 列为支持项……故实际是"不支持 max_age 所以也不发 auth_time"——耦合缺失。 azp(§2):当 aud 多值或与 client 不同时 REQUIRED。本项目 aud 永远等于 clientId,故 azp 可缺(合规)。acr/amr:OPTIONAL 但 claims_supported 应反映实际能力(当前未广告,可接受)。
- OIDC Core §2
- 见:F-021。
3.9.2 §3.1.2.1 / §3.1.3.7 请求参数 prompt / max_age(核心 MUST 档,OIDC Core 把它们列为 OP 须支持的请求参数)—— 未实现(High)
- 证据:全仓 Grep
prompt/max_age在非测试代码中零命中。AuthorizationEndpointController.cc不读这两个参数。 - 依据:OIDC Core §3.1.2.1 把
prompt、max_age、nonce列为 Authorization Endpoint 的请求参数;§3.1.3.7 规定auth_time与max_age校验为 MUST。 - 影响:客户端无法请求
prompt=none(无交互静默登录失败应报错)、prompt=login(强制重新认证)、max_age(按年龄强制重认证)。这是 OIDC 互操作性的核心能力。 - 见:F-022。
3.9.3 §3.1.3.6 id_token 仅对 openid scope 签发 —— 符合
TokenService.cc:301-303 if (jwkManager_ && ... && scope.find("openid") != npos)。
3.9.4 §5.3 UserInfo —— 部分符合(Medium)
- Bearer 校验:✅ 经
OAuth2AuthFilter(OAuth2AuthFilter.cc:39-55)。 - 不校验 openid scope:❌
TokenEndpointController.cc:1297-1389与 filter 都不查 scope,任何 access token(含client_credentials签发的sub=client:<id>M2M token)都能取 userinfo。 - CORS:✅(修正先前误判)全局
setupCors()(apps/server/src/bootstrap/CorsSetup.cc:68-79)的postHandlingAdvice对所有响应(含 userinfo)加Access-Control-Allow-Origin(精确白名单,无通配)。符合 OIDC §7.2.1。 - sub 用内部 userId:§15 推荐 pairwise/stable 标识符,本项目用内部数字 id(非 pairwise)。
- email_verified 声明但不返回:
claims_supported含email_verified(DiscoveryController.cc:212),但 userinfo 不返回(TokenEndpointController.cc:1345-1383)。 - 依据:OIDC Core §5.3 "The UserInfo Endpoint MUST accept Access Tokens [...] The information returned [...] SHOULD be scoped to the OpenID Connect scopes"。
- 见:F-023、F-024。
3.9.5 §7.2.1 UserInfo CORS —— 符合(见 3.9.4)
3.9.6 §12 refresh 时重发 id_token(核心 MUST 档,OIDC Core §12 列为 SHOULD)—— 未实现(Medium)
- 证据:
TokenService.cc:430-436refresh 响应只有access_token/token_type/expires_in/refresh_token,无id_token分支。同样TokenEndpointController.cc:1145-1153device_code 也不发 id_token。 - 影响:OIDC 客户端在 refresh 后丢失 id_token,需重新走授权码流程。
- 见:F-025。
3.9.7 §10.1 nonce 单次使用(核心 MUST 档)—— 未实现(Medium)
- 证据:nonce 仅 echo 进 id_token(
TokenService.cc:314-317),无任何存储/去重。 - 依据:OIDC Core §15.5.2 "nonce [...] MUST be [...] used only once [...] Clients MUST verify that the nonce [...] is equal to the nonce sent". 服务端虽不强制单次,但缺 replay 检测使 nonce 防护弱化。
- 见:F-026。