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