oidc.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. from typing import Any, List
  2. import logging
  3. from fastapi import APIRouter, Depends, HTTPException, Query
  4. from sqlalchemy.orm import Session
  5. from app.api.v1 import deps
  6. from app.core import security
  7. from app.models.user import User
  8. from app.models.mapping import AppUserMapping
  9. from app.schemas.token import Token, LoginRequest
  10. from app.services.hydra_service import hydra_service
  11. router = APIRouter()
  12. logger = logging.getLogger(__name__)
  13. @router.get("/login-request", summary="获取登录请求信息 (OIDC)")
  14. def get_login_request(challenge: str):
  15. """
  16. 从 Hydra 获取登录请求信息。
  17. 前端调用此接口以检查是否应跳过登录(如果响应中 skip=true)。
  18. """
  19. try:
  20. req = hydra_service.get_login_request(challenge)
  21. if req.skip:
  22. logger.info(f"Skipping login for challenge {challenge}, subject: {req.subject}")
  23. # If Hydra says skip, we just accept it immediately
  24. return hydra_service.accept_login_request(challenge, subject=req.subject)
  25. return req
  26. except Exception as e:
  27. logger.exception(f"Failed to get login request for challenge: {challenge}")
  28. raise HTTPException(status_code=400, detail=str(e))
  29. @router.post("/login/accept", summary="接受登录请求 (OIDC)")
  30. def accept_login(
  31. challenge: str,
  32. login_data: LoginRequest,
  33. db: Session = Depends(deps.get_db)
  34. ):
  35. """
  36. 用户提交凭据 -> 验证 -> 接受登录请求。
  37. """
  38. user = db.query(User).filter(User.mobile == login_data.mobile).first()
  39. if not user or not security.verify_password(login_data.password, user.password_hash):
  40. # We don't reject the request immediately to allow retry,
  41. # but in a strict flow we might. Here we just return 401.
  42. logger.warning(f"Login failed for user {login_data.mobile}: Invalid credentials")
  43. raise HTTPException(status_code=401, detail="手机号或密码错误")
  44. if user.status != "ACTIVE":
  45. logger.warning(f"Login failed for user {login_data.mobile}: User not active")
  46. raise HTTPException(status_code=400, detail="用户状态不正常")
  47. try:
  48. logger.info(f"Accepting login request for user {user.mobile} (ID: {user.id}), challenge: {challenge}")
  49. return hydra_service.accept_login_request(challenge, subject=str(user.id))
  50. except Exception as e:
  51. logger.exception(f"Failed to accept login request for challenge: {challenge}")
  52. raise HTTPException(status_code=500, detail=str(e))
  53. @router.get("/consent-request", summary="获取同意请求信息 (OIDC)")
  54. def get_consent_request(
  55. challenge: str,
  56. db: Session = Depends(deps.get_db)
  57. ):
  58. """
  59. 获取同意请求信息。
  60. 通常自动接受内部用户的同意请求,但需要注入 Claims(映射账户)。
  61. """
  62. try:
  63. req = hydra_service.get_consent_request(challenge)
  64. # If skip is true, Hydra remembers consent.
  65. # But we still might want to refresh claims?
  66. # For simplicity, if skip, we accept with old scopes.
  67. if req.skip:
  68. logger.info(f"Skipping consent for challenge {challenge}, subject: {req.subject}")
  69. return hydra_service.accept_consent_request(
  70. challenge,
  71. grant_scope=req.requested_scope,
  72. id_token_claims={} # Hydra might use previous session?
  73. )
  74. # Auto-accept logic:
  75. # 1. Identify Client (App)
  76. # 2. Identify User
  77. # 3. Find Mapping
  78. client_id = req.client.client_id
  79. user_id = int(req.subject) # subject is user.id from login
  80. # Find mapping for this specific app (client_id matches app_id in our DB)
  81. # Note: In Applications table we used 'app_id' string.
  82. mapping = db.query(AppUserMapping).join(AppUserMapping.application).filter(
  83. AppUserMapping.user_id == user_id,
  84. # Assuming client_id in Hydra == app_id in our DB
  85. AppUserMapping.application.has(app_id=client_id)
  86. ).first()
  87. id_token_claims = {}
  88. if mapping:
  89. # We inject the mapped key (e.g. email, username) into the ID Token
  90. # You can standardize the claim name, e.g. "preferred_username" or "ext_id"
  91. id_token_claims["preferred_username"] = mapping.mapped_key
  92. id_token_claims["email"] = mapping.mapped_key # If it's email
  93. logger.info(f"Injecting claims for user {user_id} in client {client_id}: {mapping.mapped_key}")
  94. else:
  95. logger.info(f"No mapping found for user {user_id} in client {client_id}, minimal claims injected.")
  96. # Also inject mobile number if needed
  97. user = db.query(User).filter(User.id == user_id).first()
  98. if user:
  99. id_token_claims["phone_number"] = user.mobile
  100. return hydra_service.accept_consent_request(
  101. challenge,
  102. grant_scope=req.requested_scope,
  103. id_token_claims=id_token_claims
  104. )
  105. except Exception as e:
  106. logger.exception(f"Failed to process consent request for challenge: {challenge}")
  107. raise HTTPException(status_code=400, detail=str(e))