Conversation
|
因为我也不懂这个鉴权部分,我就让 copilot 再审阅一下,意见供你参考。 |
|
我的建议是更新一下鉴权部分的文档 |
There was a problem hiding this comment.
Pull request overview
该 PR 旨在增强后端 OIDC 登录能力,通过新增环境变量配置来支持“原始用户名映射”、从 userinfo 自动同步部门信息,以及可选强制 prompt=login 的授权参数,从而提升企业 SSO 场景的账号映射与组织信息同步能力。
Changes:
- 新增 OIDC 配置项:原始用户名模式、部门信息拉取/字段映射、强制 prompt=login。
- OIDC userinfo 提取逻辑增加部门字段,并在回调创建用户时按部门信息自动创建/复用部门。
- 引入“sub 绑定占位记录”的逻辑以尝试在不改表结构下校验 sub 绑定关系。
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| use_raw_username=os.environ.get("OIDC_USE_RAW_USERNAME", "false").lower() == "true", | ||
| fetch_department_info=os.environ.get("OIDC_FETCH_DEPARTMENT_INFO", "false").lower() == "true", | ||
| department_claim=_env("OIDC_DEPARTMENT_CLAIM", "department"), | ||
| force_prompt_login=os.environ.get("OIDC_FORCE_PROMPT_LOGIN", "true").lower() == "true", |
There was a problem hiding this comment.
force_prompt_login 的环境变量解析默认值是 "true",这会导致在启用 OIDC 且未设置 OIDC_FORCE_PROMPT_LOGIN 时默认强制添加 prompt=login,与配置字段默认值(False)及 PR 描述的默认值(false)不一致,可能造成行为意外变化。建议将默认值改为 "false",并与其它布尔环境变量保持一致。
| force_prompt_login=os.environ.get("OIDC_FORCE_PROMPT_LOGIN", "true").lower() == "true", | |
| force_prompt_login=os.environ.get("OIDC_FORCE_PROMPT_LOGIN", "false").lower() == "true", |
| user_by_sub = await find_user_by_oidc_sub(db, sub) | ||
| if user_by_sub and user_by_sub.id == existing_user.id: | ||
| # sub 已经正确绑定到该用户,允许返回 | ||
| logger.info(f"User with raw username {user_id} already exists and bound to sub {sub}, returning existing user") | ||
| return existing_user | ||
| elif user_by_sub is None: | ||
| # sub 尚未绑定任何用户,可以将sub绑定到这个现有用户 | ||
| logger.info(f"Binding new OIDC sub {sub} to existing user with raw username {user_id}") | ||
| await _create_oidc_binding_placeholder(db, sub, existing_user) | ||
| return existing_user | ||
| else: | ||
| # sub 已经绑定到另一个用户,冲突,拒绝创建 | ||
| logger.warning(f"Cannot create OIDC user with raw username {user_id}: sub {sub} is already bound to another user {user_by_sub.id}, conflict") | ||
| raise HTTPException( | ||
| status_code=status.HTTP_409_CONFLICT, | ||
| detail=f"用户名 {user_id} 已存在且OIDC标识 {sub} 已绑定到其他账号,请联系管理员处理冲突", | ||
| ) |
There was a problem hiding this comment.
_create_oidc_binding_placeholder 创建的占位用户 user_id 正是 oidc:{sub},而 find_user_by_oidc_sub() 的方法1会优先返回该记录;但在 use_raw_username 流程中又用 user_by_sub.id == existing_user.id 来验证绑定关系。占位用户的 id 不可能等于被绑定的真实用户,因此会导致已有账号在绑定后/登录时被误判为冲突并拒绝登录。建议调整绑定数据的存储方式,使 find_user_by_oidc_sub 能解析并返回“真实被绑定用户”(例如单独的绑定表,或在 legacy 格式中编码目标 user_id 并在查询后解析再反查)。
| # 创建占位用户:使用随机密码,soft deleted 标记不参与实际登录 | ||
| # 但我们保留它存在只是为了记录 sub -> target_user 的绑定关系 | ||
| random_password = secrets.token_urlsafe(32) | ||
| password_hash = AuthUtils.hash_password(random_password) | ||
|
|
||
| try: | ||
| await user_repo.create({ | ||
| "username": f"oidc-binding-{sub[:8]}", | ||
| "user_id": oidc_user_id, | ||
| "phone_number": None, | ||
| "avatar": None, | ||
| "password_hash": password_hash, | ||
| "role": target_user.role, | ||
| "department_id": target_user.department_id, | ||
| "last_login": utc_now_naive(), | ||
| }) |
There was a problem hiding this comment.
占位用户的注释写的是“soft deleted 标记不参与实际登录”,但创建时没有设置 is_deleted=1(模型默认是 0),因此该占位用户会被当作正常用户返回(例如 find_user_by_oidc_sub 过滤 is_deleted==0)。这不仅与注释不一致,也可能导致占位账号被错误用于登录/发放 JWT。建议明确将占位记录标记为不可登录(例如 is_deleted=1 并调整查询逻辑专门读取绑定记录),或改为使用专门的绑定存储结构。
| from yuxi.repositories.user_repository import UserRepository | ||
| user_repo = UserRepository() | ||
|
|
||
| oidc_user_id = f"oidc:{sub}" | ||
| result = await db.execute(select(User).filter(User.user_id == oidc_user_id, User.is_deleted == 0)) | ||
| if result.scalar_one_or_none(): | ||
| # 占位用户已存在,无需重复创建 | ||
| return | ||
|
|
||
| # 创建占位用户:使用随机密码,soft deleted 标记不参与实际登录 | ||
| # 但我们保留它存在只是为了记录 sub -> target_user 的绑定关系 | ||
| random_password = secrets.token_urlsafe(32) | ||
| password_hash = AuthUtils.hash_password(random_password) | ||
|
|
||
| try: | ||
| await user_repo.create({ | ||
| "username": f"oidc-binding-{sub[:8]}", | ||
| "user_id": oidc_user_id, | ||
| "phone_number": None, | ||
| "avatar": None, | ||
| "password_hash": password_hash, | ||
| "role": target_user.role, | ||
| "department_id": target_user.department_id, | ||
| "last_login": utc_now_naive(), | ||
| }) | ||
| logger.info(f"Created OIDC binding placeholder for sub {sub} -> user {target_user.user_id}") | ||
| except IntegrityError: | ||
| # 并发创建冲突,忽略 | ||
| await db.rollback() | ||
| logger.info(f"OIDC binding placeholder already exists for sub {sub}") |
There was a problem hiding this comment.
_create_oidc_binding_placeholder 先用传入的 db session 查询是否存在,再调用 UserRepository.create()(该方法会开启并提交自己的 session)。这会导致同一请求内存在跨 session 的一致性问题,并且 except IntegrityError 分支里调用 await db.rollback() 也不会回滚 UserRepository.create() 使用的 session。建议统一使用同一个 db session 来创建占位记录(直接 db.add/commit),或将创建逻辑改造成接收 session 的 repository 方法。
| await user_repo.create({ | ||
| "username": f"oidc-binding-{sub[:8]}", | ||
| "user_id": oidc_user_id, | ||
| "phone_number": None, | ||
| "avatar": None, | ||
| "password_hash": password_hash, |
There was a problem hiding this comment.
占位用户的 username 使用 oidc-binding-{sub[:8]},但 users.username 在数据库中是唯一索引;不同 sub 的前 8 位碰撞时会触发 IntegrityError。当前捕获后直接认为“占位用户已存在”,但实际上可能只是用户名冲突,导致绑定记录并未创建。建议使用与 build_unique_oidc_username 类似的去重策略(例如加入更长哈希/随机后缀),并区分 user_id 冲突与 username 冲突。
| @@ -601,7 +775,10 @@ async def oidc_callback_handler(code: str, state: str, db, request: Request | No | |||
| user = await restore_deleted_oidc_user(db, deleted_user, extracted_info) | |||
| logger.info(f"OIDC deleted user restored and logged in: {user.username}") | |||
| else: | |||
| dept = await get_or_create_oidc_department(db) | |||
| # 从用户信息中获取部门信息 | |||
| dept_name = extracted_info.get("department_name") | |||
| dept_desc = extracted_info.get("department_description") | |||
| dept = await get_or_create_oidc_department(db, dept_name, dept_desc) | |||
| department_id = dept.id if dept else None | |||
| user = await create_oidc_user(db, extracted_info, department_id) | |||
There was a problem hiding this comment.
该 PR 在 OIDC 回调/用户创建路径引入了多条新分支(use_raw_username 绑定校验、部门自动创建、prompt=login 参数等),但当前测试集中未发现任何 OIDC 相关用例。建议在现有 backend/test/integration/api 的认证测试基础上补充 OIDC 回调的集成测试(可通过 mock token/userinfo 响应),覆盖:首次绑定、已绑定登录、冲突拒绝、部门创建/复用以及 prompt=login 参数是否按配置生效。
|
根据Copilot的建议再次梳理了代码,并增加了文档。 |
变更描述
简要描述这个 PR 做了什么
功能描述
此PR增强OIDC登录功能,新增三个环境变量配置:
OIDC_USE_RAW_USERNAMEoidc:前缀),支持与已有本地账号映射falseOIDC_FETCH_DEPARTMENT_INFOfalseOIDC_FORCE_PROMPT_LOGINprompt=login参数强制用户重新登录falseOIDC_DEPARTMENT_CLAIMdepartment主要改进
oidc:{sub}占位用户记录绑定关系,防止账号冒用安全修复
启用
use_raw_username后,每个OIDCsub必须验证绑定关系才能登录,避免攻击者使用相同用户名冒用他人账号。测试
相关日志或者截图
说明
(可选)有什么需要特别说明的吗?
💡 提示: 提交前可以运行
make lint和make format检查代码规范