| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- from typing import Any, List, Optional
- import logging
- from fastapi import APIRouter, Depends, HTTPException, Query
- from sqlalchemy.orm import Session
- from app.api.v1 import deps
- from app.core import security
- from app.models.user import User
- from app.models.mapping import AppUserMapping
- from app.schemas.token import Token, LoginRequest, RejectRequest
- from app.services.hydra_service import hydra_service
- router = APIRouter()
- logger = logging.getLogger(__name__)
- @router.get("/login-request", summary="获取登录请求信息 (OIDC)")
- def get_login_request(
- challenge: str,
- current_user: Optional[User] = Depends(deps.get_current_active_user_optional)
- ):
- """
- 从 Hydra 获取登录请求信息。
- 前端调用此接口以检查是否应跳过登录(如果响应中 skip=true)。
- 如果用户在统一认证平台已有会话,也会自动接受。
- """
- try:
- req = hydra_service.get_login_request(challenge)
- if req.skip:
- logger.info(f"Skipping login for challenge {challenge}, subject: {req.subject}")
- # If Hydra says skip, we just accept it immediately
- return hydra_service.accept_login_request(challenge, subject=req.subject)
-
- # 如果不是 skip,但用户在平台已登录,则自动接受
- if current_user:
- logger.info(f"Auto-accepting login for challenge {challenge}, using platform session, subject: {current_user.id}")
- return hydra_service.accept_login_request(challenge, subject=str(current_user.id))
-
- return req
- except Exception as e:
- logger.exception(f"Failed to get login request for challenge: {challenge}")
- raise HTTPException(status_code=400, detail=str(e))
- @router.post("/login/accept", summary="接受登录请求 (OIDC)")
- def accept_login(
- challenge: str,
- login_data: LoginRequest,
- db: Session = Depends(deps.get_db)
- ):
- """
- 用户提交凭据 -> 验证 -> 接受登录请求。
- """
- user = db.query(User).filter(User.mobile == login_data.mobile).first()
- if not user or not security.verify_password(login_data.password, user.password_hash):
- # We don't reject the request immediately to allow retry,
- # but in a strict flow we might. Here we just return 401.
- logger.warning(f"Login failed for user {login_data.mobile}: Invalid credentials")
- raise HTTPException(status_code=401, detail="手机号或密码错误")
-
- if user.status != "ACTIVE":
- logger.warning(f"Login failed for user {login_data.mobile}: User not active")
- raise HTTPException(status_code=400, detail="用户状态不正常")
- try:
- logger.info(f"Accepting login request for user {user.mobile} (ID: {user.id}), challenge: {challenge}")
- return hydra_service.accept_login_request(challenge, subject=str(user.id))
- except Exception as e:
- logger.exception(f"Failed to accept login request for challenge: {challenge}")
- raise HTTPException(status_code=500, detail=str(e))
- @router.post("/login/reject", summary="拒绝登录请求 (OIDC)")
- def reject_login(
- challenge: str,
- reject_data: RejectRequest
- ):
- """
- 拒绝登录请求。
- 用于在用户取消登录或发生错误时拒绝 OIDC 登录请求。
-
- 标准 OAuth2 错误码:
- - access_denied: 用户拒绝访问
- - invalid_request: 请求无效
- - server_error: 服务器错误
- """
- try:
- logger.info(f"Rejecting login request for challenge: {challenge}, error: {reject_data.error}")
- result = hydra_service.reject_login_request(
- challenge=challenge,
- error=reject_data.error,
- error_description=reject_data.error_description or ""
- )
- return result
- except Exception as e:
- logger.exception(f"Failed to reject login request for challenge: {challenge}")
- raise HTTPException(status_code=400, detail=str(e))
- @router.get("/consent-request", summary="获取同意请求信息 (OIDC)")
- def get_consent_request(
- challenge: str,
- db: Session = Depends(deps.get_db)
- ):
- """
- 获取同意请求信息。
- 通常自动接受内部用户的同意请求,但需要注入 Claims(映射账户)。
- """
- try:
- req = hydra_service.get_consent_request(challenge)
-
- # If skip is true, Hydra remembers consent.
- # But we still might want to refresh claims?
- # For simplicity, if skip, we accept with old scopes.
- if req.skip:
- logger.info(f"Skipping consent for challenge {challenge}, subject: {req.subject}")
- return hydra_service.accept_consent_request(
- challenge,
- grant_scope=req.requested_scope,
- id_token_claims={} # Hydra might use previous session?
- )
-
- # Auto-accept logic:
- # 1. Identify Client (App)
- # 2. Identify User
- # 3. Find Mapping
- client_id = req.client.client_id
- user_id = int(req.subject) # subject is user.id from login
-
- # Find mapping for this specific app (client_id matches app_id in our DB)
- # Note: In Applications table we used 'app_id' string.
- mapping = db.query(AppUserMapping).join(AppUserMapping.application).filter(
- AppUserMapping.user_id == user_id,
- # Assuming client_id in Hydra == app_id in our DB
- AppUserMapping.application.has(app_id=client_id)
- ).first()
- id_token_claims = {}
- if mapping:
- # We inject the mapped key (e.g. email, username) into the ID Token
- # You can standardize the claim name, e.g. "preferred_username" or "ext_id"
- id_token_claims["preferred_username"] = mapping.mapped_key
- id_token_claims["email"] = mapping.mapped_key # If it's email
- logger.info(f"Injecting claims for user {user_id} in client {client_id}: {mapping.mapped_key}")
- else:
- logger.info(f"No mapping found for user {user_id} in client {client_id}, minimal claims injected.")
-
- # Also inject mobile number if needed
- user = db.query(User).filter(User.id == user_id).first()
- if user:
- id_token_claims["phone_number"] = user.mobile
- return hydra_service.accept_consent_request(
- challenge,
- grant_scope=req.requested_scope,
- id_token_claims=id_token_claims
- )
- except Exception as e:
- logger.exception(f"Failed to process consent request for challenge: {challenge}")
- raise HTTPException(status_code=400, detail=str(e))
- @router.post("/consent/reject", summary="拒绝同意请求 (OIDC)")
- def reject_consent(
- challenge: str,
- reject_data: RejectRequest
- ):
- """
- 拒绝同意请求。
- 用于在用户拒绝授权或发生错误时拒绝 OIDC 同意请求。
-
- 标准 OAuth2 错误码:
- - access_denied: 用户拒绝访问
- - invalid_request: 请求无效
- - server_error: 服务器错误
- """
- try:
- logger.info(f"Rejecting consent request for challenge: {challenge}, error: {reject_data.error}")
- result = hydra_service.reject_consent_request(
- challenge=challenge,
- error=reject_data.error,
- error_description=reject_data.error_description or ""
- )
- return result
- except Exception as e:
- logger.exception(f"Failed to reject consent request for challenge: {challenge}")
- raise HTTPException(status_code=400, detail=str(e))
- @router.get("/logout-request", summary="获取登出请求信息 (OIDC)")
- def get_logout_request(challenge: str):
- """
- 从 Hydra 获取登出请求信息。
- """
- try:
- return hydra_service.get_logout_request(challenge)
- except Exception as e:
- logger.exception(f"Failed to get logout request for challenge: {challenge}")
- raise HTTPException(status_code=400, detail=str(e))
- @router.post("/logout/accept", summary="接受登出请求 (OIDC)")
- def accept_logout(
- challenge: str
- ):
- """
- 接受登出请求,返回 redirect_to。
- """
- try:
- logger.info(f"Accepting logout request for challenge: {challenge}")
- return hydra_service.accept_logout_request(challenge)
- except Exception as e:
- logger.exception(f"Failed to accept logout request for challenge: {challenge}")
- raise HTTPException(status_code=500, detail=str(e))
|