feat: add platform auth jwt adapter
This commit is contained in:
119
server-rs/crates/api-server/src/auth.rs
Normal file
119
server-rs/crates/api-server/src/auth.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Extension, Request, State},
|
||||
http::{HeaderMap, StatusCode, header::AUTHORIZATION},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use platform_auth::{AccessTokenClaims, verify_access_token};
|
||||
use serde_json::{Value, json};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
// 统一把已校验的 claims 写入 request extensions,避免后续 handler 再次重复解析 Bearer token。
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AuthenticatedAccessToken {
|
||||
claims: AccessTokenClaims,
|
||||
}
|
||||
|
||||
impl AuthenticatedAccessToken {
|
||||
pub fn new(claims: AccessTokenClaims) -> Self {
|
||||
Self { claims }
|
||||
}
|
||||
|
||||
pub fn claims(&self) -> &AccessTokenClaims {
|
||||
&self.claims
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn require_bearer_auth(
|
||||
State(state): State<AppState>,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, AppError> {
|
||||
let bearer_token = extract_bearer_token(request.headers())?;
|
||||
let request_id = request
|
||||
.extensions()
|
||||
.get::<RequestContext>()
|
||||
.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)
|
||||
})?;
|
||||
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(AuthenticatedAccessToken::new(claims));
|
||||
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
|
||||
pub async fn inspect_auth_claims(
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Json<Value> {
|
||||
json_success_body(
|
||||
Some(&request_context),
|
||||
json!({
|
||||
"claims": authenticated.claims(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn extract_bearer_token(headers: &HeaderMap) -> Result<String, AppError> {
|
||||
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())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::extract_bearer_token;
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user