| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- //! `test(dcr_manage)` read suite: GET metadata, auth denials, wire shape.
- use super::read_client;
- use super::support::{
- TestError, TestResult, dpop_request, expect_forbidden, expect_ssrf_denied, expect_unauthorized,
- harness, manage_parts, register, seed_redirect, try_register,
- };
- use crate::db::client_repo;
- #[tokio::test]
- async fn dcr_manage_read_returns_metadata() -> TestResult {
- let mut harness = harness("catalog").await?;
- let registered = register(&mut harness, "https://app.example.test/cb", Some("read")).await?;
- let response = {
- let (mut stores, ctx) = manage_parts(&mut harness);
- read_client(
- &mut stores,
- &ctx,
- ®istered.client_id,
- Some(®istered.reg_token),
- )
- .await
- .map_err(|error| super::support::TestError::Unexpected(format!("read failed: {error:?}")))?
- };
- if response.client_id != registered.client_id {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong client_id: {}",
- response.client_id
- )));
- }
- if response.redirect_uris != vec!["https://app.example.test/cb".to_owned()] {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong redirects: {:?}",
- response.redirect_uris
- )));
- }
- if response.client_name != "dcr manage client" {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong name: {}",
- response.client_name
- )));
- }
- if response.token_endpoint_auth_method != "client_secret_basic" {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong method: {}",
- response.token_endpoint_auth_method
- )));
- }
- if response.scope != "read" {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong scope: {}",
- response.scope
- )));
- }
- if response.registration_client_uri
- != format!(
- "https://auth.example.test/register/{}",
- registered.client_id
- )
- {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong management uri: {}",
- response.registration_client_uri
- )));
- }
- if response.client_secret.is_some() || response.registration_access_token.is_some() {
- return Err(super::support::TestError::Unexpected(
- "GET must not return credentials".to_owned(),
- ));
- }
- if response.client_id_issued_at <= 0 {
- return Err(super::support::TestError::Unexpected(
- "missing client_id_issued_at".to_owned(),
- ));
- }
- Ok(())
- }
- #[tokio::test]
- async fn dcr_manage_get() -> TestResult {
- let mut harness = harness("catalog").await?;
- let registered = register(&mut harness, "https://app.example.test/cb", Some("read")).await?;
- let response = {
- let (mut stores, ctx) = manage_parts(&mut harness);
- read_client(
- &mut stores,
- &ctx,
- ®istered.client_id,
- Some(®istered.reg_token),
- )
- .await
- .map_err(|error| super::support::TestError::Unexpected(format!("get failed: {error:?}")))?
- };
- if response.client_id != registered.client_id {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong client_id: {}",
- response.client_id
- )));
- }
- if response.client_name != "dcr manage client" {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong name: {}",
- response.client_name
- )));
- }
- if response.redirect_uris != vec!["https://app.example.test/cb".to_owned()] {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong redirects: {:?}",
- response.redirect_uris
- )));
- }
- if response.scope != "read" {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong scope: {}",
- response.scope
- )));
- }
- if response.client_secret.is_some() || response.registration_access_token.is_some() {
- return Err(super::support::TestError::Unexpected(
- "GET must not return credentials".to_owned(),
- ));
- }
- Ok(())
- }
- #[tokio::test]
- async fn dcr_manage_read_missing_token_401() -> TestResult {
- let mut harness = harness("catalog").await?;
- let registered = register(&mut harness, "https://app.example.test/cb", None).await?;
- let (mut stores, ctx) = manage_parts(&mut harness);
- match read_client(&mut stores, &ctx, ®istered.client_id, None).await {
- Ok(_) => Err(super::support::TestError::Unexpected(
- "tokenless read succeeded".to_owned(),
- )),
- Err(error) => expect_unauthorized(error),
- }
- }
- #[tokio::test]
- async fn dcr_manage_read_unknown_client_401() -> TestResult {
- let mut harness = harness("catalog").await?;
- let registered = register(&mut harness, "https://app.example.test/cb", None).await?;
- let (mut stores, ctx) = manage_parts(&mut harness);
- match read_client(
- &mut stores,
- &ctx,
- "no-such-client",
- Some(®istered.reg_token),
- )
- .await
- {
- Ok(_) => Err(super::support::TestError::Unexpected(
- "unknown-client read succeeded".to_owned(),
- )),
- Err(error) => expect_unauthorized(error),
- }
- }
- #[tokio::test]
- async fn dcr_manage_read_wrong_token_403() -> TestResult {
- let mut harness = harness("catalog").await?;
- let registered = register(&mut harness, "https://app.example.test/cb", None).await?;
- let (mut stores, ctx) = manage_parts(&mut harness);
- match read_client(
- &mut stores,
- &ctx,
- ®istered.client_id,
- Some("garbage-token-value"),
- )
- .await
- {
- Ok(_) => Err(super::support::TestError::Unexpected(
- "wrong-token read succeeded".to_owned(),
- )),
- Err(error) => expect_forbidden(error),
- }
- }
- #[tokio::test]
- async fn dcr_manage_forbidden_renders_403() -> TestResult {
- use axum::response::IntoResponse as _;
- use http_body_util::BodyExt as _;
- let response = super::ManageError::Forbidden.into_response();
- if response.status() != axum::http::StatusCode::FORBIDDEN {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong status: {}",
- response.status()
- )));
- }
- let body = match response.into_body().collect().await {
- Ok(collected) => collected.to_bytes(),
- Err(_) => axum::body::Bytes::new(),
- };
- let value: serde_json::Value = serde_json::from_slice(&body).map_err(|error| {
- super::support::TestError::Unexpected(format!("non-JSON 403 body: {error}"))
- })?;
- if value.get("error").and_then(|error| error.as_str()) != Some("access_denied") {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong 403 body: {value}"
- )));
- }
- Ok(())
- }
- #[tokio::test]
- async fn dcr_manage_register_request_uris_stored_verbatim() -> TestResult {
- // RFC 7591 §2 treats `request_uris` as client metadata to store, not a URL
- // to fetch at registration; this server stores it verbatim and enforces the
- // SSRF policy only when a request_uri is resolved at `/authorize`
- // (docs/OAUTH.md "JAR and PAR", docs/SECURITY.md "PAR and JAR": https only,
- // no redirects, global IPs only). A plaintext scheme, a loopback literal,
- // and a non-https scheme must therefore register cleanly and stay inert.
- let mut harness = harness("catalog").await?;
- let redirect = "https://app.example.test/cb";
- seed_redirect(&harness.pool, &harness.config.keys, redirect).await?;
- let request_uris = serde_json::json!([
- "http://app.example.test/req.jwt",
- "https://127.0.0.1/req.jwt",
- "ftp://app.example.test/req.jwt"
- ]);
- let mut request = dpop_request(redirect, None);
- request.request_uris = Some(request_uris.clone());
- let response = match try_register(&mut harness, &request).await? {
- Ok(response) => response,
- Err(error) => {
- return Err(TestError::Unexpected(format!(
- "registration with request_uris rejected: {error}"
- )));
- }
- };
- let row =
- client_repo::find_active_by_client_id(&harness.pool, &harness.config.keys, &response.client_id)
- .await?
- .ok_or_else(|| TestError::Unexpected("registered client row missing".to_owned()))?;
- if row.request_uris.as_ref() != Some(&request_uris) {
- return Err(TestError::Unexpected(format!(
- "request_uris not stored verbatim: {:?}",
- row.request_uris
- )));
- }
- for uri in [
- "http://app.example.test/req.jwt",
- "https://127.0.0.1/req.jwt",
- "ftp://app.example.test/req.jwt",
- ] {
- expect_ssrf_denied(uri).await?;
- }
- Ok(())
- }
- #[tokio::test]
- async fn dcr_manage_update_bad_redirect_rejected() -> TestResult {
- let mut harness = harness("catalog").await?;
- let registered = register(&mut harness, "https://app.example.test/cb", None).await?;
- let request = crate::protocol::dcr::RegisterRequest {
- redirect_uris: Some(vec!["https://evil.example.test/cb".to_owned()]),
- client_name: None,
- token_endpoint_auth_method: Some("client_secret_basic".to_owned()),
- grant_types: Some(vec!["authorization_code".to_owned()]),
- response_types: Some(vec!["code".to_owned()]),
- scope: None,
- jwks: None,
- jwks_uri: None,
- tls_client_auth_subject_dn: None,
- dpop_bound_access_tokens: Some(true),
- contacts: None,
- request_uris: None,
- software_id: None,
- };
- let (mut stores, ctx) = manage_parts(&mut harness);
- match super::update::update_client(
- &mut stores,
- &ctx,
- ®istered.client_id,
- Some(®istered.reg_token),
- &request,
- )
- .await
- {
- Ok(_) => Err(super::support::TestError::Unexpected(
- "non-catalog redirect accepted on PUT".to_owned(),
- )),
- Err(super::ManageError::Protocol(error)) => {
- if error.code() != crate::error::OAuthErrorCode::InvalidRedirectUri {
- return Err(super::support::TestError::Unexpected(format!(
- "wrong code: {}",
- error.code()
- )));
- }
- Ok(())
- }
- Err(super::ManageError::Forbidden) => Err(super::support::TestError::Unexpected(
- "valid token read as forbidden".to_owned(),
- )),
- }
- }
|