| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- //! `test(jar)` suite, part 2: `request_uri` references.
- //!
- //! Unknown URIs are never fetched; registered URIs behind the SSRF guard
- //! reject private-IP and plaintext targets; PAR URNs consume one-time and
- //! bind to the issuing client. No test performs an arbitrary fetch.
- #[path = "jar_support.rs"]
- mod support;
- use std::{
- collections::HashMap,
- sync::atomic::{AtomicU64, Ordering},
- time::Duration,
- };
- use d_oauth::{
- error::OAuthErrorCode,
- jose::jar::{JarRequest, PAR_REQUEST_URI_PREFIX, resolve},
- store::par::{self, ParRecord},
- };
- use serde_json::json;
- use support::{CLIENT_ID, ISSUER, TestError, TestResult, code_of, store_conn, test_client};
- static TEST_NONCE: AtomicU64 = AtomicU64::new(1);
- /// Unique PAR id per test so the shared container stays isolated.
- fn unique_par_id() -> String {
- let n = TEST_NONCE.fetch_add(1, Ordering::Relaxed);
- format!("jar-test-{n}")
- }
- #[tokio::test]
- async fn jar_non_registered_request_uri_rejected() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let client = test_client("client_secret_basic", None);
- let mut query = HashMap::new();
- query.insert("client_id".to_owned(), CLIENT_ID.to_owned());
- let uri = "https://127.0.0.1:9/unregistered.jwt";
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: None,
- request_uri: Some(uri),
- };
- let outcome = resolve(&mut conn, &store, &input).await;
- if code_of(&outcome) != Some(OAuthErrorCode::InvalidRequestObject) {
- return Err(TestError::Unexpected(
- "non-registered request_uri accepted".to_owned(),
- ));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_registered_request_uri_private_ip_rejected() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let uri = "https://127.0.0.1:9/req.jwt";
- let client = test_client("client_secret_basic", Some(json!([uri])));
- let query = HashMap::new();
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: None,
- request_uri: Some(uri),
- };
- let outcome = resolve(&mut conn, &store, &input).await;
- if code_of(&outcome) != Some(OAuthErrorCode::InvalidRequestObject) {
- return Err(TestError::Unexpected(
- "private-IP request_uri accepted".to_owned(),
- ));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_registered_request_uri_http_rejected() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let uri = "http://127.0.0.1:9/req.jwt";
- let client = test_client("client_secret_basic", Some(json!([uri])));
- let query = HashMap::new();
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: None,
- request_uri: Some(uri),
- };
- let outcome = resolve(&mut conn, &store, &input).await;
- if code_of(&outcome) != Some(OAuthErrorCode::InvalidRequestObject) {
- return Err(TestError::Unexpected(
- "http request_uri accepted".to_owned(),
- ));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_par_request_uri_consumed_once() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let client = test_client("client_secret_basic", None);
- let id = unique_par_id();
- let record = ParRecord {
- client_id: CLIENT_ID.to_owned(),
- request: serde_json::to_string(&json!({
- "response_type": "code", "client_id": CLIENT_ID,
- "redirect_uri": "https://client.example.test/cb", "scope": "read",
- }))?,
- };
- par::put(&mut conn, &store, &id, &record, Duration::from_secs(90)).await?;
- let uri = format!("{PAR_REQUEST_URI_PREFIX}{id}");
- let mut query = HashMap::new();
- query.insert("client_id".to_owned(), CLIENT_ID.to_owned());
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: None,
- request_uri: Some(&uri),
- };
- let merged = resolve(&mut conn, &store, &input)
- .await
- .map_err(|error| TestError::Unexpected(error.to_string()))?;
- if merged.get("scope").map(String::as_str) != Some("read") {
- return Err(TestError::Unexpected("PAR params not merged".to_owned()));
- }
- let replay = resolve(&mut conn, &store, &input).await;
- if code_of(&replay) != Some(OAuthErrorCode::InvalidRequest) {
- return Err(TestError::Unexpected("PAR request_uri replayed".to_owned()));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_par_cross_client_rejected() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let mut foreign = test_client("client_secret_basic", None);
- foreign.client_id = "jar-foreign-client".to_owned();
- let id = unique_par_id();
- let record = ParRecord {
- client_id: CLIENT_ID.to_owned(),
- request: serde_json::to_string(&json!({"response_type": "code"}))?,
- };
- par::put(&mut conn, &store, &id, &record, Duration::from_secs(90)).await?;
- let uri = format!("{PAR_REQUEST_URI_PREFIX}{id}");
- let query = HashMap::new();
- let input = JarRequest {
- client: &foreign,
- issuer: ISSUER,
- query: &query,
- request: None,
- request_uri: Some(&uri),
- };
- let outcome = resolve(&mut conn, &store, &input).await;
- if code_of(&outcome) != Some(OAuthErrorCode::InvalidRequest) {
- return Err(TestError::Unexpected(
- "cross-client PAR accepted".to_owned(),
- ));
- }
- Ok(())
- }
|