本页是可查不可猜的表面清单,所有类型、路径、默认值与约束均对照 BitzOrcasVNext 主干源码核验(契约定义 src/Platform/Identity/BitzOrcas.Identity.Contracts/Identity/StepUp/,端点声明分布于 .../Identity.Application/Commands|Queries/,强制点 src/Hosts/BitzOrcas.Api/StepUp/)。因子 wire 名全线使用 camelCase(totp、emailOtp),与 StepUpFactorWire 互转契约一致。
端点清单
| 方法与路径 | 权限 | 用途 |
|---|---|---|
POST /api/identity/step-up/challenge | 认证即可 | 为指定用途发起挑战,返回允许因子集 |
POST /api/identity/step-up/otp/send | 认证即可 | 对既有挑战触发邮箱/短信 OTP 投递 |
POST /api/identity/step-up/verify | 认证即可 | 因子验证,成功签发再认证凭据 |
GET /api/identity/step-up/factors/{purpose} | 认证即可 | 单用途策略决策预览(因子/窗口/是否强制) |
QUERY /api/identity/step-up/my-purposes | 认证即可 | 当前用户”将被要求二次验证”的用途投影 |
QUERY /api/identity/step-up/policies | identity.stepup.manage | 列出租户行 + Host 基线行 |
QUERY /api/identity/step-up/policies/catalog | identity.stepup.manage | 用途目录投影(自建行选择器数据源) |
POST /api/identity/step-up/policies | identity.stepup.manage | 新建策略行 |
PUT /api/identity/step-up/policies/{id} | identity.stepup.manage | 更新策略行(乐观并发) |
GET / PUT /api/identity/step-up/policies/settings | identity.stepup.manage(写仅 Host 上下文) | 窗口上限 + 租户可配置面控制面设置 |
挑战闭环三个端点自带跳过拦截标记,不会递归触发再认证。管理命令与查询还实现委托敏感语义:代理(模拟登录)会话不能替被代理用户改策略。
挑战与验证契约
发起挑战请求与成功响应(StepUpChallengeResponse):
// 请求体:用途码必须命中冻结目录(权限码或声明用途)。{ "purpose": "identity.user.remove" }
// 200 响应:因子集 = 策略允许集 ∩ 用户当前可用能力。{ "challengeId": "9fJ2vQ8rT3wL5nX1cZ7bA0dK4mH6pS2yE8uG1iO5aB3", "expiresInSeconds": 120, "windowSeconds": 300, "factors": [ { "type": "totp" }, { "type": "emailOtp", "target": "l***@example.com" } ]}| 字段 | 类型 | 说明 |
|---|---|---|
challengeId | string | 不透明挑战标识,32 字节 CSPRNG base64url,不可枚举 |
expiresInSeconds | int | 挑战有效期(出厂 120 秒),过期须重新发起 |
windowSeconds | int | 该用途当前策略窗口;0 表示一次性票据 |
factors[].type | string | 因子 wire 名 |
factors[].target | string? | 脱敏投递目标,仅邮箱/短信因子返回 |
OTP 投递(StepUpOtpSendResponse):请求 { "challengeId": "…", "factorType": "emailOtp" },响应 { "resendAllowInSeconds": 60 }——到期前重复投递返回 429。
验证(StepUpVerifyResponse):请求 { "challengeId": "…", "factorType": "totp", "code": "482913" },响应:
// stepUpToken 即再认证凭据:32 字节 CSPRNG base64url,仅此一次完整返回。// 校验失败消耗尝试次数(出厂每挑战 5 次),超限挑战作废(TooManyAttempts)。{ "stepUpToken": "Ak7Qx2Wm9Rf4Tt1Lz8Nb0Vc5Yh3Jd6Pg2Se9Ua1Wr4X=", "purpose": "identity.user.remove", "windowSeconds": 300, "expiresAt": "2026-09-02T08:15:00Z"}凭据经业务请求头重放,头名由 StepUp:TokenHeader 配置(出厂 X-Step-Up-Token)。
三类 403 ProblemDetails
所有 403 都是 RFC 9457 ProblemDetails,扩展字段平铺到响应根。与 RFC 9470 §3 的 401 + WWW-Authenticate 轨道的偏离及边界见概念页。
// 形态一:引导型(type 后缀 step-up-required)——前端据此拉起验证弹窗。// 触发条件:无票据(GrantRequired)或窗口票据过期(GrantExpired)。{ "type": "https://docs.bitzsoft.com/problems/step-up-required", "title": "Step-Up Required", "status": 403, "detail": "该操作需要二次验证。", "errorCode": "Identity.StepUp.GrantRequired", "errorType": "Forbidden", "purpose": "identity.user.remove", "factors": [{ "type": "totp" }, { "type": "emailOtp", "target": "l***@example.com" }], "windowSeconds": 300}
// 形态二:拒绝型(type 后缀 step-up-invalid)——不带 factors,避免反复探测。// errorCode 取 GrantInvalid 或 GrantPurposeMismatch。{ "type": "https://docs.bitzsoft.com/problems/step-up-invalid", "title": "Step-Up Invalid", "status": 403, "detail": "二次验证凭据与当前操作不匹配。", "errorCode": "Identity.StepUp.GrantPurposeMismatch", "errorType": "Forbidden"}
// 形态三:策略不可用(fail-closed,绝不放行)。{ "type": "https://docs.bitzsoft.com/problems/step-up-policy-unavailable", "title": "Step-Up Unavailable", "status": 403, "detail": "安全策略暂不可用,操作已被拒绝。", "errorCode": "Identity.StepUp.PolicyUnavailable", "errorType": "Forbidden"}错误码全表
| 错误码 | HTTP | 触发场景 |
|---|---|---|
Identity.StepUp.ChallengeNotFound | 400 | 挑战不存在、已过期或已消费 |
Identity.StepUp.ChallengeExpired | 400 | 挑战超时失效 |
Identity.StepUp.CodeInvalid | 400 | 验证码/恢复码/密码错误;消耗一次尝试 |
Identity.StepUp.TooManyAttempts | 400 | 尝试次数超限,挑战作废 |
Identity.StepUp.FactorNotAllowed | 422 | 请求因子不在允许集内 |
Identity.StepUp.FactorUnavailable | 422 | 因子未绑定或不可用(如恢复码耗尽) |
Identity.StepUp.EnrollmentRequired | 403 | 无可用因子,需先完成 MFA 绑定 |
Identity.StepUp.RateLimited | 429 | 挑战重发频控命中(含 retryAfterSeconds) |
Identity.StepUp.GrantRequired | 403 | 引导型:缺票据 |
Identity.StepUp.GrantInvalid | 403 | 凭据无效:主体/租户不符、版本撤销、一次性重放、版本键缺失 |
Identity.StepUp.GrantExpired | 403 | 引导型:窗口票据过期 |
Identity.StepUp.GrantPurposeMismatch | 403 | 票据用途与端点不符;高危审计 |
Identity.StepUp.PolicyUnavailable | 403 | 策略/凭据存储读取失败,fail-closed |
Identity.StepUp.PolicyConflict | 422 | 违反 Host 硬上限、白名单、Mandatory 加严或同范围去重 |
Identity.StepUp.PurposeUnknown | 422 | 用途码不在启动冻结目录内 |
Identity.StepUp.ChallengeNotAllowed | 403 | 机器调用者或受限会话触碰挑战闭环 |
Identity.StepUp.PolicyInvalid | 422 | 策略行形状非法(因子集为空、范围值缺失或超长等) |
Identity.StepUp.PolicyNotFound | 404 | 策略行不存在或已删除 |
Identity.StepUp.PolicyVersionConflict | 422 | 策略行并发版本冲突,刷新后重试 |
Identity.StepUp.PolicyHostOnly | 403 | 控制面设置仅 Host 管理上下文可写 |
配置键(StepUp 节)
类型 StepUpOptions(StepUpOptions.cs),绑定节 StepUp,Host 启动时校验组合自洽。出厂默认是工程建议值而非外部规范条款,应随观察期数据调整:
| 键 | 出厂默认 | 约束 |
|---|---|---|
WindowDefaultSeconds | 300 | ≥ 0;0 表示一次性票据 |
WindowCeilingSeconds | 900 | > 0;租户行窗口上限;≤ 技术防呆上限 |
WindowTechnicalMaxSeconds | 86400 | ≥ 窗口上限;仅防误配,不是安全策略 |
ChallengeTtlSeconds | 120 | > 0 |
MaxAttemptsPerChallenge | 5 | > 0 |
OtpResendAllowSeconds | 60 | > 0 |
VersionKeySafetyPaddingSeconds | 60 | > 0;版本键 TTL = 上限 + 挑战 TTL + 余量 |
TokenHeader | X-Step-Up-Token | 非空白 |
PolicySnapshotTtlSeconds | 60 | > 0;策略快照兜底收敛窗口 |
RoleMembershipTtlSeconds | 300 | > 0;角色成员快照兜底收敛窗口 |
appsettings.json 示例(与出厂默认等价):
{ "StepUp": { "WindowDefaultSeconds": 300, "WindowCeilingSeconds": 900, "WindowTechnicalMaxSeconds": 86400, "ChallengeTtlSeconds": 120, "MaxAttemptsPerChallenge": 5, "OtpResendAllowSeconds": 60, "VersionKeySafetyPaddingSeconds": 60, "TokenHeader": "X-Step-Up-Token", "PolicySnapshotTtlSeconds": 60, "RoleMembershipTtlSeconds": 300 }}限流与存储键
挑战闭环四端点使用独立滑动窗口限流(节 RateLimiting:StepUp,分区 = 租户 + 主体 + 端点范围):
| 端点 | 配置键 | 出厂每分钟 |
|---|---|---|
| challenge | ChallengePerMinute | 10 |
| verify | VerifyPerMinute | 5 |
| otp/send | OtpSendPerMinute | 5 |
| factors | FactorsPerMinute | 30 |
Redis 键空间经统一缓存键构建器生成,形态 {app}:{env}:v1:stepup:…:
| 键形态 | 语义 | TTL |
|---|---|---|
global:ch:{challengeId} | 挑战记录 | 挑战 TTL(120s) |
global:ch:{challengeId}:attempts | 失败计数(Lua 原子递增,超限连删) | 随挑战 |
global:grant:{token} | 凭据:窗口模式 GET 只读、一次性模式 GETDEL 原子读删 | 窗口秒数 |
global:tenant-{tid}:ver:{subjectKey} | 撤销版本计数器(INCR 递增 / SETNX 初始化) | 上限 + 挑战 TTL + 余量(出厂 1080s) |
已知限制
fido2因子为契约占位:枚举与 wire 名保留、管理面不渲染、挑战不签发,等 WebAuthn 断言接通后开放(规划中)。- 出厂基线的交付形态分两层:组合根代码声明清单已包含一个试点用途(
operations.sql-masking.manage,平台强制 + 一次性票据),声明经接线校验必须有真实端点以同权限码承载,漂移即启动失败;演示作用域另含两个纯策略行用途的仅观察种子行(灰度起点,幂等不回拨)。其余用途无 Host 行时回退端点声明基线,基线行可经 Host「安全基线」页配置。 - 验证停留超过访问令牌剩余寿命时,前端会放弃重放(重试命中内存缓存自愈);多标签页不共享凭据。