feat(identity): Huawei Account Kit login via OIDC JWKS verification
POST /auth/huawei:JWKS 验签(kid 缓存/轮换)+ claims 严格校验 + UnionID 映射 + T4 双令牌签发(FR-ACC-01;真实联调待 AGC 配置)。 - packages/identity/huawei.py(新建):HuaweiIdTokenVerifier——RS256 验签, JWKS 内存缓存(TTL 可配 + 单调钟;kid 未命中强制刷新一次重试,锁内用 拉取代数防止强制刷新被新鲜缓存短路);aud/iss/exp/iat 全查,audience 空配置直接拒绝而非跳过;union_id 缺失拒绝(降级 open_id 策略不做, 留联调期决定);错误映射 40102/40101/60000,无新错误码。 HuaweiLoginRequest/HuaweiClaims 一并定义于此,未动 schemas.py。 - apps/api/routers/auth.py:追加 /huawei 端点;get_huawei_verifier 依赖 建于路由模块内(缓存到 app.state,可预注入替换),未动 deps.py; 复用 T4 的 get_or_create_by_external_identity + issue_session + TokenPairResponse 与既有 _token_pair/_access_token。 - tests/unit/test_huawei_verifier.py:cryptography 自造 RSA + 本地 JWKS (零外网),19 例覆盖合法 claims/错误签名/过期/aud/iss/iat 缺失/ audience 未配置/union_id 缺失/HS256 混淆/JWKS 拉取失败/kid 轮换/缓存 TTL。 - tests/integration/test_auth_huawei.py:FakeFetcher 预注入 app.state, 真实 PG 验证首次建用户 + 双令牌、同 union_id 复登同 user_id、新 access 调 /quota、refresh 可轮换、伪造 token 40102。 检查:ruff/format/mypy(T6 列表)/lint-imports 全绿;单测 257 passed (基线 238 + 新增 19);集成 18 passed(含新增 2)。 偏差说明:无
This commit is contained in:
@@ -5,17 +5,22 @@
|
||||
且接受的设计权衡(要即时失效就得引入黑名单或极短 TTL,V1 不做)——
|
||||
「登出后旧令牌立即失效」的验收对象是 refresh 令牌与会话。
|
||||
|
||||
明确不做(防止后人误判「漏了」):华为账号登录(T-P1-12,等资质)、手机验证码
|
||||
(T-P1-12b,等短信通道)、游客/合并/注销。dev-token 端点不是正式鉴权设计,
|
||||
仅为纵向切片与联调服务:``DEV_AUTH_ENABLED`` + 本地环境双开关才放行。
|
||||
华为账号登录(T-P1-12)已实现(``/auth/huawei``):Account Kit id_token 的
|
||||
OIDC 验签与 UnionID 映射在 :mod:`packages.identity.huawei`,代码先行——真实
|
||||
联调待 AGC 配置(见该模块 docstring)。明确不做(防止后人误判「漏了」):
|
||||
手机验证码(T-P1-12b,等短信通道)、游客/合并/注销。dev-token 端点不是正式
|
||||
鉴权设计,仅为纵向切片与联调服务:``DEV_AUTH_ENABLED`` + 本地环境双开关才放行。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from apps.api.deps import CurrentUserId, IdentityServiceDep, SettingsDep
|
||||
from apps.api.schemas import DevTokenRequest
|
||||
from packages.identity.huawei import HuaweiIdTokenVerifier, HuaweiLoginRequest
|
||||
from packages.identity.schemas import LogoutRequest, RefreshRequest, TokenPairResponse
|
||||
from packages.identity.service import DEV_PROVIDER, SessionIssued
|
||||
from packages.shared.config import Settings
|
||||
@@ -92,3 +97,48 @@ async def logout(
|
||||
revoked = await identity.revoke(payload.refresh_token)
|
||||
# 幂等登出:查无此 token 或已撤销都返回 revoked=false,不报错
|
||||
return ok({"revoked": revoked})
|
||||
|
||||
|
||||
def get_huawei_verifier(request: Request, settings: SettingsDep) -> HuaweiIdTokenVerifier:
|
||||
"""从配置构造华为验签器,缓存到 app.state(JWKS 缓存跨请求复用)。
|
||||
|
||||
依赖建在路由模块内而非 deps.py——deps.py 是本批共享禁改文件。
|
||||
预注入 ``app.state.huawei_verifier`` 可整体替换(集成测试借这条缝
|
||||
注入 FakeFetcher,全链路零外网)。
|
||||
"""
|
||||
verifier: HuaweiIdTokenVerifier | None = getattr(request.app.state, "huawei_verifier", None)
|
||||
if verifier is None:
|
||||
verifier = HuaweiIdTokenVerifier(
|
||||
jwks_url=settings.huawei_jwks_url,
|
||||
audience=settings.huawei_audience,
|
||||
issuer=settings.huawei_issuer,
|
||||
cache_seconds=settings.huawei_jwks_cache_seconds,
|
||||
)
|
||||
request.app.state.huawei_verifier = verifier
|
||||
return verifier
|
||||
|
||||
|
||||
HuaweiVerifierDep = Annotated[HuaweiIdTokenVerifier, Depends(get_huawei_verifier)]
|
||||
|
||||
|
||||
@router.post("/huawei", summary="华为账号授权登录(FR-ACC-01)")
|
||||
async def huawei_login(
|
||||
payload: HuaweiLoginRequest,
|
||||
settings: SettingsDep,
|
||||
identity: IdentityServiceDep,
|
||||
verifier: HuaweiVerifierDep,
|
||||
) -> dict[str, object]:
|
||||
"""Account Kit id_token → JWKS 验签 → UnionID 映射 → T4 双令牌。
|
||||
|
||||
audience 未配置(AGC 未就绪)时 verify 直接 40102,端点天然不可用——
|
||||
这是有意的:联调前放行等于允许任意自签 token 登录。
|
||||
"""
|
||||
claims = await verifier.verify(payload.id_token)
|
||||
user = await identity.get_or_create_by_external_identity(
|
||||
provider="huawei",
|
||||
union_id=claims.union_id,
|
||||
open_id=claims.open_id,
|
||||
register_source="huawei",
|
||||
)
|
||||
issued = await identity.issue_session(user.user_id)
|
||||
return ok(_token_pair(settings, _access_token(settings, user.user_id), issued))
|
||||
|
||||
Reference in New Issue
Block a user