this repo has no description
1use axum::{ 2 Form, Json, 3 extract::{Query, State}, 4 http::{HeaderMap, header::SET_COOKIE}, 5 response::{IntoResponse, Redirect, Response, Html}, 6}; 7use chrono::Utc; 8use serde::{Deserialize, Serialize}; 9use subtle::ConstantTimeEq; 10use urlencoding::encode as url_encode; 11use crate::state::{AppState, RateLimitKind}; 12use crate::oauth::{Code, DeviceAccount, DeviceData, DeviceId, OAuthError, SessionId, db, templates}; 13use crate::notifications::{NotificationChannel, channel_display_name, enqueue_2fa_code}; 14const DEVICE_COOKIE_NAME: &str = "oauth_device_id"; 15fn extract_device_cookie(headers: &HeaderMap) -> Option<String> { 16 headers 17 .get("cookie") 18 .and_then(|v| v.to_str().ok()) 19 .and_then(|cookie_str| { 20 for cookie in cookie_str.split(';') { 21 let cookie = cookie.trim(); 22 if let Some(value) = cookie.strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME)) { 23 return Some(value.to_string()); 24 } 25 } 26 None 27 }) 28} 29fn extract_client_ip(headers: &HeaderMap) -> String { 30 if let Some(forwarded) = headers.get("x-forwarded-for") { 31 if let Ok(value) = forwarded.to_str() { 32 if let Some(first_ip) = value.split(',').next() { 33 return first_ip.trim().to_string(); 34 } 35 } 36 } 37 if let Some(real_ip) = headers.get("x-real-ip") { 38 if let Ok(value) = real_ip.to_str() { 39 return value.trim().to_string(); 40 } 41 } 42 "0.0.0.0".to_string() 43} 44fn extract_user_agent(headers: &HeaderMap) -> Option<String> { 45 headers 46 .get("user-agent") 47 .and_then(|v| v.to_str().ok()) 48 .map(|s| s.to_string()) 49} 50fn make_device_cookie(device_id: &str) -> String { 51 format!( 52 "{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000", 53 DEVICE_COOKIE_NAME, 54 device_id 55 ) 56} 57#[derive(Debug, Deserialize)] 58pub struct AuthorizeQuery { 59 pub request_uri: Option<String>, 60 pub client_id: Option<String>, 61 pub new_account: Option<bool>, 62} 63#[derive(Debug, Serialize)] 64pub struct AuthorizeResponse { 65 pub client_id: String, 66 pub client_name: Option<String>, 67 pub scope: Option<String>, 68 pub redirect_uri: String, 69 pub state: Option<String>, 70 pub login_hint: Option<String>, 71} 72#[derive(Debug, Deserialize)] 73pub struct AuthorizeSubmit { 74 pub request_uri: String, 75 pub username: String, 76 pub password: String, 77 #[serde(default)] 78 pub remember_device: bool, 79} 80#[derive(Debug, Deserialize)] 81pub struct AuthorizeSelectSubmit { 82 pub request_uri: String, 83 pub did: String, 84} 85fn wants_json(headers: &HeaderMap) -> bool { 86 headers 87 .get("accept") 88 .and_then(|v| v.to_str().ok()) 89 .map(|accept| accept.contains("application/json")) 90 .unwrap_or(false) 91} 92pub async fn authorize_get( 93 State(state): State<AppState>, 94 headers: HeaderMap, 95 Query(query): Query<AuthorizeQuery>, 96) -> Response { 97 let request_uri = match query.request_uri { 98 Some(uri) => uri, 99 None => { 100 if wants_json(&headers) { 101 return ( 102 axum::http::StatusCode::BAD_REQUEST, 103 Json(serde_json::json!({ 104 "error": "invalid_request", 105 "error_description": "Missing request_uri parameter. Use PAR to initiate authorization." 106 })), 107 ).into_response(); 108 } 109 return ( 110 axum::http::StatusCode::BAD_REQUEST, 111 Html(templates::error_page( 112 "invalid_request", 113 Some("Missing request_uri parameter. Use PAR to initiate authorization."), 114 )), 115 ).into_response(); 116 } 117 }; 118 let request_data = match db::get_authorization_request(&state.db, &request_uri).await { 119 Ok(Some(data)) => data, 120 Ok(None) => { 121 if wants_json(&headers) { 122 return ( 123 axum::http::StatusCode::BAD_REQUEST, 124 Json(serde_json::json!({ 125 "error": "invalid_request", 126 "error_description": "Invalid or expired request_uri. Please start a new authorization request." 127 })), 128 ).into_response(); 129 } 130 return ( 131 axum::http::StatusCode::BAD_REQUEST, 132 Html(templates::error_page( 133 "invalid_request", 134 Some("Invalid or expired request_uri. Please start a new authorization request."), 135 )), 136 ).into_response(); 137 } 138 Err(e) => { 139 if wants_json(&headers) { 140 return ( 141 axum::http::StatusCode::INTERNAL_SERVER_ERROR, 142 Json(serde_json::json!({ 143 "error": "server_error", 144 "error_description": format!("Database error: {:?}", e) 145 })), 146 ).into_response(); 147 } 148 return ( 149 axum::http::StatusCode::INTERNAL_SERVER_ERROR, 150 Html(templates::error_page( 151 "server_error", 152 Some(&format!("Database error: {:?}", e)), 153 )), 154 ).into_response(); 155 } 156 }; 157 if request_data.expires_at < Utc::now() { 158 let _ = db::delete_authorization_request(&state.db, &request_uri).await; 159 if wants_json(&headers) { 160 return ( 161 axum::http::StatusCode::BAD_REQUEST, 162 Json(serde_json::json!({ 163 "error": "invalid_request", 164 "error_description": "Authorization request has expired. Please start a new request." 165 })), 166 ).into_response(); 167 } 168 return ( 169 axum::http::StatusCode::BAD_REQUEST, 170 Html(templates::error_page( 171 "invalid_request", 172 Some("Authorization request has expired. Please start a new request."), 173 )), 174 ).into_response(); 175 } 176 if wants_json(&headers) { 177 return Json(AuthorizeResponse { 178 client_id: request_data.parameters.client_id.clone(), 179 client_name: None, 180 scope: request_data.parameters.scope.clone(), 181 redirect_uri: request_data.parameters.redirect_uri.clone(), 182 state: request_data.parameters.state.clone(), 183 login_hint: request_data.parameters.login_hint.clone(), 184 }).into_response(); 185 } 186 let force_new_account = query.new_account.unwrap_or(false); 187 if !force_new_account { 188 if let Some(device_id) = extract_device_cookie(&headers) { 189 if let Ok(accounts) = db::get_device_accounts(&state.db, &device_id).await { 190 if !accounts.is_empty() { 191 let device_accounts: Vec<DeviceAccount> = accounts 192 .into_iter() 193 .map(|row| DeviceAccount { 194 did: row.did, 195 handle: row.handle, 196 email: row.email, 197 last_used_at: row.last_used_at, 198 }) 199 .collect(); 200 return Html(templates::account_selector_page( 201 &request_data.parameters.client_id, 202 None, 203 &request_uri, 204 &device_accounts, 205 )).into_response(); 206 } 207 } 208 } 209 } 210 Html(templates::login_page( 211 &request_data.parameters.client_id, 212 None, 213 request_data.parameters.scope.as_deref(), 214 &request_uri, 215 None, 216 request_data.parameters.login_hint.as_deref(), 217 )).into_response() 218} 219pub async fn authorize_get_json( 220 State(state): State<AppState>, 221 Query(query): Query<AuthorizeQuery>, 222) -> Result<Json<AuthorizeResponse>, OAuthError> { 223 let request_uri = query.request_uri.ok_or_else(|| { 224 OAuthError::InvalidRequest("request_uri is required".to_string()) 225 })?; 226 let request_data = db::get_authorization_request(&state.db, &request_uri) 227 .await? 228 .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?; 229 if request_data.expires_at < Utc::now() { 230 db::delete_authorization_request(&state.db, &request_uri).await?; 231 return Err(OAuthError::InvalidRequest("request_uri has expired".to_string())); 232 } 233 Ok(Json(AuthorizeResponse { 234 client_id: request_data.parameters.client_id.clone(), 235 client_name: None, 236 scope: request_data.parameters.scope.clone(), 237 redirect_uri: request_data.parameters.redirect_uri.clone(), 238 state: request_data.parameters.state.clone(), 239 login_hint: request_data.parameters.login_hint.clone(), 240 })) 241} 242pub async fn authorize_post( 243 State(state): State<AppState>, 244 headers: HeaderMap, 245 Form(form): Form<AuthorizeSubmit>, 246) -> Response { 247 let json_response = wants_json(&headers); 248 let client_ip = extract_client_ip(&headers); 249 if !state.check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip).await { 250 tracing::warn!(ip = %client_ip, "OAuth authorize rate limit exceeded"); 251 if json_response { 252 return ( 253 axum::http::StatusCode::TOO_MANY_REQUESTS, 254 Json(serde_json::json!({ 255 "error": "RateLimitExceeded", 256 "error_description": "Too many login attempts. Please try again later." 257 })), 258 ).into_response(); 259 } 260 return ( 261 axum::http::StatusCode::TOO_MANY_REQUESTS, 262 Html(templates::error_page( 263 "RateLimitExceeded", 264 Some("Too many login attempts. Please try again later."), 265 )), 266 ).into_response(); 267 } 268 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 269 Ok(Some(data)) => data, 270 Ok(None) => { 271 if json_response { 272 return ( 273 axum::http::StatusCode::BAD_REQUEST, 274 Json(serde_json::json!({ 275 "error": "invalid_request", 276 "error_description": "Invalid or expired request_uri." 277 })), 278 ).into_response(); 279 } 280 return Html(templates::error_page( 281 "invalid_request", 282 Some("Invalid or expired request_uri. Please start a new authorization request."), 283 )).into_response(); 284 } 285 Err(e) => { 286 if json_response { 287 return ( 288 axum::http::StatusCode::INTERNAL_SERVER_ERROR, 289 Json(serde_json::json!({ 290 "error": "server_error", 291 "error_description": format!("Database error: {:?}", e) 292 })), 293 ).into_response(); 294 } 295 return Html(templates::error_page( 296 "server_error", 297 Some(&format!("Database error: {:?}", e)), 298 )).into_response(); 299 } 300 }; 301 if request_data.expires_at < Utc::now() { 302 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 303 if json_response { 304 return ( 305 axum::http::StatusCode::BAD_REQUEST, 306 Json(serde_json::json!({ 307 "error": "invalid_request", 308 "error_description": "Authorization request has expired." 309 })), 310 ).into_response(); 311 } 312 return Html(templates::error_page( 313 "invalid_request", 314 Some("Authorization request has expired. Please start a new request."), 315 )).into_response(); 316 } 317 let show_login_error = |error_msg: &str, json: bool| -> Response { 318 if json { 319 return ( 320 axum::http::StatusCode::FORBIDDEN, 321 Json(serde_json::json!({ 322 "error": "access_denied", 323 "error_description": error_msg 324 })), 325 ).into_response(); 326 } 327 Html(templates::login_page( 328 &request_data.parameters.client_id, 329 None, 330 request_data.parameters.scope.as_deref(), 331 &form.request_uri, 332 Some(error_msg), 333 Some(&form.username), 334 )).into_response() 335 }; 336 let user = match sqlx::query!( 337 r#" 338 SELECT id, did, email, password_hash, two_factor_enabled, 339 preferred_notification_channel as "preferred_notification_channel: NotificationChannel", 340 deactivated_at, takedown_ref 341 FROM users 342 WHERE handle = $1 OR email = $1 343 "#, 344 form.username 345 ) 346 .fetch_optional(&state.db) 347 .await 348 { 349 Ok(Some(u)) => u, 350 Ok(None) => { 351 let _ = bcrypt::verify(&form.password, "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK"); 352 return show_login_error("Invalid handle/email or password.", json_response); 353 } 354 Err(_) => return show_login_error("An error occurred. Please try again.", json_response), 355 }; 356 if user.deactivated_at.is_some() { 357 return show_login_error("This account has been deactivated.", json_response); 358 } 359 if user.takedown_ref.is_some() { 360 return show_login_error("This account has been taken down.", json_response); 361 } 362 let password_valid = match bcrypt::verify(&form.password, &user.password_hash) { 363 Ok(valid) => valid, 364 Err(_) => return show_login_error("An error occurred. Please try again.", json_response), 365 }; 366 if !password_valid { 367 return show_login_error("Invalid handle/email or password.", json_response); 368 } 369 if user.two_factor_enabled { 370 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 371 match db::create_2fa_challenge(&state.db, &user.did, &form.request_uri).await { 372 Ok(challenge) => { 373 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 374 if let Err(e) = enqueue_2fa_code( 375 &state.db, 376 user.id, 377 &challenge.code, 378 &hostname, 379 ).await { 380 tracing::warn!( 381 did = %user.did, 382 error = %e, 383 "Failed to enqueue 2FA notification" 384 ); 385 } 386 let channel_name = channel_display_name(user.preferred_notification_channel); 387 let redirect_url = format!( 388 "/oauth/authorize/2fa?request_uri={}&channel={}", 389 url_encode(&form.request_uri), 390 url_encode(channel_name) 391 ); 392 return Redirect::temporary(&redirect_url).into_response(); 393 } 394 Err(_) => { 395 return show_login_error("An error occurred. Please try again.", json_response); 396 } 397 } 398 } 399 let code = Code::generate(); 400 let mut device_id: Option<String> = extract_device_cookie(&headers); 401 let mut new_cookie: Option<String> = None; 402 if form.remember_device { 403 let final_device_id = if let Some(existing_id) = &device_id { 404 existing_id.clone() 405 } else { 406 let new_id = DeviceId::generate(); 407 let device_data = DeviceData { 408 session_id: SessionId::generate().0, 409 user_agent: extract_user_agent(&headers), 410 ip_address: extract_client_ip(&headers), 411 last_seen_at: Utc::now(), 412 }; 413 if db::create_device(&state.db, &new_id.0, &device_data).await.is_ok() { 414 new_cookie = Some(make_device_cookie(&new_id.0)); 415 device_id = Some(new_id.0.clone()); 416 } 417 new_id.0 418 }; 419 let _ = db::upsert_account_device(&state.db, &user.did, &final_device_id).await; 420 } 421 if let Err(_) = db::update_authorization_request( 422 &state.db, 423 &form.request_uri, 424 &user.did, 425 device_id.as_deref(), 426 &code.0, 427 ) 428 .await 429 { 430 return show_login_error("An error occurred. Please try again.", json_response); 431 } 432 let redirect_url = build_success_redirect( 433 &request_data.parameters.redirect_uri, 434 &code.0, 435 request_data.parameters.state.as_deref(), 436 ); 437 let redirect = Redirect::temporary(&redirect_url); 438 if let Some(cookie) = new_cookie { 439 ([(SET_COOKIE, cookie)], redirect).into_response() 440 } else { 441 redirect.into_response() 442 } 443} 444pub async fn authorize_select( 445 State(state): State<AppState>, 446 headers: HeaderMap, 447 Form(form): Form<AuthorizeSelectSubmit>, 448) -> Response { 449 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 450 Ok(Some(data)) => data, 451 Ok(None) => { 452 return Html(templates::error_page( 453 "invalid_request", 454 Some("Invalid or expired request_uri. Please start a new authorization request."), 455 )).into_response(); 456 } 457 Err(_) => { 458 return Html(templates::error_page( 459 "server_error", 460 Some("An error occurred. Please try again."), 461 )).into_response(); 462 } 463 }; 464 if request_data.expires_at < Utc::now() { 465 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 466 return Html(templates::error_page( 467 "invalid_request", 468 Some("Authorization request has expired. Please start a new request."), 469 )).into_response(); 470 } 471 let device_id = match extract_device_cookie(&headers) { 472 Some(id) => id, 473 None => { 474 return Html(templates::error_page( 475 "invalid_request", 476 Some("No device session found. Please sign in."), 477 )).into_response(); 478 } 479 }; 480 let account_valid = match db::verify_account_on_device(&state.db, &device_id, &form.did).await { 481 Ok(valid) => valid, 482 Err(_) => { 483 return Html(templates::error_page( 484 "server_error", 485 Some("An error occurred. Please try again."), 486 )).into_response(); 487 } 488 }; 489 if !account_valid { 490 return Html(templates::error_page( 491 "access_denied", 492 Some("This account is not available on this device. Please sign in."), 493 )).into_response(); 494 } 495 let user = match sqlx::query!( 496 r#" 497 SELECT id, two_factor_enabled, 498 preferred_notification_channel as "preferred_notification_channel: NotificationChannel" 499 FROM users 500 WHERE did = $1 501 "#, 502 form.did 503 ) 504 .fetch_optional(&state.db) 505 .await 506 { 507 Ok(Some(u)) => u, 508 Ok(None) => { 509 return Html(templates::error_page( 510 "access_denied", 511 Some("Account not found. Please sign in."), 512 )).into_response(); 513 } 514 Err(_) => { 515 return Html(templates::error_page( 516 "server_error", 517 Some("An error occurred. Please try again."), 518 )).into_response(); 519 } 520 }; 521 if user.two_factor_enabled { 522 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 523 match db::create_2fa_challenge(&state.db, &form.did, &form.request_uri).await { 524 Ok(challenge) => { 525 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 526 if let Err(e) = enqueue_2fa_code( 527 &state.db, 528 user.id, 529 &challenge.code, 530 &hostname, 531 ).await { 532 tracing::warn!( 533 did = %form.did, 534 error = %e, 535 "Failed to enqueue 2FA notification" 536 ); 537 } 538 let channel_name = channel_display_name(user.preferred_notification_channel); 539 let redirect_url = format!( 540 "/oauth/authorize/2fa?request_uri={}&channel={}", 541 url_encode(&form.request_uri), 542 url_encode(channel_name) 543 ); 544 return Redirect::temporary(&redirect_url).into_response(); 545 } 546 Err(_) => { 547 return Html(templates::error_page( 548 "server_error", 549 Some("An error occurred. Please try again."), 550 )).into_response(); 551 } 552 } 553 } 554 let _ = db::upsert_account_device(&state.db, &form.did, &device_id).await; 555 let code = Code::generate(); 556 if let Err(_) = db::update_authorization_request( 557 &state.db, 558 &form.request_uri, 559 &form.did, 560 Some(&device_id), 561 &code.0, 562 ) 563 .await 564 { 565 return Html(templates::error_page( 566 "server_error", 567 Some("An error occurred. Please try again."), 568 )).into_response(); 569 } 570 let redirect_url = build_success_redirect( 571 &request_data.parameters.redirect_uri, 572 &code.0, 573 request_data.parameters.state.as_deref(), 574 ); 575 Redirect::temporary(&redirect_url).into_response() 576} 577fn build_success_redirect(redirect_uri: &str, code: &str, state: Option<&str>) -> String { 578 let mut redirect_url = redirect_uri.to_string(); 579 let separator = if redirect_url.contains('?') { '&' } else { '?' }; 580 redirect_url.push(separator); 581 redirect_url.push_str(&format!("code={}", url_encode(code))); 582 if let Some(req_state) = state { 583 redirect_url.push_str(&format!("&state={}", url_encode(req_state))); 584 } 585 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 586 redirect_url.push_str(&format!("&iss={}", url_encode(&format!("https://{}", pds_hostname)))); 587 redirect_url 588} 589#[derive(Debug, Serialize)] 590pub struct AuthorizeDenyResponse { 591 pub error: String, 592 pub error_description: String, 593} 594pub async fn authorize_deny( 595 State(state): State<AppState>, 596 Form(form): Form<AuthorizeDenyForm>, 597) -> Result<Response, OAuthError> { 598 let request_data = db::get_authorization_request(&state.db, &form.request_uri) 599 .await? 600 .ok_or_else(|| OAuthError::InvalidRequest("Invalid request_uri".to_string()))?; 601 db::delete_authorization_request(&state.db, &form.request_uri).await?; 602 let redirect_uri = &request_data.parameters.redirect_uri; 603 let mut redirect_url = redirect_uri.to_string(); 604 let separator = if redirect_url.contains('?') { '&' } else { '?' }; 605 redirect_url.push(separator); 606 redirect_url.push_str("error=access_denied"); 607 redirect_url.push_str("&error_description=User%20denied%20the%20request"); 608 if let Some(state) = &request_data.parameters.state { 609 redirect_url.push_str(&format!("&state={}", url_encode(state))); 610 } 611 Ok(Redirect::temporary(&redirect_url).into_response()) 612} 613#[derive(Debug, Deserialize)] 614pub struct AuthorizeDenyForm { 615 pub request_uri: String, 616} 617#[derive(Debug, Deserialize)] 618pub struct Authorize2faQuery { 619 pub request_uri: String, 620 pub channel: Option<String>, 621} 622#[derive(Debug, Deserialize)] 623pub struct Authorize2faSubmit { 624 pub request_uri: String, 625 pub code: String, 626} 627const MAX_2FA_ATTEMPTS: i32 = 5; 628pub async fn authorize_2fa_get( 629 State(state): State<AppState>, 630 Query(query): Query<Authorize2faQuery>, 631) -> Response { 632 let challenge = match db::get_2fa_challenge(&state.db, &query.request_uri).await { 633 Ok(Some(c)) => c, 634 Ok(None) => { 635 return Html(templates::error_page( 636 "invalid_request", 637 Some("No 2FA challenge found. Please start over."), 638 )).into_response(); 639 } 640 Err(_) => { 641 return Html(templates::error_page( 642 "server_error", 643 Some("An error occurred. Please try again."), 644 )).into_response(); 645 } 646 }; 647 if challenge.expires_at < Utc::now() { 648 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 649 return Html(templates::error_page( 650 "invalid_request", 651 Some("2FA code has expired. Please start over."), 652 )).into_response(); 653 } 654 let _request_data = match db::get_authorization_request(&state.db, &query.request_uri).await { 655 Ok(Some(d)) => d, 656 Ok(None) => { 657 return Html(templates::error_page( 658 "invalid_request", 659 Some("Authorization request not found. Please start over."), 660 )).into_response(); 661 } 662 Err(_) => { 663 return Html(templates::error_page( 664 "server_error", 665 Some("An error occurred. Please try again."), 666 )).into_response(); 667 } 668 }; 669 let channel = query.channel.as_deref().unwrap_or("email"); 670 Html(templates::two_factor_page( 671 &query.request_uri, 672 channel, 673 None, 674 )).into_response() 675} 676pub async fn authorize_2fa_post( 677 State(state): State<AppState>, 678 headers: HeaderMap, 679 Form(form): Form<Authorize2faSubmit>, 680) -> Response { 681 let client_ip = extract_client_ip(&headers); 682 if !state.check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip).await { 683 tracing::warn!(ip = %client_ip, "OAuth 2FA rate limit exceeded"); 684 return ( 685 axum::http::StatusCode::TOO_MANY_REQUESTS, 686 Html(templates::error_page( 687 "RateLimitExceeded", 688 Some("Too many attempts. Please try again later."), 689 )), 690 ).into_response(); 691 } 692 let challenge = match db::get_2fa_challenge(&state.db, &form.request_uri).await { 693 Ok(Some(c)) => c, 694 Ok(None) => { 695 return Html(templates::error_page( 696 "invalid_request", 697 Some("No 2FA challenge found. Please start over."), 698 )).into_response(); 699 } 700 Err(_) => { 701 return Html(templates::error_page( 702 "server_error", 703 Some("An error occurred. Please try again."), 704 )).into_response(); 705 } 706 }; 707 if challenge.expires_at < Utc::now() { 708 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 709 return Html(templates::error_page( 710 "invalid_request", 711 Some("2FA code has expired. Please start over."), 712 )).into_response(); 713 } 714 if challenge.attempts >= MAX_2FA_ATTEMPTS { 715 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 716 return Html(templates::error_page( 717 "access_denied", 718 Some("Too many failed attempts. Please start over."), 719 )).into_response(); 720 } 721 let code_valid: bool = form.code.trim().as_bytes().ct_eq(challenge.code.as_bytes()).into(); 722 if !code_valid { 723 let _ = db::increment_2fa_attempts(&state.db, challenge.id).await; 724 let channel = match sqlx::query_scalar!( 725 r#"SELECT preferred_notification_channel as "channel: NotificationChannel" FROM users WHERE did = $1"#, 726 challenge.did 727 ) 728 .fetch_optional(&state.db) 729 .await 730 { 731 Ok(Some(ch)) => channel_display_name(ch).to_string(), 732 Ok(None) | Err(_) => "email".to_string(), 733 }; 734 let _request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 735 Ok(Some(d)) => d, 736 Ok(None) => { 737 return Html(templates::error_page( 738 "invalid_request", 739 Some("Authorization request not found. Please start over."), 740 )).into_response(); 741 } 742 Err(_) => { 743 return Html(templates::error_page( 744 "server_error", 745 Some("An error occurred. Please try again."), 746 )).into_response(); 747 } 748 }; 749 return Html(templates::two_factor_page( 750 &form.request_uri, 751 &channel, 752 Some("Invalid verification code. Please try again."), 753 )).into_response(); 754 } 755 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 756 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 757 Ok(Some(d)) => d, 758 Ok(None) => { 759 return Html(templates::error_page( 760 "invalid_request", 761 Some("Authorization request not found."), 762 )).into_response(); 763 } 764 Err(_) => { 765 return Html(templates::error_page( 766 "server_error", 767 Some("An error occurred."), 768 )).into_response(); 769 } 770 }; 771 let code = Code::generate(); 772 let device_id = extract_device_cookie(&headers); 773 if let Err(_) = db::update_authorization_request( 774 &state.db, 775 &form.request_uri, 776 &challenge.did, 777 device_id.as_deref(), 778 &code.0, 779 ) 780 .await 781 { 782 return Html(templates::error_page( 783 "server_error", 784 Some("An error occurred. Please try again."), 785 )).into_response(); 786 } 787 let redirect_url = build_success_redirect( 788 &request_data.parameters.redirect_uri, 789 &code.0, 790 request_data.parameters.state.as_deref(), 791 ); 792 Redirect::temporary(&redirect_url).into_response() 793}