kych

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

commit 07f18ff7bf19595900a2921d47d6d57f1abdadf6
parent 7c2ebcc2b94552b74e4ae7283de68c6d6d774d97
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Mon, 19 Jan 2026 18:31:35 +0100

Enforce redirect_uri binding in token exchange

Bind authorization codes to the original redirect_uri and validate it during
token exchange. The token endpoint now requires redirect_uri and rejects
mismatches or unexpected values, preventing authorization code interception per
RFC 6749, 4.1.3.

Diffstat:
Mkych_oauth2_gateway/src/db/authorization_codes.rs | 5++++-
Mkych_oauth2_gateway/src/handlers.rs | 28++++++++++++++++++++++++++++
Mkych_oauth2_gateway/src/models.rs | 4+---
3 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/kych_oauth2_gateway/src/db/authorization_codes.rs b/kych_oauth2_gateway/src/db/authorization_codes.rs @@ -27,6 +27,7 @@ pub struct CodeExchangeData { pub session_status: SessionStatus, pub existing_token: Option<String>, pub existing_token_expires_at: Option<DateTime<Utc>>, + pub redirect_uri: Option<String>, } /// Create a new authorization code for a session @@ -85,7 +86,8 @@ pub async fn get_code_for_token_exchange( vs.client_id, vs.status AS session_status, at.token AS existing_token, - at.expires_at AS existing_token_expires_at + at.expires_at AS existing_token_expires_at, + vs.redirect_uri FROM updated_code uc JOIN code_data cd ON uc.id = cd.id JOIN oauth2gw.verification_sessions vs ON vs.id = uc.session_id @@ -107,6 +109,7 @@ pub async fn get_code_for_token_exchange( session_status: row.get("session_status"), existing_token: row.get("existing_token"), existing_token_expires_at: row.get("existing_token_expires_at"), + redirect_uri: row.get("redirect_uri"), } })) } diff --git a/kych_oauth2_gateway/src/handlers.rs b/kych_oauth2_gateway/src/handlers.rs @@ -545,6 +545,34 @@ pub async fn token( )); } + // RFC 6749 Section 4.1.3: If redirect_uri was included in the authorization request, + // it MUST be present and match exactly in the token request + match &data.redirect_uri { + Some(stored_uri) if stored_uri != &request.redirect_uri => { + tracing::warn!( + "redirect_uri mismatch for code {}: expected '{}', got '{}'", + request.code, + stored_uri, + request.redirect_uri + ); + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse::new("invalid_grant")), + )); + } + None => { + tracing::warn!( + "redirect_uri provided in token request but was not stored during authorization for code {}", + request.code + ); + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse::new("invalid_grant")), + )); + } + Some(_) => {} + } + // Check for existing token if let Some(existing_token) = data.existing_token { tracing::info!( diff --git a/kych_oauth2_gateway/src/models.rs b/kych_oauth2_gateway/src/models.rs @@ -51,15 +51,13 @@ pub struct AuthorizeResponse { pub state: String, } -// Token endpoint -// WARING: RFC 6749 also requires: -// - redirect_uri #[derive(Debug, Deserialize, Serialize)] pub struct TokenRequest { pub grant_type: String, pub code: String, pub client_id: String, pub client_secret: String, + pub redirect_uri: String, } #[derive(Debug, Deserialize, Serialize)]