commit c1a72c659fb4be558d2e2f78e7821a525ce41888
parent 1aa236021a718960cd37e439c41f1e96ae35194e
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date: Mon, 19 Jan 2026 22:28:57 +0100
Add allowed scopes to kych.conf
Scope of verification request is validated against the configured scopes.
Diffstat:
3 files changed, 49 insertions(+), 0 deletions(-)
diff --git a/kych_oauth2_gateway/kych.conf.example b/kych_oauth2_gateway/kych.conf.example
@@ -8,6 +8,7 @@ NONCE_BYTES = 32
TOKEN_BYTES = 32
AUTH_CODE_BYTES = 32
AUTH_CODE_TTL_MINUTES = 10
+#ALLOWED_SCOPES = {family_name, given_name, birth_date}
# ---- Clients (one section per client) ----
diff --git a/kych_oauth2_gateway/src/config.rs b/kych_oauth2_gateway/src/config.rs
@@ -10,6 +10,7 @@ pub struct Config {
pub server: ServerConfig,
pub database: DatabaseConfig,
pub crypto: CryptoConfig,
+ pub allowed_scopes: Option<Vec<String>>,
pub clients: Vec<ClientConfig>,
}
@@ -138,6 +139,11 @@ impl Config {
.context("Invalid AUTH_CODE_TTL_MINUTES")?,
};
+ let allowed_scopes = match main_section.get("ALLOWED_SCOPES") {
+ Some(raw) if !raw.trim().is_empty() => Some(parse_allowed_scopes(raw)?),
+ _ => None,
+ };
+
let mut clients = Vec::new();
for (section_name, properties) in ini.iter() {
let section_name = match section_name {
@@ -180,7 +186,27 @@ impl Config {
server,
database,
crypto,
+ allowed_scopes,
clients,
})
}
}
+
+fn parse_allowed_scopes(raw: &str) -> Result<Vec<String>> {
+ let trimmed = raw.trim();
+ let trimmed = trimmed.strip_prefix('{').unwrap_or(trimmed);
+ let trimmed = trimmed.strip_suffix('}').unwrap_or(trimmed);
+
+ let scopes: Vec<String> = trimmed
+ .split(|c: char| c == ',' || c.is_whitespace())
+ .map(|s| s.trim())
+ .filter(|s| !s.is_empty())
+ .map(|s| s.to_string())
+ .collect();
+
+ if scopes.is_empty() {
+ anyhow::bail!("ALLOWED_SCOPES must contain at least one scope");
+ }
+
+ Ok(scopes)
+}
diff --git a/kych_oauth2_gateway/src/handlers.rs b/kych_oauth2_gateway/src/handlers.rs
@@ -319,6 +319,28 @@ pub async fn authorize(
}
}
+ if let Some(allowed_scopes) = state.config.allowed_scopes.as_ref() {
+ let allowed_set: std::collections::HashSet<&str> =
+ allowed_scopes.iter().map(String::as_str).collect();
+ let invalid_scopes: Vec<&str> = data
+ .scope
+ .split_whitespace()
+ .filter(|scope| !allowed_set.contains(*scope))
+ .collect();
+
+ if !invalid_scopes.is_empty() {
+ tracing::warn!(
+ "Rejected invalid scopes for client {}: {:?}",
+ params.client_id,
+ invalid_scopes
+ );
+ return Err((
+ StatusCode::BAD_REQUEST,
+ Json(ErrorResponse::new("invalid_scope")),
+ ));
+ }
+ }
+
// Build presentation definition from scope
let presentation_definition = build_presentation_definition(&data.scope);