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