#![cfg(feature = "http")]
use std::sync::Arc;
use oauth_as::client::{Client, ClientAuth, ClientId};
use oauth_as::grant::GrantType;
use oauth_as::http::{ApprovalDecision, Body, MAX_FORM_PARAMETERS};
use oauth_as::scope::ScopeSet;
use oauth_as::server::{AuthorizationServer, ServerConfig, SystemClock};
use oauth_as::store::MemoryStorage;
const SECRET: &str = "a-high-entropy-registered-client-secret";
async fn service() -> oauth_as::http::AuthorizationService<MemoryStorage, SystemClock> {
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("confidential-app"),
auth: ClientAuth::ConfidentialSecret {
secret: SECRET.to_string(),
},
grant_types: vec![GrantType::ClientCredentials, GrantType::AuthorizationCode],
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,
})
.await
.unwrap();
oauth_as::http::ServiceBuilder::new(Arc::new(srv))
.with_subject_resolver(|_headers| Some("user-1".to_string()))
.with_approval_resolver(|_request| ApprovalDecision::Approve)
.build()
.expect("service")
}
fn with_extras(base: &str, extra: usize) -> String {
let mut body = base.to_string();
for i in 0..extra {
body.push_str(&format!("&unknown_parameter_{i}=some%20encoded%20value"));
}
body
}
fn post(uri: &str, body: String) -> http::Request<Body> {
http::Request::builder()
.method("POST")
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded")
.body(Body::from(body))
.expect("a well-formed request")
}
#[tokio::test]
async fn a_token_request_over_the_parameter_cap_is_refused() {
let service = service().await;
let base =
format!("grant_type=client_credentials&client_id=confidential-app&client_secret={SECRET}");
let body = with_extras(&base, MAX_FORM_PARAMETERS);
let response = service.handle(post("/token", body)).await;
assert_eq!(
response.status(),
http::StatusCode::PAYLOAD_TOO_LARGE,
"a request carrying more than MAX_FORM_PARAMETERS parameters must be refused before it \
is decoded"
);
}
#[tokio::test]
async fn a_token_request_at_the_parameter_cap_is_answered() {
let service = service().await;
let base =
format!("grant_type=client_credentials&client_id=confidential-app&client_secret={SECRET}");
let body = with_extras(&base, MAX_FORM_PARAMETERS - 3);
let response = service.handle(post("/token", body)).await;
assert_eq!(
response.status(),
http::StatusCode::OK,
"a request at the cap must still be served: the cap is a ceiling on abuse, not a change \
to what a conforming client may send"
);
}
#[tokio::test]
async fn a_token_request_one_past_the_parameter_cap_is_refused() {
let service = service().await;
let base =
format!("grant_type=client_credentials&client_id=confidential-app&client_secret={SECRET}");
let body = with_extras(&base, MAX_FORM_PARAMETERS - 2);
let response = service.handle(post("/token", body)).await;
assert_eq!(
response.status(),
http::StatusCode::PAYLOAD_TOO_LARGE,
"the cap is `separators >= MAX_FORM_PARAMETERS`, so this is the smallest request it \
refuses; a test that only ever sends two past the cap cannot see it move by one"
);
}
#[tokio::test]
async fn an_authorization_request_over_the_parameter_cap_is_refused() {
let service = service().await;
let base = "response_type=code&client_id=confidential-app\
&redirect_uri=https%3A%2F%2Fapp.example%2Fcb&scope=read\
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\
&code_challenge_method=S256";
let query = with_extras(base, MAX_FORM_PARAMETERS);
let request = http::Request::builder()
.method("GET")
.uri(format!("/authorize?{query}"))
.body(Body::empty())
.expect("a well-formed request");
let response = service.handle(request).await;
assert_eq!(
response.status(),
http::StatusCode::PAYLOAD_TOO_LARGE,
"the authorization endpoint's parameters come from the URL, which the body cap never saw"
);
assert!(
response.headers().get(http::header::LOCATION).is_none(),
"a refusal this early has not validated the redirect URI, so it must not redirect"
);
}