request_uri.rs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. //! `request_uri` resolution: PAR URNs or pre-registered URIs.
  2. //!
  3. //! A PAR-issued URN is consumed one-time from the PAR store and bound to
  4. //! the presenting client; any other URI must exactly match the client's
  5. //! registered `request_uris` and is then fetched behind the SSRF guard.
  6. //! Arbitrary URIs are never fetched.
  7. use std::collections::HashMap;
  8. use serde_json::Value;
  9. use super::merge::merge_claims;
  10. use super::verify::verify_request_object;
  11. use super::{PAR_REQUEST_URI_PREFIX, fetch::fetch_request_object, merge::MergedClaims};
  12. use crate::{
  13. db::client_repo::Client,
  14. error::AppError,
  15. state::RedisConn,
  16. store::{Store, par},
  17. };
  18. /// Resolve a `request_uri`: PAR URN or pre-registered URI.
  19. ///
  20. /// # Errors
  21. ///
  22. /// Returns `invalid_request` for PAR misuse and `invalid_request_object`
  23. /// for unknown, unfetchable, or invalid referenced objects.
  24. pub(crate) async fn resolve_uri(
  25. conn: &mut RedisConn,
  26. store: &Store,
  27. client: &Client,
  28. issuer: &str,
  29. query: &HashMap<String, String>,
  30. uri: &str,
  31. ) -> Result<MergedClaims, AppError> {
  32. if let Some(id) = uri.strip_prefix(PAR_REQUEST_URI_PREFIX) {
  33. return resolve_par(conn, store, client, query, id).await;
  34. }
  35. if !is_registered_request_uri(client, uri) {
  36. return Err(AppError::invalid_request_object(
  37. "request_uri is neither PAR-issued nor pre-registered",
  38. ));
  39. }
  40. let body = fetch_request_object(uri).await?;
  41. let claims = verify_request_object(client, issuer, &body)?;
  42. merge_claims(query, &claims, &client.client_id)
  43. }
  44. /// Whether `uri` is exactly one of the client's pre-registered JAR
  45. /// `request_uris` (no normalization: fail closed on any deviation).
  46. fn is_registered_request_uri(client: &Client, uri: &str) -> bool {
  47. match client.request_uris {
  48. Some(Value::Array(ref items)) => items.iter().any(|item| item.as_str() == Some(uri)),
  49. _ => false,
  50. }
  51. }
  52. /// Consume a PAR-issued `request_uri` one-time, bound to `client`.
  53. ///
  54. /// The expected client travels inside the consume script: a foreign presenter
  55. /// observes `Mismatched` and the victim record survives for its owner, so a
  56. /// cross-client presentation can never burn the single-use value.
  57. ///
  58. /// # Errors
  59. ///
  60. /// Returns `invalid_request` for an empty, unknown, consumed, or
  61. /// foreign-client id, and a masked `server_error` when the stored record
  62. /// does not parse.
  63. async fn resolve_par(
  64. conn: &mut RedisConn,
  65. store: &Store,
  66. client: &Client,
  67. query: &HashMap<String, String>,
  68. id: &str,
  69. ) -> Result<MergedClaims, AppError> {
  70. if id.is_empty() {
  71. return Err(AppError::invalid_request("request_uri is malformed"));
  72. }
  73. let record = match par::consume_bound(conn, store, id, client.client_id.as_str()).await? {
  74. par::ConsumeOutcome::Consumed(record) => record,
  75. par::ConsumeOutcome::Mismatched => {
  76. return Err(AppError::invalid_request(
  77. "request_uri was issued to a different client",
  78. ));
  79. }
  80. par::ConsumeOutcome::Absent => {
  81. return Err(AppError::invalid_request(
  82. "request_uri is unknown, expired, or already used",
  83. ));
  84. }
  85. };
  86. let stored: Value = serde_json::from_str(&record.request).map_err(AppError::internal)?;
  87. let Some(object) = stored.as_object() else {
  88. return Err(AppError::internal(std::io::Error::new(
  89. std::io::ErrorKind::InvalidData,
  90. "stored PAR record is not a JSON object",
  91. )));
  92. };
  93. // The PAR-pinned audience rides out-of-band (record `aud`, validated at
  94. // push and re-validated by the authorize caller); the stored JSON object
  95. // itself carries no `resource` (stripped at push, stripped by the merge).
  96. let mut merged = merge_claims(query, object, &client.client_id)?;
  97. merged.resource = record.aud;
  98. Ok(merged)
  99. }
  100. #[cfg(test)]
  101. mod tests {
  102. use super::*;
  103. fn fixture(request_uris: Option<Value>) -> Client {
  104. Client {
  105. id: uuid::Uuid::nil(),
  106. client_id: "jar-fixture".to_owned(),
  107. client_secret_expires_at: None,
  108. client_id_issued_at: time::OffsetDateTime::UNIX_EPOCH,
  109. client_name: "JAR Fixture".to_owned(),
  110. grant_types: Vec::new(),
  111. response_types: Vec::new(),
  112. scope: String::new(),
  113. token_endpoint_auth_method: "none".to_owned(),
  114. jwks: None,
  115. jwks_uri: None,
  116. tls_client_auth_subject_dn: None,
  117. tls_client_auth_cert_thumbprint: None,
  118. request_uris,
  119. contacts: None,
  120. software_id: None,
  121. dpop_bound_access_tokens: false,
  122. access_token_format: "opaque".to_owned(),
  123. active: true,
  124. created_at: time::OffsetDateTime::UNIX_EPOCH,
  125. updated_at: time::OffsetDateTime::UNIX_EPOCH,
  126. }
  127. }
  128. #[test]
  129. fn jar_preregistered_request_uri_matches_decrypted_set() {
  130. let uri = "https://client.example.test/req.jwt";
  131. let client = fixture(Some(serde_json::json!([uri])));
  132. assert!(is_registered_request_uri(&client, uri));
  133. assert!(!is_registered_request_uri(
  134. &client,
  135. "https://evil.example.test/req.jwt"
  136. ));
  137. assert!(!is_registered_request_uri(
  138. &client,
  139. "https://client.example.test/req.jwt/extra"
  140. ));
  141. assert!(!is_registered_request_uri(&fixture(None), uri));
  142. }
  143. }