| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- //! `test(jar)` suite, part 1: inline signed request objects (RFC 9101).
- //!
- //! Valid objects merge over the query; bad signature, wrong `aud`,
- //! expiry, `alg:none`, conflicting duplicates, and the JAR/PAR-mandatory
- //! rule are covered here. `request_uri` paths live in `jar_request_uri.rs`.
- #[path = "jar_support.rs"]
- mod support;
- use std::collections::HashMap;
- use d_oauth::{
- error::OAuthErrorCode,
- jose::jar::{JarRequest, resolve},
- };
- use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
- use serde_json::{Value, json};
- use support::{CLIENT_ID, ISSUER, KEY_ID, TestError, TestResult, code_of, store_conn, test_client};
- use time::OffsetDateTime;
- /// PKCS#8 DER (base64) of the client's RSA signing key (test-only fixture).
- const CLIENT_KEY_B64: &str = concat!(
- "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCzwV9fZoba06PGlNq2GXjZ4dZaQDALF6zUCp6kDARqV++kJ43c7u6uULPOweopNeiGk17R8xKk1CMyMbvkbMyLwOmN0ZvxKq+dH1Q/bX6l5OirR26jx8G+vFsGsuRz/zOV",
- "B5PgQ3REhxP6QnxS7rwBcD2QLGO3bBZXLufpXTpiF+IIKwiSdzjdO6ccTZVABCi8LQrENBt/GxmGtA8u5z44SirZohoYOu3LYbROp21TtpfOZgMcyCYOUb+g9A0DNWMc2Xn7645RPdRYQXWAXUG+y+nYWqxevOB5v9SaUYFmnxp23oYzUxrX",
- "gShJhMTAxhgIthmaT0vnrKRPRGvKQcRhAgMBAAECggEAWUTOmt+dWB4jpvJ0nVuQB1Be9coLse6Lf3BYsWpgZPmPwqt8CbzE3t+oNCVcJyQk+nREcipvV1kcs4g8YefgOwVOV4ZBleaXeQW+fpNfgn5oR+s9GGwTHSiIYg7EYpDZ6ohtWUHW",
- "g1O7yZxsBnmV93bos+k/ii9M9vYN6PhKDr0qlX+NKtSZkUxWDvnsfTizkocEBKMGBHLNm6g4KS7N8bClzbgwxCAMLzNtT+/rKEvb6/dQhuSIiVeRlDSDvCaTBdSEqajcoWsrTpzkplb/KbAT8Rkin1KnlQ0s8NHZaC3R/cddvvfJZYgOBDhq",
- "WPW7qNGdzeqb6pevZyS4+LjrcwKBgQD9ZLEo0AlJj9wm243JH0Ob2ubi/wnifa28vg4pEvEXbYhQbLJBJluxn3fQ4LuWObiNwo0SpagOmLScjpqV6mr9/U7/f9BldrKrtPsYDM/JzwzBSox+HgBUvvIgT6Gm3PwCbGfqyLnivFj5PfEoQQ+D",
- "tq+oQjFnnU3gUGo4BGl7QwKBgQC1msGDZdYr4mmaBLXwIsWqJHQ7adogF16xSPRGGUONena5bhrhcPGAKRG+XxCrpV0vQZZ2cY5VXv/K80+RFm+z1fEI+FENiLTxfkj0aE+5FRxViEl6LDJy8oGUk9DSIglcrAd6DzUA9RZnLiqapsP/TQxH",
- "Mh1NmzZ1QO9OD3HdiwKBgQCAb0Mkjp3GfXZ2853gLUqVetTAhci61jzzh9mhlApDd686JH4Na6XaWBvg8afjMwqKP+xFYVXXYCtblyRfjFwCtRwPgPdeFlXllmwW+pdvkNTEPEcNvaMpUUdkfThQXX4WtF/Pr0IY3KK0YmMTpjZ46mYKgNKN",
- "1Due0OOl1QaqBQKBgQCqftge3ITuPmqWRsXo5TxeOEtHXGGEr2wfVTbAeQeK0+aSwGRMMxUIFYarC+VCwMVzkEgKeyH91Gqhxzx0InRMihb5LOgrpKT79GvZyfTO9prbUlBpewUCFdrB+uSZ44qWjouWdKB2faLCy/0DpH1uOo/EZ7BLI+VE",
- "v+nrh2SHcQKBgQD0LMmasHOwG1SFsIObaquSVb6ETbloZ4J1OmpeUrMJEN9TcpY6KbzSrqXl0npSziiUTCE8nQgqDyqCTduhswD413irGN2vIkyfrR2pqBlLOF+INfH0HUQQZLiH6P4L+sFNbeizpHT9qLa5KtBBs9UD40kfZjUPqYMmgA6J",
- "DNOf1Q==",
- );
- /// PKCS#8 DER (base64) of a foreign RSA key: signatures under it must fail.
- const OTHER_KEY_B64: &str = concat!(
- "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDNSLFGmYGgzFFpJ+Pj2p6aTttNl2vG2YRTvfKd45WYGX044GJx8zHwIQuOCSGFtLW92HOdXxsU81na/BXPwIXI4cw97D+y5ufIEo9toJmUcyaPF6JFWLKcqgoJqrci520/",
- "1ovafGYBqL+JyjSKsvZHRH38nHtlzLiPU7fS1NMW1RAnd9smyRRKUK0tawFx+dXyrACuppNFWHGHJZDKsRN8t0z2SOx4RjhLHOGZzw1b9/gJA0ag+1GK4NnzemCd62TM5wSrgXw5IEEJ2NqKfRk54uZCM26DD9moZV3xrqI8OBL8dLbsFie6",
- "/zaxC9UTV0A54h66mxXYhGLdC6tdyNV1AgMBAAECggEAGASaBzBVKA7XtJWv9lwBA/1Xb8HYYIzZDGI0NXBn84KB8epg4kOps1Zub2kWlLnyrJpbVo2G0eHyAKrPU34LORhgCNuwDTqyYih3wwQQC/wYBhRmDjaFOUq0v3JLmIgasmYj+UVm",
- "Oun3NQQlNxges/Jpyik1z9COxGgvYPm5E9nWo+eWSj/iz+slp14KVb/YKtbt0kBtqNKlH8SR8g0JRbt2M7Vs/7bMFX2NqzeiSsj3re73+3abSx1p76bj6YNqCW4nYy/7dVawP8V4f8uXM4TkwexMWqP82PDP01zsEuiKVxFVcF/ogOZ9ZtjX",
- "rRdeTBaOeTDCJVSgqwL3IlXN0QKBgQDwpKR2BADW/2JntB17Bu67g48jRz7xJ54C8fV2ZV7hW6ytFVTUH6LdjK5OR/1oiqZG0lyQcgdDTuUoqdVcphjbnJHt8RNMAG9JkehuUoQq6xPAmYZHs72450kFuQgAKoaDCqOnbHdsHlmpN/hMZmvK",
- "ulOF4bJoZK/7oYlQ8A0JhQKBgQDaYmQ2ExfML3rgZ2BIFYSEMDFIordSWOENvVIIkDTuFiqWBiRSzS5IZY1gq+ToVh0rALGMZN6lb0y+v6WdRAqbY8IPcmlb8FZvnq1MXge0Xr+DUHvdHo1uZ9RlXqlfdc7M9pwzdQ7smK6oNnQctrvjBkBy",
- "Aq0WhF85h6jX1dfnMQKBgQCQENFQwfbKwBvmn+MiWzUjV5Ci+y1IbIJhmv9sASY7XonTGTkuwT3cyzMzNFAWfSMstRWreEi22A3DOWG91MZ++4412CTIVF5LuilfmtD7bL7niEHmvUmw3+5MmcibaNocq4rsWbhpJwZgk1x4SLzxonkMZuFL",
- "NDRTPhMaPIxhjQKBgQDFQ8Ffsyl48J6fG+I12vID8gBiq1DLGqvYHF6Ai7q0IeO/9rZS8TJQhSMK4tr4H+ljEdJPjkIrz/jUCQuRnoeyS49dIq0XNmbtlrjV1c5B2KBH/qiUWWfKBhIuqy51o1X1gQIh3ShpAZYrLlz+HvZjIR+PnOk7AMF/",
- "lxImPO000QKBgQDEwUbOROo7xR5rIB2CFTeg9HK/UyC3EYPZvqq6HBMSRQv6H3eOpu1Oro/5G/AzA9TbFJDUtlvDT+RPxk7uWTjtqoUZpeokpLRpgWhJZNlZUuFmEXZlObrHN4TtLiusU1B4FhCJznUOEpPXkQ02ZpJCV6/vW8MNcMKBUzLp",
- "2J8sJQ==",
- );
- /// Sign `claims` with the given fixture key under RS256.
- fn sign(key_b64: &str, kid: Option<&str>, claims: &Value) -> Result<String, TestError> {
- use base64::{Engine as _, engine::general_purpose::STANDARD};
- let der = STANDARD
- .decode(key_b64)
- .map_err(|_| TestError::Unexpected("fixture key is not decodable".to_owned()))?;
- let pem = format!(
- "-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----\n",
- STANDARD.encode(&der)
- );
- let mut header = Header::new(Algorithm::RS256);
- header.kid = kid.map(str::to_owned);
- Ok(encode(
- &header,
- claims,
- &EncodingKey::from_rsa_pem(pem.as_bytes())?,
- )?)
- }
- /// Base request-object claims bound to this suite's client and issuer.
- fn base_claims(extra: Value) -> Value {
- let now = OffsetDateTime::now_utc().unix_timestamp();
- let mut claims = json!({
- "iss": CLIENT_ID, "aud": ISSUER, "exp": now + 300,
- "response_type": "code", "client_id": CLIENT_ID,
- "redirect_uri": "https://client.example.test/cb",
- "scope": "read", "state": "xyz",
- });
- if let (Some(into), Some(from)) = (claims.as_object_mut(), extra.as_object()) {
- for (key, value) in from {
- into.insert(key.clone(), value.clone());
- }
- }
- claims
- }
- #[tokio::test]
- async fn jar_valid_signed_request_merged() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let client = test_client("client_secret_basic", None);
- let jwt = sign(CLIENT_KEY_B64, Some(KEY_ID), &base_claims(json!({})))?;
- let mut query = HashMap::new();
- query.insert("client_id".to_owned(), CLIENT_ID.to_owned());
- query.insert("state".to_owned(), "xyz".to_owned());
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: Some(&jwt),
- request_uri: None,
- };
- let merged = resolve(&mut conn, &store, &input)
- .await
- .map_err(|error| TestError::Unexpected(error.to_string()))?;
- if merged.params.get("scope").map(String::as_str) != Some("read")
- || merged.params.get("redirect_uri").map(String::as_str)
- != Some("https://client.example.test/cb")
- || merged.params.get("state").map(String::as_str) != Some("xyz")
- {
- return Err(TestError::Unexpected("merged params mismatch".to_owned()));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_bad_signature_rejected() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let client = test_client("client_secret_basic", None);
- let jwt = sign(OTHER_KEY_B64, Some(KEY_ID), &base_claims(json!({})))?;
- let query = HashMap::new();
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: Some(&jwt),
- request_uri: None,
- };
- let outcome = resolve(&mut conn, &store, &input).await;
- if code_of(&outcome) != Some(OAuthErrorCode::InvalidRequestObject) {
- return Err(TestError::Unexpected("bad signature accepted".to_owned()));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_wrong_aud_rejected() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let client = test_client("client_secret_basic", None);
- let jwt = sign(
- CLIENT_KEY_B64,
- Some(KEY_ID),
- &base_claims(json!({"aud": "https://other.example.test"})),
- )?;
- let query = HashMap::new();
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: Some(&jwt),
- request_uri: None,
- };
- let outcome = resolve(&mut conn, &store, &input).await;
- if code_of(&outcome) != Some(OAuthErrorCode::InvalidRequestObject) {
- return Err(TestError::Unexpected("wrong aud accepted".to_owned()));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_expired_rejected() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let client = test_client("client_secret_basic", None);
- let now = OffsetDateTime::now_utc().unix_timestamp();
- let jwt = sign(
- CLIENT_KEY_B64,
- Some(KEY_ID),
- &base_claims(json!({"exp": now - 3600})),
- )?;
- let query = HashMap::new();
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: Some(&jwt),
- request_uri: None,
- };
- let outcome = resolve(&mut conn, &store, &input).await;
- if code_of(&outcome) != Some(OAuthErrorCode::InvalidRequestObject) {
- return Err(TestError::Unexpected("expired object accepted".to_owned()));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_alg_none_rejected() -> TestResult {
- use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
- let (_redis, mut conn, store) = store_conn().await?;
- let client = test_client("client_secret_basic", None);
- let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
- let body = URL_SAFE_NO_PAD.encode(r#"{"iss":"jar-test-client"}"#);
- let jwt = format!("{header}.{body}.");
- let query = HashMap::new();
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: Some(&jwt),
- request_uri: None,
- };
- let outcome = resolve(&mut conn, &store, &input).await;
- if code_of(&outcome) != Some(OAuthErrorCode::InvalidRequestObject) {
- return Err(TestError::Unexpected("alg:none accepted".to_owned()));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_conflicting_duplicates_rejected() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let client = test_client("client_secret_basic", None);
- let jwt = sign(CLIENT_KEY_B64, Some(KEY_ID), &base_claims(json!({})))?;
- let mut query = HashMap::new();
- query.insert("scope".to_owned(), "write".to_owned());
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: Some(&jwt),
- request_uri: None,
- };
- let outcome = resolve(&mut conn, &store, &input).await;
- if code_of(&outcome) != Some(OAuthErrorCode::InvalidRequestObject) {
- return Err(TestError::Unexpected(
- "conflicting duplicate accepted".to_owned(),
- ));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_confidential_client_without_par_or_request_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());
- query.insert("response_type".to_owned(), "code".to_owned());
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: None,
- request_uri: None,
- };
- let outcome = resolve(&mut conn, &store, &input).await;
- if code_of(&outcome) != Some(OAuthErrorCode::InvalidRequest) {
- return Err(TestError::Unexpected(
- "plain query accepted for confidential client".to_owned(),
- ));
- }
- Ok(())
- }
- #[tokio::test]
- async fn jar_public_client_without_request_object_uses_query() -> TestResult {
- let (_redis, mut conn, store) = store_conn().await?;
- let client = test_client("none", None);
- let mut query = HashMap::new();
- query.insert("client_id".to_owned(), CLIENT_ID.to_owned());
- query.insert("response_type".to_owned(), "code".to_owned());
- let input = JarRequest {
- client: &client,
- issuer: ISSUER,
- query: &query,
- request: None,
- request_uri: None,
- };
- let merged = resolve(&mut conn, &store, &input)
- .await
- .map_err(|error| TestError::Unexpected(error.to_string()))?;
- if merged.params != query {
- return Err(TestError::Unexpected(
- "public query passthrough altered".to_owned(),
- ));
- }
- if !merged.resource.is_empty() {
- return Err(TestError::Unexpected(
- "plain query must pin no resource".to_owned(),
- ));
- }
- Ok(())
- }
|