use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use jsonwebtoken::{EncodingKey, Header, encode};
use spikard_http::server::build_router_with_handlers_and_config;
use spikard_http::{
ApiKeyConfig, Claims, Handler, HandlerResult, JwtConfig, Method, RequestData, Route, ServerConfig,
auth::INTERNAL_JWT_CLAIMS_HEADER,
};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tower::ServiceExt;
struct OkHandler;
impl Handler for OkHandler {
fn call(
&self,
_request: Request<Body>,
_request_data: RequestData,
) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
Box::pin(async move {
Ok(axum::http::Response::builder()
.status(StatusCode::OK)
.body(Body::from("ok"))
.expect("response"))
})
}
}
struct ClaimsHeaderEchoHandler;
impl Handler for ClaimsHeaderEchoHandler {
fn call(
&self,
_request: Request<Body>,
request_data: RequestData,
) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
Box::pin(async move {
let claims = request_data
.headers
.get(INTERNAL_JWT_CLAIMS_HEADER)
.cloned()
.unwrap_or_else(|| "{}".to_string());
Ok(axum::http::Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(Body::from(claims))
.expect("response"))
})
}
}
fn route(method: Method, path: &str, handler_name: &str) -> Route {
Route {
method,
path: path.to_string(),
handler_name: handler_name.to_string(),
expects_json_body: false,
cors: None,
is_async: true,
file_params: None,
request_validator: None,
response_validator: None,
parameter_validator: None,
jsonrpc_method: None,
compression: None,
#[cfg(feature = "di")]
handler_dependencies: Vec::new(),
}
}
fn now_plus(seconds: u64) -> usize {
let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("time");
usize::try_from((now + Duration::from_secs(seconds)).as_secs()).expect("timestamp fits usize")
}
#[tokio::test]
async fn jwt_auth_layer_rejects_missing_authorization() {
let config = ServerConfig {
jwt_auth: Some(JwtConfig {
secret: "secret".to_string(),
algorithm: "HS256".to_string(),
audience: None,
issuer: None,
leeway: 0,
}),
..Default::default()
};
let router = build_router_with_handlers_and_config(
vec![(
route(Method::Get, "/protected", "ok"),
Arc::new(OkHandler) as Arc<dyn Handler>,
)],
config,
Vec::new(),
)
.expect("router");
let response = router
.oneshot(
Request::builder()
.method("GET")
.uri("/protected")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn jwt_auth_layer_accepts_valid_bearer_token() {
let secret = "secret";
let config = ServerConfig {
jwt_auth: Some(JwtConfig {
secret: secret.to_string(),
algorithm: "HS256".to_string(),
audience: None,
issuer: None,
leeway: 0,
}),
..Default::default()
};
let claims = Claims {
sub: "user123".to_string(),
exp: now_plus(60),
iat: None,
nbf: None,
aud: None,
iss: None,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)
.expect("token");
let router = build_router_with_handlers_and_config(
vec![(
route(Method::Get, "/protected", "ok"),
Arc::new(OkHandler) as Arc<dyn Handler>,
)],
config,
Vec::new(),
)
.expect("router");
let response = router
.oneshot(
Request::builder()
.method("GET")
.uri("/protected")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn jwt_auth_layer_exposes_claims_to_handler() {
let secret = "secret";
let config = ServerConfig {
jwt_auth: Some(JwtConfig {
secret: secret.to_string(),
algorithm: "HS256".to_string(),
audience: None,
issuer: None,
leeway: 0,
}),
..Default::default()
};
let claims = Claims {
sub: "claims-user".to_string(),
exp: now_plus(60),
iat: None,
nbf: None,
aud: None,
iss: None,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)
.expect("token");
let router = build_router_with_handlers_and_config(
vec![(
route(Method::Get, "/protected", "claims"),
Arc::new(ClaimsHeaderEchoHandler) as Arc<dyn Handler>,
)],
config,
Vec::new(),
)
.expect("router");
let response = router
.oneshot(
Request::builder()
.method("GET")
.uri("/protected")
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.expect("collect").to_bytes();
let claims_json: serde_json::Value = serde_json::from_slice(&body).expect("valid claims json");
assert_eq!(claims_json["sub"], "claims-user");
}
#[tokio::test]
async fn api_key_auth_layer_rejects_missing_key() {
let config = ServerConfig {
api_key_auth: Some(ApiKeyConfig {
keys: vec!["k1".to_string()],
header_name: "X-API-Key".to_string(),
}),
..Default::default()
};
let router = build_router_with_handlers_and_config(
vec![(
route(Method::Get, "/protected", "ok"),
Arc::new(OkHandler) as Arc<dyn Handler>,
)],
config,
Vec::new(),
)
.expect("router");
let response = router
.oneshot(
Request::builder()
.method("GET")
.uri("/protected")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn api_key_auth_layer_accepts_valid_key_from_header() {
let config = ServerConfig {
api_key_auth: Some(ApiKeyConfig {
keys: vec!["k1".to_string()],
header_name: "X-API-Key".to_string(),
}),
..Default::default()
};
let router = build_router_with_handlers_and_config(
vec![(
route(Method::Get, "/protected", "ok"),
Arc::new(OkHandler) as Arc<dyn Handler>,
)],
config,
Vec::new(),
)
.expect("router");
let response = router
.oneshot(
Request::builder()
.method("GET")
.uri("/protected")
.header("x-api-key", "k1")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn api_key_auth_layer_accepts_valid_key_from_query_param() {
let config = ServerConfig {
api_key_auth: Some(ApiKeyConfig {
keys: vec!["k1".to_string()],
header_name: "X-API-Key".to_string(),
}),
..Default::default()
};
let router = build_router_with_handlers_and_config(
vec![(
route(Method::Get, "/protected", "ok"),
Arc::new(OkHandler) as Arc<dyn Handler>,
)],
config,
Vec::new(),
)
.expect("router");
let response = router
.oneshot(
Request::builder()
.method("GET")
.uri("/protected?api_key=k1")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
}