use axum::{ Json, extract::{Extension, Request, State}, http::{ HeaderMap, StatusCode, header::{AUTHORIZATION, COOKIE}, }, middleware::Next, response::Response, }; use platform_auth::{ AccessTokenClaims, AuthProvider, BindingStatus, read_refresh_session_token, verify_access_token, }; use serde_json::{Value, json}; use time::OffsetDateTime; use tracing::warn; use crate::{ api_response::json_success_body, http_error::AppError, request_context::RequestContext, state::AppState, }; const INTERNAL_AUTH_USER_ID_HEADER: &str = "x-genarrative-authenticated-user-id"; const INTERNAL_API_SECRET_HEADER: &str = "x-genarrative-internal-api-secret"; // 统一把已校验的 claims 写入 request extensions,避免后续 handler 再次重复解析 Bearer token。 #[derive(Clone, Debug)] pub struct AuthenticatedAccessToken { claims: AccessTokenClaims, } #[derive(Clone, Debug)] pub struct RefreshSessionToken { token: String, } impl AuthenticatedAccessToken { pub fn new(claims: AccessTokenClaims) -> Self { Self { claims } } pub fn claims(&self) -> &AccessTokenClaims { &self.claims } } impl RefreshSessionToken { pub fn new(token: String) -> Self { Self { token } } pub fn token(&self) -> &str { &self.token } } pub async fn require_bearer_auth( State(state): State, mut request: Request, next: Next, ) -> Result { if allows_internal_forwarded_auth(request.uri().path()) && let Some(claims) = try_build_internal_forwarded_claims(&state, request.headers()) { request .extensions_mut() .insert(AuthenticatedAccessToken::new(claims)); return Ok(next.run(request).await); } let bearer_token = extract_bearer_token(request.headers())?; let request_id = request .extensions() .get::() .map(|context| context.request_id().to_string()) .unwrap_or_else(|| "unknown".to_string()); let claims = verify_access_token(&bearer_token, state.auth_jwt_config()).map_err(|error| { warn!( %request_id, error = %error, "Bearer JWT 校验失败" ); AppError::from_status(StatusCode::UNAUTHORIZED) })?; let current_user = state .auth_user_service() .get_user_by_id(claims.user_id()) .map_err(|error| { warn!( %request_id, error = %error, "Bearer JWT 用户快照读取失败" ); AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR) })? .ok_or_else(|| { warn!( %request_id, user_id = %claims.user_id(), "Bearer JWT 对应用户不存在" ); AppError::from_status(StatusCode::UNAUTHORIZED) })?; if current_user.token_version != claims.token_version() { warn!( %request_id, user_id = %claims.user_id(), token_version = claims.token_version(), current_token_version = current_user.token_version, "Bearer JWT 版本已失效" ); return Err(AppError::from_status(StatusCode::UNAUTHORIZED) .with_message("当前登录态已失效,请重新登录")); } request .extensions_mut() .insert(AuthenticatedAccessToken::new(claims)); Ok(next.run(request).await) } pub async fn inspect_auth_claims( Extension(request_context): Extension, Extension(authenticated): Extension, ) -> Json { json_success_body( Some(&request_context), json!({ "claims": authenticated.claims(), }), ) } pub async fn attach_refresh_session_token( State(state): State, mut request: Request, next: Next, ) -> Response { if let Some(token) = request .headers() .get(COOKIE) .and_then(|value| value.to_str().ok()) .and_then(|cookie_header| { read_refresh_session_token(cookie_header, state.refresh_cookie_config()) }) { request .extensions_mut() .insert(RefreshSessionToken::new(token)); } next.run(request).await } pub async fn inspect_refresh_session_cookie( State(state): State, Extension(request_context): Extension, request: Request, ) -> Json { let maybe_token = request.extensions().get::(); json_success_body( Some(&request_context), json!({ "cookieName": state.refresh_cookie_config().cookie_name(), "present": maybe_token.is_some(), "tokenLength": maybe_token.map(|token| token.token().len()), }), ) } fn extract_bearer_token(headers: &HeaderMap) -> Result { let authorization = headers .get(AUTHORIZATION) .and_then(|value| value.to_str().ok()) .map(str::trim) .ok_or_else(|| AppError::from_status(StatusCode::UNAUTHORIZED))?; let token = authorization .strip_prefix("Bearer ") .or_else(|| authorization.strip_prefix("bearer ")) .map(str::trim) .filter(|token| !token.is_empty()) .ok_or_else(|| AppError::from_status(StatusCode::UNAUTHORIZED))?; Ok(token.to_string()) } fn allows_internal_forwarded_auth(path: &str) -> bool { // Node 代理已经完成平台账号 JWT 校验,Rust 运行时只信任这些明确的内部转发路径。 path.starts_with("/api/runtime/big-fish/") || path.starts_with("/api/runtime/chat/") || path.starts_with("/api/runtime/creative-agent/") || path.starts_with("/api/runtime/puzzle/") } fn try_build_internal_forwarded_claims( state: &AppState, headers: &HeaderMap, ) -> Option { let expected_secret = state.config.internal_api_secret.as_ref()?.trim(); if expected_secret.is_empty() { return None; } let provided_secret = headers .get(INTERNAL_API_SECRET_HEADER) .and_then(|value| value.to_str().ok()) .map(str::trim) .filter(|value| !value.is_empty())?; if provided_secret != expected_secret { return None; } let user_id = headers .get(INTERNAL_AUTH_USER_ID_HEADER) .and_then(|value| value.to_str().ok()) .map(str::trim) .filter(|value| !value.is_empty())? .to_string(); // 这里的 claims 只服务于经 Node 已鉴权后的本地内部转发链路,避免在开发态复制整套账号仓储。 AccessTokenClaims::from_input( platform_auth::AccessTokenClaimsInput { user_id: user_id.clone(), session_id: format!("internal-forwarded-{user_id}"), provider: AuthProvider::Password, roles: vec!["user".to_string()], token_version: 0, phone_verified: false, binding_status: BindingStatus::Active, display_name: None, }, state.auth_jwt_config(), OffsetDateTime::now_utc(), ) .ok() } #[cfg(test)] mod tests { use super::{ INTERNAL_API_SECRET_HEADER, INTERNAL_AUTH_USER_ID_HEADER, RefreshSessionToken, allows_internal_forwarded_auth, extract_bearer_token, try_build_internal_forwarded_claims, }; use crate::{config::AppConfig, state::AppState}; use axum::{ http::{HeaderMap, HeaderValue, StatusCode, header::AUTHORIZATION}, response::IntoResponse, }; #[test] fn extract_bearer_token_accepts_standard_header() { let mut headers = HeaderMap::new(); headers.insert( AUTHORIZATION, HeaderValue::from_static("Bearer token-value"), ); let token = extract_bearer_token(&headers).expect("bearer token should be extracted"); assert_eq!(token, "token-value"); } #[test] fn extract_bearer_token_rejects_missing_scheme() { let mut headers = HeaderMap::new(); headers.insert(AUTHORIZATION, HeaderValue::from_static("Basic abc")); let error = extract_bearer_token(&headers).expect_err("basic auth should be rejected"); assert_eq!(error.into_response().status(), StatusCode::UNAUTHORIZED); } #[test] fn refresh_session_token_retains_original_value() { let token = RefreshSessionToken::new("refresh-token-01".to_string()); assert_eq!(token.token(), "refresh-token-01"); } #[test] fn internal_forwarded_auth_allows_node_proxy_runtime_paths() { assert!(allows_internal_forwarded_auth( "/api/runtime/big-fish/sessions" )); assert!(allows_internal_forwarded_auth( "/api/runtime/chat/npc/turn/stream" )); assert!(allows_internal_forwarded_auth( "/api/runtime/creative-agent/sessions" )); assert!(allows_internal_forwarded_auth("/api/runtime/puzzle/works")); assert!(!allows_internal_forwarded_auth("/api/auth/me")); } #[test] fn internal_forwarded_claims_require_matching_secret() { let mut config = AppConfig::default(); config.internal_api_secret = Some("bridge-secret".to_string()); let state = AppState::new(config).expect("state should build"); let mut headers = HeaderMap::new(); headers.insert( INTERNAL_AUTH_USER_ID_HEADER, HeaderValue::from_static("user_forwarded_01"), ); headers.insert( INTERNAL_API_SECRET_HEADER, HeaderValue::from_static("bridge-secret"), ); let claims = try_build_internal_forwarded_claims(&state, &headers).expect("claims should resolve"); assert_eq!(claims.user_id(), "user_forwarded_01"); assert_eq!(claims.token_version(), 0); } }