kych

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

sessions.rs (10078B)


      1 // Database operations for verification_sessions table
      2 
      3 use sqlx::PgPool;
      4 use anyhow::Result;
      5 use uuid::Uuid;
      6 use chrono::{DateTime, Utc};
      7 
      8 /// Status of a verification session
      9 #[derive(Debug, Clone, sqlx::Type, serde::Serialize, serde::Deserialize, PartialEq)]
     10 #[sqlx(type_name = "varchar")]
     11 pub enum SessionStatus {
     12     #[sqlx(rename = "pending")]
     13     Pending,
     14     #[sqlx(rename = "authorized")]
     15     Authorized,
     16     #[sqlx(rename = "verified")]
     17     Verified,
     18     #[sqlx(rename = "completed")]
     19     Completed,
     20     #[sqlx(rename = "expired")]
     21     Expired,
     22     #[sqlx(rename = "failed")]
     23     Failed,
     24 }
     25 
     26 /// Verification session record used in /setup endpoint
     27 #[derive(Debug, Clone, sqlx::FromRow)]
     28 pub struct VerificationSession {
     29     pub id: Uuid,
     30     pub client_id: Uuid,
     31     pub nonce: String,
     32     pub scope: String,
     33     pub redirect_uri: Option<String>,
     34     pub state: Option<String>,
     35     pub verification_url: Option<String>,
     36     pub request_id: Option<String>,
     37     pub verifier_nonce: Option<String>,
     38     pub status: SessionStatus,
     39     pub created_at: DateTime<Utc>,
     40     pub authorized_at: Option<DateTime<Utc>>,
     41     pub verified_at: Option<DateTime<Utc>>,
     42     pub completed_at: Option<DateTime<Utc>>,
     43     pub failed_at: Option<DateTime<Utc>>,
     44     pub expires_at: DateTime<Utc>,
     45 }
     46 
     47 /// Authorize record data used in /authorize endpoint
     48 #[derive(Debug, Clone)]
     49 pub struct AuthorizeSessionData {
     50     // Session fields
     51     pub session_id: Uuid,
     52     pub status: SessionStatus,
     53     pub expires_at: DateTime<Utc>,
     54     pub scope: String,
     55     pub redirect_uri: Option<String>,
     56     pub state: Option<String>,
     57     pub verification_url: Option<String>,
     58     pub verification_deeplink: Option<String>,
     59     pub request_id: Option<String>,
     60     pub verifier_nonce: Option<String>,
     61     // Client fields
     62     pub verifier_url: String,
     63     pub verifier_management_api_path: String,
     64 }
     65 
     66 /// Notification record data used in /notification webhook endpoint
     67 #[derive(Debug, Clone)]
     68 pub struct NotificationSessionData {
     69     // Session fields
     70     pub session_id: Uuid,
     71     pub nonce: String,
     72     pub status: SessionStatus,
     73     pub redirect_uri: Option<String>,
     74     pub state: Option<String>,
     75     // Client fields
     76     pub client_id: Uuid,
     77     pub webhook_url: String,
     78     pub verifier_url: String,
     79     pub verifier_management_api_path: String,
     80 }
     81 
     82 
     83 /// Create a new verification session
     84 ///
     85 /// Returns None if client_id doesn't exist
     86 ///
     87 /// Used by the /setup endpoint
     88 pub async fn create_session(
     89     pool: &PgPool,
     90     client_id: &str,
     91     nonce: &str,
     92     expires_in_minutes: i64,
     93 ) -> Result<Option<VerificationSession>> {
     94     let session = sqlx::query_as::<_, VerificationSession>(
     95         r#"
     96         INSERT INTO oauth2gw.verification_sessions (client_id, nonce, scope,
     97                                                             expires_at, status)
     98         SELECT c.id, $1, '', NOW() + $2 * INTERVAL '1 minute', 'pending'
     99         FROM oauth2gw.clients c
    100         WHERE c.client_id = $3
    101         RETURNING
    102             id, client_id, nonce, scope, redirect_uri, state, verification_url,
    103             request_id, verifier_nonce, status, created_at, authorized_at,
    104             verified_at, completed_at, failed_at, expires_at
    105         "#
    106     )
    107         .bind(nonce)
    108         .bind(expires_in_minutes)
    109         .bind(client_id)
    110         .fetch_optional(pool)
    111         .await?;
    112 
    113     Ok(session)
    114 }
    115 
    116 /// Fetch session and client data for /authorize endpoint
    117 ///
    118 /// Returns all data needed for backend validation and idempotent responses.
    119 /// Updates the session scope with the provided scope parameter.
    120 ///
    121 /// Used by the /authorize endpoint
    122 pub async fn get_session_for_authorize(
    123     pool: &PgPool,
    124     nonce: &str,
    125     client_id: &str,
    126     scope: &str,
    127     redirect_uri: &str,
    128     state: &str,
    129 ) -> Result<Option<AuthorizeSessionData>> {
    130     let result = sqlx::query(
    131         r#"
    132         UPDATE oauth2gw.verification_sessions s
    133         SET scope = $3, redirect_uri = $4, state = $5
    134         FROM oauth2gw.clients c
    135         WHERE s.client_id = c.id
    136           AND s.nonce = $1
    137           AND c.client_id = $2
    138         RETURNING
    139             s.id AS session_id,
    140             s.status,
    141             s.expires_at,
    142             s.scope,
    143             s.redirect_uri,
    144             s.state,
    145             s.verification_url,
    146             s.verification_deeplink,
    147             s.request_id,
    148             s.verifier_nonce,
    149             c.verifier_url,
    150             c.verifier_management_api_path
    151         "#
    152     )
    153     .bind(nonce)
    154     .bind(client_id)
    155     .bind(scope)
    156     .bind(redirect_uri)
    157     .bind(state)
    158     .fetch_optional(pool)
    159     .await?;
    160 
    161     Ok(result.map(|row: sqlx::postgres::PgRow| {
    162         use sqlx::Row;
    163         AuthorizeSessionData {
    164             session_id: row.get("session_id"),
    165             status: row.get("status"),
    166             expires_at: row.get("expires_at"),
    167             scope: row.get("scope"),
    168             redirect_uri: row.get("redirect_uri"),
    169             state: row.get("state"),
    170             verification_url: row.get("verification_url"),
    171             verification_deeplink: row.get("verification_deeplink"),
    172             request_id: row.get("request_id"),
    173             verifier_nonce: row.get("verifier_nonce"),
    174             verifier_url: row.get("verifier_url"),
    175             verifier_management_api_path: row.get("verifier_management_api_path"),
    176         }
    177     }))
    178 }
    179 
    180 /// Fetch session and client data for /notification webhook
    181 ///
    182 /// Returns all data needed for backend validation and client notification.
    183 ///
    184 /// Used by the /notification endpoint (incoming webhook from Swiyu)
    185 pub async fn get_session_for_notification(
    186     pool: &PgPool,
    187     request_id: &str,
    188 ) -> Result<Option<NotificationSessionData>> {
    189     let result = sqlx::query(
    190         r#"
    191         UPDATE oauth2gw.verification_sessions s
    192         SET status = s.status
    193         FROM oauth2gw.clients c
    194         WHERE s.client_id = c.id
    195           AND s.request_id = $1
    196         RETURNING
    197             s.id AS session_id,
    198             s.nonce,
    199             s.status,
    200             s.redirect_uri,
    201             s.state,
    202             c.id AS client_id,
    203             c.webhook_url,
    204             c.verifier_url,
    205             c.verifier_management_api_path
    206         "#
    207     )
    208     .bind(request_id)
    209     .fetch_optional(pool)
    210     .await?;
    211 
    212     Ok(result.map(|row: sqlx::postgres::PgRow| {
    213         use sqlx::Row;
    214         NotificationSessionData {
    215             session_id: row.get("session_id"),
    216             nonce: row.get("nonce"),
    217             status: row.get("status"),
    218             redirect_uri: row.get("redirect_uri"),
    219             state: row.get("state"),
    220             client_id: row.get("client_id"),
    221             webhook_url: row.get("webhook_url"),
    222             verifier_url: row.get("verifier_url"),
    223             verifier_management_api_path: row.get("verifier_management_api_path"),
    224         }
    225     }))
    226 }
    227 
    228 /// Data returned after updating session to authorized
    229 #[derive(Debug, Clone)]
    230 pub struct AuthorizedSessionResult {
    231     pub request_id: String,
    232     pub verification_url: String,
    233 }
    234 
    235 /// Update session to authorized with verifier response data
    236 ///
    237 /// Called after successful POST to Swiyu Verifier.
    238 /// Sets status to 'authorized' and stores verification_url, request_id, verifier_nonce.
    239 /// Returns the request_id (verification_id) and verification_url.
    240 ///
    241 /// Used by the /authorize endpoint
    242 pub async fn update_session_authorized(
    243     pool: &PgPool,
    244     session_id: Uuid,
    245     verification_url: &str,
    246     verification_deeplink: Option<&str>,
    247     request_id: &str,
    248     verifier_nonce: Option<&str>,
    249 ) -> Result<AuthorizedSessionResult> {
    250     let result = sqlx::query(
    251         r#"
    252         UPDATE oauth2gw.verification_sessions
    253         SET status = 'authorized',
    254             verification_url = $1,
    255             verification_deeplink = $2,
    256             request_id = $3,
    257             verifier_nonce = $4,
    258             authorized_at = NOW()
    259         WHERE id = $5
    260         RETURNING request_id, verification_url
    261         "#
    262     )
    263     .bind(verification_url)
    264     .bind(verification_deeplink)
    265     .bind(request_id)
    266     .bind(verifier_nonce)
    267     .bind(session_id)
    268     .fetch_one(pool)
    269     .await?;
    270 
    271     use sqlx::Row;
    272     Ok(AuthorizedSessionResult {
    273         request_id: result.get("request_id"),
    274         verification_url: result.get("verification_url"),
    275     })
    276 }
    277 
    278 /// Atomically update session to verified, create authorization code, and queue webhook
    279 ///
    280 /// Returns the generated authorization code on success.
    281 pub async fn verify_session_and_queue_notification(
    282     pool: &PgPool,
    283     session_id: Uuid,
    284     status: SessionStatus,
    285     authorization_code: &str,
    286     code_expires_in_minutes: i64,
    287     client_id: Uuid,
    288     webhook_url: &str,
    289     webhook_body: &str,
    290     verifiable_credential: Option<&serde_json::Value>,
    291 ) -> Result<String> {
    292     let timestamp_field = match status {
    293         SessionStatus::Verified => "verified_at",
    294         SessionStatus::Failed => "failed_at",
    295         _ => return Err(anyhow::anyhow!("Invalid status for notification: must be Verified or Failed")),
    296     };
    297 
    298     let query = format!(
    299         r#"
    300         WITH updated_session AS (
    301             UPDATE oauth2gw.verification_sessions
    302             SET status = $1, {} = NOW(), verifiable_credential = $8
    303             WHERE id = $2
    304             RETURNING id
    305         ),
    306         inserted_code AS (
    307             INSERT INTO oauth2gw.authorization_codes (session_id, code, expires_at)
    308             VALUES ($2, $3, NOW() + $4 * INTERVAL '1 minute')
    309             RETURNING code
    310         )
    311         INSERT INTO oauth2gw.notification_pending_webhooks
    312             (session_id, client_id, url, http_method, body, next_attempt)
    313         VALUES ($2, $5, $6, 'GET', $7, 0)
    314         RETURNING (SELECT code FROM inserted_code)
    315         "#,
    316         timestamp_field
    317     );
    318 
    319     let code = sqlx::query_scalar::<_, String>(&query)
    320         .bind(status)
    321         .bind(session_id)
    322         .bind(authorization_code)
    323         .bind(code_expires_in_minutes)
    324         .bind(client_id)
    325         .bind(webhook_url)
    326         .bind(webhook_body)
    327         .bind(verifiable_credential)
    328         .fetch_one(pool)
    329         .await?;
    330 
    331     Ok(code)
    332 }