kych

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

sessions.rs (11295B)


      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     pub allowed_redirect_uris: Option<String>,
     65     pub accepted_issuer_dids: Option<String>,
     66 }
     67 
     68 /// Notification record data used in /notification webhook endpoint
     69 #[derive(Debug, Clone)]
     70 pub struct NotificationSessionData {
     71     // Session fields
     72     pub session_id: Uuid,
     73     pub nonce: String,
     74     pub status: SessionStatus,
     75     pub redirect_uri: Option<String>,
     76     pub state: Option<String>,
     77     // Client fields
     78     pub client_id: Uuid,
     79     pub allowed_redirect_uris: Option<String>,
     80     pub verifier_url: String,
     81     pub verifier_management_api_path: String,
     82 }
     83 
     84 #[derive(Debug, Clone)]
     85 pub struct StatusSessionData {
     86     pub session_id: Uuid,
     87     pub status: SessionStatus,
     88     pub state: Option<String>,
     89     pub redirect_uri: Option<String>,
     90     pub authorization_code: Option<String>,
     91 }
     92 
     93 /// Create a new verification session
     94 ///
     95 /// Returns None if client_id doesn't exist
     96 ///
     97 /// Used by the /setup endpoint
     98 pub async fn create_session(
     99     pool: &PgPool,
    100     client_id: &str,
    101     nonce: &str,
    102     expires_in_minutes: i64,
    103 ) -> Result<Option<VerificationSession>> {
    104     let session = sqlx::query_as::<_, VerificationSession>(
    105         r#"
    106         INSERT INTO oauth2gw.verification_sessions (client_id, nonce, scope,
    107                                                             expires_at, status)
    108         SELECT c.id, $1, '', NOW() + $2 * INTERVAL '1 minute', 'pending'
    109         FROM oauth2gw.clients c
    110         WHERE c.client_id = $3
    111         RETURNING
    112             id, client_id, nonce, scope, redirect_uri, state, verification_url,
    113             request_id, verifier_nonce, status, created_at, authorized_at,
    114             verified_at, completed_at, failed_at, expires_at
    115         "#
    116     )
    117         .bind(nonce)
    118         .bind(expires_in_minutes)
    119         .bind(client_id)
    120         .fetch_optional(pool)
    121         .await?;
    122 
    123     Ok(session)
    124 }
    125 
    126 /// Fetch session and client data for /authorize endpoint
    127 ///
    128 /// Returns all data needed for backend validation and idempotent responses.
    129 /// Updates the session scope with the provided scope parameter.
    130 /// Does NOT validate redirect_uri - caller must check against allowed_redirect_uris.
    131 ///
    132 /// Used by the /authorize endpoint
    133 pub async fn get_session_for_authorize(
    134     pool: &PgPool,
    135     nonce: &str,
    136     client_id: &str,
    137     scope: &str,
    138     redirect_uri: &str,
    139     state: &str,
    140 ) -> Result<Option<AuthorizeSessionData>> {
    141     let result = sqlx::query(
    142         r#"
    143         UPDATE oauth2gw.verification_sessions s
    144         SET scope = $3, redirect_uri = $4, state = $5
    145         FROM oauth2gw.clients c
    146         WHERE s.client_id = c.id
    147           AND s.nonce = $1
    148           AND c.client_id = $2
    149         RETURNING
    150             s.id AS session_id,
    151             s.status,
    152             s.expires_at,
    153             s.scope,
    154             s.redirect_uri,
    155             s.state,
    156             s.verification_url,
    157             s.verification_deeplink,
    158             s.request_id,
    159             s.verifier_nonce,
    160             c.verifier_url,
    161             c.verifier_management_api_path,
    162             c.redirect_uri AS allowed_redirect_uris,
    163             c.accepted_issuer_dids
    164         "#
    165     )
    166     .bind(nonce)
    167     .bind(client_id)
    168     .bind(scope)
    169     .bind(redirect_uri)
    170     .bind(state)
    171     .fetch_optional(pool)
    172     .await?;
    173 
    174     Ok(result.map(|row: sqlx::postgres::PgRow| {
    175         use sqlx::Row;
    176         AuthorizeSessionData {
    177             session_id: row.get("session_id"),
    178             status: row.get("status"),
    179             expires_at: row.get("expires_at"),
    180             scope: row.get("scope"),
    181             redirect_uri: row.get("redirect_uri"),
    182             state: row.get("state"),
    183             verification_url: row.get("verification_url"),
    184             verification_deeplink: row.get("verification_deeplink"),
    185             request_id: row.get("request_id"),
    186             verifier_nonce: row.get("verifier_nonce"),
    187             verifier_url: row.get("verifier_url"),
    188             verifier_management_api_path: row.get("verifier_management_api_path"),
    189             allowed_redirect_uris: row.get("allowed_redirect_uris"),
    190             accepted_issuer_dids: row.get("accepted_issuer_dids"),
    191         }
    192     }))
    193 }
    194 
    195 /// Fetch session and client data for /notification webhook
    196 ///
    197 /// Returns all data needed for backend validation and client notification.
    198 ///
    199 /// Used by the /notification endpoint (incoming webhook from Swiyu)
    200 pub async fn get_session_for_notification(
    201     pool: &PgPool,
    202     request_id: &str,
    203 ) -> Result<Option<NotificationSessionData>> {
    204     let result = sqlx::query(
    205         r#"
    206         UPDATE oauth2gw.verification_sessions s
    207         SET status = s.status
    208         FROM oauth2gw.clients c
    209         WHERE s.client_id = c.id
    210           AND s.request_id = $1
    211         RETURNING
    212             s.id AS session_id,
    213             s.nonce,
    214             s.status,
    215             s.redirect_uri,
    216             s.state,
    217             c.id AS client_id,
    218             c.redirect_uri AS allowed_redirect_uris,
    219             c.verifier_url,
    220             c.verifier_management_api_path
    221         "#
    222     )
    223     .bind(request_id)
    224     .fetch_optional(pool)
    225     .await?;
    226 
    227     Ok(result.map(|row: sqlx::postgres::PgRow| {
    228         use sqlx::Row;
    229         NotificationSessionData {
    230             session_id: row.get("session_id"),
    231             nonce: row.get("nonce"),
    232             status: row.get("status"),
    233             redirect_uri: row.get("redirect_uri"),
    234             state: row.get("state"),
    235             client_id: row.get("client_id"),
    236             allowed_redirect_uris: row.get("allowed_redirect_uris"),
    237             verifier_url: row.get("verifier_url"),
    238             verifier_management_api_path: row.get("verifier_management_api_path"),
    239         }
    240     }))
    241 }
    242 
    243 pub async fn get_session_for_status(
    244     pool: &PgPool,
    245     request_id: &str,
    246 ) -> Result<Option<StatusSessionData>> {
    247     let result = sqlx::query(
    248         r#"
    249         SELECT
    250             s.id AS session_id,
    251             s.status,
    252             s.state,
    253             s.redirect_uri,
    254             ac.code AS authorization_code
    255         FROM oauth2gw.verification_sessions s
    256         LEFT JOIN oauth2gw.authorization_codes ac
    257             ON ac.session_id = s.id
    258         WHERE s.request_id = $1
    259         "#
    260     )
    261     .bind(request_id)
    262     .fetch_optional(pool)
    263     .await?;
    264 
    265     Ok(result.map(|row: sqlx::postgres::PgRow| {
    266         use sqlx::Row;
    267         StatusSessionData {
    268             session_id: row.get("session_id"),
    269             status: row.get("status"),
    270             state: row.get("state"),
    271             redirect_uri: row.get("redirect_uri"),
    272             authorization_code: row.get("authorization_code"),
    273         }
    274     }))
    275 }
    276 
    277 /// Data returned after updating session to authorized
    278 #[derive(Debug, Clone)]
    279 pub struct AuthorizedSessionResult {
    280     pub request_id: String,
    281     pub verification_url: String,
    282 }
    283 
    284 /// Update session to authorized with verifier response data
    285 ///
    286 /// Called after successful POST to Swiyu Verifier.
    287 /// Sets status to 'authorized' and stores verification_url, request_id, verifier_nonce.
    288 /// Returns the request_id (verification_id) and verification_url.
    289 ///
    290 /// Used by the /authorize endpoint
    291 pub async fn update_session_authorized(
    292     pool: &PgPool,
    293     session_id: Uuid,
    294     verification_url: &str,
    295     verification_deeplink: Option<&str>,
    296     request_id: &str,
    297     verifier_nonce: Option<&str>,
    298 ) -> Result<AuthorizedSessionResult> {
    299     let result = sqlx::query(
    300         r#"
    301         UPDATE oauth2gw.verification_sessions
    302         SET status = 'authorized',
    303             verification_url = $1,
    304             verification_deeplink = $2,
    305             request_id = $3,
    306             verifier_nonce = $4,
    307             authorized_at = NOW()
    308         WHERE id = $5
    309         RETURNING request_id, verification_url
    310         "#
    311     )
    312     .bind(verification_url)
    313     .bind(verification_deeplink)
    314     .bind(request_id)
    315     .bind(verifier_nonce)
    316     .bind(session_id)
    317     .fetch_one(pool)
    318     .await?;
    319 
    320     use sqlx::Row;
    321     Ok(AuthorizedSessionResult {
    322         request_id: result.get("request_id"),
    323         verification_url: result.get("verification_url"),
    324     })
    325 }
    326 
    327 /// Atomically update session to verified and create authorization code
    328 ///
    329 /// Returns the generated authorization code on success.
    330 pub async fn verify_session_and_issue_code(
    331     pool: &PgPool,
    332     session_id: Uuid,
    333     status: SessionStatus,
    334     authorization_code: &str,
    335     code_expires_in_minutes: i64,
    336     _client_id: Uuid,
    337     _callback_body: &str,
    338     verifiable_credential: Option<&serde_json::Value>,
    339 ) -> Result<String> {
    340     let timestamp_field = match status {
    341         SessionStatus::Verified => "verified_at",
    342         SessionStatus::Failed => "failed_at",
    343         _ => return Err(anyhow::anyhow!("Invalid status for notification: must be Verified or Failed")),
    344     };
    345 
    346     let query = format!(
    347         r#"
    348         WITH updated_session AS (
    349             UPDATE oauth2gw.verification_sessions
    350             SET status = $1, {} = NOW(), verifiable_credential = $5
    351             WHERE id = $2
    352             RETURNING id
    353         )
    354         INSERT INTO oauth2gw.authorization_codes (session_id, code, expires_at)
    355         VALUES ($2, $3, NOW() + $4 * INTERVAL '1 minute')
    356         RETURNING code
    357         "#,
    358         timestamp_field
    359     );
    360 
    361     let code = sqlx::query_scalar::<_, String>(&query)
    362         .bind(status)
    363         .bind(session_id)
    364         .bind(authorization_code)
    365         .bind(code_expires_in_minutes)
    366         .bind(verifiable_credential)
    367         .fetch_one(pool)
    368         .await?;
    369 
    370     Ok(code)
    371 }