use std::collections::HashMap;
use std::collections::HashSet;
use std::ops::ControlFlow;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::time::Duration;
use axum::handler::HandlerWithoutStateExt;
use base64::Engine as _;
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use http::HeaderMap;
use http::HeaderName;
use http::HeaderValue;
use http::StatusCode;
use http_body_util::BodyExt;
use insta::assert_yaml_snapshot;
use jsonwebtoken::Algorithm;
use jsonwebtoken::EncodingKey;
use jsonwebtoken::encode;
use jsonwebtoken::get_current_timestamp;
use jsonwebtoken::jwk::AlgorithmParameters;
use jsonwebtoken::jwk::CommonParameters;
use jsonwebtoken::jwk::EllipticCurve;
use jsonwebtoken::jwk::EllipticCurveKeyParameters;
use jsonwebtoken::jwk::EllipticCurveKeyType;
use jsonwebtoken::jwk::Jwk;
use jsonwebtoken::jwk::JwkSet;
use jsonwebtoken::jwk::KeyAlgorithm;
use jsonwebtoken::jwk::KeyOperations;
use jsonwebtoken::jwk::PublicKeyUse;
use mime::APPLICATION_JSON;
use p256::ecdsa::SigningKey;
use p256::ecdsa::signature::rand_core::OsRng;
use p256::pkcs8::EncodePrivateKey;
use serde::Deserialize;
use serde::Serialize;
use tower::ServiceExt;
use tracing::subscriber;
use url::Url;
use super::APOLLO_AUTHENTICATION_JWT_CLAIMS;
use super::HEADER_TOKEN_TRUNCATED;
use super::Header;
use super::JWT_CONTEXT_KEY;
use super::JWTConf;
use super::JwtStatus;
use super::Source;
use super::authenticate;
use crate::assert_errors_eq_ignoring_id;
use crate::assert_response_eq_ignoring_error_id;
use crate::assert_snapshot_subscriber;
use crate::graphql;
use crate::plugin::test;
use crate::plugins::authentication::Issuers;
use crate::plugins::authentication::jwks::Audiences;
use crate::plugins::authentication::jwks::JWTCriteria;
use crate::plugins::authentication::jwks::JwksConfig;
use crate::plugins::authentication::jwks::JwksManager;
use crate::plugins::authentication::jwks::parse_jwks;
use crate::plugins::authentication::jwks::search_jwks;
use crate::services::router;
use crate::services::router::body::RouterBody;
use crate::services::supergraph;
pub(crate) fn create_an_url(filename: &str) -> String {
let jwks_base = Path::new("tests");
let jwks_path = jwks_base.join("fixtures").join(filename);
let jwks_absolute_path = std::fs::canonicalize(jwks_path).unwrap();
Url::from_file_path(jwks_absolute_path).unwrap().to_string()
}
async fn build_a_default_test_harness() -> router::BoxCloneService {
build_a_test_harness(None, None, false, false, false).await
}
async fn build_a_test_harness(
header_name: Option<String>,
header_value_prefix: Option<String>,
multiple_jwks: bool,
ignore_other_prefixes: bool,
continue_on_error: bool,
) -> router::BoxCloneService {
let mut mock_service = test::MockSupergraphService::new();
let expected_mock_response_data = "response created within the mock";
mock_service.expect_clone().return_once(move || {
let mut mock_service = test::MockSupergraphService::new();
mock_service
.expect_call()
.once()
.returning(move |req: supergraph::Request| {
Ok(supergraph::Response::fake_builder()
.data(expected_mock_response_data)
.context(req.context)
.build()
.unwrap())
});
mock_service
});
let jwks_url = create_an_url("jwks.json");
let mut config = if multiple_jwks {
serde_json::json!({
"authentication": {
"router": {
"jwt": {
"jwks": [
{
"url": &jwks_url
},
{
"url": &jwks_url
}
]
}
}
}
})
} else {
serde_json::json!({
"authentication": {
"router": {
"jwt" : {
"jwks": [
{
"url": &jwks_url
}
]
}
}
}
})
};
if let Some(hn) = header_name {
config["authentication"]["router"]["jwt"]["header_name"] = serde_json::Value::String(hn);
}
if let Some(hp) = header_value_prefix {
config["authentication"]["router"]["jwt"]["header_value_prefix"] =
serde_json::Value::String(hp);
}
config["authentication"]["router"]["jwt"]["ignore_other_prefixes"] =
serde_json::Value::Bool(ignore_other_prefixes);
if continue_on_error {
config["authentication"]["router"]["jwt"]["on_error"] =
serde_json::Value::String("Continue".to_string());
}
match crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.supergraph_hook(move |_| mock_service.clone().boxed())
.build_router()
.await
{
Ok(test_harness) => test_harness,
Err(e) => panic!("Failed to build test harness: {e}"),
}
}
#[tokio::test]
async fn load_plugin() {
let _test_harness = build_a_default_test_harness().await;
}
#[tokio::test]
async fn it_rejects_when_there_is_no_auth_header() {
let mut mock_service = test::MockSupergraphService::new();
mock_service.expect_clone().return_once(move || {
println!("cloned to supergraph mock");
let mut mock_service = test::MockSupergraphService::new();
mock_service.expect_call().never();
mock_service
});
let jwks_url = create_an_url("jwks.json");
let config = serde_json::json!({
"authentication": {
"router": {
"jwt" : {
"jwks": [
{
"url": &jwks_url
}
]
}
}
},
"rhai": {
"scripts":"tests/fixtures",
"main":"require_authentication.rhai"
}
});
let test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.supergraph_hook(move |_| mock_service.clone().boxed())
.build_router()
.await
.unwrap();
let request_with_appropriate_name = supergraph::Request::canned_builder().build().unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
let expected_error = graphql::Error::builder()
.message("The request is not authenticated")
.extension_code("AUTH_ERROR")
.build();
assert_errors_eq_ignoring_id!(response.errors, [expected_error]);
assert_eq!(StatusCode::UNAUTHORIZED, service_response.response.status());
}
#[tokio::test]
async fn it_rejects_when_auth_prefix_is_missing() {
let test_harness = build_a_default_test_harness().await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, "invalid")
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
let expected_error = graphql::Error::builder()
.message(format!(
"Value of '{0}' JWT header should be prefixed with 'Bearer'",
http::header::AUTHORIZATION,
))
.extension_code("AUTH_ERROR")
.build();
assert_errors_eq_ignoring_id!(response.errors, [expected_error]);
assert_eq!(StatusCode::BAD_REQUEST, service_response.response.status());
}
#[tokio::test]
async fn it_rejects_when_auth_prefix_has_no_jwt_token() {
let test_harness = build_a_default_test_harness().await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, "Bearer")
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
let expected_error = graphql::Error::builder()
.message(format!(
"Value of '{0}' JWT header has only 'Bearer' prefix but no JWT token",
http::header::AUTHORIZATION,
))
.extension_code("AUTH_ERROR")
.build();
assert_errors_eq_ignoring_id!(response.errors, [expected_error]);
assert_eq!(StatusCode::BAD_REQUEST, service_response.response.status());
}
#[tokio::test]
async fn it_rejects_when_auth_prefix_has_invalid_format_jwt() {
let test_harness = build_a_default_test_harness().await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, "Bearer header.payload")
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
let expected_error = graphql::Error::builder()
.message(format!(
"'{HEADER_TOKEN_TRUNCATED}' is not a valid JWT header: InvalidToken"
))
.extension_code("AUTH_ERROR")
.build();
assert_errors_eq_ignoring_id!(response.errors, [expected_error]);
assert_eq!(StatusCode::BAD_REQUEST, service_response.response.status());
}
#[tokio::test]
async fn it_rejects_when_auth_prefix_has_correct_format_but_invalid_jwt() {
let test_harness = build_a_default_test_harness().await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
http::header::AUTHORIZATION,
"Bearer header.payload.signature",
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
let expected_error = graphql::Error::builder()
.message(format!("'{HEADER_TOKEN_TRUNCATED}' is not a valid JWT header: Base64 error: Invalid last symbol 114, offset 5."))
.extension_code("AUTH_ERROR")
.build();
assert_errors_eq_ignoring_id!(response.errors, [expected_error]);
assert_eq!(StatusCode::BAD_REQUEST, service_response.response.status());
}
#[tokio::test]
async fn it_rejects_when_auth_prefix_has_correct_format_and_invalid_jwt() {
let test_harness = build_a_default_test_harness().await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
http::header::AUTHORIZATION,
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5B",
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
let expected_error = graphql::Error::builder()
.message("Cannot decode JWT: Base64 error: Invalid last symbol 66, offset 42.")
.extension_code("AUTH_ERROR")
.build();
assert_errors_eq_ignoring_id!(response.errors, [expected_error]);
assert_eq!(StatusCode::UNAUTHORIZED, service_response.response.status());
}
#[tokio::test]
async fn it_accepts_when_auth_prefix_has_correct_format_and_valid_jwt() {
let test_harness = build_a_default_test_harness().await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
http::header::AUTHORIZATION,
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5A",
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
assert_eq!(response.errors, vec![]);
assert_eq!(StatusCode::OK, service_response.response.status());
let expected_mock_response_data = "response created within the mock";
assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
}
#[tokio::test]
async fn it_accepts_when_auth_prefix_does_not_match_config_and_is_ignored() {
let test_harness = build_a_test_harness(None, None, false, true, false).await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==")
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
assert_eq!(response.errors, vec![]);
assert_eq!(StatusCode::OK, service_response.response.status());
let expected_mock_response_data = "response created within the mock";
assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
}
#[tokio::test]
async fn it_accepts_when_auth_prefix_has_correct_format_multiple_jwks_and_valid_jwt() {
let test_harness = build_a_test_harness(None, None, true, false, false).await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
http::header::AUTHORIZATION,
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5A",
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
assert_eq!(response.errors, vec![]);
assert_eq!(StatusCode::OK, service_response.response.status());
let expected_mock_response_data = "response created within the mock";
assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
}
#[tokio::test]
async fn it_accepts_when_auth_prefix_has_correct_format_and_valid_jwt_custom_auth() {
let test_harness =
build_a_test_harness(Some("SOMETHING".to_string()), None, false, false, false).await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
"SOMETHING",
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5A",
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
assert_eq!(response.errors, vec![]);
assert_eq!(StatusCode::OK, service_response.response.status());
let expected_mock_response_data = "response created within the mock";
assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
}
#[tokio::test]
async fn it_accepts_when_auth_prefix_has_correct_format_and_valid_jwt_custom_prefix() {
let test_harness =
build_a_test_harness(None, Some("SOMETHING".to_string()), false, false, false).await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
http::header::AUTHORIZATION,
"SOMETHING eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5A",
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
assert_eq!(response.errors, vec![]);
assert_eq!(StatusCode::OK, service_response.response.status());
let expected_mock_response_data = "response created within the mock";
assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
}
#[tokio::test]
async fn it_accepts_when_no_auth_prefix_and_valid_jwt_custom_prefix() {
let test_harness = build_a_test_harness(None, Some("".to_string()), false, false, false).await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
http::header::AUTHORIZATION,
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5A",
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
assert_eq!(response.errors, vec![]);
assert_eq!(StatusCode::OK, service_response.response.status());
let expected_mock_response_data = "response created within the mock";
assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
}
#[tokio::test]
async fn it_inserts_success_jwt_status_into_context() {
let test_harness = build_a_test_harness(None, None, false, false, false).await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
http::header::AUTHORIZATION,
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5A",
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let jwt_context = service_response
.context
.get::<_, JwtStatus>(JWT_CONTEXT_KEY)
.expect("deserialization succeeds")
.expect("a context value was set");
match jwt_context {
JwtStatus::Success { r#type, name } => {
assert_eq!(r#type, "header");
assert!(name.eq_ignore_ascii_case("Authorization"));
}
JwtStatus::Failure { .. } => panic!("expected a success but got {jwt_context:?}"),
}
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
assert_eq!(response.errors, vec![]);
assert_eq!(StatusCode::OK, service_response.response.status());
let expected_mock_response_data = "response created within the mock";
assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
let jwt_claims = service_response
.context
.get::<_, serde_json::Value>(APOLLO_AUTHENTICATION_JWT_CLAIMS)
.expect("deserialization succeeds")
.expect("a context value was set");
assert_eq!(
jwt_claims,
serde_json::json!({
"exp": 10_000_000_000i64,
"another claim": "this is another claim"
})
);
}
#[tokio::test]
async fn it_inserts_failure_jwt_status_into_context() {
let test_harness = build_a_test_harness(None, None, false, false, false).await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
http::header::AUTHORIZATION,
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5B",
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let jwt_context = service_response
.context
.get::<_, JwtStatus>(JWT_CONTEXT_KEY)
.expect("deserialization succeeds")
.expect("a context value was set");
let error = jwt_context.error();
match error {
Some(err) => {
assert_eq!(err.code, "CANNOT_DECODE_JWT");
assert_eq!(
err.message,
"Cannot decode JWT: Base64 error: Invalid last symbol 66, offset 42."
);
}
None => panic!("expected an error"),
}
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
let expected_error = graphql::Error::builder()
.message("Cannot decode JWT: Base64 error: Invalid last symbol 66, offset 42.")
.extension_code("AUTH_ERROR")
.build();
assert_errors_eq_ignoring_id!(response.errors, [expected_error]);
assert_eq!(StatusCode::UNAUTHORIZED, service_response.response.status());
let jwt_claims = service_response
.context
.get::<_, serde_json::Value>(APOLLO_AUTHENTICATION_JWT_CLAIMS)
.expect("deserialization succeeds");
assert!(
jwt_claims.is_none(),
"because the JWT was invalid, no claims should be set"
);
}
#[tokio::test]
async fn it_moves_on_after_jwt_errors_when_configured() {
let test_harness = build_a_test_harness(None, None, false, false, true).await;
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
http::header::AUTHORIZATION,
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5B",
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let jwt_context = service_response
.context
.get::<_, JwtStatus>(JWT_CONTEXT_KEY)
.expect("deserialization succeeds")
.expect("a context value was set");
let error = jwt_context.error();
match error {
Some(err) => {
assert_eq!(err.code, "CANNOT_DECODE_JWT");
assert_eq!(
err.message,
"Cannot decode JWT: Base64 error: Invalid last symbol 66, offset 42."
);
}
None => panic!("expected an error"),
}
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
assert_eq!(response.errors, vec![]);
assert_eq!(StatusCode::OK, service_response.response.status());
let jwt_claims = service_response
.context
.get::<_, serde_json::Value>(APOLLO_AUTHENTICATION_JWT_CLAIMS)
.expect("deserialization succeeds");
assert!(
jwt_claims.is_none(),
"because the JWT was invalid, no claims should be set"
);
}
#[tokio::test]
#[should_panic]
async fn it_panics_when_auth_prefix_has_correct_format_but_contains_whitespace() {
let _test_harness =
build_a_test_harness(None, Some("SOMET HING".to_string()), false, false, false).await;
}
#[tokio::test]
#[should_panic]
async fn it_panics_when_auth_prefix_has_correct_format_but_contains_trailing_whitespace() {
let _test_harness =
build_a_test_harness(None, Some("SOMETHING ".to_string()), false, false, false).await;
}
#[tokio::test]
async fn it_extracts_the_token_from_cookies() {
let mut mock_service = test::MockSupergraphService::new();
mock_service.expect_clone().return_once(move || {
println!("cloned to supergraph mock");
let mut mock_service = test::MockSupergraphService::new();
mock_service
.expect_call()
.once()
.returning(move |req: supergraph::Request| {
Ok(supergraph::Response::fake_builder()
.data("response created within the mock")
.context(req.context)
.build()
.unwrap())
});
mock_service
});
let jwks_url = create_an_url("jwks.json");
let config = serde_json::json!({
"authentication": {
"router": {
"jwt" : {
"jwks": [
{
"url": &jwks_url
}
],
"sources": [
{
"type": "cookie",
"name": "authz"
}
],
}
}
},
"rhai": {
"scripts":"tests/fixtures",
"main":"require_authentication.rhai"
}
});
let test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.supergraph_hook(move |_| mock_service.clone().boxed())
.build_router()
.await
.unwrap();
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5A";
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header(
http::header::COOKIE,
format!("a= b; c = d HttpOnly; authz = {token}; e = f"),
)
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
assert_eq!(response.errors, vec![]);
assert_eq!(StatusCode::OK, service_response.response.status());
let expected_mock_response_data = "response created within the mock";
assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
}
#[tokio::test]
async fn it_supports_multiple_sources() {
let mut mock_service = test::MockSupergraphService::new();
mock_service.expect_clone().return_once(move || {
println!("cloned to supergraph mock");
let mut mock_service = test::MockSupergraphService::new();
mock_service
.expect_call()
.once()
.returning(move |req: supergraph::Request| {
Ok(supergraph::Response::fake_builder()
.data("response created within the mock")
.context(req.context)
.build()
.unwrap())
});
mock_service
});
let jwks_url = create_an_url("jwks.json");
let config = serde_json::json!({
"authentication": {
"router": {
"jwt" : {
"jwks": [
{
"url": &jwks_url
}
],
"sources": [
{
"type": "cookie",
"name": "authz"
},
{
"type": "header",
"name": "authz1"
},
{
"type": "header",
"name": "authz2",
"value_prefix": "bear"
}
],
}
}
},
"rhai": {
"scripts":"tests/fixtures",
"main":"require_authentication.rhai"
}
});
let test_harness = crate::TestHarness::builder()
.configuration_json(config)
.unwrap()
.supergraph_hook(move |_| mock_service.clone().boxed())
.build_router()
.await
.unwrap();
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJleHAiOjEwMDAwMDAwMDAwLCJhbm90aGVyIGNsYWltIjoidGhpcyBpcyBhbm90aGVyIGNsYWltIn0.4GrmfxuUST96cs0YUC0DfLAG218m7vn8fO_ENfXnu5A";
let request_with_appropriate_name = supergraph::Request::canned_builder()
.header("Authz2", format!("Bear {token}"))
.build()
.unwrap();
let mut service_response = test_harness
.oneshot(request_with_appropriate_name.try_into().unwrap())
.await
.unwrap();
let response: graphql::Response = serde_json::from_slice(
service_response
.next_response()
.await
.unwrap()
.unwrap()
.to_vec()
.as_slice(),
)
.unwrap();
assert_eq!(response.errors, vec![]);
assert_eq!(StatusCode::OK, service_response.response.status());
let expected_mock_response_data = "response created within the mock";
assert_eq!(expected_mock_response_data, response.data.as_ref().unwrap());
}
async fn build_jwks_search_components() -> JwksManager {
let mut sets = vec![];
let mut urls = vec![];
let jwks_url = create_an_url("jwks.json");
sets.push(jwks_url);
for s_url in &sets {
let url: Url = Url::from_str(s_url).expect("created a valid url");
urls.push(JwksConfig {
url,
issuers: None,
audiences: None,
algorithms: None,
poll_interval: Duration::from_secs(60),
headers: Vec::new(),
});
}
JwksManager::new(urls).await.unwrap()
}
#[tokio::test]
async fn it_finds_key_with_criteria_kid_and_algorithm() {
let jwks_manager = build_jwks_search_components().await;
let criteria = JWTCriteria {
kid: Some("key2".to_string()),
alg: Algorithm::HS256,
};
let (_issuer, _audience, key) = search_jwks(&jwks_manager, &criteria)
.expect("found a key")
.pop()
.expect("list isn't empty");
assert_eq!(KeyAlgorithm::HS256, key.common.key_algorithm.unwrap());
assert_eq!("key2", key.common.key_id.unwrap());
}
#[tokio::test]
async fn it_finds_best_matching_key_with_criteria_algorithm() {
let jwks_manager = build_jwks_search_components().await;
let criteria = JWTCriteria {
kid: None,
alg: Algorithm::HS256,
};
let (_issuer, _audience, key) = search_jwks(&jwks_manager, &criteria)
.expect("found a key")
.pop()
.expect("list isn't empty");
assert_eq!(KeyAlgorithm::HS256, key.common.key_algorithm.unwrap());
assert_eq!("key1", key.common.key_id.unwrap());
}
#[tokio::test]
async fn it_fails_to_find_key_with_criteria_algorithm_not_in_set() {
let jwks_manager = build_jwks_search_components().await;
let criteria = JWTCriteria {
kid: None,
alg: Algorithm::RS512,
};
assert!(search_jwks(&jwks_manager, &criteria).is_none());
}
#[tokio::test]
async fn it_finds_key_with_criteria_algorithm_ec() {
let jwks_manager = build_jwks_search_components().await;
let criteria = JWTCriteria {
kid: None,
alg: Algorithm::ES256,
};
let (_issuer, _audience, key) = search_jwks(&jwks_manager, &criteria)
.expect("found a key")
.pop()
.expect("list isn't empty");
assert_eq!(KeyAlgorithm::ES256, key.common.key_algorithm.unwrap());
assert_eq!(
"afda85e09a320cf748177874592de64d",
key.common.key_id.unwrap()
);
}
#[tokio::test]
async fn it_finds_key_with_criteria_algorithm_rsa() {
let jwks_manager = build_jwks_search_components().await;
let criteria = JWTCriteria {
kid: None,
alg: Algorithm::RS256,
};
let (_issuer, _audience, key) = search_jwks(&jwks_manager, &criteria)
.expect("found a key")
.pop()
.expect("list isn't empty");
assert_eq!(KeyAlgorithm::RS256, key.common.key_algorithm.unwrap());
assert_eq!(
"022516583d56b68faf40260fda72978a",
key.common.key_id.unwrap()
);
}
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
exp: u64,
iss: Option<String>,
aud: Option<String>,
}
fn make_manager(jwk: &Jwk, issuers: Option<Issuers>, audiences: Option<Audiences>) -> JwksManager {
let jwks = JwkSet {
keys: vec![jwk.clone()],
};
let url = Url::from_str("file:///jwks.json").unwrap();
let list = vec![JwksConfig {
url: url.clone(),
issuers,
audiences,
algorithms: None,
poll_interval: Duration::from_secs(60),
headers: Vec::new(),
}];
let map = HashMap::from([(url, jwks); 1]);
JwksManager::new_test(list, map)
}
#[tokio::test]
async fn issuer_check() {
let signing_key = SigningKey::random(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let point = verifying_key.to_encoded_point(false);
let encoding_key = EncodingKey::from_ec_der(&signing_key.to_pkcs8_der().unwrap().to_bytes());
let jwk = Jwk {
common: CommonParameters {
public_key_use: Some(PublicKeyUse::Signature),
key_operations: Some(vec![KeyOperations::Verify]),
key_algorithm: Some(KeyAlgorithm::ES256),
key_id: Some("hello".to_string()),
..Default::default()
},
algorithm: AlgorithmParameters::EllipticCurve(EllipticCurveKeyParameters {
key_type: EllipticCurveKeyType::EC,
curve: EllipticCurve::P256,
x: BASE64_URL_SAFE_NO_PAD.encode(point.x().unwrap()),
y: BASE64_URL_SAFE_NO_PAD.encode(point.y().unwrap()),
}),
};
let manager = make_manager(
&jwk,
Some(HashSet::from(["hello".to_string(), "goodbye".to_string()])),
None,
);
let token = encode(
&jsonwebtoken::Header::new(Algorithm::ES256),
&Claims {
sub: "test".to_string(),
exp: get_current_timestamp(),
iss: None,
aud: None,
},
&encoding_key,
)
.unwrap();
let request = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, format!("Bearer {token}"))
.build()
.unwrap();
let mut config = JWTConf::default();
config.sources.push(Source::Header {
name: super::default_header_name(),
value_prefix: super::default_header_value_prefix(),
});
match authenticate(&config, &manager, request.try_into().unwrap()) {
ControlFlow::Break(res) => {
panic!("unexpected response: {res:?}");
}
ControlFlow::Continue(req) => {
println!("got req with issuer check");
let claims: serde_json::Value = req
.context
.get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
.unwrap()
.unwrap();
println!("claims: {claims:?}");
}
}
let token = encode(
&jsonwebtoken::Header::new(Algorithm::ES256),
&Claims {
sub: "test".to_string(),
exp: get_current_timestamp(),
iss: Some("hello".to_string()),
aud: None,
},
&encoding_key,
)
.unwrap();
let request = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, format!("Bearer {token}"))
.build()
.unwrap();
match authenticate(&config, &manager, request.try_into().unwrap()) {
ControlFlow::Break(res) => {
let response: graphql::Response = serde_json::from_slice(
&router::body::into_bytes(res.response.into_body())
.await
.unwrap(),
)
.unwrap();
assert_response_eq_ignoring_error_id!(response, graphql::Response::builder()
.errors(vec![graphql::Error::builder()
.extension_code("AUTH_ERROR")
.message("Invalid issuer: the token's `iss` was 'hallo', but signed with a key from JWKS configured to only accept from 'hello'")
.build()
]).build());
}
ControlFlow::Continue(req) => {
println!("got req with issuer check");
let claims: serde_json::Value = req
.context
.get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
.unwrap()
.unwrap();
println!("claims: {claims:?}");
}
}
let token = encode(
&jsonwebtoken::Header::new(Algorithm::ES256),
&Claims {
sub: "test".to_string(),
exp: get_current_timestamp(),
iss: Some("AAAA".to_string()),
aud: None,
},
&encoding_key,
)
.unwrap();
let request = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, format!("Bearer {token}"))
.build()
.unwrap();
match authenticate(&config, &manager, request.try_into().unwrap()) {
ControlFlow::Break(res) => {
let response: graphql::Response = serde_json::from_slice(
&router::body::into_bytes(res.response.into_body())
.await
.unwrap(),
)
.unwrap();
assert_response_eq_ignoring_error_id!(response, graphql::Response::builder()
.errors(vec![graphql::Error::builder()
.extension_code("AUTH_ERROR")
.message("Invalid issuer: the token's `iss` was 'AAAA', but signed with a key from JWKS configured to only accept from 'goodbye, hello'")
.build()]).build());
}
ControlFlow::Continue(_) => {
panic!("issuer check should have failed")
}
}
let manager = make_manager(&jwk, None, None);
let token = encode(
&jsonwebtoken::Header::new(Algorithm::ES256),
&Claims {
sub: "test".to_string(),
exp: get_current_timestamp(),
iss: Some("hello".to_string()),
aud: None,
},
&encoding_key,
)
.unwrap();
let request = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, format!("Bearer {token}"))
.build()
.unwrap();
match authenticate(&config, &manager, request.try_into().unwrap()) {
ControlFlow::Break(res) => {
let response: graphql::Response = serde_json::from_slice(
&router::body::into_bytes(res.response.into_body())
.await
.unwrap(),
)
.unwrap();
assert_eq!(response, graphql::Response::builder()
.errors(vec![graphql::Error::builder().extension_code("AUTH_ERROR").message("Invalid issuer: the token's `iss` was 'AAAA', but signed with a key from JWKS configured to only accept from 'hello'").build()]).build());
}
ControlFlow::Continue(req) => {
println!("got req with issuer check");
let claims: serde_json::Value = req
.context
.get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
.unwrap()
.unwrap();
println!("claims: {claims:?}");
}
}
}
#[tokio::test]
async fn audience_check() {
let signing_key = SigningKey::random(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let point = verifying_key.to_encoded_point(false);
let encoding_key = EncodingKey::from_ec_der(&signing_key.to_pkcs8_der().unwrap().to_bytes());
let jwk = Jwk {
common: CommonParameters {
public_key_use: Some(PublicKeyUse::Signature),
key_operations: Some(vec![KeyOperations::Verify]),
key_algorithm: Some(KeyAlgorithm::ES256),
key_id: Some("hello".to_string()),
..Default::default()
},
algorithm: AlgorithmParameters::EllipticCurve(EllipticCurveKeyParameters {
key_type: EllipticCurveKeyType::EC,
curve: EllipticCurve::P256,
x: BASE64_URL_SAFE_NO_PAD.encode(point.x().unwrap()),
y: BASE64_URL_SAFE_NO_PAD.encode(point.y().unwrap()),
}),
};
let manager = make_manager(
&jwk,
None,
Some(HashSet::from(["hello".to_string(), "goodbye".to_string()])),
);
let token = encode(
&jsonwebtoken::Header::new(Algorithm::ES256),
&Claims {
sub: "test".to_string(),
exp: get_current_timestamp(),
aud: None,
iss: None,
},
&encoding_key,
)
.unwrap();
let request = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, format!("Bearer {token}"))
.build()
.unwrap();
let mut config = JWTConf::default();
config.sources.push(Source::Header {
name: super::default_header_name(),
value_prefix: super::default_header_value_prefix(),
});
match authenticate(&config, &manager, request.try_into().unwrap()) {
ControlFlow::Break(res) => {
assert_eq!(res.response.status(), StatusCode::UNAUTHORIZED);
let body = res.response.into_body().collect().await.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body.to_bytes()).unwrap();
let expected_body = serde_json::json!({
"errors": [
{
"message": "Invalid audience: the token's `aud` was 'null', but 'goodbye, hello' was expected",
"extensions": {
"code": "AUTH_ERROR"
}
}
]
});
assert_eq!(body, expected_body);
}
ControlFlow::Continue(_req) => {
panic!("expected a rejection for a lack of audience");
}
}
let token = encode(
&jsonwebtoken::Header::new(Algorithm::ES256),
&Claims {
sub: "test".to_string(),
exp: get_current_timestamp(),
aud: Some("hello".to_string()),
iss: None,
},
&encoding_key,
)
.unwrap();
let request = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, format!("Bearer {token}"))
.build()
.unwrap();
match authenticate(&config, &manager, request.try_into().unwrap()) {
ControlFlow::Break(_res) => {
panic!("expected audience to be valid");
}
ControlFlow::Continue(req) => {
let claims: serde_json::Value = req
.context
.get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
.unwrap()
.unwrap();
assert_eq!(claims["aud"], "hello");
}
}
let token = encode(
&jsonwebtoken::Header::new(Algorithm::ES256),
&Claims {
sub: "test".to_string(),
exp: get_current_timestamp(),
aud: Some("AAAA".to_string()),
iss: None,
},
&encoding_key,
)
.unwrap();
let request = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, format!("Bearer {token}"))
.build()
.unwrap();
match authenticate(&config, &manager, request.try_into().unwrap()) {
ControlFlow::Break(res) => {
let response: graphql::Response = serde_json::from_slice(
&router::body::into_bytes(res.response.into_body())
.await
.unwrap(),
)
.unwrap();
assert_response_eq_ignoring_error_id!(response, graphql::Response::builder()
.errors(vec![
graphql::Error::builder()
.extension_code("AUTH_ERROR")
.message("Invalid audience: the token's `aud` was 'AAAA', but 'goodbye, hello' was expected")
.build()
]).build());
}
ControlFlow::Continue(_) => {
panic!("audience check should have failed")
}
}
let manager = make_manager(&jwk, None, None);
let token = encode(
&jsonwebtoken::Header::new(Algorithm::ES256),
&Claims {
sub: "test".to_string(),
exp: get_current_timestamp(),
aud: Some("hello".to_string()),
iss: None,
},
&encoding_key,
)
.unwrap();
let request = supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, format!("Bearer {token}"))
.build()
.unwrap();
match authenticate(&config, &manager, request.try_into().unwrap()) {
ControlFlow::Break(_res) => {
panic!("expected audience to be valid");
}
ControlFlow::Continue(req) => {
let claims: serde_json::Value = req
.context
.get(APOLLO_AUTHENTICATION_JWT_CLAIMS)
.unwrap()
.unwrap();
assert_eq!(claims["aud"], "hello");
}
}
}
#[tokio::test]
async fn it_rejects_key_with_restricted_algorithm() {
let mut sets = vec![];
let mut urls = vec![];
let jwks_url = create_an_url("jwks.json");
sets.push(jwks_url);
for s_url in &sets {
let url: Url = Url::from_str(s_url).expect("created a valid url");
urls.push(JwksConfig {
url,
issuers: None,
audiences: None,
algorithms: Some(HashSet::from([Algorithm::RS256])),
poll_interval: Duration::from_secs(60),
headers: Vec::new(),
});
}
let jwks_manager = JwksManager::new(urls).await.unwrap();
let criteria = JWTCriteria {
kid: None,
alg: Algorithm::HS256,
};
assert!(search_jwks(&jwks_manager, &criteria).is_none());
}
#[tokio::test]
async fn it_rejects_and_accepts_keys_with_restricted_algorithms_and_unknown_jwks_algorithm() {
let mut sets = vec![];
let mut urls = vec![];
let jwks_url = create_an_url("jwks-unknown-alg.json");
sets.push(jwks_url);
for s_url in &sets {
let url: Url = Url::from_str(s_url).expect("created a valid url");
urls.push(JwksConfig {
url,
issuers: None,
audiences: None,
algorithms: Some(HashSet::from([Algorithm::RS256])),
poll_interval: Duration::from_secs(60),
headers: Vec::new(),
});
}
let jwks_manager = JwksManager::new(urls).await.unwrap();
let criteria = JWTCriteria {
kid: None,
alg: Algorithm::HS256,
};
assert!(search_jwks(&jwks_manager, &criteria).is_none());
let criteria = JWTCriteria {
kid: None,
alg: Algorithm::RS256,
};
assert!(search_jwks(&jwks_manager, &criteria).is_some());
}
#[tokio::test]
async fn it_accepts_key_without_use_or_keyops() {
let mut sets = vec![];
let mut urls = vec![];
let jwks_url = create_an_url("jwks-no-use.json");
sets.push(jwks_url);
for s_url in &sets {
let url: Url = Url::from_str(s_url).expect("created a valid url");
urls.push(JwksConfig {
url,
issuers: None,
audiences: None,
algorithms: None,
poll_interval: Duration::from_secs(60),
headers: Vec::new(),
});
}
let jwks_manager = JwksManager::new(urls).await.unwrap();
let criteria = JWTCriteria {
kid: None,
alg: Algorithm::ES256,
};
assert!(search_jwks(&jwks_manager, &criteria).is_some());
}
#[tokio::test]
async fn it_accepts_elliptic_curve_key_without_alg() {
let mut sets = vec![];
let mut urls = vec![];
let jwks_url = create_an_url("jwks-ec-no-alg.json");
sets.push(jwks_url);
for s_url in &sets {
let url: Url = Url::from_str(s_url).expect("created a valid url");
urls.push(JwksConfig {
url,
issuers: None,
audiences: None,
algorithms: None,
poll_interval: Duration::from_secs(60),
headers: Vec::new(),
});
}
let jwks_manager = JwksManager::new(urls).await.unwrap();
let criteria = JWTCriteria {
kid: None,
alg: Algorithm::ES256,
};
assert!(search_jwks(&jwks_manager, &criteria).is_some());
}
#[tokio::test]
async fn it_accepts_rsa_key_without_alg() {
let mut sets = vec![];
let mut urls = vec![];
let jwks_url = create_an_url("jwks-rsa-no-alg.json");
sets.push(jwks_url);
for s_url in &sets {
let url: Url = Url::from_str(s_url).expect("created a valid url");
urls.push(JwksConfig {
url,
issuers: None,
audiences: None,
algorithms: None,
poll_interval: Duration::from_secs(60),
headers: Vec::new(),
});
}
let jwks_manager = JwksManager::new(urls).await.unwrap();
let criteria = JWTCriteria {
kid: None,
alg: Algorithm::RS384,
};
assert!(search_jwks(&jwks_manager, &criteria).is_some());
}
#[test]
fn test_parse_failure_logs() {
subscriber::with_default(assert_snapshot_subscriber!(), || {
let jwks = parse_jwks(include_str!("testdata/jwks.json")).expect("expected to parse jwks");
assert_yaml_snapshot!(jwks);
});
}
#[tokio::test(flavor = "multi_thread")]
async fn jwks_send_headers() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let socket_addr = listener.local_addr().unwrap();
let got_header = Arc::new(AtomicBool::new(false));
let gh = got_header.clone();
let service = move |headers: HeaderMap| {
println!("got re: {headers:?}");
let gh: Arc<AtomicBool> = gh.clone();
async move {
if headers.get("jwks-authz").and_then(|v| v.to_str().ok()) == Some("user1") {
gh.store(true, Ordering::Release);
}
http::Response::builder()
.header(http::header::CONTENT_TYPE, APPLICATION_JSON.essence_str())
.status(StatusCode::OK)
.version(http::Version::HTTP_11)
.body::<RouterBody>(router::body::from_bytes(include_str!("testdata/jwks.json")))
.unwrap()
}
};
let server = axum::serve(listener, service.into_make_service());
tokio::task::spawn(async { server.await.unwrap() });
let url = Url::parse(&format!("http://{socket_addr}/")).unwrap();
let _jwks_manager = JwksManager::new(vec![JwksConfig {
url,
issuers: None,
audiences: None,
algorithms: Some(HashSet::from([Algorithm::RS256])),
poll_interval: Duration::from_secs(60),
headers: vec![Header {
name: HeaderName::from_static("jwks-authz"),
value: HeaderValue::from_static("user1"),
}],
}])
.await
.unwrap();
assert!(got_header.load(Ordering::Acquire));
}
mod common {
use base64::Engine as _;
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use jsonwebtoken::Algorithm;
use jsonwebtoken::EncodingKey;
use jsonwebtoken::encode;
use jsonwebtoken::jwk::AlgorithmParameters;
use jsonwebtoken::jwk::CommonParameters;
use jsonwebtoken::jwk::EllipticCurve;
use jsonwebtoken::jwk::EllipticCurveKeyParameters;
use jsonwebtoken::jwk::EllipticCurveKeyType;
use jsonwebtoken::jwk::Jwk;
use jsonwebtoken::jwk::KeyAlgorithm;
use jsonwebtoken::jwk::KeyOperations;
use jsonwebtoken::jwk::PublicKeyUse;
use p256::ecdsa::SigningKey;
use p256::pkcs8::EncodePrivateKey;
use crate::plugins::authentication::JWTConf;
use crate::plugins::authentication::Source;
use crate::plugins::authentication::default_header_name;
use crate::plugins::authentication::default_header_value_prefix;
use crate::services::router;
use crate::services::supergraph;
pub(super) fn jwk(signing_key: &SigningKey) -> Jwk {
let verifying_key = signing_key.verifying_key();
let point = verifying_key.to_encoded_point(false);
Jwk {
common: CommonParameters {
public_key_use: Some(PublicKeyUse::Signature),
key_operations: Some(vec![KeyOperations::Verify]),
key_algorithm: Some(KeyAlgorithm::ES256),
key_id: Some("hello".to_string()),
..Default::default()
},
algorithm: AlgorithmParameters::EllipticCurve(EllipticCurveKeyParameters {
key_type: EllipticCurveKeyType::EC,
curve: EllipticCurve::P256,
x: BASE64_URL_SAFE_NO_PAD.encode(point.x().unwrap()),
y: BASE64_URL_SAFE_NO_PAD.encode(point.y().unwrap()),
}),
}
}
fn encoding_key(signing_key: &SigningKey) -> EncodingKey {
EncodingKey::from_ec_der(&signing_key.to_pkcs8_der().unwrap().to_bytes())
}
pub(super) fn jwt_conf_with_header_source() -> JWTConf {
let mut config = JWTConf::default();
config.sources.push(Source::Header {
name: default_header_name(),
value_prefix: default_header_value_prefix(),
});
config
}
pub(super) fn build_request_with_header_token(
signing_key: SigningKey,
token_claims: serde_json::Value,
) -> router::Request {
let token = encode(
&jsonwebtoken::Header::new(Algorithm::ES256),
&token_claims,
&encoding_key(&signing_key),
)
.unwrap();
supergraph::Request::canned_builder()
.header(http::header::AUTHORIZATION, format!("Bearer {token}"))
.build()
.unwrap()
.try_into()
.unwrap()
}
}
mod audience_validation {
use std::ops::ControlFlow;
use http::StatusCode;
use jsonwebtoken::get_current_timestamp;
use p256::ecdsa::SigningKey;
use p256::ecdsa::signature::rand_core::OsRng;
use super::common::build_request_with_header_token;
use super::common::jwk;
use super::common::jwt_conf_with_header_source;
use super::make_manager;
use crate::plugins::authentication::authenticate;
use crate::services::router;
fn authenticate_request(
manager_aud: &[&str],
token_aud: serde_json::Value,
) -> ControlFlow<router::Response, router::Request> {
let signing_key = SigningKey::random(&mut OsRng);
let manager_audiences = if manager_aud.is_empty() {
None
} else {
Some(manager_aud.iter().map(ToString::to_string).collect())
};
let manager = make_manager(&jwk(&signing_key), None, manager_audiences);
let token_claims = serde_json::json!({
"sub": "test",
"exp": get_current_timestamp(),
"aud": token_aud
});
let request = build_request_with_header_token(signing_key, token_claims);
authenticate(&jwt_conf_with_header_source(), &manager, request)
}
#[rstest::rstest]
#[case::multiple_auds(&["hello", "world"], serde_json::json!(["hello", "world"]))]
#[case::multiple_with_array_accepted(&["hello", "world", "goodbye"], serde_json::json!(["hello"]))]
#[case::multiple_with_str_accepted(&["hello", "world"], serde_json::json!("hello"))]
#[case::multiple_with_str_accepted(&["hello", "world"], serde_json::json!("world"))]
#[case::single_with_array_accepted_any_of(&["hello"], serde_json::json!(["hello", "world"]))]
#[case::single_with_array_accepted(&["hello"], serde_json::json!(["hello"]))]
#[case::single_aud(&["hello"], serde_json::json!("hello"))]
#[case::multiple_with_single_intersection(&["hello", "world", "goodbye"], serde_json::json!(["hola", "bonjour", "hello"]))]
#[case::null_mgr_aud(&[], serde_json::Value::Null)]
#[case::null_mgr_aud_with_token_aud(&[], serde_json::json!("hello"))]
fn it_accepts_jwt(#[case] manager_aud: &[&str], #[case] token_aud: serde_json::Value) {
match authenticate_request(manager_aud, token_aud.clone()) {
ControlFlow::Continue(_) => {}
ControlFlow::Break(response) => {
panic!(
"Request should be permitted: manager_aud = {manager_aud:?}, token_aud = {token_aud}, response = {response:?}"
);
}
}
}
#[rstest::rstest]
#[case::missing_token_aud(&["hello", "world"], serde_json::json!(""))]
#[case::mismatched_single_aud(&["hello"], serde_json::json!("world"))]
#[case::mismatched_single_aud_array(&["hello"], serde_json::json!(["world", "planet"]))]
#[case::mismatched_multiple_aud(&["hello", "world"], serde_json::json!(["goodbye", "planet"]))]
fn it_rejects_jwt(#[case] manager_aud: &[&str], #[case] token_aud: serde_json::Value) {
match authenticate_request(manager_aud, token_aud.clone()) {
ControlFlow::Continue(_) => {
panic!(
"Request should be rejected: manager_aud = {manager_aud:?}, token_aud = {token_aud}"
);
}
ControlFlow::Break(response) => {
assert_eq!(
response.response.status(),
StatusCode::UNAUTHORIZED,
"manager_aud = {manager_aud:?}, token_aud = {token_aud}, response = {response:?}"
);
}
}
}
}
mod issuer_validation {
use std::ops::ControlFlow;
use http::StatusCode;
use jsonwebtoken::get_current_timestamp;
use p256::ecdsa::SigningKey;
use p256::ecdsa::signature::rand_core::OsRng;
use super::common::build_request_with_header_token;
use super::common::jwk;
use super::common::jwt_conf_with_header_source;
use super::make_manager;
use crate::plugins::authentication::authenticate;
use crate::services::router;
fn authenticate_request(
manager_iss: &[&str],
token_iss: serde_json::Value,
) -> ControlFlow<router::Response, router::Request> {
let signing_key = SigningKey::random(&mut OsRng);
let manager_issuers = if manager_iss.is_empty() {
None
} else {
Some(manager_iss.iter().map(ToString::to_string).collect())
};
let manager = make_manager(&jwk(&signing_key), manager_issuers, None);
let token_claims = serde_json::json!({
"sub": "test",
"exp": get_current_timestamp(),
"iss": token_iss
});
let request = build_request_with_header_token(signing_key, token_claims);
authenticate(&jwt_conf_with_header_source(), &manager, request)
}
#[rstest::rstest]
#[case::multiple_with_str_accepted(&["hello", "world"], serde_json::json!("hello"))]
#[case::multiple_with_str_accepted(&["hello", "world"], serde_json::json!("world"))]
#[case::single_iss(&["hello"], serde_json::json!("hello"))]
#[case::null_mgr_iss_with_token_iss(&[], serde_json::json!("hello"))]
#[case::null_mgr_iss_with_empty_token_iss(&[], serde_json::Value::Null)]
#[case::empty_token_iss(&["hello", "world"], serde_json::Value::Null)]
#[case::empty_token_iss(&["hello"], serde_json::Value::Null)]
fn it_accepts_jwt(#[case] manager_iss: &[&str], #[case] token_iss: serde_json::Value) {
match authenticate_request(manager_iss, token_iss.clone()) {
ControlFlow::Continue(_) => {}
ControlFlow::Break(response) => {
panic!(
"Request should be permitted: manager_iss = {manager_iss:?}, token_iss = {token_iss}, response = {response:?}"
);
}
}
}
#[rstest::rstest]
#[case::multiple_iss(&["hello", "world"], serde_json::json!(["hello", "world"]))]
#[case::multiple_with_array_accepted(&["hello", "world", "goodbye"], serde_json::json!(["hello"]))]
#[case::single_with_array_accepted_any_of(&["hello"], serde_json::json!(["hello", "world"]))]
#[case::single_with_array_accepted(&["hello"], serde_json::json!(["hello"]))]
#[case::missing_token_iss(&["hello", "world"], serde_json::json!(""))]
#[case::mismatched_single_iss(&["hello"], serde_json::json!("world"))]
#[case::mismatched_single_iss_array(&["hello"], serde_json::json!(["world"]))]
#[case::mismatched_single_iss_array(&["hello"], serde_json::json!(["world", "planet"]))]
#[case::mismatched_multiple_iss(&["hello", "world"], serde_json::json!(["goodbye", "planet"]))]
fn it_rejects_jwt(#[case] manager_iss: &[&str], #[case] token_iss: serde_json::Value) {
match authenticate_request(manager_iss, token_iss.clone()) {
ControlFlow::Continue(_) => {
panic!(
"Request should be rejected: manager_iss = {manager_iss:?}, token_iss = {token_iss}"
);
}
ControlFlow::Break(response) => {
assert_eq!(
response.response.status(),
StatusCode::INTERNAL_SERVER_ERROR,
"manager_iss = {manager_iss:?}, token_iss = {token_iss}, response = {response:?}"
);
}
}
}
}