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 }