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 }