kych

OAuth 2.0 API for Swiyu to enable Taler integration of Swiyu for KYC (experimental)
Log | Files | Refs | README

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:
Mkych_oauth2_gateway/kych.conf.example | 1+
Mkych_oauth2_gateway/src/config.rs | 26++++++++++++++++++++++++++
Mkych_oauth2_gateway/src/handlers.rs | 22++++++++++++++++++++++
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);