use std::sync::Arc;
use http::{header, Method, Response, StatusCode};
use openauth_core::db::MemoryAdapter;
use openauth_core::plugin::{AuthPlugin, PluginAfterHookAction};
use openauth_plugins::jwt::{
jwt, jwt_with_options, verify_jwt, JwkAlgorithm, JwtJwksOptions, JwtOptions, JwtSigningOptions,
};
use serde_json::Value;
use super::helpers::*;
#[tokio::test]
async fn sign_and_verify_endpoints_are_server_only() -> Result<(), Box<dyn std::error::Error>> {
let router = router_with_plugin(Arc::new(MemoryAdapter::new()), jwt()?)?;
let sign = router
.handle_async(request(
Method::POST,
"/api/auth/sign-jwt",
r#"{"payload":{"sub":"user_1"}}"#,
None,
)?)
.await?;
let verify = router
.handle_async(request(
Method::POST,
"/api/auth/verify-jwt",
r#"{"token":"malformed"}"#,
None,
)?)
.await?;
assert_eq!(sign.status(), StatusCode::NOT_FOUND);
assert_eq!(verify.status(), StatusCode::NOT_FOUND);
Ok(())
}
#[tokio::test]
async fn get_session_merges_exposed_headers() -> Result<(), Box<dyn std::error::Error>> {
let adapter = Arc::new(MemoryAdapter::new());
seed_user_session(adapter.as_ref()).await?;
let preexisting = AuthPlugin::new("preexisting-expose-header").with_after_hook(
"/get-session",
|_context, _request, response| {
let (mut parts, body) = response.into_parts();
parts.headers.insert(
header::ACCESS_CONTROL_EXPOSE_HEADERS,
header::HeaderValue::from_static("x-existing"),
);
Ok(PluginAfterHookAction::Continue(Response::from_parts(
parts, body,
)))
},
);
let context = openauth_core::context::create_auth_context_with_adapter(
openauth_core::options::OpenAuthOptions {
base_url: Some(TEST_BASE_URL.to_owned()),
secret: Some("test-secret-123456789012345678901234".to_owned()),
plugins: vec![preexisting, jwt()?],
advanced: openauth_core::options::AdvancedOptions {
disable_csrf_check: true,
disable_origin_check: true,
..openauth_core::options::AdvancedOptions::default()
},
..openauth_core::options::OpenAuthOptions::default()
},
adapter.clone(),
)?;
let router = openauth_core::api::AuthRouter::with_async_endpoints(
context,
Vec::new(),
openauth_core::api::core_auth_async_endpoints(adapter),
)?;
let cookie = signed_session_cookie("token_1")?;
let response = router
.handle_async(request(
Method::GET,
"/api/auth/get-session",
"",
Some(&cookie),
)?)
.await?;
let expose = response
.headers()
.get(header::ACCESS_CONTROL_EXPOSE_HEADERS)
.and_then(|value| value.to_str().ok())
.ok_or("missing expose header")?;
assert!(expose.contains("x-existing"));
assert!(expose.contains("set-auth-jwt"));
Ok(())
}
#[tokio::test]
async fn jwks_drops_keys_expired_beyond_grace() -> Result<(), Box<dyn std::error::Error>> {
let adapter = Arc::new(MemoryAdapter::new());
let options = JwtOptions {
jwks: JwtJwksOptions {
rotation_interval: Some(-120),
grace_period: 1,
disable_private_key_encryption: true,
..JwtJwksOptions::default()
},
..JwtOptions::default()
};
let context = openauth_core::context::create_auth_context_with_adapter(
options_with_plugin(jwt_with_options(options.clone())?),
adapter.clone(),
)?;
let mut claims = openauth_plugins::jwt::JwtClaims::new();
claims.insert("sub".to_owned(), serde_json::json!("user_1"));
openauth_plugins::jwt::sign_jwt(&context, claims, Some(options.clone())).await?;
let router = router_with_plugin(adapter, jwt_with_options(options)?)?;
let response = router
.handle_async(request(Method::GET, "/api/auth/jwks", "", None)?)
.await?;
let body: Value = serde_json::from_slice(response.body())?;
assert_eq!(body["keys"].as_array().ok_or("missing keys")?.len(), 0);
Ok(())
}
#[test]
fn remote_url_accepts_plain_strings_and_query_params() {
let result = jwt_with_options(JwtOptions {
jwks: JwtJwksOptions {
remote_url: Some("not a url ?x=1".to_owned()),
..JwtJwksOptions::default()
},
jwt: JwtSigningOptions {
sign: Some(Arc::new(|_claims| {
Box::pin(async move { Ok("remote.jwt.signature".to_owned()) })
})),
..JwtSigningOptions::default()
},
..JwtOptions::default()
});
assert!(result.is_ok());
}
#[tokio::test]
async fn remote_url_still_allows_local_signing_without_custom_signer_for_supported_algorithms(
) -> Result<(), Box<dyn std::error::Error>> {
for algorithm in [
JwkAlgorithm::EdDsa,
JwkAlgorithm::Es256,
JwkAlgorithm::Es512,
JwkAlgorithm::Rs256,
JwkAlgorithm::Ps256,
] {
let adapter = Arc::new(MemoryAdapter::new());
seed_user_session(adapter.as_ref()).await?;
let options = JwtOptions {
jwks: JwtJwksOptions {
remote_url: Some("https://example.com/.well-known/jwks.json".to_owned()),
key_pair_algorithm: Some(algorithm),
disable_private_key_encryption: true,
..JwtJwksOptions::default()
},
..JwtOptions::default()
};
let context = openauth_core::context::create_auth_context_with_adapter(
options_with_plugin(jwt_with_options(options.clone())?),
adapter.clone(),
)?;
let router = router_with_plugin(adapter, jwt_with_options(options)?)?;
let cookie = signed_session_cookie("token_1")?;
let response = router
.handle_async(request(Method::GET, "/api/auth/token", "", Some(&cookie))?)
.await?;
let body: Value = serde_json::from_slice(response.body())?;
assert_eq!(response.status(), StatusCode::OK);
assert!(verify_jwt(
&context,
body["token"].as_str().ok_or("missing token")?,
None
)
.await?
.is_some());
}
Ok(())
}
#[test]
fn rsa_modulus_length_must_be_at_least_2048() {
let result = jwt_with_options(JwtOptions {
jwks: JwtJwksOptions {
key_pair_algorithm: Some(JwkAlgorithm::Rs256),
rsa_modulus_length: Some(1024),
..JwtJwksOptions::default()
},
..JwtOptions::default()
});
assert!(result.is_err());
}
#[tokio::test]
async fn rsa_modulus_length_can_be_configured() -> Result<(), Box<dyn std::error::Error>> {
let router = router_with_plugin(
Arc::new(MemoryAdapter::new()),
jwt_with_options(JwtOptions {
jwks: JwtJwksOptions {
key_pair_algorithm: Some(JwkAlgorithm::Rs256),
rsa_modulus_length: Some(2048),
..JwtJwksOptions::default()
},
..JwtOptions::default()
})?,
)?;
let response = router
.handle_async(request(Method::GET, "/api/auth/jwks", "", None)?)
.await?;
let body: Value = serde_json::from_slice(response.body())?;
assert_eq!(body["keys"][0]["alg"], "RS256");
assert!(body["keys"][0]["n"].as_str().is_some());
Ok(())
}