| ... | ... | @@ -431,10 +431,11 @@ impl Store { |
| 431 | 431 | return Err(fail("invalid_request")); |
| 432 | 432 | } |
| 433 | 433 | if !scopes.contains(&scopes_allowed[0]) |
| 434 | | || scopes.iter().any(|s| !scopes_allowed.contains(s)) |
| 434 | || scopes.iter().any(|scope| !CATALOGS.iter().any(|(_, _, allowed)| allowed.contains(scope))) |
| 435 | 435 | { |
| 436 | 436 | return Err(fail("invalid_scope")); |
| 437 | 437 | } |
| 438 | let scopes: Vec<_> = scopes.into_iter().filter(|scope| scopes_allowed.contains(scope)).collect(); |
| 438 | 439 | let pending = secret(); |
| 439 | 440 | put( |
| 440 | 441 | &tx, |
| ... | ... | @@ -1033,6 +1034,47 @@ mod tests { |
| 1033 | 1034 | } |
| 1034 | 1035 | } |
| 1035 | 1036 | #[tokio::test] |
| 1037 | async fn authorization_restricts_issuer_scopes_to_the_requested_resource() { |
| 1038 | let fixture = Fixture::new(); |
| 1039 | let uri = "http://127.0.0.1:57072/callback/H9rAWI-8eQbv"; |
| 1040 | let client = value(fixture.store.oauth("register", &Method::POST, &HashMap::new(), &HeaderMap::new(), |
| 1041 | json!({"client_name":"Codex","redirect_uris":[uri]})).unwrap()).await; |
| 1042 | let mut request = fields(json!({"response_type":"code","client_id":client["client_id"],"state":"fixture-state", |
| 1043 | "code_challenge":"AdJpfUsRa6XMg54f2khmgo2onMwePN3vmU9DOOoaTzo","code_challenge_method":"S256", |
| 1044 | "redirect_uri":uri,"scope":"observability:read offline_access sessions:read sessions:write shale:read shale:write"})); |
| 1045 | for (catalog, _, allowed) in CATALOGS { |
| 1046 | request.insert("resource".into(), fixture.store.resource(catalog)); |
| 1047 | let response = fixture.store.oauth("authorize", &Method::GET, &request, &HeaderMap::new(), Value::Null).unwrap(); |
| 1048 | assert_eq!(response.status(), StatusCode::FOUND); |
| 1049 | let pending_id = response.headers()["location"].to_str().unwrap().strip_prefix("/connect/").unwrap(); |
| 1050 | let db = fixture.store.db.lock().unwrap(); |
| 1051 | let mut pending = get(&db, &format!("pending:{}", hash(pending_id))).unwrap(); |
| 1052 | let expected: Vec<_> = request["scope"].split_whitespace().filter(|scope| allowed.contains(scope)).collect(); |
| 1053 | assert_eq!(pending["scopes"], json!(expected)); |
| 1054 | assert_eq!(pending["redirect"], uri); |
| 1055 | assert_eq!(pending["state"], "fixture-state"); |
| 1056 | pending["id"] = json!(format!("fixture-{catalog}")); |
| 1057 | put(&db, &format!("grant:fixture-{catalog}"), &pending, 0).unwrap(); |
| 1058 | let tokens = fixture.store.issue(&db, &pending).unwrap(); |
| 1059 | assert_eq!(tokens["scope"], expected.join(" ")); |
| 1060 | assert!(tokens["refresh_token"].is_string()); |
| 1061 | drop(db); |
| 1062 | let headers = bearer(string(&tokens["access_token"])); |
| 1063 | assert!(fixture.store.authenticate(&headers, &fixture.store.resource(catalog)).is_ok()); |
| 1064 | for (other, _, _) in CATALOGS.iter().filter(|(other, _, _)| other != catalog) { |
| 1065 | assert!(fixture.store.authenticate(&headers, &fixture.store.resource(other)).is_err()); |
| 1066 | } |
| 1067 | } |
| 1068 | for scopes in ["shale:read unknown:scope", "observability:read offline_access", "offline_access", ""] { |
| 1069 | request.insert("resource".into(), fixture.store.resource("shale")); |
| 1070 | request.insert("scope".into(), scopes.into()); |
| 1071 | assert_eq!(fixture.store.oauth("authorize", &Method::GET, &request, &HeaderMap::new(), Value::Null).unwrap_err().message, "invalid_scope"); |
| 1072 | } |
| 1073 | request.insert("scope".into(), "shale:read".into()); |
| 1074 | request.insert("redirect_uri".into(), uri.replace("H9rAWI-8eQbv", "different")); |
| 1075 | assert_eq!(fixture.store.oauth("authorize", &Method::GET, &request, &HeaderMap::new(), Value::Null).unwrap_err().message, "invalid_redirect_uri"); |
| 1076 | } |
| 1077 | #[tokio::test] |
| 1036 | 1078 | async fn code_binds_pkce_client_redirect_and_audience_without_consuming_on_failure() { |
| 1037 | 1079 | let fixture = Fixture::new(); |
| 1038 | 1080 | let input = fixture.code("one", "owned-code"); |