fix(security): complete security and delivery compliance remediation
This commit is contained in:
+42
-19
@@ -1,33 +1,48 @@
|
||||
"""认证 API"""
|
||||
import base64
|
||||
import json
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header
|
||||
import hmac
|
||||
import secrets
|
||||
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header, Request, Response
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.core.casdoor import casdoor_sdk
|
||||
from app.core.security import create_access_token, verify_token
|
||||
from app.core.errors import internal_error
|
||||
from app.core.security import create_access_token, verify_casdoor_token, verify_token
|
||||
from app.core.config import settings
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import Token, UserInfo
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||
OAUTH_STATE_COOKIE = "h3c_oauth_state"
|
||||
OAUTH_STATE_TTL_SECONDS = 300
|
||||
|
||||
|
||||
def decode_jwt_payload(token: str) -> dict:
|
||||
"""直接解码 JWT payload,不验签(Casdoor 已完成认证)"""
|
||||
payload_b64 = token.split(".")[1]
|
||||
rem = len(payload_b64) % 4
|
||||
if rem:
|
||||
payload_b64 += "=" * (4 - rem)
|
||||
return json.loads(base64.urlsafe_b64decode(payload_b64))
|
||||
def _with_oauth_state(url: str, state: str) -> str:
|
||||
"""Replace the SDK-generated state with the browser-bound state value."""
|
||||
parts = urlsplit(url)
|
||||
query = [(key, value) for key, value in parse_qsl(parts.query, keep_blank_values=True) if key != "state"]
|
||||
query.append(("state", state))
|
||||
return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))
|
||||
|
||||
|
||||
@router.get("/login")
|
||||
def login():
|
||||
def login(response: Response):
|
||||
"""获取 Casdoor 登录 URL"""
|
||||
return {"url": casdoor_sdk.get_auth_link(settings.CASDOOR_REDIRECT_URL)}
|
||||
state = secrets.token_urlsafe(32)
|
||||
response.set_cookie(
|
||||
key=OAUTH_STATE_COOKIE,
|
||||
value=state,
|
||||
max_age=OAUTH_STATE_TTL_SECONDS,
|
||||
httponly=True,
|
||||
secure=not settings.DEBUG,
|
||||
samesite="lax",
|
||||
path="/api/auth",
|
||||
)
|
||||
login_url = casdoor_sdk.get_auth_link(settings.CASDOOR_REDIRECT_URL)
|
||||
return {"url": _with_oauth_state(login_url, state)}
|
||||
|
||||
|
||||
class CallbackRequest(BaseModel):
|
||||
@@ -36,18 +51,29 @@ class CallbackRequest(BaseModel):
|
||||
|
||||
|
||||
@router.post("/callback", response_model=Token)
|
||||
def callback(body: CallbackRequest, db: Session = Depends(get_db)):
|
||||
def callback(
|
||||
body: CallbackRequest,
|
||||
request: Request,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Casdoor 登录回调"""
|
||||
try:
|
||||
expected_state = request.cookies.get(OAUTH_STATE_COOKIE)
|
||||
response.delete_cookie(OAUTH_STATE_COOKIE, path="/api/auth")
|
||||
if not expected_state or not hmac.compare_digest(body.state, expected_state):
|
||||
raise HTTPException(status_code=400, detail="登录状态校验失败,请重新登录")
|
||||
|
||||
token_response = casdoor_sdk.get_oauth_token(code=body.code)
|
||||
if isinstance(token_response, dict) and "error" in token_response:
|
||||
raise HTTPException(status_code=400, detail=token_response.get("error_description", token_response["error"]))
|
||||
|
||||
access_token = token_response.get("access_token") if isinstance(token_response, dict) else token_response
|
||||
identity_token = token_response.get("id_token") if isinstance(token_response, dict) else None
|
||||
if not access_token:
|
||||
raise HTTPException(status_code=400, detail="Casdoor 未返回 access_token")
|
||||
|
||||
casdoor_user = decode_jwt_payload(access_token)
|
||||
casdoor_user = verify_casdoor_token(identity_token or access_token)
|
||||
|
||||
user = db.query(User).filter(User.casdoor_id == casdoor_user["sub"]).first()
|
||||
if not user:
|
||||
@@ -76,10 +102,7 @@ def callback(body: CallbackRequest, db: Session = Depends(get_db)):
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
import logging
|
||||
logging.getLogger(__name__).error("callback error: %s\n%s", e, traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise internal_error("Casdoor login callback", e)
|
||||
|
||||
|
||||
@router.get("/permissions")
|
||||
|
||||
Reference in New Issue
Block a user