kych

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

commit c90333cab497dcf2d801255023502464dd76b0ce
parent 8190ae9da8df13436f0e9d361ed07c72a75feb92
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Sun,  7 Dec 2025 16:10:34 +0100

oauth2_gateway: change /setup scope to /authorize, add setup bearer token validation

Diffstat:
Moauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql | 5++++-
Moauth2_gateway/src/bin/client_management_cli.rs | 22++++++++++++++++++++--
Moauth2_gateway/src/db/clients.rs | 50+++++++++++++++++++++++++++++++++++++++-----------
Moauth2_gateway/src/db/sessions.rs | 6++----
Moauth2_gateway/src/handlers.rs | 418+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Moauth2_gateway/src/main.rs | 2+-
Moauth2_gateway/src/models.rs | 8++------
7 files changed, 351 insertions(+), 160 deletions(-)

diff --git a/oauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql b/oauth2_gateway/oauth2_gatewaydb/oauth2gw-0001.sql @@ -14,8 +14,9 @@ CREATE TABLE IF NOT EXISTS clients ( client_id VARCHAR(255) UNIQUE NOT NULL, secret_hash VARCHAR(255) NOT NULL, webhook_url TEXT NOT NULL, - verifier_url TEXT NOT NULL, + verifier_url TEXT NOT NULL, verifier_management_api_path VARCHAR(255) DEFAULT '/management/api/verifications', + scope TEXT NOT NULL DEFAULT 'age_over_18', created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); @@ -31,6 +32,8 @@ COMMENT ON COLUMN clients.verifier_url IS 'Client URL where oauth2 gateway will callback'; COMMENT ON COLUMN clients.verifier_management_api_path IS 'Swiyu verifier api endpoint to create verification requests'; +COMMENT ON COLUMN clients.scope + IS 'Space-delimited OAuth2 scope (requested verification attributes, e.g., "first_name last_name age_over_18")'; CREATE INDEX IF NOT EXISTS idx_clients_client_id ON clients(client_id); diff --git a/oauth2_gateway/src/bin/client_management_cli.rs b/oauth2_gateway/src/bin/client_management_cli.rs @@ -54,6 +54,10 @@ enum Commands { /// Verifier management API path (default: /management/api/verifications) #[arg(long)] verifier_api_path: Option<String>, + + /// OAuth2 scope (space-delimited attributes, default: "age_over_18") + #[arg(long)] + scope: Option<String>, }, Update { @@ -67,6 +71,9 @@ enum Commands { #[arg(long)] verifier_api_path: Option<String>, + + #[arg(long)] + scope: Option<String>, }, /// Delete a client (WARNING: cascades to all sessions) @@ -101,6 +108,7 @@ async fn main() -> Result<()> { webhook_url, verifier_url, verifier_api_path, + scope, } => { cmd_create_client( &pool, @@ -109,6 +117,7 @@ async fn main() -> Result<()> { &webhook_url, &verifier_url, verifier_api_path.as_deref(), + scope.as_deref(), ) .await? } @@ -117,6 +126,7 @@ async fn main() -> Result<()> { webhook_url, verifier_url, verifier_api_path, + scope, } => { cmd_update_client( &pool, @@ -124,6 +134,7 @@ async fn main() -> Result<()> { webhook_url.as_deref(), verifier_url.as_deref(), verifier_api_path.as_deref(), + scope.as_deref(), ) .await? } @@ -170,6 +181,7 @@ async fn cmd_show_client(pool: &sqlx::PgPool, client_id: &str) -> Result<()> { println!("Webhook URL: {}", client.webhook_url); println!("Verifier URL: {}", client.verifier_url); println!("Verifier API Path: {}", client.verifier_management_api_path); + println!("Scope: {}", client.scope); println!("Created: {}", client.created_at); println!("Updated: {}", client.updated_at); @@ -183,6 +195,7 @@ async fn cmd_create_client( webhook_url: &str, verifier_url: &str, verifier_api_path: Option<&str>, + scope: Option<&str>, ) -> Result<()> { let client = db::clients::register_client( pool, @@ -191,6 +204,7 @@ async fn cmd_create_client( webhook_url, verifier_url, verifier_api_path, + scope, ) .await .context("Failed to create client")?; @@ -200,6 +214,7 @@ async fn cmd_create_client( println!("UUID: {}", client.id); println!("Client ID: {}", client.client_id); println!("Webhook URL: {}", client.webhook_url); + println!("Scope: {}", client.scope); Ok(()) } @@ -210,9 +225,10 @@ async fn cmd_update_client( webhook_url: Option<&str>, verifier_url: Option<&str>, verifier_api_path: Option<&str>, + scope: Option<&str>, ) -> Result<()> { - if webhook_url.is_none() && verifier_url.is_none() && verifier_api_path.is_none() { - anyhow::bail!("No fields to update. Specify at least one of: --webhook-url, --verifier-url, --verifier-api-path"); + if webhook_url.is_none() && verifier_url.is_none() && verifier_api_path.is_none() && scope.is_none() { + anyhow::bail!("No fields to update. Specify at least one of: --webhook-url, --verifier-url, --verifier-api-path, --scope"); } let client = db::clients::get_client_by_id(pool, client_id) @@ -225,6 +241,7 @@ async fn cmd_update_client( webhook_url, verifier_url, verifier_api_path, + scope, ) .await .context("Failed to update client")?; @@ -236,6 +253,7 @@ async fn cmd_update_client( println!("Webhook URL: {}", updated.webhook_url); println!("Verifier URL: {}", updated.verifier_url); println!("Verifier API Path: {}", updated.verifier_management_api_path); + println!("Scope: {}", updated.scope); Ok(()) } diff --git a/oauth2_gateway/src/db/clients.rs b/oauth2_gateway/src/db/clients.rs @@ -14,6 +14,7 @@ pub struct Client { pub webhook_url: String, pub verifier_url: String, pub verifier_management_api_path: String, + pub scope: String, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, } @@ -26,19 +27,21 @@ pub async fn register_client( webhook_url: &str, verifier_url: &str, verifier_management_api_path: Option<&str>, + scope: Option<&str>, ) -> Result<Client> { let api_path = verifier_management_api_path .unwrap_or("/management/api/verifications"); + let scope_value = scope.unwrap_or("age_over_18"); let secret_hash = bcrypt::hash(client_secret, bcrypt::DEFAULT_COST)?; let client = sqlx::query_as::<_, Client>( r#" INSERT INTO oauth2gw.clients - (client_id, secret_hash, webhook_url, verifier_url, verifier_management_api_path) - VALUES ($1, $2, $3, $4, $5) + (client_id, secret_hash, webhook_url, verifier_url, verifier_management_api_path, scope) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, client_id, secret_hash, webhook_url, verifier_url, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, scope, created_at, updated_at "# ) .bind(client_id) @@ -46,6 +49,7 @@ pub async fn register_client( .bind(webhook_url) .bind(verifier_url) .bind(api_path) + .bind(scope_value) .fetch_one(pool) .await?; @@ -60,7 +64,7 @@ pub async fn get_client_by_id( let client = sqlx::query_as::<_, Client>( r#" SELECT id, client_id, secret_hash, webhook_url, verifier_url, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, scope, created_at, updated_at FROM oauth2gw.clients WHERE client_id = $1 "# @@ -80,7 +84,7 @@ pub async fn get_client_by_uuid( let client = sqlx::query_as::<_, Client>( r#" SELECT id, client_id, secret_hash, webhook_url, verifier_url, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, scope, created_at, updated_at FROM oauth2gw.clients WHERE id = $1 "# @@ -92,6 +96,27 @@ pub async fn get_client_by_uuid( Ok(client) } +/// Get client secret hash by client_id +/// +/// Returns the secret hash if client exists, None otherwise +pub async fn get_client_secret_hash( + pool: &PgPool, + client_id: &str, +) -> Result<Option<String>> { + let result = sqlx::query_scalar::<_, String>( + r#" + SELECT secret_hash + FROM oauth2gw.clients + WHERE client_id = $1 + "# + ) + .bind(client_id) + .fetch_optional(pool) + .await?; + + Ok(result) +} + /// Authenticate a client by validating client_secret /// /// Returns the client if authentication succeeds, None otherwise @@ -103,7 +128,7 @@ pub async fn authenticate_client( let client = sqlx::query_as::<_, Client>( r#" SELECT id, client_id, secret_hash, webhook_url, verifier_url, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, scope, created_at, updated_at FROM oauth2gw.clients WHERE client_id = $1 "# @@ -113,7 +138,7 @@ pub async fn authenticate_client( .await?; match client { - Some(c) => { + Some(c) => { if bcrypt::verify(client_secret, &c.secret_hash)? { Ok(Some(c)) } else { @@ -131,8 +156,8 @@ pub async fn update_client( webhook_url: Option<&str>, verifier_url: Option<&str>, verifier_management_api_path: Option<&str>, + scope: Option<&str>, ) -> Result<Client> { - // Get current client to use for fields that aren't being updated let current = get_client_by_uuid(pool, id).await? .ok_or_else(|| anyhow::anyhow!("Client not found"))?; @@ -140,6 +165,7 @@ pub async fn update_client( let new_verifier_url = verifier_url.unwrap_or(&current.verifier_url); let new_verifier_api_path = verifier_management_api_path .unwrap_or(&current.verifier_management_api_path); + let new_scope = scope.unwrap_or(&current.scope); let client = sqlx::query_as::<_, Client>( r#" @@ -148,15 +174,17 @@ pub async fn update_client( webhook_url = $1, verifier_url = $2, verifier_management_api_path = $3, + scope = $4, updated_at = CURRENT_TIMESTAMP - WHERE id = $4 + WHERE id = $5 RETURNING id, client_id, secret_hash, webhook_url, verifier_url, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, scope, created_at, updated_at "# ) .bind(new_webhook_url) .bind(new_verifier_url) .bind(new_verifier_api_path) + .bind(new_scope) .bind(id) .fetch_one(pool) .await?; @@ -189,7 +217,7 @@ pub async fn list_clients(pool: &PgPool) -> Result<Vec<Client>> { let clients = sqlx::query_as::<_, Client>( r#" SELECT id, client_id, secret_hash, webhook_url, verifier_url, - verifier_management_api_path, created_at, updated_at + verifier_management_api_path, scope, created_at, updated_at FROM oauth2gw.clients ORDER BY created_at DESC "# diff --git a/oauth2_gateway/src/db/sessions.rs b/oauth2_gateway/src/db/sessions.rs @@ -82,16 +82,15 @@ pub async fn create_session( pool: &PgPool, client_id: &str, nonce: &str, - scope: &str, expires_in_minutes: i64, ) -> Result<Option<VerificationSession>> { let session = sqlx::query_as::<_, VerificationSession>( r#" INSERT INTO oauth2gw.verification_sessions (client_id, nonce, scope, expires_at, status) - SELECT c.id, $1, $2, NOW() + $3 * INTERVAL '1 minute', 'pending' + SELECT c.id, $1, c.scope, NOW() + $2 * INTERVAL '1 minute', 'pending' FROM oauth2gw.clients c - WHERE c.client_id = $4 + WHERE c.client_id = $3 RETURNING id, client_id, nonce, scope, verification_url, request_id, verifier_nonce, status, created_at, authorized_at, @@ -99,7 +98,6 @@ pub async fn create_session( "# ) .bind(nonce) - .bind(scope) .bind(expires_in_minutes) .bind(client_id) .fetch_optional(pool) diff --git a/oauth2_gateway/src/handlers.rs b/oauth2_gateway/src/handlers.rs @@ -1,18 +1,13 @@ use axum::{ + Json, extract::{Path, Query, State}, - http::{header, StatusCode}, + http::{StatusCode, header}, response::IntoResponse, - Json, }; -use serde_json::json; use chrono::Utc; +use serde_json::json; -use crate::{ - db::sessions::SessionStatus, - models::*, - state::AppState, - crypto, -}; +use crate::{crypto, db::sessions::SessionStatus, models::*, state::AppState}; // Health check endpoint pub async fn health_check() -> impl IntoResponse { @@ -27,110 +22,189 @@ pub async fn health_check() -> impl IntoResponse { pub async fn setup( State(state): State<AppState>, Path(client_id): Path<String>, - Json(request): Json<SetupRequest>, + headers: axum::http::HeaderMap, ) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> { + tracing::info!("Setup request for client: {}", client_id); - tracing::info!("Setup request for client: {}, scope: {}", client_id, request.scope); + let auth_header = headers + .get(header::AUTHORIZATION) + .and_then(|h| h.to_str().ok()); - let nonce = crypto::generate_nonce(); + let bearer_token = match auth_header { + Some(h) if h.starts_with("Bearer ") => &h[7..], + _ => { + tracing::warn!( + "Missing or malformed Authorization header for client: {}", + client_id + ); + return Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse::new("unauthorized")), + )); + } + }; - tracing::debug!("Generated nonce: {}", nonce); + let secret_hash = crate::db::clients::get_client_secret_hash(&state.pool, &client_id) + .await + .map_err(|e| { + tracing::error!("DB error fetching client secret: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new("internal_error")), + ) + })?; - let session = crate::db::sessions::create_session( - &state.pool, - &client_id, - &nonce, - &request.scope, - 15, // 15 minutes expiration - ).await - .map_err(|e| { - tracing::error!("Failed to create session: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error"))) + let secret_hash = match secret_hash { + Some(hash) => hash, + None => { + tracing::warn!("Client not found: {}", client_id); + return Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse::new("unauthorized")), + )); + } + }; + + let is_valid = bcrypt::verify(bearer_token, &secret_hash).map_err(|e| { + tracing::error!("Bcrypt verification error: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new("internal_error")), + ) })?; + if !is_valid { + tracing::warn!("Invalid bearer token for client: {}", client_id); + return Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse::new("unauthorized")), + )); + } + + let nonce = crypto::generate_nonce(); + + tracing::debug!("Generated nonce: {}", nonce); + + let session = crate::db::sessions::create_session(&state.pool, &client_id, &nonce, 15) + .await + .map_err(|e| { + tracing::error!("Failed to create session: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new("internal_error")), + ) + })?; let session = match session { Some(s) => s, None => { tracing::warn!("Client not found: {}", client_id); - return Err((StatusCode::NOT_FOUND, Json(ErrorResponse::new("client_not_found")))) + return Err(( + StatusCode::NOT_FOUND, + Json(ErrorResponse::new("client_not_found")), + )); } }; - tracing::info!("Created session {} for client {} with nonce {}", session.id, client_id, nonce); + tracing::info!( + "Created session {} for client {} with nonce {}", + session.id, + client_id, + nonce + ); Ok((StatusCode::OK, Json(SetupResponse { nonce }))) } -// GET /authorize?response_type=code&client_id={client_id}&nonce={nonce} +// GET /authorize/{nonce}?response_type=code&client_id={client_id}&redirect_uri={uri}&state={state} pub async fn authorize( State(state): State<AppState>, + Path(nonce): Path<String>, Query(params): Query<AuthorizeQuery>, ) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> { + tracing::info!( + "Authorize request for client: {}, nonce: {}", + params.client_id, + nonce + ); - tracing::info!("Authorize request for client: {}, nonce: {}",params.client_id, params.nonce); - - - // Validate response_type if params.response_type != "code" { - return Err((StatusCode::BAD_REQUEST, - Json(ErrorResponse::new("invalid_request")))); + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse::new("invalid_request")), + )); } - - // Fetch session and client data (idempotent) let session_data = crate::db::sessions::get_session_for_authorize( &state.pool, - &params.nonce, + &nonce, &params.client_id, - ).await + ) + .await .map_err(|e| { tracing::error!("DB error in authorize: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error"))) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new("internal_error")), + ) })?; - let data = match session_data { Some(d) => d, None => { - tracing::warn!("Session not found for nonce: {}", params.nonce); - return Err((StatusCode::NOT_FOUND, - Json(ErrorResponse::new("session_not_found")))); + tracing::warn!("Session not found for nonce: {}", nonce); + return Err(( + StatusCode::NOT_FOUND, + Json(ErrorResponse::new("session_not_found")), + )); } }; - // Backend validation if data.expires_at < Utc::now() { tracing::warn!("Session expired: {}", data.session_id); - return Err((StatusCode::GONE, Json(ErrorResponse::new("session_expired")))); + return Err(( + StatusCode::GONE, + Json(ErrorResponse::new("session_expired")), + )); } - // Check status for idempotency match data.status { SessionStatus::Authorized => { // Already authorized - return cached response - tracing::info!("Session {} already authorized, returning cached response", data.session_id); + tracing::info!( + "Session {} already authorized, returning cached response", + data.session_id + ); - let verification_id = data.request_id + let verification_id = data + .request_id .and_then(|id| uuid::Uuid::parse_str(&id).ok()) .unwrap_or(uuid::Uuid::nil()); - return Ok((StatusCode::OK, Json(AuthorizeResponse { - verification_id, - verification_url: data.verification_url.unwrap_or_default(), - }))); + return Ok(( + StatusCode::OK, + Json(AuthorizeResponse { + verification_id, + verification_url: data.verification_url.unwrap_or_default(), + }), + )); } SessionStatus::Pending => { // Proceed with authorization } _ => { - tracing::warn!("Session {} in invalid status: {:?}", - data.session_id, data.status); - return Err((StatusCode::CONFLICT, - Json(ErrorResponse::new("invalid_session_status")))); + tracing::warn!( + "Session {} in invalid status: {:?}", + data.session_id, + data.status + ); + return Err(( + StatusCode::CONFLICT, + Json(ErrorResponse::new("invalid_session_status")), + )); } } @@ -151,38 +225,43 @@ pub async fn authorize( dcql_query: None, }; - - tracing::debug!("Swiyu verifier request: {}", serde_json::to_string_pretty(&verifier_request).unwrap()); + tracing::debug!( + "Swiyu verifier request: {}", + serde_json::to_string_pretty(&verifier_request).unwrap() + ); tracing::debug!("Calling Swiyu verifier at: {}", verifier_url); - - let verifier_response = state.http_client + let verifier_response = state + .http_client .post(&verifier_url) .json(&verifier_request) .send() .await .map_err(|e| { tracing::error!("Failed to call Swiyu verifier: {}", e); - (StatusCode::BAD_GATEWAY, Json(ErrorResponse::new("verifier_unavailable"))) + ( + StatusCode::BAD_GATEWAY, + Json(ErrorResponse::new("verifier_unavailable")), + ) })?; - if !verifier_response.status().is_success() { let status = verifier_response.status(); let body = verifier_response.text().await.unwrap_or_default(); tracing::error!("Swiyu verifier returned error {}: {}", status, body); - return Err((StatusCode::BAD_GATEWAY, Json(ErrorResponse::new("verifier_error")))); + return Err(( + StatusCode::BAD_GATEWAY, + Json(ErrorResponse::new("verifier_error")), + )); } - - let swiyu_response: SwiyuManagementResponse = verifier_response - .json() - .await - .map_err(|e| { - tracing::error!("Failed to parse Swiyu response: {}", e); - (StatusCode::BAD_GATEWAY, Json(ErrorResponse::new("verifier_invalid_response"))) - })?; - + let swiyu_response: SwiyuManagementResponse = verifier_response.json().await.map_err(|e| { + tracing::error!("Failed to parse Swiyu response: {}", e); + ( + StatusCode::BAD_GATEWAY, + Json(ErrorResponse::new("verifier_invalid_response")), + ) + })?; // Update session with verifier data let result = crate::db::sessions::update_session_authorized( @@ -191,32 +270,44 @@ pub async fn authorize( &swiyu_response.verification_url, &swiyu_response.id.to_string(), swiyu_response.request_nonce.as_deref(), - ).await + ) + .await .map_err(|e| { tracing::error!("Failed to update session: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error"))) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new("internal_error")), + ) })?; + tracing::info!( + "Session {} authorized, verification_id: {}", + data.session_id, + swiyu_response.id + ); - tracing::info!("Session {} authorized, verification_id: {}", - data.session_id, swiyu_response.id); - - Ok((StatusCode::OK, Json(AuthorizeResponse { - verification_id: swiyu_response.id, - verification_url: result.verification_url, - }))) + Ok(( + StatusCode::OK, + Json(AuthorizeResponse { + verification_id: swiyu_response.id, + verification_url: result.verification_url, + }), + )) } /// Build a presentation definition from a space-delimited scope string /// /// Example: "age_over_18" or "first_name last_name" fn build_presentation_definition(scope: &str) -> PresentationDefinition { - use uuid::Uuid; use std::collections::HashMap; + use uuid::Uuid; let attributes: Vec<&str> = scope.split_whitespace().collect(); - tracing::debug!("Building presentation definition for attributes: {:?}", attributes); + tracing::debug!( + "Building presentation definition for attributes: {:?}", + attributes + ); // First field: $.vct with filter for credential type let vct_field = Field { @@ -230,7 +321,6 @@ fn build_presentation_definition(scope: &str) -> PresentationDefinition { }), }; - // Attribute fields from scope let mut fields: Vec<Field> = vec![vct_field]; for attr in &attributes { @@ -243,7 +333,6 @@ fn build_presentation_definition(scope: &str) -> PresentationDefinition { }); } - let mut format = HashMap::new(); format.insert( "vc+sd-jwt".to_string(), @@ -275,14 +364,13 @@ pub async fn token( State(state): State<AppState>, Json(request): Json<TokenRequest>, ) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> { - tracing::info!("Token request for code: {}", request.code); // Validate grant_type if request.grant_type != "authorization_code" { return Err(( StatusCode::BAD_REQUEST, - Json(ErrorResponse::new("unsupported_grant_type")) + Json(ErrorResponse::new("unsupported_grant_type")), )); } @@ -291,69 +379,100 @@ pub async fn token( &state.pool, &request.client_id, &request.client_secret, - ).await + ) + .await .map_err(|e| { tracing::error!("DB error during client authentication: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error"))) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new("internal_error")), + ) })?; let client = match client { Some(c) => c, None => { tracing::warn!("Client authentication failed for {}", request.client_id); - return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse::new("invalid_client")))); + return Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse::new("invalid_client")), + )); } }; - // Fetch code (idempotent) - let code_data = crate::db::authorization_codes::get_code_for_token_exchange( - &state.pool, - &request.code, - ).await - .map_err(|e| { - tracing::error!("DB error in token exchange: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error"))) - })?; + let code_data = + crate::db::authorization_codes::get_code_for_token_exchange(&state.pool, &request.code) + .await + .map_err(|e| { + tracing::error!("DB error in token exchange: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new("internal_error")), + ) + })?; let data = match code_data { Some(d) => d, None => { tracing::warn!("Authorization code not found or expired: {}", request.code); - return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse::new("invalid_grant")))); + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse::new("invalid_grant")), + )); } }; // Verify the authorization code belongs to the client if data.client_id != client.id { - tracing::warn!("Authorization code {} does not belong to the client {}", - request.code, request.client_id); + tracing::warn!( + "Authorization code {} does not belong to the client {}", + request.code, + request.client_id + ); - return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse::new("invalid_grant")))); + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse::new("invalid_grant")), + )); } // Check for existing token if let Some(existing_token) = data.existing_token { - tracing::info!("Token already exists for session {}, returning cached response", - data.session_id); - return Ok((StatusCode::OK, Json(TokenResponse { - access_token: existing_token, - token_type: "Bearer".to_string(), - expires_in: 3600, - }))); + tracing::info!( + "Token already exists for session {}, returning cached response", + data.session_id + ); + return Ok(( + StatusCode::OK, + Json(TokenResponse { + access_token: existing_token, + token_type: "Bearer".to_string(), + expires_in: 3600, + }), + )); } // Check if code was already used if data.was_already_used { tracing::warn!("Authorization code {} was already used", request.code); - return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse::new("invalid_grant")))); + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse::new("invalid_grant")), + )); } // Validate session status if data.session_status != SessionStatus::Verified { - tracing::warn!("Session {} not in verified status: {:?}", - data.session_id, data.session_status); - return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse::new("invalid_grant")))); + tracing::warn!( + "Session {} not in verified status: {:?}", + data.session_id, + data.session_status + ); + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse::new("invalid_grant")), + )); } // Generate new token and complete session @@ -363,19 +482,26 @@ pub async fn token( data.session_id, &access_token, 3600, // 1 hour - ).await + ) + .await .map_err(|e| { tracing::error!("Failed to create token: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse::new("internal_error"))) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new("internal_error")), + ) })?; tracing::info!("Token created for session {}", data.session_id); - Ok((StatusCode::OK, Json(TokenResponse { - access_token: token.token, - token_type: "Bearer".to_string(), - expires_in: 3600, - }))) + Ok(( + StatusCode::OK, + Json(TokenResponse { + access_token: token.token, + token_type: "Bearer".to_string(), + expires_in: 3600, + }), + )) } // GET /info @@ -394,8 +520,10 @@ pub async fn info( Some(h) if h.starts_with("Bearer ") => &h[7..], _ => { tracing::warn!("Missing or malformed Authorization header"); - return Err((StatusCode::UNAUTHORIZED, - Json(ErrorResponse::new("invalid_token")))); + return Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse::new("invalid_token")), + )); } }; @@ -404,30 +532,38 @@ pub async fn info( .await .map_err(|e| { tracing::error!("DB error in info: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse::new("internal_error"))) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new("internal_error")), + ) })?; let data = match token_data { Some(d) => d, None => { tracing::warn!("Token not found or expired"); - return Err((StatusCode::UNAUTHORIZED, - Json(ErrorResponse::new("invalid_token")))); + return Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse::new("invalid_token")), + )); } }; // Validate token if data.revoked { tracing::warn!("Token {} is revoked", data.token_id); - return Err((StatusCode::UNAUTHORIZED, - Json(ErrorResponse::new("invalid_token")))); + return Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse::new("invalid_token")), + )); } if data.session_status != SessionStatus::Completed { tracing::warn!("Session not completed: {:?}", data.session_status); - return Err((StatusCode::UNAUTHORIZED, - Json(ErrorResponse::new("invalid_token")))); + return Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse::new("invalid_token")), + )); } // Return verifiable credential @@ -456,10 +592,15 @@ pub async fn notification_webhook( let session_data = match crate::db::sessions::get_session_for_notification( &state.pool, &webhook.verification_id.to_string(), - ).await { + ) + .await + { Ok(Some(data)) => data, Ok(None) => { - tracing::warn!("Session not found for verification_id: {}", webhook.verification_id); + tracing::warn!( + "Session not found for verification_id: {}", + webhook.verification_id + ); return StatusCode::OK; } Err(e) => { @@ -472,7 +613,8 @@ pub async fn notification_webhook( if session_data.status != SessionStatus::Authorized { tracing::warn!( "Session {} not in authorized status: {:?}", - session_data.session_id, session_data.status + session_data.session_id, + session_data.status ); return StatusCode::OK; } @@ -514,7 +656,10 @@ pub async fn notification_webhook( SwiyuVerificationStatus::Success => (SessionStatus::Verified, "verified"), SwiyuVerificationStatus::Failed => (SessionStatus::Failed, "failed"), SwiyuVerificationStatus::Pending => { - tracing::info!("Verification {} still pending, ignoring webhook", webhook.verification_id); + tracing::info!( + "Verification {} still pending, ignoring webhook", + webhook.verification_id + ); return StatusCode::OK; } }; @@ -550,11 +695,14 @@ pub async fn notification_webhook( &session_data.webhook_url, &webhook_body, swiyu_result.wallet_response.as_ref(), - ).await { + ) + .await + { Ok(code) => { tracing::info!( "Session {} updated to {}, auth code created, webhook queued", - session_data.session_id, status_str + session_data.session_id, + status_str ); tracing::debug!("Generated authorization code: {}", code); } diff --git a/oauth2_gateway/src/main.rs b/oauth2_gateway/src/main.rs @@ -42,7 +42,7 @@ async fn main() -> Result<()> { let app = Router::new() .route("/health", get(handlers::health_check)) .route("/setup/{client_id}", post(handlers::setup)) - .route("/authorize", get(handlers::authorize)) + .route("/authorize/{nonce}", get(handlers::authorize)) .route("/token", post(handlers::token)) .route("/info", get(handlers::info)) .route("/notification", post(handlers::notification_webhook)) diff --git a/oauth2_gateway/src/models.rs b/oauth2_gateway/src/models.rs @@ -3,11 +3,6 @@ use uuid::Uuid; use std::collections::HashMap; #[derive(Debug, Deserialize, Serialize)] -pub struct SetupRequest { - pub scope: String, -} - -#[derive(Debug, Deserialize, Serialize)] pub struct SetupResponse { pub nonce: String, } @@ -16,7 +11,8 @@ pub struct SetupResponse { pub struct AuthorizeQuery { pub response_type: String, pub client_id: String, - pub nonce: String, + pub redirect_uri: String, + pub state: String, } #[derive(Debug, Deserialize, Serialize)]