tests.rs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. //! `test(dcr_manage)` read suite: GET metadata, auth denials, wire shape.
  2. use super::read_client;
  3. use super::support::{
  4. TestError, TestResult, dpop_request, expect_forbidden, expect_ssrf_denied, expect_unauthorized,
  5. harness, manage_parts, register, seed_redirect, try_register,
  6. };
  7. use crate::db::client_repo;
  8. #[tokio::test]
  9. async fn dcr_manage_read_returns_metadata() -> TestResult {
  10. let mut harness = harness("catalog").await?;
  11. let registered = register(&mut harness, "https://app.example.test/cb", Some("read")).await?;
  12. let response = {
  13. let (mut stores, ctx) = manage_parts(&mut harness);
  14. read_client(
  15. &mut stores,
  16. &ctx,
  17. &registered.client_id,
  18. Some(&registered.reg_token),
  19. )
  20. .await
  21. .map_err(|error| super::support::TestError::Unexpected(format!("read failed: {error:?}")))?
  22. };
  23. if response.client_id != registered.client_id {
  24. return Err(super::support::TestError::Unexpected(format!(
  25. "wrong client_id: {}",
  26. response.client_id
  27. )));
  28. }
  29. if response.redirect_uris != vec!["https://app.example.test/cb".to_owned()] {
  30. return Err(super::support::TestError::Unexpected(format!(
  31. "wrong redirects: {:?}",
  32. response.redirect_uris
  33. )));
  34. }
  35. if response.client_name != "dcr manage client" {
  36. return Err(super::support::TestError::Unexpected(format!(
  37. "wrong name: {}",
  38. response.client_name
  39. )));
  40. }
  41. if response.token_endpoint_auth_method != "client_secret_basic" {
  42. return Err(super::support::TestError::Unexpected(format!(
  43. "wrong method: {}",
  44. response.token_endpoint_auth_method
  45. )));
  46. }
  47. if response.scope != "read" {
  48. return Err(super::support::TestError::Unexpected(format!(
  49. "wrong scope: {}",
  50. response.scope
  51. )));
  52. }
  53. if response.registration_client_uri
  54. != format!(
  55. "https://auth.example.test/register/{}",
  56. registered.client_id
  57. )
  58. {
  59. return Err(super::support::TestError::Unexpected(format!(
  60. "wrong management uri: {}",
  61. response.registration_client_uri
  62. )));
  63. }
  64. if response.client_secret.is_some() || response.registration_access_token.is_some() {
  65. return Err(super::support::TestError::Unexpected(
  66. "GET must not return credentials".to_owned(),
  67. ));
  68. }
  69. if response.client_id_issued_at <= 0 {
  70. return Err(super::support::TestError::Unexpected(
  71. "missing client_id_issued_at".to_owned(),
  72. ));
  73. }
  74. Ok(())
  75. }
  76. #[tokio::test]
  77. async fn dcr_manage_get() -> TestResult {
  78. let mut harness = harness("catalog").await?;
  79. let registered = register(&mut harness, "https://app.example.test/cb", Some("read")).await?;
  80. let response = {
  81. let (mut stores, ctx) = manage_parts(&mut harness);
  82. read_client(
  83. &mut stores,
  84. &ctx,
  85. &registered.client_id,
  86. Some(&registered.reg_token),
  87. )
  88. .await
  89. .map_err(|error| super::support::TestError::Unexpected(format!("get failed: {error:?}")))?
  90. };
  91. if response.client_id != registered.client_id {
  92. return Err(super::support::TestError::Unexpected(format!(
  93. "wrong client_id: {}",
  94. response.client_id
  95. )));
  96. }
  97. if response.client_name != "dcr manage client" {
  98. return Err(super::support::TestError::Unexpected(format!(
  99. "wrong name: {}",
  100. response.client_name
  101. )));
  102. }
  103. if response.redirect_uris != vec!["https://app.example.test/cb".to_owned()] {
  104. return Err(super::support::TestError::Unexpected(format!(
  105. "wrong redirects: {:?}",
  106. response.redirect_uris
  107. )));
  108. }
  109. if response.scope != "read" {
  110. return Err(super::support::TestError::Unexpected(format!(
  111. "wrong scope: {}",
  112. response.scope
  113. )));
  114. }
  115. if response.client_secret.is_some() || response.registration_access_token.is_some() {
  116. return Err(super::support::TestError::Unexpected(
  117. "GET must not return credentials".to_owned(),
  118. ));
  119. }
  120. Ok(())
  121. }
  122. #[tokio::test]
  123. async fn dcr_manage_read_missing_token_401() -> TestResult {
  124. let mut harness = harness("catalog").await?;
  125. let registered = register(&mut harness, "https://app.example.test/cb", None).await?;
  126. let (mut stores, ctx) = manage_parts(&mut harness);
  127. match read_client(&mut stores, &ctx, &registered.client_id, None).await {
  128. Ok(_) => Err(super::support::TestError::Unexpected(
  129. "tokenless read succeeded".to_owned(),
  130. )),
  131. Err(error) => expect_unauthorized(error),
  132. }
  133. }
  134. #[tokio::test]
  135. async fn dcr_manage_read_unknown_client_401() -> TestResult {
  136. let mut harness = harness("catalog").await?;
  137. let registered = register(&mut harness, "https://app.example.test/cb", None).await?;
  138. let (mut stores, ctx) = manage_parts(&mut harness);
  139. match read_client(
  140. &mut stores,
  141. &ctx,
  142. "no-such-client",
  143. Some(&registered.reg_token),
  144. )
  145. .await
  146. {
  147. Ok(_) => Err(super::support::TestError::Unexpected(
  148. "unknown-client read succeeded".to_owned(),
  149. )),
  150. Err(error) => expect_unauthorized(error),
  151. }
  152. }
  153. #[tokio::test]
  154. async fn dcr_manage_read_wrong_token_403() -> TestResult {
  155. let mut harness = harness("catalog").await?;
  156. let registered = register(&mut harness, "https://app.example.test/cb", None).await?;
  157. let (mut stores, ctx) = manage_parts(&mut harness);
  158. match read_client(
  159. &mut stores,
  160. &ctx,
  161. &registered.client_id,
  162. Some("garbage-token-value"),
  163. )
  164. .await
  165. {
  166. Ok(_) => Err(super::support::TestError::Unexpected(
  167. "wrong-token read succeeded".to_owned(),
  168. )),
  169. Err(error) => expect_forbidden(error),
  170. }
  171. }
  172. #[tokio::test]
  173. async fn dcr_manage_forbidden_renders_403() -> TestResult {
  174. use axum::response::IntoResponse as _;
  175. use http_body_util::BodyExt as _;
  176. let response = super::ManageError::Forbidden.into_response();
  177. if response.status() != axum::http::StatusCode::FORBIDDEN {
  178. return Err(super::support::TestError::Unexpected(format!(
  179. "wrong status: {}",
  180. response.status()
  181. )));
  182. }
  183. let body = match response.into_body().collect().await {
  184. Ok(collected) => collected.to_bytes(),
  185. Err(_) => axum::body::Bytes::new(),
  186. };
  187. let value: serde_json::Value = serde_json::from_slice(&body).map_err(|error| {
  188. super::support::TestError::Unexpected(format!("non-JSON 403 body: {error}"))
  189. })?;
  190. if value.get("error").and_then(|error| error.as_str()) != Some("access_denied") {
  191. return Err(super::support::TestError::Unexpected(format!(
  192. "wrong 403 body: {value}"
  193. )));
  194. }
  195. Ok(())
  196. }
  197. #[tokio::test]
  198. async fn dcr_manage_register_request_uris_stored_verbatim() -> TestResult {
  199. // RFC 7591 §2 treats `request_uris` as client metadata to store, not a URL
  200. // to fetch at registration; this server stores it verbatim and enforces the
  201. // SSRF policy only when a request_uri is resolved at `/authorize`
  202. // (docs/OAUTH.md "JAR and PAR", docs/SECURITY.md "PAR and JAR": https only,
  203. // no redirects, global IPs only). A plaintext scheme, a loopback literal,
  204. // and a non-https scheme must therefore register cleanly and stay inert.
  205. let mut harness = harness("catalog").await?;
  206. let redirect = "https://app.example.test/cb";
  207. seed_redirect(&harness.pool, &harness.config.keys, redirect).await?;
  208. let request_uris = serde_json::json!([
  209. "http://app.example.test/req.jwt",
  210. "https://127.0.0.1/req.jwt",
  211. "ftp://app.example.test/req.jwt"
  212. ]);
  213. let mut request = dpop_request(redirect, None);
  214. request.request_uris = Some(request_uris.clone());
  215. let response = match try_register(&mut harness, &request).await? {
  216. Ok(response) => response,
  217. Err(error) => {
  218. return Err(TestError::Unexpected(format!(
  219. "registration with request_uris rejected: {error}"
  220. )));
  221. }
  222. };
  223. let row =
  224. client_repo::find_active_by_client_id(&harness.pool, &harness.config.keys, &response.client_id)
  225. .await?
  226. .ok_or_else(|| TestError::Unexpected("registered client row missing".to_owned()))?;
  227. if row.request_uris.as_ref() != Some(&request_uris) {
  228. return Err(TestError::Unexpected(format!(
  229. "request_uris not stored verbatim: {:?}",
  230. row.request_uris
  231. )));
  232. }
  233. for uri in [
  234. "http://app.example.test/req.jwt",
  235. "https://127.0.0.1/req.jwt",
  236. "ftp://app.example.test/req.jwt",
  237. ] {
  238. expect_ssrf_denied(uri).await?;
  239. }
  240. Ok(())
  241. }
  242. #[tokio::test]
  243. async fn dcr_manage_update_bad_redirect_rejected() -> TestResult {
  244. let mut harness = harness("catalog").await?;
  245. let registered = register(&mut harness, "https://app.example.test/cb", None).await?;
  246. let request = crate::protocol::dcr::RegisterRequest {
  247. redirect_uris: Some(vec!["https://evil.example.test/cb".to_owned()]),
  248. client_name: None,
  249. token_endpoint_auth_method: Some("client_secret_basic".to_owned()),
  250. grant_types: Some(vec!["authorization_code".to_owned()]),
  251. response_types: Some(vec!["code".to_owned()]),
  252. scope: None,
  253. jwks: None,
  254. jwks_uri: None,
  255. tls_client_auth_subject_dn: None,
  256. dpop_bound_access_tokens: Some(true),
  257. contacts: None,
  258. request_uris: None,
  259. software_id: None,
  260. };
  261. let (mut stores, ctx) = manage_parts(&mut harness);
  262. match super::update::update_client(
  263. &mut stores,
  264. &ctx,
  265. &registered.client_id,
  266. Some(&registered.reg_token),
  267. &request,
  268. )
  269. .await
  270. {
  271. Ok(_) => Err(super::support::TestError::Unexpected(
  272. "non-catalog redirect accepted on PUT".to_owned(),
  273. )),
  274. Err(super::ManageError::Protocol(error)) => {
  275. if error.code() != crate::error::OAuthErrorCode::InvalidRedirectUri {
  276. return Err(super::support::TestError::Unexpected(format!(
  277. "wrong code: {}",
  278. error.code()
  279. )));
  280. }
  281. Ok(())
  282. }
  283. Err(super::ManageError::Forbidden) => Err(super::support::TestError::Unexpected(
  284. "valid token read as forbidden".to_owned(),
  285. )),
  286. }
  287. }