//! `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(), )), } }