mod support;
use oauth_as::server::UserApproval;
use std::mem::size_of;
use std::panic::{catch_unwind, AssertUnwindSafe};
use oauth_as::{
AuthorizationRequest, AuthorizationServer, AuthorizationServerMetadata, Client, ClientAuth,
ClientId, ErrorResponse, GrantType, IssuedToken, MemoryStorage, ScopeSet, ServerConfig,
TokenRequest,
};
use support::alloc::{measure, CountingAllocator, Delta, TEST_LOCK};
#[global_allocator]
static ALLOC: CountingAllocator = CountingAllocator;
#[test]
fn zero_cost_efficiency_gates() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let gates: &[(&str, fn())] = &[
(
"no_global_statics_or_lazy_singletons_in_the_library_source",
no_global_statics_or_lazy_singletons_in_the_library_source,
),
(
"no_lazy_init_dependency_is_declared",
no_lazy_init_dependency_is_declared,
),
(
"code_challenge_s256_allocates_only_its_return_value",
code_challenge_s256_allocates_only_its_return_value,
),
(
"scope_set_parse_is_linear_in_token_count",
scope_set_parse_is_linear_in_token_count,
),
(
"metadata_derivation_allocates_a_bounded_small_amount",
metadata_derivation_allocates_a_bounded_small_amount,
),
(
"authorization_request_from_borrowed_pairs_allocates_nothing",
authorization_request_from_borrowed_pairs_allocates_nothing,
),
(
"authorization_response_location_allocates_exactly_once_at_the_exact_size",
authorization_response_location_allocates_exactly_once_at_the_exact_size,
),
(
"device_authorization_hot_path_allocation_bound",
device_authorization_hot_path_allocation_bound,
),
(
"device_token_pending_poll_hot_path_allocation_bound",
device_token_pending_poll_hot_path_allocation_bound,
),
(
"authorization_code_redemption_hot_path_allocation_bound",
authorization_code_redemption_hot_path_allocation_bound,
),
(
"refresh_rotation_hot_path_allocation_bound",
refresh_rotation_hot_path_allocation_bound,
),
(
"introspection_hot_path_allocation_bound",
introspection_hot_path_allocation_bound,
),
(
"a_refusal_built_from_a_literal_allocates_nothing",
a_refusal_built_from_a_literal_allocates_nothing,
),
(
"refused_token_request_allocation_bound",
refused_token_request_allocation_bound,
),
(
"metadata_serialization_allocation_bound",
metadata_serialization_allocation_bound,
),
(
"core_public_types_stay_within_their_size_budget",
core_public_types_stay_within_their_size_budget,
),
(
"token_future_stays_under_tokios_debug_boxing_threshold",
token_future_stays_under_tokios_debug_boxing_threshold,
),
];
let mut failures = Vec::new();
for (name, gate) in gates {
if let Err(cause) = catch_unwind(AssertUnwindSafe(gate)) {
let msg = cause
.downcast_ref::<String>()
.cloned()
.or_else(|| cause.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_else(|| "panicked with a non-string payload".to_string());
failures.push(format!("{name}: {msg}"));
}
}
assert!(
failures.is_empty(),
"{} of {} efficiency gate(s) failed:\n{}",
failures.len(),
gates.len(),
failures.join("\n")
);
}
fn no_global_statics_or_lazy_singletons_in_the_library_source() {
let src_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src");
let mut offending = Vec::new();
for entry in std::fs::read_dir(src_dir).expect("crate src/ must exist") {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let text = std::fs::read_to_string(&path).unwrap();
for (lineno, line) in text.lines().enumerate() {
let trimmed = line.trim_start();
let trimmed = trimmed
.strip_prefix("pub(crate)")
.or_else(|| trimmed.strip_prefix("pub"))
.unwrap_or(trimmed)
.trim_start();
if trimmed.starts_with("static ") {
offending.push(format!("{}:{}: {line}", path.display(), lineno + 1));
}
}
}
assert!(
offending.is_empty(),
"the crate doc promises NO global statics and NO lazy singletons; found:\n{}",
offending.join("\n")
);
}
fn no_lazy_init_dependency_is_declared() {
let manifest = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml");
let text = std::fs::read_to_string(manifest).unwrap();
for forbidden in ["lazy_static", "once_cell"] {
assert!(
!text.contains(forbidden),
"found a dependency on {forbidden}, which exists to build lazy singletons"
);
}
}
fn code_challenge_s256_allocates_only_its_return_value() {
let (challenge, d) = measure(|| oauth_as::pkce::code_challenge_s256(support::RFC7636_VERIFIER));
assert_eq!(challenge.len(), 43);
assert!(
d.allocs <= 2,
"code_challenge_s256 should allocate at most its own 43-byte String, got {d:?}"
);
assert!(
d.bytes <= 128,
"unexpectedly large allocation traffic: {d:?}"
);
}
fn scope_set_parse_is_linear_in_token_count() {
let (set, d) = measure(|| ScopeSet::parse("read write admin").unwrap());
assert_eq!(set.len(), 3);
assert!(
d.allocs <= 3 * 2 + 4,
"ScopeSet::parse(3 tokens) should stay near linear in token count, got {d:?}"
);
}
fn metadata_derivation_allocates_a_bounded_small_amount() {
let cfg = ServerConfig::new("https://as.example", "https://as.example/device");
let (_doc, d) = measure(|| AuthorizationServerMetadata::from_config(&cfg));
assert!(
d.allocs <= 60,
"metadata derivation should stay a small fixed cost, got {d:?}"
);
}
fn authorization_request_from_borrowed_pairs_allocates_nothing() {
let pairs = [
("response_type", "code"),
("client_id", "public-app"),
("redirect_uri", "https://app.example/cb"),
("scope", "read write"),
("state", "opaque-state"),
(
"code_challenge",
"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
),
("code_challenge_method", "S256"),
];
let (req, d) = measure(|| AuthorizationRequest::from_pairs(pairs));
assert_eq!(req.client_id.as_deref(), Some("public-app"));
assert_eq!(
d,
Delta::default(),
"AuthorizationRequest::from_pairs on borrowed &str must not allocate at all, got {d:?}"
);
}
fn authorization_response_location_allocates_exactly_once_at_the_exact_size() {
let code = "&=#?";
let state = " /+";
let iss = "^|`";
let redirect_uri = "https://app.example/cb";
let response = oauth_as::AuthorizationResponse {
code: code.to_string(),
state: Some(state.to_string()),
iss: iss.to_string(),
};
let (location, d) = measure(|| response.location(redirect_uri));
let exact_len =
redirect_uri.len() + 6 + code.len() * 3 + 7 + state.len() * 3 + 5 + iss.len() * 3;
assert_eq!(
location.len(),
exact_len,
"test setup: every input byte must percent-encode to three characters, got {location}"
);
assert_eq!(
d,
Delta {
allocs: 1,
deallocs: 0,
bytes: exact_len,
freed: 0
},
"location() must allocate its buffer once, at exactly the size the output needs: {d:?}"
);
}
fn current_thread_runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.build()
.expect("current-thread runtime")
}
fn device_test_client() -> Client {
Client {
client_id: ClientId::new("device-client"),
auth: ClientAuth::Public,
grant_types: vec![GrantType::DeviceCode, GrantType::RefreshToken],
redirect_uris: vec![],
allowed_scopes: ScopeSet::parse("read write").unwrap(),
default_scopes: ScopeSet::parse("read").unwrap(),
name: None,
registration: None,
}
}
fn device_authorization_hot_path_allocation_bound() {
let rt = current_thread_runtime();
let srv = rt.block_on(async {
let cfg = ServerConfig::new("https://as.example", "https://as.example/device");
let srv = AuthorizationServer::new(cfg, MemoryStorage::new());
srv.register_client(device_test_client()).await.unwrap();
srv
});
let (auth, d) = measure(|| {
rt.block_on(srv.device_authorization(&ClientId::new("device-client"), None, None))
});
let auth = auth.unwrap();
assert!(!auth.device_code.is_empty());
assert!(
d.allocs <= 26,
"device_authorization allocation count regressed: {d:?}"
);
assert!(
d.bytes <= 2560,
"device_authorization allocation bytes regressed: {d:?}"
);
}
fn device_token_pending_poll_hot_path_allocation_bound() {
let rt = current_thread_runtime();
let (srv, device_code) = rt.block_on(async {
let cfg = ServerConfig::new("https://as.example", "https://as.example/device");
let srv = AuthorizationServer::new(cfg, MemoryStorage::new());
srv.register_client(device_test_client()).await.unwrap();
let auth = srv
.device_authorization(&ClientId::new("device-client"), None, None)
.await
.unwrap();
(srv, auth.device_code)
});
let (result, d) = measure(|| {
rt.block_on(srv.token(TokenRequest::DeviceCode {
client_id: ClientId::new("device-client"),
client_secret: None,
device_code: device_code.clone(),
}))
});
assert_eq!(
result.unwrap_err().error,
oauth_as::ErrorCode::AuthorizationPending
);
assert!(
d.allocs <= 17,
"device_token(authorization_pending) allocation count regressed: {d:?}"
);
assert!(
d.bytes <= 1280,
"device_token(authorization_pending) allocation bytes regressed: {d:?}"
);
}
fn code_test_client() -> Client {
Client {
client_id: ClientId::new("public-app"),
auth: ClientAuth::Public,
grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
redirect_uris: vec!["https://app.example/cb".to_string()],
allowed_scopes: ScopeSet::parse("read write").unwrap(),
default_scopes: ScopeSet::parse("read").unwrap(),
name: None,
registration: None,
}
}
fn authorization_code_redemption_hot_path_allocation_bound() {
let rt = current_thread_runtime();
let verifier = support::RFC7636_VERIFIER;
let (srv, code) = rt.block_on(async {
let cfg = ServerConfig::new("https://as.example", "https://as.example/device");
let srv = AuthorizationServer::new(cfg, MemoryStorage::new());
srv.register_client(code_test_client()).await.unwrap();
let challenge = oauth_as::pkce::code_challenge_s256(verifier);
let req = AuthorizationRequest::from_pairs([
("response_type", "code"),
("client_id", "public-app"),
("redirect_uri", "https://app.example/cb"),
("scope", "read write"),
("state", "s"),
("code_challenge", challenge.as_str()),
("code_challenge_method", "S256"),
]);
let validated = srv.validate_authorization_request(&req).await.unwrap();
let response = srv
.issue_authorization_code(UserApproval::granted(&validated, "user-1"))
.await
.unwrap();
(srv, response.code)
});
let (token, d) = measure(|| {
rt.block_on(srv.token(TokenRequest::AuthorizationCode {
client_id: ClientId::new("public-app"),
client_secret: None,
code: code.clone(),
redirect_uri: Some("https://app.example/cb".to_string()),
code_verifier: Some(verifier.to_string()),
}))
});
let token = token.unwrap();
assert!(token.refresh_token.is_some());
assert!(
d.allocs <= 52,
"authorization_code redemption allocation count regressed: {d:?}"
);
assert!(
d.bytes <= 4608,
"authorization_code redemption allocation bytes regressed: {d:?}"
);
}
fn refresh_rotation_hot_path_allocation_bound() {
let rt = current_thread_runtime();
let verifier = support::RFC7636_VERIFIER;
let (srv, refresh_token) = rt.block_on(async {
let cfg = ServerConfig::new("https://as.example", "https://as.example/device");
let srv = AuthorizationServer::new(cfg, MemoryStorage::new());
srv.register_client(code_test_client()).await.unwrap();
let challenge = oauth_as::pkce::code_challenge_s256(verifier);
let req = AuthorizationRequest::from_pairs([
("response_type", "code"),
("client_id", "public-app"),
("redirect_uri", "https://app.example/cb"),
("scope", "read write"),
("state", "s"),
("code_challenge", challenge.as_str()),
("code_challenge_method", "S256"),
]);
let validated = srv.validate_authorization_request(&req).await.unwrap();
let response = srv
.issue_authorization_code(UserApproval::granted(&validated, "user-1"))
.await
.unwrap();
let token = srv
.token(TokenRequest::AuthorizationCode {
client_id: ClientId::new("public-app"),
client_secret: None,
code: response.code,
redirect_uri: Some("https://app.example/cb".to_string()),
code_verifier: Some(verifier.to_string()),
})
.await
.unwrap();
(srv, token.refresh_token.unwrap())
});
let (result, d) = measure(|| {
rt.block_on(srv.token(TokenRequest::RefreshToken {
client_id: ClientId::new("public-app"),
client_secret: None,
refresh_token: refresh_token.clone(),
scope: None,
}))
});
let result = result.unwrap();
assert!(result.refresh_token.is_some());
assert!(
d.allocs <= 44,
"refresh rotation allocation count regressed: {d:?}"
);
assert!(
d.bytes <= 3840,
"refresh rotation allocation bytes regressed: {d:?}"
);
}
fn confidential_test_client() -> Client {
Client {
client_id: ClientId::new("confidential-app"),
auth: ClientAuth::ConfidentialSecret {
secret: "s3cret".to_string(),
},
grant_types: vec![GrantType::ClientCredentials],
redirect_uris: vec![],
allowed_scopes: ScopeSet::parse("read write").unwrap(),
default_scopes: ScopeSet::parse("read").unwrap(),
name: None,
registration: None,
}
}
fn introspection_hot_path_allocation_bound() {
let rt = current_thread_runtime();
let (srv, token) = rt.block_on(async {
let cfg = ServerConfig::new("https://as.example", "https://as.example/device");
let srv = AuthorizationServer::new(cfg, MemoryStorage::new());
srv.register_client(confidential_test_client())
.await
.unwrap();
let issued = srv
.token(TokenRequest::ClientCredentials {
client_id: ClientId::new("confidential-app"),
client_secret: Some("s3cret".to_string()),
scope: None,
})
.await
.unwrap();
(srv, issued.access_token)
});
let (response, d) = measure(|| {
rt.block_on(srv.introspection_response(
&ClientId::new("confidential-app"),
Some("s3cret"),
&token,
))
});
assert!(response.unwrap().active, "the token must introspect active");
assert!(
d.allocs <= 8,
"introspection allocation count regressed: {d:?}"
);
assert!(
d.bytes <= 256,
"introspection allocation bytes regressed: {d:?}"
);
}
fn a_refusal_built_from_a_literal_allocates_nothing() {
let (err, d) = measure(|| {
ErrorResponse::new(oauth_as::ErrorCode::InvalidRequest)
.with_description("this server does not offer pushed authorization requests")
});
assert!(err.error_description.is_some());
assert_eq!(
d,
Delta::default(),
"a refusal described by a string constant must not copy it onto the heap: {d:?}"
);
}
fn refused_token_request_allocation_bound() {
let rt = current_thread_runtime();
let srv = rt.block_on(async {
let cfg = ServerConfig::new("https://as.example", "https://as.example/device");
let srv = AuthorizationServer::new(cfg, MemoryStorage::new());
srv.register_client(Client {
client_id: ClientId::new("public-app"),
auth: ClientAuth::Public,
grant_types: vec![GrantType::ClientCredentials],
redirect_uris: vec![],
allowed_scopes: ScopeSet::parse("read write").unwrap(),
default_scopes: ScopeSet::parse("read").unwrap(),
name: None,
registration: None,
})
.await
.unwrap();
srv
});
let request = TokenRequest::ClientCredentials {
client_id: ClientId::new("public-app"),
client_secret: None,
scope: None,
};
let (result, d) = measure(|| rt.block_on(srv.token(request)));
assert_eq!(
result.unwrap_err().error,
oauth_as::ErrorCode::InvalidClient
);
assert_eq!(
d.allocs, 0,
"a refused token request must allocate NOTHING, on every feature set: {d:?}"
);
}
fn metadata_serialization_allocation_bound() {
let cfg = ServerConfig::new("https://as.example", "https://as.example/device");
let doc = AuthorizationServerMetadata::from_config(&cfg);
let (json, d) = measure(|| serde_json::to_string(&doc).unwrap());
assert!(json.contains("\"issuer\""));
assert!(
d.allocs <= 8,
"metadata serialization allocation count regressed: {d:?}"
);
}
fn core_public_types_stay_within_their_size_budget() {
#[cfg(feature = "par")]
const PAR: usize = 8 + 48;
#[cfg(not(feature = "par"))]
const PAR: usize = 0;
#[cfg(feature = "jar")]
const JAR: usize = 8;
#[cfg(not(feature = "jar"))]
const JAR: usize = 0;
#[cfg(feature = "rar")]
const RAR: usize = 24;
#[cfg(not(feature = "rar"))]
const RAR: usize = 0;
#[cfg(any(feature = "client-assertion", feature = "dpop"))]
const TOKEN_ENDPOINT: usize = 16;
#[cfg(not(any(feature = "client-assertion", feature = "dpop")))]
const TOKEN_ENDPOINT: usize = 0;
const REVOCATION_BARRIERS: usize = 48;
const RESOURCE_SERVERS: usize = 16;
#[cfg(feature = "cimd")]
const CIMD: usize = 8;
#[cfg(not(feature = "cimd"))]
const CIMD: usize = 0;
let server_budget =
832 + REVOCATION_BARRIERS + RESOURCE_SERVERS + PAR + JAR + RAR + TOKEN_ENDPOINT + CIMD;
assert!(
size_of::<AuthorizationServer<MemoryStorage>>() <= server_budget,
"AuthorizationServer<MemoryStorage> grew past its size budget: {}",
size_of::<AuthorizationServer<MemoryStorage>>()
);
assert!(
size_of::<ServerConfig>() <= 448 + RESOURCE_SERVERS + RAR + CIMD,
"ServerConfig grew past its size budget: {}",
size_of::<ServerConfig>()
);
assert!(
size_of::<TokenRequest>() <= 160,
"TokenRequest grew past its size budget: {}",
size_of::<TokenRequest>()
);
assert!(
size_of::<ErrorResponse>() <= 80,
"ErrorResponse grew past its size budget: {}",
size_of::<ErrorResponse>()
);
let issued_token_budget = 192
+ if cfg!(feature = "dpop") { 16 } else { 0 }
+ if cfg!(feature = "mtls") { 8 } else { 0 }
+ if cfg!(feature = "rar") { 24 } else { 0 }
+ if cfg!(feature = "consent") { 8 } else { 0 }
+ if cfg!(feature = "token-exchange") {
8
} else {
0
};
assert!(
size_of::<IssuedToken>() <= issued_token_budget,
"IssuedToken grew past its size budget: {}",
size_of::<IssuedToken>()
);
assert!(
size_of::<Client>() <= 232,
"Client grew past its size budget: {}",
size_of::<Client>()
);
assert!(
size_of::<ClientAuth>() <= 56,
"ClientAuth grew past its size budget, which every Client pays: {}",
size_of::<ClientAuth>()
);
assert!(
size_of::<oauth_as::DeviceGrant>() <= 208,
"DeviceGrant grew past its size budget: {}",
size_of::<oauth_as::DeviceGrant>()
);
let refresh_budget = 192
+ if cfg!(feature = "rar") { 24 } else { 0 }
+ if cfg!(feature = "dpop") { 16 } else { 0 }
+ if cfg!(feature = "mtls") { 8 } else { 0 }
+ if cfg!(feature = "consent") { 8 } else { 0 };
assert!(
size_of::<oauth_as::RefreshTokenRecord>() <= refresh_budget,
"RefreshTokenRecord grew past its size budget: {}",
size_of::<oauth_as::RefreshTokenRecord>()
);
let code_budget = 264
+ if cfg!(feature = "rar") { 24 } else { 0 }
+ if cfg!(feature = "consent") { 8 } else { 0 };
assert!(
size_of::<oauth_as::AuthorizationCodeRecord>() <= code_budget,
"AuthorizationCodeRecord grew past its size budget: {}",
size_of::<oauth_as::AuthorizationCodeRecord>()
);
#[cfg(feature = "consent")]
assert!(
size_of::<oauth_as::consent::ConsentRecord>() <= 144,
"ConsentRecord grew past its size budget: {}",
size_of::<oauth_as::consent::ConsentRecord>()
);
#[cfg(feature = "par")]
{
let pushed_budget = 264
+ if cfg!(feature = "rar") { 24 } else { 0 }
+ if cfg!(feature = "consent") { 48 } else { 0 };
assert!(
size_of::<oauth_as::par::PushedAuthorizationRequest>() <= pushed_budget,
"PushedAuthorizationRequest grew past its size budget: {}",
size_of::<oauth_as::par::PushedAuthorizationRequest>()
);
}
}
fn token_future_stays_under_tokios_debug_boxing_threshold() {
let cfg = ServerConfig::new("https://as.example", "https://as.example/device");
let srv = AuthorizationServer::new(cfg, MemoryStorage::new());
let future = srv.token(TokenRequest::AuthorizationCode {
client_id: ClientId::new("public-app"),
client_secret: None,
code: String::new(),
redirect_uri: None,
code_verifier: None,
});
let size = std::mem::size_of_val(&future);
drop(future);
assert!(
size <= 2048,
"the token future is {size} bytes, past tokio's 2048-byte debug boxing threshold: every \
token request now pays a 2 KB allocation. Restructure the path (do not raise this bound, \
which is tokio's and not this crate's)"
);
}