feat: add platform auth jwt adapter

This commit is contained in:
2026-04-21 13:02:44 +08:00
parent e37163d4d3
commit adaf514a1a
20 changed files with 1220 additions and 44 deletions

View File

@@ -1,8 +1,9 @@
use axum::{body::Body, extract::Extension, http::Request, middleware, routing::get, Router};
use axum::{Router, body::Body, extract::Extension, http::Request, middleware, routing::get};
use tower_http::trace::{DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::{info_span, Level};
use tracing::{Level, info_span};
use crate::{
auth::{inspect_auth_claims, require_bearer_auth},
error_middleware::normalize_error_response,
health::health_check,
request_context::{attach_request_context, resolve_request_id},
@@ -19,6 +20,13 @@ pub fn build_router(state: AppState) -> Router {
health_check(Extension(request_context)).await
}),
)
.route(
"/_internal/auth/claims",
get(inspect_auth_claims).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
// 错误归一化层放在 tracing 里侧,让 tracing 记录到最终对外返回的状态与错误体形态。
.layer(middleware::from_fn(normalize_error_response))
// 响应头回写放在错误归一化外侧,确保最终写回的是归一化后的最终响应。
@@ -53,7 +61,11 @@ mod tests {
http::{Request, StatusCode},
};
use http_body_util::BodyExt;
use platform_auth::{
AccessTokenClaims, AccessTokenClaimsInput, AuthProvider, BindingStatus, sign_access_token,
};
use serde_json::Value;
use time::OffsetDateTime;
use tower::ServiceExt;
use crate::{config::AppConfig, state::AppState};
@@ -62,7 +74,7 @@ mod tests {
#[tokio::test]
async fn healthz_returns_legacy_compatible_payload_and_headers() {
let app = build_router(AppState::new(AppConfig::default()));
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
@@ -117,7 +129,7 @@ mod tests {
#[tokio::test]
async fn healthz_returns_standard_envelope_when_requested() {
let app = build_router(AppState::new(AppConfig::default()));
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
@@ -152,4 +164,79 @@ mod tests {
Value::String("req-health-envelope".to_string())
);
}
#[tokio::test]
async fn internal_auth_claims_rejects_missing_bearer_token() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.uri("/_internal/auth/claims")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn internal_auth_claims_returns_verified_claims() {
let config = AppConfig::default();
let state = AppState::new(config.clone()).expect("state should build");
let claims = AccessTokenClaims::from_input(
AccessTokenClaimsInput {
user_id: "usr_auth_debug".to_string(),
session_id: "sess_auth_debug".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 7,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("测试用户".to_string()),
},
state.auth_jwt_config(),
OffsetDateTime::now_utc(),
)
.expect("claims should build");
let token = sign_access_token(&claims, state.auth_jwt_config()).expect("token should sign");
let app = build_router(state);
let response = app
.oneshot(
Request::builder()
.uri("/_internal/auth/claims")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let body = response
.into_body()
.collect()
.await
.expect("response body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("response body should be valid json");
assert_eq!(
payload["claims"]["sub"],
Value::String("usr_auth_debug".to_string())
);
assert_eq!(
payload["claims"]["sid"],
Value::String("sess_auth_debug".to_string())
);
assert_eq!(
payload["claims"]["ver"],
Value::Number(serde_json::Number::from(7))
);
}
}