Our Personal Data Server from scratch!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(oauth): split authorize.rs into domain modules

authored by lu5a.myatproto.social and committed by tangled.org e38343ce e454e99b

+3621 -3610
-3610
crates/tranquil-oauth-server/src/endpoints/authorize.rs
··· 1 - use axum::{ 2 - Json, 3 - extract::{Query, State}, 4 - http::{ 5 - HeaderMap, StatusCode, 6 - header::{LOCATION, SET_COOKIE}, 7 - }, 8 - response::{IntoResponse, Response}, 9 - }; 10 - use chrono::Utc; 11 - use serde::{Deserialize, Serialize}; 12 - use subtle::ConstantTimeEq; 13 - use tranquil_db_traits::{ScopePreference, WebauthnChallengeType}; 14 - use tranquil_pds::auth::{BareLoginIdentifier, NormalizedLoginIdentifier}; 15 - use tranquil_pds::comms::comms_repo::enqueue_2fa_code; 16 - use tranquil_pds::oauth::{ 17 - AuthFlow, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, Prompt, SessionId, 18 - db::should_show_consent, scopes::expand_include_scopes, 19 - }; 20 - use tranquil_pds::rate_limit::{ 21 - OAuthAuthorizeLimit, OAuthRateLimited, OAuthRegisterCompleteLimit, TotpVerifyLimit, 22 - check_user_rate_limit, 23 - }; 24 - use tranquil_pds::state::AppState; 25 - use tranquil_pds::types::{Did, Handle, PlainPassword}; 26 - use tranquil_pds::util::extract_client_ip; 27 - use tranquil_types::{AuthorizationCode, ClientId, DeviceId as DeviceIdType, RequestId}; 28 - use urlencoding::encode as url_encode; 29 - 30 - const DEVICE_COOKIE_NAME: &str = "oauth_device_id"; 31 - const RENEW_EXPIRY_SECONDS: i64 = 600; 32 - const MAX_RENEWAL_STALENESS_SECONDS: i64 = 3600; 33 - 34 - fn redirect_see_other(uri: &str) -> Response { 35 - ( 36 - StatusCode::SEE_OTHER, 37 - [ 38 - (LOCATION, uri.to_string()), 39 - (axum::http::header::CACHE_CONTROL, "no-store".to_string()), 40 - ( 41 - SET_COOKIE, 42 - "bfCacheBypass=foo; max-age=1; SameSite=Lax".to_string(), 43 - ), 44 - ], 45 - ) 46 - .into_response() 47 - } 48 - 49 - fn redirect_to_frontend_error(error: &str, description: &str) -> Response { 50 - redirect_see_other(&format!( 51 - "/app/oauth/error?error={}&error_description={}", 52 - url_encode(error), 53 - url_encode(description) 54 - )) 55 - } 56 - 57 - fn json_error(status: StatusCode, error: &str, description: &str) -> Response { 58 - ( 59 - status, 60 - Json(serde_json::json!({ 61 - "error": error, 62 - "error_description": description 63 - })), 64 - ) 65 - .into_response() 66 - } 67 - 68 - fn is_granular_scope(s: &str) -> bool { 69 - s.starts_with("repo:") 70 - || s.starts_with("repo?") 71 - || s == "repo" 72 - || s.starts_with("blob:") 73 - || s.starts_with("blob?") 74 - || s == "blob" 75 - || s.starts_with("rpc:") 76 - || s.starts_with("rpc?") 77 - || s.starts_with("account:") 78 - || s.starts_with("identity:") 79 - } 80 - 81 - fn is_valid_scope(s: &str) -> bool { 82 - s == "atproto" 83 - || s == "transition:generic" 84 - || s == "transition:chat.bsky" 85 - || s == "transition:email" 86 - || is_granular_scope(s) 87 - || s.starts_with("include:") 88 - } 89 - 90 - fn extract_device_cookie(headers: &HeaderMap) -> Option<tranquil_types::DeviceId> { 91 - headers 92 - .get("cookie") 93 - .and_then(|v| v.to_str().ok()) 94 - .and_then(|cookie_str| { 95 - cookie_str.split(';').map(|c| c.trim()).find_map(|cookie| { 96 - cookie 97 - .strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME)) 98 - .and_then(|value| { 99 - tranquil_pds::config::AuthConfig::get().verify_device_cookie(value) 100 - }) 101 - .map(tranquil_types::DeviceId::new) 102 - }) 103 - }) 104 - } 105 - 106 - fn extract_user_agent(headers: &HeaderMap) -> Option<String> { 107 - headers 108 - .get("user-agent") 109 - .and_then(|v| v.to_str().ok()) 110 - .map(|s| s.to_string()) 111 - } 112 - 113 - fn make_device_cookie(device_id: &tranquil_types::DeviceId) -> String { 114 - let signed_value = 115 - tranquil_pds::config::AuthConfig::get().sign_device_cookie(device_id.as_str()); 116 - format!( 117 - "{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000", 118 - DEVICE_COOKIE_NAME, signed_value 119 - ) 120 - } 121 - 122 - #[derive(Debug, Deserialize)] 123 - pub struct AuthorizeQuery { 124 - pub request_uri: Option<String>, 125 - pub client_id: Option<String>, 126 - pub new_account: Option<bool>, 127 - } 128 - 129 - #[derive(Debug, Serialize)] 130 - pub struct AuthorizeResponse { 131 - pub client_id: String, 132 - pub client_name: Option<String>, 133 - pub scope: Option<String>, 134 - pub redirect_uri: String, 135 - pub state: Option<String>, 136 - pub login_hint: Option<String>, 137 - } 138 - 139 - #[derive(Debug, Deserialize)] 140 - pub struct AuthorizeSubmit { 141 - pub request_uri: String, 142 - pub username: String, 143 - pub password: PlainPassword, 144 - #[serde(default)] 145 - pub remember_device: bool, 146 - } 147 - 148 - #[derive(Debug, Deserialize)] 149 - pub struct AuthorizeSelectSubmit { 150 - pub request_uri: String, 151 - pub did: String, 152 - } 153 - 154 - fn wants_json(headers: &HeaderMap) -> bool { 155 - headers 156 - .get("accept") 157 - .and_then(|v| v.to_str().ok()) 158 - .map(|accept| accept.contains("application/json")) 159 - .unwrap_or(false) 160 - } 161 - 162 - pub async fn authorize_get( 163 - State(state): State<AppState>, 164 - headers: HeaderMap, 165 - Query(query): Query<AuthorizeQuery>, 166 - ) -> Response { 167 - let request_uri = match query.request_uri { 168 - Some(uri) => uri, 169 - None => { 170 - if wants_json(&headers) { 171 - return ( 172 - StatusCode::BAD_REQUEST, 173 - Json(serde_json::json!({ 174 - "error": "invalid_request", 175 - "error_description": "Missing request_uri parameter. Use PAR to initiate authorization." 176 - })), 177 - ).into_response(); 178 - } 179 - return redirect_to_frontend_error( 180 - "invalid_request", 181 - "Missing request_uri parameter. Use PAR to initiate authorization.", 182 - ); 183 - } 184 - }; 185 - let request_id = RequestId::from(request_uri.clone()); 186 - let request_data = match state 187 - .oauth_repo 188 - .get_authorization_request(&request_id) 189 - .await 190 - { 191 - Ok(Some(data)) => data, 192 - Ok(None) => { 193 - if wants_json(&headers) { 194 - return ( 195 - StatusCode::BAD_REQUEST, 196 - Json(serde_json::json!({ 197 - "error": "invalid_request", 198 - "error_description": "Invalid or expired request_uri. Please start a new authorization request." 199 - })), 200 - ).into_response(); 201 - } 202 - return redirect_to_frontend_error( 203 - "invalid_request", 204 - "Invalid or expired request_uri. Please start a new authorization request.", 205 - ); 206 - } 207 - Err(e) => { 208 - if wants_json(&headers) { 209 - return ( 210 - StatusCode::INTERNAL_SERVER_ERROR, 211 - Json(serde_json::json!({ 212 - "error": "server_error", 213 - "error_description": format!("Database error: {:?}", e) 214 - })), 215 - ) 216 - .into_response(); 217 - } 218 - return redirect_to_frontend_error("server_error", "A database error occurred."); 219 - } 220 - }; 221 - if request_data.expires_at < Utc::now() { 222 - let _ = state 223 - .oauth_repo 224 - .delete_authorization_request(&request_id) 225 - .await; 226 - if wants_json(&headers) { 227 - return ( 228 - StatusCode::BAD_REQUEST, 229 - Json(serde_json::json!({ 230 - "error": "invalid_request", 231 - "error_description": "Authorization request has expired. Please start a new request." 232 - })), 233 - ).into_response(); 234 - } 235 - return redirect_to_frontend_error( 236 - "invalid_request", 237 - "Authorization request has expired. Please start a new request.", 238 - ); 239 - } 240 - let client_cache = ClientMetadataCache::new(3600); 241 - let client_name = client_cache 242 - .get(&request_data.parameters.client_id) 243 - .await 244 - .ok() 245 - .and_then(|m| m.client_name); 246 - if wants_json(&headers) { 247 - return Json(AuthorizeResponse { 248 - client_id: request_data.parameters.client_id.clone(), 249 - client_name: client_name.clone(), 250 - scope: request_data.parameters.scope.clone(), 251 - redirect_uri: request_data.parameters.redirect_uri.clone(), 252 - state: request_data.parameters.state.clone(), 253 - login_hint: request_data.parameters.login_hint.clone(), 254 - }) 255 - .into_response(); 256 - } 257 - let force_new_account = query.new_account.unwrap_or(false); 258 - 259 - if let Some(ref login_hint) = request_data.parameters.login_hint { 260 - tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation"); 261 - let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 262 - let normalized = NormalizedLoginIdentifier::normalize(login_hint, hostname_for_handles); 263 - tracing::info!(normalized = %normalized, "Normalized login_hint"); 264 - 265 - match state 266 - .user_repo 267 - .get_login_check_by_handle_or_email(normalized.as_str()) 268 - .await 269 - { 270 - Ok(Some(user)) => { 271 - tracing::info!(did = %user.did, has_password = user.password_hash.is_some(), "Found user for login_hint"); 272 - let is_delegated = state 273 - .delegation_repo 274 - .is_delegated_account(&user.did) 275 - .await 276 - .unwrap_or(false); 277 - let has_password = user.password_hash.is_some(); 278 - tracing::info!(is_delegated = %is_delegated, has_password = %has_password, "Delegation check"); 279 - 280 - if is_delegated { 281 - tracing::info!("Redirecting to delegation auth"); 282 - if let Err(e) = state 283 - .oauth_repo 284 - .set_request_did(&request_id, &user.did) 285 - .await 286 - { 287 - tracing::error!(error = %e, "Failed to set delegated DID on authorization request"); 288 - return redirect_to_frontend_error( 289 - "server_error", 290 - "Failed to initialize delegation flow", 291 - ); 292 - } 293 - return redirect_see_other(&format!( 294 - "/app/oauth/delegation?request_uri={}&delegated_did={}", 295 - url_encode(&request_uri), 296 - url_encode(&user.did) 297 - )); 298 - } 299 - } 300 - Ok(None) => { 301 - tracing::info!(normalized = %normalized, "No user found for login_hint"); 302 - } 303 - Err(e) => { 304 - tracing::error!(error = %e, "Error looking up user for login_hint"); 305 - } 306 - } 307 - } else { 308 - tracing::info!("No login_hint in request"); 309 - } 310 - 311 - if request_data.parameters.prompt == Some(Prompt::Create) { 312 - return redirect_see_other(&format!( 313 - "/app/oauth/register?request_uri={}", 314 - url_encode(&request_uri) 315 - )); 316 - } 317 - 318 - if !force_new_account 319 - && let Some(device_id) = extract_device_cookie(&headers) 320 - && let Ok(accounts) = state 321 - .oauth_repo 322 - .get_device_accounts(&device_id.clone()) 323 - .await 324 - && !accounts.is_empty() 325 - { 326 - let login_hint_param = request_data 327 - .parameters 328 - .login_hint 329 - .as_ref() 330 - .map(|h| format!("&login_hint={}", url_encode(h))) 331 - .unwrap_or_default(); 332 - return redirect_see_other(&format!( 333 - "/app/oauth/accounts?request_uri={}{}", 334 - url_encode(&request_uri), 335 - login_hint_param 336 - )); 337 - } 338 - redirect_see_other(&format!( 339 - "/app/oauth/login?request_uri={}", 340 - url_encode(&request_uri) 341 - )) 342 - } 343 - 344 - pub async fn authorize_get_json( 345 - State(state): State<AppState>, 346 - Query(query): Query<AuthorizeQuery>, 347 - ) -> Result<Json<AuthorizeResponse>, OAuthError> { 348 - let request_uri = query 349 - .request_uri 350 - .ok_or_else(|| OAuthError::InvalidRequest("request_uri is required".to_string()))?; 351 - let request_id_json = RequestId::from(request_uri.clone()); 352 - let request_data = state 353 - .oauth_repo 354 - .get_authorization_request(&request_id_json) 355 - .await 356 - .map_err(tranquil_pds::oauth::db_err_to_oauth)? 357 - .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?; 358 - if request_data.expires_at < Utc::now() { 359 - let _ = state 360 - .oauth_repo 361 - .delete_authorization_request(&request_id_json) 362 - .await; 363 - return Err(OAuthError::InvalidRequest( 364 - "request_uri has expired".to_string(), 365 - )); 366 - } 367 - Ok(Json(AuthorizeResponse { 368 - client_id: request_data.parameters.client_id.clone(), 369 - client_name: None, 370 - scope: request_data.parameters.scope.clone(), 371 - redirect_uri: request_data.parameters.redirect_uri.clone(), 372 - state: request_data.parameters.state.clone(), 373 - login_hint: request_data.parameters.login_hint.clone(), 374 - })) 375 - } 376 - 377 - #[derive(Debug, Serialize)] 378 - pub struct AccountInfo { 379 - pub did: String, 380 - pub handle: Handle, 381 - #[serde(skip_serializing_if = "Option::is_none")] 382 - pub email: Option<String>, 383 - } 384 - 385 - #[derive(Debug, Serialize)] 386 - pub struct AccountsResponse { 387 - pub accounts: Vec<AccountInfo>, 388 - pub request_uri: String, 389 - } 390 - 391 - fn mask_email(email: &str) -> String { 392 - if let Some(at_pos) = email.find('@') { 393 - let local = &email[..at_pos]; 394 - let domain = &email[at_pos..]; 395 - if local.len() <= 2 { 396 - format!("{}***{}", local.chars().next().unwrap_or('*'), domain) 397 - } else { 398 - let first = local.chars().next().unwrap_or('*'); 399 - let last = local.chars().last().unwrap_or('*'); 400 - format!("{}***{}{}", first, last, domain) 401 - } 402 - } else { 403 - "***".to_string() 404 - } 405 - } 406 - 407 - pub async fn authorize_accounts( 408 - State(state): State<AppState>, 409 - headers: HeaderMap, 410 - Query(query): Query<AuthorizeQuery>, 411 - ) -> Response { 412 - let request_uri = match query.request_uri { 413 - Some(uri) => uri, 414 - None => { 415 - return ( 416 - StatusCode::BAD_REQUEST, 417 - Json(serde_json::json!({ 418 - "error": "invalid_request", 419 - "error_description": "Missing request_uri parameter" 420 - })), 421 - ) 422 - .into_response(); 423 - } 424 - }; 425 - let device_id = match extract_device_cookie(&headers) { 426 - Some(id) => id, 427 - None => { 428 - return Json(AccountsResponse { 429 - accounts: vec![], 430 - request_uri, 431 - }) 432 - .into_response(); 433 - } 434 - }; 435 - let accounts = match state.oauth_repo.get_device_accounts(&device_id).await { 436 - Ok(accts) => accts, 437 - Err(_) => { 438 - return Json(AccountsResponse { 439 - accounts: vec![], 440 - request_uri, 441 - }) 442 - .into_response(); 443 - } 444 - }; 445 - let account_infos: Vec<AccountInfo> = accounts 446 - .into_iter() 447 - .map(|row| AccountInfo { 448 - did: row.did.to_string(), 449 - handle: row.handle, 450 - email: row.email.map(|e| mask_email(&e)), 451 - }) 452 - .collect(); 453 - Json(AccountsResponse { 454 - accounts: account_infos, 455 - request_uri, 456 - }) 457 - .into_response() 458 - } 459 - 460 - pub async fn authorize_post( 461 - State(state): State<AppState>, 462 - _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 463 - headers: HeaderMap, 464 - Json(form): Json<AuthorizeSubmit>, 465 - ) -> Response { 466 - let json_response = wants_json(&headers); 467 - let form_request_id = RequestId::from(form.request_uri.clone()); 468 - let request_data = match state 469 - .oauth_repo 470 - .get_authorization_request(&form_request_id) 471 - .await 472 - { 473 - Ok(Some(data)) => data, 474 - Ok(None) => { 475 - if json_response { 476 - return ( 477 - axum::http::StatusCode::BAD_REQUEST, 478 - Json(serde_json::json!({ 479 - "error": "invalid_request", 480 - "error_description": "Invalid or expired request_uri." 481 - })), 482 - ) 483 - .into_response(); 484 - } 485 - return redirect_to_frontend_error( 486 - "invalid_request", 487 - "Invalid or expired request_uri. Please start a new authorization request.", 488 - ); 489 - } 490 - Err(e) => { 491 - if json_response { 492 - return ( 493 - axum::http::StatusCode::INTERNAL_SERVER_ERROR, 494 - Json(serde_json::json!({ 495 - "error": "server_error", 496 - "error_description": format!("Database error: {:?}", e) 497 - })), 498 - ) 499 - .into_response(); 500 - } 501 - return redirect_to_frontend_error("server_error", &format!("Database error: {:?}", e)); 502 - } 503 - }; 504 - if request_data.expires_at < Utc::now() { 505 - let _ = state 506 - .oauth_repo 507 - .delete_authorization_request(&form_request_id) 508 - .await; 509 - if json_response { 510 - return ( 511 - axum::http::StatusCode::BAD_REQUEST, 512 - Json(serde_json::json!({ 513 - "error": "invalid_request", 514 - "error_description": "Authorization request has expired." 515 - })), 516 - ) 517 - .into_response(); 518 - } 519 - return redirect_to_frontend_error( 520 - "invalid_request", 521 - "Authorization request has expired. Please start a new request.", 522 - ); 523 - } 524 - let show_login_error = |error_msg: &str, json: bool| -> Response { 525 - if json { 526 - return ( 527 - axum::http::StatusCode::FORBIDDEN, 528 - Json(serde_json::json!({ 529 - "error": "access_denied", 530 - "error_description": error_msg 531 - })), 532 - ) 533 - .into_response(); 534 - } 535 - redirect_see_other(&format!( 536 - "/app/oauth/login?request_uri={}&error={}", 537 - url_encode(&form.request_uri), 538 - url_encode(error_msg) 539 - )) 540 - }; 541 - let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 542 - let normalized_username = 543 - NormalizedLoginIdentifier::normalize(&form.username, hostname_for_handles); 544 - tracing::debug!( 545 - original_username = %form.username, 546 - normalized_username = %normalized_username, 547 - pds_hostname = %tranquil_config::get().server.hostname, 548 - "Normalized username for lookup" 549 - ); 550 - let user = match state 551 - .user_repo 552 - .get_login_info_by_handle_or_email(normalized_username.as_str()) 553 - .await 554 - { 555 - Ok(Some(u)) => u, 556 - Ok(None) => { 557 - let _ = bcrypt::verify( 558 - &form.password, 559 - "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK", 560 - ); 561 - return show_login_error("Invalid handle/email or password.", json_response); 562 - } 563 - Err(_) => return show_login_error("An error occurred. Please try again.", json_response), 564 - }; 565 - if user.deactivated_at.is_some() { 566 - return show_login_error("This account has been deactivated.", json_response); 567 - } 568 - if user.takedown_ref.is_some() { 569 - return show_login_error("This account has been taken down.", json_response); 570 - } 571 - if user.account_type.is_delegated() { 572 - if state 573 - .oauth_repo 574 - .set_authorization_did(&form_request_id, &user.did, None) 575 - .await 576 - .is_err() 577 - { 578 - return show_login_error("An error occurred. Please try again.", json_response); 579 - } 580 - let redirect_url = format!( 581 - "/app/oauth/delegation?request_uri={}&delegated_did={}", 582 - url_encode(&form.request_uri), 583 - url_encode(&user.did) 584 - ); 585 - if json_response { 586 - return ( 587 - StatusCode::OK, 588 - Json(serde_json::json!({ 589 - "next": "delegation", 590 - "delegated_did": user.did, 591 - "redirect": redirect_url 592 - })), 593 - ) 594 - .into_response(); 595 - } 596 - return redirect_see_other(&redirect_url); 597 - } 598 - 599 - if !user.password_required { 600 - if state 601 - .oauth_repo 602 - .set_authorization_did(&form_request_id, &user.did, None) 603 - .await 604 - .is_err() 605 - { 606 - return show_login_error("An error occurred. Please try again.", json_response); 607 - } 608 - let redirect_url = format!( 609 - "/app/oauth/passkey?request_uri={}", 610 - url_encode(&form.request_uri) 611 - ); 612 - if json_response { 613 - return ( 614 - StatusCode::OK, 615 - Json(serde_json::json!({ 616 - "next": "passkey", 617 - "redirect": redirect_url 618 - })), 619 - ) 620 - .into_response(); 621 - } 622 - return redirect_see_other(&redirect_url); 623 - } 624 - 625 - let password_valid = match &user.password_hash { 626 - Some(hash) => match bcrypt::verify(&form.password, hash) { 627 - Ok(valid) => valid, 628 - Err(_) => { 629 - return show_login_error("An error occurred. Please try again.", json_response); 630 - } 631 - }, 632 - None => false, 633 - }; 634 - if !password_valid { 635 - return show_login_error("Invalid handle/email or password.", json_response); 636 - } 637 - let is_verified = user.channel_verification.has_any_verified(); 638 - if !is_verified { 639 - let resend_info = tranquil_api::server::auto_resend_verification(&state, &user.did).await; 640 - let handle = resend_info 641 - .as_ref() 642 - .map(|r| r.handle.to_string()) 643 - .unwrap_or_else(|| form.username.clone()); 644 - let channel = resend_info 645 - .map(|r| r.channel.as_str().to_owned()) 646 - .unwrap_or_else(|| user.preferred_comms_channel.as_str().to_owned()); 647 - if json_response { 648 - return ( 649 - axum::http::StatusCode::FORBIDDEN, 650 - Json(serde_json::json!({ 651 - "error": "account_not_verified", 652 - "error_description": "Please verify your account before logging in.", 653 - "did": user.did, 654 - "handle": handle, 655 - "channel": channel 656 - })), 657 - ) 658 - .into_response(); 659 - } 660 - return redirect_see_other(&format!( 661 - "/app/oauth/login?request_uri={}&error={}", 662 - url_encode(&form.request_uri), 663 - url_encode("account_not_verified") 664 - )); 665 - } 666 - let has_totp = tranquil_api::server::has_totp_enabled(&state, &user.did).await; 667 - if has_totp { 668 - let device_cookie = extract_device_cookie(&headers); 669 - let device_is_trusted = if let Some(ref dev_id) = device_cookie { 670 - tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &user.did) 671 - .await 672 - } else { 673 - false 674 - }; 675 - 676 - if device_is_trusted { 677 - if let Some(ref dev_id) = device_cookie { 678 - let _ = 679 - tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id) 680 - .await; 681 - } 682 - } else { 683 - if state 684 - .oauth_repo 685 - .set_authorization_did(&form_request_id, &user.did, None) 686 - .await 687 - .is_err() 688 - { 689 - return show_login_error("An error occurred. Please try again.", json_response); 690 - } 691 - if json_response { 692 - return Json(serde_json::json!({ 693 - "needs_totp": true 694 - })) 695 - .into_response(); 696 - } 697 - return redirect_see_other(&format!( 698 - "/app/oauth/totp?request_uri={}", 699 - url_encode(&form.request_uri) 700 - )); 701 - } 702 - } 703 - if user.two_factor_enabled { 704 - let _ = state 705 - .oauth_repo 706 - .delete_2fa_challenge_by_request_uri(&form_request_id) 707 - .await; 708 - match state 709 - .oauth_repo 710 - .create_2fa_challenge(&user.did, &form_request_id) 711 - .await 712 - { 713 - Ok(challenge) => { 714 - let hostname = &tranquil_config::get().server.hostname; 715 - if let Err(e) = enqueue_2fa_code( 716 - state.user_repo.as_ref(), 717 - state.infra_repo.as_ref(), 718 - user.id, 719 - &challenge.code, 720 - hostname, 721 - ) 722 - .await 723 - { 724 - tracing::warn!( 725 - did = %user.did, 726 - error = %e, 727 - "Failed to enqueue 2FA notification" 728 - ); 729 - } 730 - let channel_name = user.preferred_comms_channel.display_name(); 731 - if json_response { 732 - return Json(serde_json::json!({ 733 - "needs_2fa": true, 734 - "channel": channel_name 735 - })) 736 - .into_response(); 737 - } 738 - return redirect_see_other(&format!( 739 - "/app/oauth/2fa?request_uri={}&channel={}", 740 - url_encode(&form.request_uri), 741 - url_encode(channel_name) 742 - )); 743 - } 744 - Err(_) => { 745 - return show_login_error("An error occurred. Please try again.", json_response); 746 - } 747 - } 748 - } 749 - let mut device_id: Option<DeviceIdType> = extract_device_cookie(&headers); 750 - let mut new_cookie: Option<String> = None; 751 - if form.remember_device { 752 - let final_device_id = if let Some(existing_id) = &device_id { 753 - existing_id.clone() 754 - } else { 755 - let new_id = DeviceId::generate(); 756 - let new_device_id_typed = DeviceIdType::new(new_id.0.clone()); 757 - let device_data = DeviceData { 758 - session_id: SessionId::generate(), 759 - user_agent: extract_user_agent(&headers), 760 - ip_address: extract_client_ip(&headers, None), 761 - last_seen_at: Utc::now(), 762 - }; 763 - if state 764 - .oauth_repo 765 - .create_device(&new_device_id_typed, &device_data) 766 - .await 767 - .is_ok() 768 - { 769 - new_cookie = Some(make_device_cookie(&new_device_id_typed)); 770 - device_id = Some(new_device_id_typed.clone()); 771 - } 772 - new_device_id_typed 773 - }; 774 - let _ = state 775 - .oauth_repo 776 - .upsert_account_device(&user.did, &final_device_id) 777 - .await; 778 - } 779 - let set_auth_device_id = device_id.clone(); 780 - if state 781 - .oauth_repo 782 - .set_authorization_did(&form_request_id, &user.did, set_auth_device_id.as_ref()) 783 - .await 784 - .is_err() 785 - { 786 - return show_login_error("An error occurred. Please try again.", json_response); 787 - } 788 - let requested_scope_str = request_data 789 - .parameters 790 - .scope 791 - .as_deref() 792 - .unwrap_or("atproto"); 793 - let requested_scopes: Vec<String> = requested_scope_str 794 - .split_whitespace() 795 - .map(|s| s.to_string()) 796 - .collect(); 797 - let client_id_typed = ClientId::from(request_data.parameters.client_id.clone()); 798 - let needs_consent = should_show_consent( 799 - state.oauth_repo.as_ref(), 800 - &user.did, 801 - &client_id_typed, 802 - &requested_scopes, 803 - ) 804 - .await 805 - .unwrap_or(true); 806 - if needs_consent { 807 - let consent_url = format!( 808 - "/app/oauth/consent?request_uri={}", 809 - url_encode(&form.request_uri) 810 - ); 811 - if json_response { 812 - if let Some(cookie) = new_cookie { 813 - return ( 814 - StatusCode::OK, 815 - [(SET_COOKIE, cookie)], 816 - Json(serde_json::json!({"redirect_uri": consent_url})), 817 - ) 818 - .into_response(); 819 - } 820 - return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 821 - } 822 - if let Some(cookie) = new_cookie { 823 - return ( 824 - StatusCode::SEE_OTHER, 825 - [(SET_COOKIE, cookie), (LOCATION, consent_url)], 826 - ) 827 - .into_response(); 828 - } 829 - return redirect_see_other(&consent_url); 830 - } 831 - let code = Code::generate(); 832 - let auth_post_device_id = device_id.clone(); 833 - let auth_post_code = AuthorizationCode::from(code.0.clone()); 834 - if state 835 - .oauth_repo 836 - .update_authorization_request( 837 - &form_request_id, 838 - &user.did, 839 - auth_post_device_id.as_ref(), 840 - &auth_post_code, 841 - ) 842 - .await 843 - .is_err() 844 - { 845 - return show_login_error("An error occurred. Please try again.", json_response); 846 - } 847 - if json_response { 848 - let redirect_url = build_intermediate_redirect_url( 849 - &request_data.parameters.redirect_uri, 850 - &code.0, 851 - request_data.parameters.state.as_deref(), 852 - request_data.parameters.response_mode.map(|m| m.as_str()), 853 - ); 854 - if let Some(cookie) = new_cookie { 855 - ( 856 - StatusCode::OK, 857 - [(SET_COOKIE, cookie)], 858 - Json(serde_json::json!({"redirect_uri": redirect_url})), 859 - ) 860 - .into_response() 861 - } else { 862 - Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 863 - } 864 - } else { 865 - let redirect_url = build_success_redirect( 866 - &request_data.parameters.redirect_uri, 867 - &code.0, 868 - request_data.parameters.state.as_deref(), 869 - request_data.parameters.response_mode.map(|m| m.as_str()), 870 - ); 871 - if let Some(cookie) = new_cookie { 872 - ( 873 - StatusCode::SEE_OTHER, 874 - [(SET_COOKIE, cookie), (LOCATION, redirect_url)], 875 - ) 876 - .into_response() 877 - } else { 878 - redirect_see_other(&redirect_url) 879 - } 880 - } 881 - } 882 - 883 - pub async fn authorize_select( 884 - State(state): State<AppState>, 885 - headers: HeaderMap, 886 - Json(form): Json<AuthorizeSelectSubmit>, 887 - ) -> Response { 888 - let json_error = |status: StatusCode, error: &str, description: &str| -> Response { 889 - ( 890 - status, 891 - Json(serde_json::json!({ 892 - "error": error, 893 - "error_description": description 894 - })), 895 - ) 896 - .into_response() 897 - }; 898 - let select_request_id = RequestId::from(form.request_uri.clone()); 899 - let request_data = match state 900 - .oauth_repo 901 - .get_authorization_request(&select_request_id) 902 - .await 903 - { 904 - Ok(Some(data)) => data, 905 - Ok(None) => { 906 - return json_error( 907 - StatusCode::BAD_REQUEST, 908 - "invalid_request", 909 - "Invalid or expired request_uri. Please start a new authorization request.", 910 - ); 911 - } 912 - Err(_) => { 913 - return json_error( 914 - StatusCode::INTERNAL_SERVER_ERROR, 915 - "server_error", 916 - "An error occurred. Please try again.", 917 - ); 918 - } 919 - }; 920 - if request_data.expires_at < Utc::now() { 921 - let _ = state 922 - .oauth_repo 923 - .delete_authorization_request(&select_request_id) 924 - .await; 925 - return json_error( 926 - StatusCode::BAD_REQUEST, 927 - "invalid_request", 928 - "Authorization request has expired. Please start a new request.", 929 - ); 930 - } 931 - let device_id = match extract_device_cookie(&headers) { 932 - Some(id) => id, 933 - None => { 934 - return json_error( 935 - StatusCode::BAD_REQUEST, 936 - "invalid_request", 937 - "No device session found. Please sign in.", 938 - ); 939 - } 940 - }; 941 - let did: Did = match form.did.parse() { 942 - Ok(d) => d, 943 - Err(_) => { 944 - return json_error( 945 - StatusCode::BAD_REQUEST, 946 - "invalid_request", 947 - "Invalid DID format.", 948 - ); 949 - } 950 - }; 951 - let verify_device_id = device_id.clone(); 952 - let account_valid = match state 953 - .oauth_repo 954 - .verify_account_on_device(&verify_device_id, &did) 955 - .await 956 - { 957 - Ok(valid) => valid, 958 - Err(_) => { 959 - return json_error( 960 - StatusCode::INTERNAL_SERVER_ERROR, 961 - "server_error", 962 - "An error occurred. Please try again.", 963 - ); 964 - } 965 - }; 966 - if !account_valid { 967 - return json_error( 968 - StatusCode::FORBIDDEN, 969 - "access_denied", 970 - "This account is not available on this device. Please sign in.", 971 - ); 972 - } 973 - let user = match state.user_repo.get_2fa_status_by_did(&did).await { 974 - Ok(Some(u)) => u, 975 - Ok(None) => { 976 - return json_error( 977 - StatusCode::FORBIDDEN, 978 - "access_denied", 979 - "Account not found. Please sign in.", 980 - ); 981 - } 982 - Err(_) => { 983 - return json_error( 984 - StatusCode::INTERNAL_SERVER_ERROR, 985 - "server_error", 986 - "An error occurred. Please try again.", 987 - ); 988 - } 989 - }; 990 - let is_verified = user.channel_verification.has_any_verified(); 991 - if !is_verified { 992 - let resend_info = tranquil_api::server::auto_resend_verification(&state, &did).await; 993 - return ( 994 - StatusCode::FORBIDDEN, 995 - Json(serde_json::json!({ 996 - "error": "account_not_verified", 997 - "error_description": "Please verify your account before logging in.", 998 - "did": did, 999 - "handle": resend_info.as_ref().map(|r| r.handle.to_string()), 1000 - "channel": resend_info.as_ref().map(|r| r.channel.as_str()) 1001 - })), 1002 - ) 1003 - .into_response(); 1004 - } 1005 - let has_totp = tranquil_api::server::has_totp_enabled(&state, &did).await; 1006 - let select_early_device_typed = device_id.clone(); 1007 - if has_totp { 1008 - let device_is_trusted = 1009 - tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), &device_id, &did) 1010 - .await; 1011 - if !device_is_trusted { 1012 - if state 1013 - .oauth_repo 1014 - .set_authorization_did(&select_request_id, &did, Some(&select_early_device_typed)) 1015 - .await 1016 - .is_err() 1017 - { 1018 - return json_error( 1019 - StatusCode::INTERNAL_SERVER_ERROR, 1020 - "server_error", 1021 - "An error occurred. Please try again.", 1022 - ); 1023 - } 1024 - return Json(serde_json::json!({ 1025 - "needs_totp": true 1026 - })) 1027 - .into_response(); 1028 - } 1029 - let _ = 1030 - tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), &device_id).await; 1031 - } 1032 - if user.two_factor_enabled { 1033 - let _ = state 1034 - .oauth_repo 1035 - .delete_2fa_challenge_by_request_uri(&select_request_id) 1036 - .await; 1037 - match state 1038 - .oauth_repo 1039 - .create_2fa_challenge(&did, &select_request_id) 1040 - .await 1041 - { 1042 - Ok(challenge) => { 1043 - let hostname = &tranquil_config::get().server.hostname; 1044 - if let Err(e) = enqueue_2fa_code( 1045 - state.user_repo.as_ref(), 1046 - state.infra_repo.as_ref(), 1047 - user.id, 1048 - &challenge.code, 1049 - hostname, 1050 - ) 1051 - .await 1052 - { 1053 - tracing::warn!( 1054 - did = %form.did, 1055 - error = %e, 1056 - "Failed to enqueue 2FA notification" 1057 - ); 1058 - } 1059 - let channel_name = user.preferred_comms_channel.display_name(); 1060 - return Json(serde_json::json!({ 1061 - "needs_2fa": true, 1062 - "channel": channel_name 1063 - })) 1064 - .into_response(); 1065 - } 1066 - Err(_) => { 1067 - return json_error( 1068 - StatusCode::INTERNAL_SERVER_ERROR, 1069 - "server_error", 1070 - "An error occurred. Please try again.", 1071 - ); 1072 - } 1073 - } 1074 - } 1075 - let select_device_typed = device_id.clone(); 1076 - let _ = state 1077 - .oauth_repo 1078 - .upsert_account_device(&did, &select_device_typed) 1079 - .await; 1080 - 1081 - if state 1082 - .oauth_repo 1083 - .set_authorization_did(&select_request_id, &did, Some(&select_device_typed)) 1084 - .await 1085 - .is_err() 1086 - { 1087 - return json_error( 1088 - StatusCode::INTERNAL_SERVER_ERROR, 1089 - "server_error", 1090 - "An error occurred. Please try again.", 1091 - ); 1092 - } 1093 - let consent_url = format!( 1094 - "/app/oauth/consent?request_uri={}", 1095 - url_encode(&form.request_uri) 1096 - ); 1097 - Json(serde_json::json!({"redirect_uri": consent_url})).into_response() 1098 - } 1099 - 1100 - fn build_success_redirect( 1101 - redirect_uri: &str, 1102 - code: &str, 1103 - state: Option<&str>, 1104 - response_mode: Option<&str>, 1105 - ) -> String { 1106 - let mut redirect_url = redirect_uri.to_string(); 1107 - let use_fragment = response_mode == Some("fragment"); 1108 - let separator = if use_fragment { 1109 - '#' 1110 - } else if redirect_url.contains('?') { 1111 - '&' 1112 - } else { 1113 - '?' 1114 - }; 1115 - redirect_url.push(separator); 1116 - let pds_host = &tranquil_config::get().server.hostname; 1117 - redirect_url.push_str(&format!( 1118 - "iss={}", 1119 - url_encode(&format!("https://{}", pds_host)) 1120 - )); 1121 - if let Some(req_state) = state { 1122 - redirect_url.push_str(&format!("&state={}", url_encode(req_state))); 1123 - } 1124 - redirect_url.push_str(&format!("&code={}", url_encode(code))); 1125 - redirect_url 1126 - } 1127 - 1128 - fn build_intermediate_redirect_url( 1129 - redirect_uri: &str, 1130 - code: &str, 1131 - state: Option<&str>, 1132 - response_mode: Option<&str>, 1133 - ) -> String { 1134 - let pds_host = &tranquil_config::get().server.hostname; 1135 - let mut url = format!( 1136 - "https://{}/oauth/authorize/redirect?redirect_uri={}&code={}", 1137 - pds_host, 1138 - url_encode(redirect_uri), 1139 - url_encode(code) 1140 - ); 1141 - if let Some(s) = state { 1142 - url.push_str(&format!("&state={}", url_encode(s))); 1143 - } 1144 - if let Some(rm) = response_mode { 1145 - url.push_str(&format!("&response_mode={}", url_encode(rm))); 1146 - } 1147 - url 1148 - } 1149 - 1150 - #[derive(Debug, Deserialize)] 1151 - pub struct AuthorizeRedirectParams { 1152 - redirect_uri: String, 1153 - code: String, 1154 - state: Option<String>, 1155 - response_mode: Option<String>, 1156 - } 1157 - 1158 - pub async fn authorize_redirect(Query(params): Query<AuthorizeRedirectParams>) -> Response { 1159 - let final_url = build_success_redirect( 1160 - &params.redirect_uri, 1161 - &params.code, 1162 - params.state.as_deref(), 1163 - params.response_mode.as_deref(), 1164 - ); 1165 - tracing::info!( 1166 - final_url = %final_url, 1167 - client_redirect = %params.redirect_uri, 1168 - "authorize_redirect performing 303 redirect" 1169 - ); 1170 - ( 1171 - StatusCode::SEE_OTHER, 1172 - [ 1173 - (axum::http::header::LOCATION, final_url), 1174 - (axum::http::header::CACHE_CONTROL, "no-store".to_string()), 1175 - ], 1176 - ) 1177 - .into_response() 1178 - } 1179 - 1180 - #[derive(Debug, Serialize)] 1181 - pub struct AuthorizeDenyResponse { 1182 - pub error: String, 1183 - pub error_description: String, 1184 - } 1185 - 1186 - pub async fn authorize_deny( 1187 - State(state): State<AppState>, 1188 - Json(form): Json<AuthorizeDenyForm>, 1189 - ) -> Response { 1190 - let deny_request_id = RequestId::from(form.request_uri.clone()); 1191 - let request_data = match state 1192 - .oauth_repo 1193 - .get_authorization_request(&deny_request_id) 1194 - .await 1195 - { 1196 - Ok(Some(data)) => data, 1197 - Ok(None) => { 1198 - return ( 1199 - StatusCode::BAD_REQUEST, 1200 - Json(serde_json::json!({ 1201 - "error": "invalid_request", 1202 - "error_description": "Invalid request_uri" 1203 - })), 1204 - ) 1205 - .into_response(); 1206 - } 1207 - Err(_) => { 1208 - return ( 1209 - StatusCode::INTERNAL_SERVER_ERROR, 1210 - Json(serde_json::json!({ 1211 - "error": "server_error", 1212 - "error_description": "An error occurred" 1213 - })), 1214 - ) 1215 - .into_response(); 1216 - } 1217 - }; 1218 - let _ = state 1219 - .oauth_repo 1220 - .delete_authorization_request(&deny_request_id) 1221 - .await; 1222 - let redirect_uri = &request_data.parameters.redirect_uri; 1223 - let mut redirect_url = redirect_uri.to_string(); 1224 - let separator = if redirect_url.contains('?') { '&' } else { '?' }; 1225 - redirect_url.push(separator); 1226 - redirect_url.push_str("error=access_denied"); 1227 - redirect_url.push_str("&error_description=User%20denied%20the%20request"); 1228 - if let Some(state) = &request_data.parameters.state { 1229 - redirect_url.push_str(&format!("&state={}", url_encode(state))); 1230 - } 1231 - Json(serde_json::json!({ 1232 - "redirect_uri": redirect_url 1233 - })) 1234 - .into_response() 1235 - } 1236 - 1237 - #[derive(Debug, Deserialize)] 1238 - pub struct AuthorizeDenyForm { 1239 - pub request_uri: String, 1240 - } 1241 - 1242 - #[derive(Debug, Deserialize)] 1243 - pub struct Authorize2faQuery { 1244 - pub request_uri: String, 1245 - pub channel: Option<String>, 1246 - } 1247 - 1248 - #[derive(Debug, Deserialize)] 1249 - pub struct Authorize2faSubmit { 1250 - pub request_uri: String, 1251 - pub code: String, 1252 - #[serde(default)] 1253 - pub trust_device: bool, 1254 - } 1255 - 1256 - const MAX_2FA_ATTEMPTS: i32 = 5; 1257 - 1258 - pub async fn authorize_2fa_get( 1259 - State(state): State<AppState>, 1260 - Query(query): Query<Authorize2faQuery>, 1261 - ) -> Response { 1262 - let twofa_request_id = RequestId::from(query.request_uri.clone()); 1263 - let challenge = match state.oauth_repo.get_2fa_challenge(&twofa_request_id).await { 1264 - Ok(Some(c)) => c, 1265 - Ok(None) => { 1266 - return redirect_to_frontend_error( 1267 - "invalid_request", 1268 - "No 2FA challenge found. Please start over.", 1269 - ); 1270 - } 1271 - Err(_) => { 1272 - return redirect_to_frontend_error( 1273 - "server_error", 1274 - "An error occurred. Please try again.", 1275 - ); 1276 - } 1277 - }; 1278 - if challenge.expires_at < Utc::now() { 1279 - let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await; 1280 - return redirect_to_frontend_error( 1281 - "invalid_request", 1282 - "2FA code has expired. Please start over.", 1283 - ); 1284 - } 1285 - let _request_data = match state 1286 - .oauth_repo 1287 - .get_authorization_request(&twofa_request_id) 1288 - .await 1289 - { 1290 - Ok(Some(d)) => d, 1291 - Ok(None) => { 1292 - return redirect_to_frontend_error( 1293 - "invalid_request", 1294 - "Authorization request not found. Please start over.", 1295 - ); 1296 - } 1297 - Err(_) => { 1298 - return redirect_to_frontend_error( 1299 - "server_error", 1300 - "An error occurred. Please try again.", 1301 - ); 1302 - } 1303 - }; 1304 - let channel = query.channel.as_deref().unwrap_or("email"); 1305 - redirect_see_other(&format!( 1306 - "/app/oauth/2fa?request_uri={}&channel={}", 1307 - url_encode(&query.request_uri), 1308 - url_encode(channel) 1309 - )) 1310 - } 1311 - 1312 - #[derive(Debug, Serialize)] 1313 - pub struct ScopeInfo { 1314 - pub scope: String, 1315 - pub category: String, 1316 - pub required: bool, 1317 - pub description: String, 1318 - pub display_name: String, 1319 - pub granted: Option<bool>, 1320 - } 1321 - 1322 - #[derive(Debug, Serialize)] 1323 - pub struct ConsentResponse { 1324 - pub request_uri: String, 1325 - pub client_id: String, 1326 - pub client_name: Option<String>, 1327 - pub client_uri: Option<String>, 1328 - pub logo_uri: Option<String>, 1329 - pub scopes: Vec<ScopeInfo>, 1330 - pub show_consent: bool, 1331 - pub did: String, 1332 - #[serde(skip_serializing_if = "Option::is_none")] 1333 - pub handle: Option<String>, 1334 - #[serde(skip_serializing_if = "Option::is_none")] 1335 - pub is_delegation: Option<bool>, 1336 - #[serde(skip_serializing_if = "Option::is_none")] 1337 - pub controller_did: Option<String>, 1338 - #[serde(skip_serializing_if = "Option::is_none")] 1339 - pub controller_handle: Option<String>, 1340 - #[serde(skip_serializing_if = "Option::is_none")] 1341 - pub delegation_level: Option<String>, 1342 - } 1343 - 1344 - #[derive(Debug, Deserialize)] 1345 - pub struct ConsentQuery { 1346 - pub request_uri: String, 1347 - } 1348 - 1349 - #[derive(Debug, Deserialize)] 1350 - pub struct ConsentSubmit { 1351 - pub request_uri: String, 1352 - pub approved_scopes: Vec<String>, 1353 - pub remember: bool, 1354 - } 1355 - 1356 - pub async fn consent_get( 1357 - State(state): State<AppState>, 1358 - Query(query): Query<ConsentQuery>, 1359 - ) -> Response { 1360 - let consent_request_id = RequestId::from(query.request_uri.clone()); 1361 - let request_data = match state 1362 - .oauth_repo 1363 - .get_authorization_request(&consent_request_id) 1364 - .await 1365 - { 1366 - Ok(Some(data)) => data, 1367 - Ok(None) => { 1368 - return json_error( 1369 - StatusCode::BAD_REQUEST, 1370 - "invalid_request", 1371 - "Invalid or expired request_uri", 1372 - ); 1373 - } 1374 - Err(e) => { 1375 - return json_error( 1376 - StatusCode::INTERNAL_SERVER_ERROR, 1377 - "server_error", 1378 - &format!("Database error: {:?}", e), 1379 - ); 1380 - } 1381 - }; 1382 - let flow_with_user = match AuthFlow::from_request_data(request_data.clone()) { 1383 - Ok(flow) => match flow.require_user() { 1384 - Ok(u) => u, 1385 - Err(_) => { 1386 - return json_error(StatusCode::FORBIDDEN, "access_denied", "Not authenticated"); 1387 - } 1388 - }, 1389 - Err(_) => { 1390 - return json_error( 1391 - StatusCode::BAD_REQUEST, 1392 - "expired_request", 1393 - "Authorization request has expired", 1394 - ); 1395 - } 1396 - }; 1397 - 1398 - let did = flow_with_user.did().clone(); 1399 - let client_cache = ClientMetadataCache::new(3600); 1400 - let client_metadata = client_cache 1401 - .get(&request_data.parameters.client_id) 1402 - .await 1403 - .ok(); 1404 - let requested_scope_str = request_data 1405 - .parameters 1406 - .scope 1407 - .as_deref() 1408 - .filter(|s| !s.trim().is_empty()) 1409 - .unwrap_or("atproto"); 1410 - 1411 - let controller_did_parsed: Option<Did> = request_data 1412 - .controller_did 1413 - .as_ref() 1414 - .and_then(|s| s.parse().ok()); 1415 - let delegation_grant = if let Some(ref ctrl_did) = controller_did_parsed { 1416 - state 1417 - .delegation_repo 1418 - .get_delegation(&did, ctrl_did) 1419 - .await 1420 - .ok() 1421 - .flatten() 1422 - } else { 1423 - None 1424 - }; 1425 - 1426 - let effective_scope_str = if let Some(ref grant) = delegation_grant { 1427 - tranquil_pds::delegation::intersect_scopes( 1428 - requested_scope_str, 1429 - grant.granted_scopes.as_str(), 1430 - ) 1431 - } else { 1432 - requested_scope_str.to_string() 1433 - }; 1434 - 1435 - let expanded_scope_str = match expand_include_scopes(&effective_scope_str).await { 1436 - Ok(s) => s, 1437 - Err(e) => { 1438 - return json_error( 1439 - StatusCode::BAD_REQUEST, 1440 - "invalid_scope", 1441 - &format!("Failed to expand permission set: {e}"), 1442 - ); 1443 - } 1444 - }; 1445 - let requested_scopes: Vec<&str> = expanded_scope_str.split_whitespace().collect(); 1446 - let consent_client_id = ClientId::from(request_data.parameters.client_id.clone()); 1447 - let preferences = state 1448 - .oauth_repo 1449 - .get_scope_preferences(&did, &consent_client_id) 1450 - .await 1451 - .unwrap_or_default(); 1452 - let pref_map: std::collections::HashMap<_, _> = preferences 1453 - .iter() 1454 - .map(|p| (p.scope.as_str(), p.granted)) 1455 - .collect(); 1456 - let requested_scope_strings: Vec<String> = 1457 - requested_scopes.iter().map(|s| s.to_string()).collect(); 1458 - let show_consent = should_show_consent( 1459 - state.oauth_repo.as_ref(), 1460 - &did, 1461 - &consent_client_id, 1462 - &requested_scope_strings, 1463 - ) 1464 - .await 1465 - .unwrap_or(true); 1466 - let has_granular_scopes = requested_scopes.iter().any(|s| is_granular_scope(s)); 1467 - let scopes: Vec<ScopeInfo> = requested_scopes 1468 - .iter() 1469 - .map(|scope| { 1470 - let (category, required, description, display_name) = if let Some(def) = 1471 - tranquil_pds::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) 1472 - { 1473 - let desc = if *scope == "atproto" && has_granular_scopes { 1474 - "AT Protocol baseline scope (permissions determined by selected options below)" 1475 - .to_string() 1476 - } else { 1477 - def.description.to_string() 1478 - }; 1479 - let name = if *scope == "atproto" && has_granular_scopes { 1480 - "AT Protocol Access".to_string() 1481 - } else { 1482 - def.display_name.to_string() 1483 - }; 1484 - ( 1485 - def.category.display_name().to_string(), 1486 - def.required, 1487 - desc, 1488 - name, 1489 - ) 1490 - } else if scope.starts_with("ref:") { 1491 - ( 1492 - "Reference".to_string(), 1493 - false, 1494 - "Referenced scope".to_string(), 1495 - scope.to_string(), 1496 - ) 1497 - } else { 1498 - ( 1499 - "Other".to_string(), 1500 - false, 1501 - format!("Access to {}", scope), 1502 - scope.to_string(), 1503 - ) 1504 - }; 1505 - let granted = pref_map.get(*scope).copied(); 1506 - ScopeInfo { 1507 - scope: scope.to_string(), 1508 - category, 1509 - required, 1510 - description, 1511 - display_name, 1512 - granted, 1513 - } 1514 - }) 1515 - .collect(); 1516 - 1517 - let account_handle = state 1518 - .user_repo 1519 - .get_handle_by_did(&did) 1520 - .await 1521 - .ok() 1522 - .flatten() 1523 - .map(|h| h.to_string()); 1524 - 1525 - let (is_delegation, controller_did_resp, controller_handle, delegation_level) = 1526 - if let Some(ref ctrl_did) = controller_did_parsed { 1527 - let ctrl_handle = state 1528 - .user_repo 1529 - .get_handle_by_did(ctrl_did) 1530 - .await 1531 - .ok() 1532 - .flatten() 1533 - .map(|h| h.to_string()); 1534 - 1535 - let level = if let Some(ref grant) = delegation_grant { 1536 - let preset = tranquil_pds::delegation::SCOPE_PRESETS 1537 - .iter() 1538 - .find(|p| p.scopes == grant.granted_scopes.as_str()); 1539 - preset 1540 - .map(|p| p.label.to_string()) 1541 - .unwrap_or_else(|| "Custom".to_string()) 1542 - } else { 1543 - "Unknown".to_string() 1544 - }; 1545 - 1546 - ( 1547 - Some(true), 1548 - Some(ctrl_did.to_string()), 1549 - ctrl_handle, 1550 - Some(level), 1551 - ) 1552 - } else { 1553 - (None, None, None, None) 1554 - }; 1555 - 1556 - Json(ConsentResponse { 1557 - request_uri: query.request_uri.clone(), 1558 - client_id: request_data.parameters.client_id.clone(), 1559 - client_name: client_metadata.as_ref().and_then(|m| m.client_name.clone()), 1560 - client_uri: client_metadata.as_ref().and_then(|m| m.client_uri.clone()), 1561 - logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()), 1562 - scopes, 1563 - show_consent, 1564 - did: did.to_string(), 1565 - handle: account_handle, 1566 - is_delegation, 1567 - controller_did: controller_did_resp, 1568 - controller_handle, 1569 - delegation_level, 1570 - }) 1571 - .into_response() 1572 - } 1573 - 1574 - pub async fn consent_post( 1575 - State(state): State<AppState>, 1576 - Json(form): Json<ConsentSubmit>, 1577 - ) -> Response { 1578 - tracing::info!( 1579 - "consent_post: approved_scopes={:?}, remember={}", 1580 - form.approved_scopes, 1581 - form.remember 1582 - ); 1583 - let consent_post_request_id = RequestId::from(form.request_uri.clone()); 1584 - let request_data = match state 1585 - .oauth_repo 1586 - .get_authorization_request(&consent_post_request_id) 1587 - .await 1588 - { 1589 - Ok(Some(data)) => data, 1590 - Ok(None) => { 1591 - return json_error( 1592 - StatusCode::BAD_REQUEST, 1593 - "invalid_request", 1594 - "Invalid or expired request_uri", 1595 - ); 1596 - } 1597 - Err(e) => { 1598 - return json_error( 1599 - StatusCode::INTERNAL_SERVER_ERROR, 1600 - "server_error", 1601 - &format!("Database error: {:?}", e), 1602 - ); 1603 - } 1604 - }; 1605 - let flow_with_user = match AuthFlow::from_request_data(request_data.clone()) { 1606 - Ok(flow) => match flow.require_user() { 1607 - Ok(u) => u, 1608 - Err(_) => { 1609 - return json_error(StatusCode::FORBIDDEN, "access_denied", "Not authenticated"); 1610 - } 1611 - }, 1612 - Err(_) => { 1613 - let _ = state 1614 - .oauth_repo 1615 - .delete_authorization_request(&consent_post_request_id) 1616 - .await; 1617 - return json_error( 1618 - StatusCode::BAD_REQUEST, 1619 - "invalid_request", 1620 - "Authorization request has expired", 1621 - ); 1622 - } 1623 - }; 1624 - 1625 - let did = flow_with_user.did().clone(); 1626 - let original_scope_str = request_data 1627 - .parameters 1628 - .scope 1629 - .as_deref() 1630 - .unwrap_or("atproto"); 1631 - 1632 - let controller_did_parsed: Option<Did> = request_data 1633 - .controller_did 1634 - .as_ref() 1635 - .and_then(|s| s.parse().ok()); 1636 - 1637 - let delegation_grant = match controller_did_parsed.as_ref() { 1638 - Some(ctrl_did) => state 1639 - .delegation_repo 1640 - .get_delegation(&did, ctrl_did) 1641 - .await 1642 - .ok() 1643 - .flatten(), 1644 - None => None, 1645 - }; 1646 - 1647 - let effective_scope_str = if let Some(ref grant) = delegation_grant { 1648 - tranquil_pds::delegation::intersect_scopes( 1649 - original_scope_str, 1650 - grant.granted_scopes.as_str(), 1651 - ) 1652 - } else { 1653 - original_scope_str.to_string() 1654 - }; 1655 - 1656 - let requested_scopes: Vec<&str> = effective_scope_str.split_whitespace().collect(); 1657 - let has_granular_scopes = requested_scopes.iter().any(|s| is_granular_scope(s)); 1658 - let user_denied_some_granular = has_granular_scopes 1659 - && requested_scopes 1660 - .iter() 1661 - .filter(|s| is_granular_scope(s)) 1662 - .any(|s| !form.approved_scopes.contains(&s.to_string())); 1663 - let atproto_was_requested = requested_scopes.contains(&"atproto"); 1664 - if atproto_was_requested 1665 - && !has_granular_scopes 1666 - && !form.approved_scopes.contains(&"atproto".to_string()) 1667 - { 1668 - return json_error( 1669 - StatusCode::BAD_REQUEST, 1670 - "invalid_request", 1671 - "The atproto scope was requested and must be approved", 1672 - ); 1673 - } 1674 - let final_approved: Vec<String> = if user_denied_some_granular { 1675 - form.approved_scopes 1676 - .iter() 1677 - .filter(|s| *s != "atproto") 1678 - .cloned() 1679 - .collect() 1680 - } else { 1681 - form.approved_scopes.clone() 1682 - }; 1683 - if final_approved.is_empty() { 1684 - return json_error( 1685 - StatusCode::BAD_REQUEST, 1686 - "invalid_request", 1687 - "At least one scope must be approved", 1688 - ); 1689 - } 1690 - let approved_scope_str = final_approved.join(" "); 1691 - let has_valid_scope = final_approved.iter().all(|s| is_valid_scope(s)); 1692 - if !has_valid_scope { 1693 - return json_error( 1694 - StatusCode::BAD_REQUEST, 1695 - "invalid_request", 1696 - "Invalid scope format", 1697 - ); 1698 - } 1699 - if form.remember { 1700 - let preferences: Vec<ScopePreference> = requested_scopes 1701 - .iter() 1702 - .map(|s| ScopePreference { 1703 - scope: s.to_string(), 1704 - granted: form.approved_scopes.contains(&s.to_string()), 1705 - }) 1706 - .collect(); 1707 - let consent_post_client_id = ClientId::from(request_data.parameters.client_id.clone()); 1708 - let _ = state 1709 - .oauth_repo 1710 - .upsert_scope_preferences(&did, &consent_post_client_id, &preferences) 1711 - .await; 1712 - } 1713 - if let Err(e) = state 1714 - .oauth_repo 1715 - .update_request_scope(&consent_post_request_id, &approved_scope_str) 1716 - .await 1717 - { 1718 - tracing::warn!("Failed to update request scope: {:?}", e); 1719 - } 1720 - let code = Code::generate(); 1721 - let consent_post_device_id = request_data 1722 - .device_id 1723 - .as_ref() 1724 - .map(|d| DeviceIdType::new(d.0.clone())); 1725 - let consent_post_code = AuthorizationCode::from(code.0.clone()); 1726 - if state 1727 - .oauth_repo 1728 - .update_authorization_request( 1729 - &consent_post_request_id, 1730 - &did, 1731 - consent_post_device_id.as_ref(), 1732 - &consent_post_code, 1733 - ) 1734 - .await 1735 - .is_err() 1736 - { 1737 - return json_error( 1738 - StatusCode::INTERNAL_SERVER_ERROR, 1739 - "server_error", 1740 - "Failed to complete authorization", 1741 - ); 1742 - } 1743 - let redirect_uri = &request_data.parameters.redirect_uri; 1744 - let intermediate_url = build_intermediate_redirect_url( 1745 - redirect_uri, 1746 - &code.0, 1747 - request_data.parameters.state.as_deref(), 1748 - request_data.parameters.response_mode.map(|m| m.as_str()), 1749 - ); 1750 - tracing::info!( 1751 - intermediate_url = %intermediate_url, 1752 - client_redirect = %redirect_uri, 1753 - "consent_post returning JSON with intermediate URL (for 303 redirect)" 1754 - ); 1755 - Json(serde_json::json!({ "redirect_uri": intermediate_url })).into_response() 1756 - } 1757 - 1758 - #[derive(Debug, Deserialize)] 1759 - pub struct RenewRequest { 1760 - pub request_uri: String, 1761 - } 1762 - 1763 - pub async fn authorize_renew( 1764 - State(state): State<AppState>, 1765 - _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 1766 - Json(form): Json<RenewRequest>, 1767 - ) -> Response { 1768 - let request_id = RequestId::from(form.request_uri.clone()); 1769 - let request_data = match state 1770 - .oauth_repo 1771 - .get_authorization_request(&request_id) 1772 - .await 1773 - { 1774 - Ok(Some(data)) => data, 1775 - Ok(None) => { 1776 - return json_error( 1777 - StatusCode::BAD_REQUEST, 1778 - "invalid_request", 1779 - "Unknown authorization request", 1780 - ); 1781 - } 1782 - Err(_) => { 1783 - return json_error( 1784 - StatusCode::INTERNAL_SERVER_ERROR, 1785 - "server_error", 1786 - "Database error", 1787 - ); 1788 - } 1789 - }; 1790 - 1791 - if request_data.did.is_none() { 1792 - return json_error( 1793 - StatusCode::BAD_REQUEST, 1794 - "invalid_request", 1795 - "Authorization request not yet authenticated", 1796 - ); 1797 - } 1798 - 1799 - let now = Utc::now(); 1800 - if request_data.expires_at >= now { 1801 - return Json(serde_json::json!({ 1802 - "request_uri": form.request_uri, 1803 - "renewed": false 1804 - })) 1805 - .into_response(); 1806 - } 1807 - 1808 - let staleness = now - request_data.expires_at; 1809 - if staleness.num_seconds() > MAX_RENEWAL_STALENESS_SECONDS { 1810 - let _ = state 1811 - .oauth_repo 1812 - .delete_authorization_request(&request_id) 1813 - .await; 1814 - return json_error( 1815 - StatusCode::BAD_REQUEST, 1816 - "invalid_request", 1817 - "Authorization request expired too long ago to renew", 1818 - ); 1819 - } 1820 - 1821 - let new_expires_at = now + chrono::Duration::seconds(RENEW_EXPIRY_SECONDS); 1822 - match state 1823 - .oauth_repo 1824 - .extend_authorization_request_expiry(&request_id, new_expires_at) 1825 - .await 1826 - { 1827 - Ok(true) => Json(serde_json::json!({ 1828 - "request_uri": form.request_uri, 1829 - "renewed": true 1830 - })) 1831 - .into_response(), 1832 - Ok(false) => json_error( 1833 - StatusCode::BAD_REQUEST, 1834 - "invalid_request", 1835 - "Authorization request could not be renewed", 1836 - ), 1837 - Err(_) => json_error( 1838 - StatusCode::INTERNAL_SERVER_ERROR, 1839 - "server_error", 1840 - "Database error", 1841 - ), 1842 - } 1843 - } 1844 - 1845 - pub async fn authorize_2fa_post( 1846 - State(state): State<AppState>, 1847 - _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 1848 - headers: HeaderMap, 1849 - Json(form): Json<Authorize2faSubmit>, 1850 - ) -> Response { 1851 - let json_error = |status: StatusCode, error: &str, description: &str| -> Response { 1852 - ( 1853 - status, 1854 - Json(serde_json::json!({ 1855 - "error": error, 1856 - "error_description": description 1857 - })), 1858 - ) 1859 - .into_response() 1860 - }; 1861 - let twofa_post_request_id = RequestId::from(form.request_uri.clone()); 1862 - let request_data = match state 1863 - .oauth_repo 1864 - .get_authorization_request(&twofa_post_request_id) 1865 - .await 1866 - { 1867 - Ok(Some(d)) => d, 1868 - Ok(None) => { 1869 - return json_error( 1870 - StatusCode::BAD_REQUEST, 1871 - "invalid_request", 1872 - "Authorization request not found.", 1873 - ); 1874 - } 1875 - Err(_) => { 1876 - return json_error( 1877 - StatusCode::INTERNAL_SERVER_ERROR, 1878 - "server_error", 1879 - "An error occurred.", 1880 - ); 1881 - } 1882 - }; 1883 - if request_data.expires_at < Utc::now() { 1884 - let _ = state 1885 - .oauth_repo 1886 - .delete_authorization_request(&twofa_post_request_id) 1887 - .await; 1888 - return json_error( 1889 - StatusCode::BAD_REQUEST, 1890 - "invalid_request", 1891 - "Authorization request has expired.", 1892 - ); 1893 - } 1894 - let challenge = state 1895 - .oauth_repo 1896 - .get_2fa_challenge(&twofa_post_request_id) 1897 - .await 1898 - .ok() 1899 - .flatten(); 1900 - if let Some(challenge) = challenge { 1901 - if challenge.expires_at < Utc::now() { 1902 - let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await; 1903 - return json_error( 1904 - StatusCode::BAD_REQUEST, 1905 - "invalid_request", 1906 - "2FA code has expired. Please start over.", 1907 - ); 1908 - } 1909 - if challenge.attempts >= MAX_2FA_ATTEMPTS { 1910 - let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await; 1911 - return json_error( 1912 - StatusCode::FORBIDDEN, 1913 - "access_denied", 1914 - "Too many failed attempts. Please start over.", 1915 - ); 1916 - } 1917 - let code_valid: bool = form 1918 - .code 1919 - .trim() 1920 - .as_bytes() 1921 - .ct_eq(challenge.code.as_bytes()) 1922 - .into(); 1923 - if !code_valid { 1924 - let _ = state.oauth_repo.increment_2fa_attempts(challenge.id).await; 1925 - return json_error( 1926 - StatusCode::FORBIDDEN, 1927 - "invalid_code", 1928 - "Invalid verification code. Please try again.", 1929 - ); 1930 - } 1931 - let _ = state.oauth_repo.delete_2fa_challenge(challenge.id).await; 1932 - let code = Code::generate(); 1933 - let device_id = extract_device_cookie(&headers); 1934 - let twofa_totp_device_id = device_id.clone(); 1935 - let twofa_totp_code = AuthorizationCode::from(code.0.clone()); 1936 - if state 1937 - .oauth_repo 1938 - .update_authorization_request( 1939 - &twofa_post_request_id, 1940 - &challenge.did, 1941 - twofa_totp_device_id.as_ref(), 1942 - &twofa_totp_code, 1943 - ) 1944 - .await 1945 - .is_err() 1946 - { 1947 - return json_error( 1948 - StatusCode::INTERNAL_SERVER_ERROR, 1949 - "server_error", 1950 - "An error occurred. Please try again.", 1951 - ); 1952 - } 1953 - let redirect_url = build_intermediate_redirect_url( 1954 - &request_data.parameters.redirect_uri, 1955 - &code.0, 1956 - request_data.parameters.state.as_deref(), 1957 - request_data.parameters.response_mode.map(|m| m.as_str()), 1958 - ); 1959 - return Json(serde_json::json!({ 1960 - "redirect_uri": redirect_url 1961 - })) 1962 - .into_response(); 1963 - } 1964 - let did_str = match &request_data.did { 1965 - Some(d) => d.clone(), 1966 - None => { 1967 - return json_error( 1968 - StatusCode::BAD_REQUEST, 1969 - "invalid_request", 1970 - "No 2FA challenge found. Please start over.", 1971 - ); 1972 - } 1973 - }; 1974 - let did: tranquil_types::Did = match did_str.parse() { 1975 - Ok(d) => d, 1976 - Err(_) => { 1977 - return json_error( 1978 - StatusCode::BAD_REQUEST, 1979 - "invalid_request", 1980 - "Invalid DID format.", 1981 - ); 1982 - } 1983 - }; 1984 - if !tranquil_api::server::has_totp_enabled(&state, &did).await { 1985 - return json_error( 1986 - StatusCode::BAD_REQUEST, 1987 - "invalid_request", 1988 - "No 2FA challenge found. Please start over.", 1989 - ); 1990 - } 1991 - let _rate_proof = match check_user_rate_limit::<TotpVerifyLimit>(&state, &did).await { 1992 - Ok(proof) => proof, 1993 - Err(_) => { 1994 - return json_error( 1995 - StatusCode::TOO_MANY_REQUESTS, 1996 - "RateLimitExceeded", 1997 - "Too many verification attempts. Please try again in a few minutes.", 1998 - ); 1999 - } 2000 - }; 2001 - let totp_valid = 2002 - tranquil_api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await; 2003 - if !totp_valid { 2004 - return json_error( 2005 - StatusCode::FORBIDDEN, 2006 - "invalid_code", 2007 - "Invalid verification code. Please try again.", 2008 - ); 2009 - } 2010 - let mut device_id = extract_device_cookie(&headers); 2011 - let mut new_cookie: Option<String> = None; 2012 - if form.trust_device { 2013 - let trust_device_id = match &device_id { 2014 - Some(existing_id) => existing_id.clone(), 2015 - None => { 2016 - let new_id = DeviceId::generate(); 2017 - let new_device_id_typed = DeviceIdType::new(new_id.0.clone()); 2018 - let device_data = DeviceData { 2019 - session_id: SessionId::generate(), 2020 - user_agent: extract_user_agent(&headers), 2021 - ip_address: extract_client_ip(&headers, None), 2022 - last_seen_at: Utc::now(), 2023 - }; 2024 - if state 2025 - .oauth_repo 2026 - .create_device(&new_device_id_typed, &device_data) 2027 - .await 2028 - .is_ok() 2029 - { 2030 - new_cookie = Some(make_device_cookie(&new_device_id_typed)); 2031 - device_id = Some(new_device_id_typed.clone()); 2032 - } 2033 - new_device_id_typed 2034 - } 2035 - }; 2036 - let _ = state 2037 - .oauth_repo 2038 - .upsert_account_device(&did, &trust_device_id) 2039 - .await; 2040 - let _ = 2041 - tranquil_api::server::trust_device(state.oauth_repo.as_ref(), &trust_device_id).await; 2042 - } 2043 - let requested_scope_str = request_data 2044 - .parameters 2045 - .scope 2046 - .as_deref() 2047 - .unwrap_or("atproto"); 2048 - let requested_scopes: Vec<String> = requested_scope_str 2049 - .split_whitespace() 2050 - .map(|s| s.to_string()) 2051 - .collect(); 2052 - let twofa_post_client_id = ClientId::from(request_data.parameters.client_id.clone()); 2053 - let needs_consent = should_show_consent( 2054 - state.oauth_repo.as_ref(), 2055 - &did, 2056 - &twofa_post_client_id, 2057 - &requested_scopes, 2058 - ) 2059 - .await 2060 - .unwrap_or(true); 2061 - if needs_consent { 2062 - let consent_url = format!( 2063 - "/app/oauth/consent?request_uri={}", 2064 - url_encode(&form.request_uri) 2065 - ); 2066 - if let Some(cookie) = new_cookie { 2067 - return ( 2068 - StatusCode::OK, 2069 - [(SET_COOKIE, cookie)], 2070 - Json(serde_json::json!({"redirect_uri": consent_url})), 2071 - ) 2072 - .into_response(); 2073 - } 2074 - return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 2075 - } 2076 - let code = Code::generate(); 2077 - let twofa_final_device_id = device_id.clone(); 2078 - let twofa_final_code = AuthorizationCode::from(code.0.clone()); 2079 - if state 2080 - .oauth_repo 2081 - .update_authorization_request( 2082 - &twofa_post_request_id, 2083 - &did, 2084 - twofa_final_device_id.as_ref(), 2085 - &twofa_final_code, 2086 - ) 2087 - .await 2088 - .is_err() 2089 - { 2090 - return json_error( 2091 - StatusCode::INTERNAL_SERVER_ERROR, 2092 - "server_error", 2093 - "An error occurred. Please try again.", 2094 - ); 2095 - } 2096 - let redirect_url = build_intermediate_redirect_url( 2097 - &request_data.parameters.redirect_uri, 2098 - &code.0, 2099 - request_data.parameters.state.as_deref(), 2100 - request_data.parameters.response_mode.map(|m| m.as_str()), 2101 - ); 2102 - if let Some(cookie) = new_cookie { 2103 - ( 2104 - StatusCode::OK, 2105 - [(SET_COOKIE, cookie)], 2106 - Json(serde_json::json!({"redirect_uri": redirect_url})), 2107 - ) 2108 - .into_response() 2109 - } else { 2110 - Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 2111 - } 2112 - } 2113 - 2114 - #[derive(Debug, Deserialize)] 2115 - #[serde(rename_all = "camelCase")] 2116 - pub struct CheckPasskeysQuery { 2117 - pub identifier: String, 2118 - } 2119 - 2120 - #[derive(Debug, Serialize)] 2121 - #[serde(rename_all = "camelCase")] 2122 - pub struct CheckPasskeysResponse { 2123 - pub has_passkeys: bool, 2124 - } 2125 - 2126 - pub async fn check_user_has_passkeys( 2127 - State(state): State<AppState>, 2128 - Query(query): Query<CheckPasskeysQuery>, 2129 - ) -> Response { 2130 - let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 2131 - let bare_identifier = 2132 - BareLoginIdentifier::from_identifier(&query.identifier, hostname_for_handles); 2133 - 2134 - let user = state 2135 - .user_repo 2136 - .get_login_check_by_handle_or_email(bare_identifier.as_str()) 2137 - .await; 2138 - 2139 - let has_passkeys = match user { 2140 - Ok(Some(u)) => tranquil_api::server::has_passkeys_for_user(&state, &u.did).await, 2141 - _ => false, 2142 - }; 2143 - 2144 - Json(CheckPasskeysResponse { has_passkeys }).into_response() 2145 - } 2146 - 2147 - #[derive(Debug, Serialize)] 2148 - #[serde(rename_all = "camelCase")] 2149 - pub struct SecurityStatusResponse { 2150 - pub has_passkeys: bool, 2151 - pub has_totp: bool, 2152 - pub has_password: bool, 2153 - pub is_delegated: bool, 2154 - #[serde(skip_serializing_if = "Option::is_none")] 2155 - pub did: Option<String>, 2156 - } 2157 - 2158 - pub async fn check_user_security_status( 2159 - State(state): State<AppState>, 2160 - Query(query): Query<CheckPasskeysQuery>, 2161 - ) -> Response { 2162 - let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 2163 - let normalized_identifier = 2164 - NormalizedLoginIdentifier::normalize(&query.identifier, hostname_for_handles); 2165 - 2166 - let user = state 2167 - .user_repo 2168 - .get_login_check_by_handle_or_email(normalized_identifier.as_str()) 2169 - .await; 2170 - 2171 - let (has_passkeys, has_totp, has_password, is_delegated, did): ( 2172 - bool, 2173 - bool, 2174 - bool, 2175 - bool, 2176 - Option<String>, 2177 - ) = match user { 2178 - Ok(Some(u)) => { 2179 - let passkeys = tranquil_api::server::has_passkeys_for_user(&state, &u.did).await; 2180 - let totp = tranquil_api::server::has_totp_enabled(&state, &u.did).await; 2181 - let has_pw = u.password_hash.is_some(); 2182 - let has_controllers = state 2183 - .delegation_repo 2184 - .is_delegated_account(&u.did) 2185 - .await 2186 - .unwrap_or(false); 2187 - ( 2188 - passkeys, 2189 - totp, 2190 - has_pw, 2191 - has_controllers, 2192 - Some(u.did.to_string()), 2193 - ) 2194 - } 2195 - _ => (false, false, false, false, None), 2196 - }; 2197 - 2198 - Json(SecurityStatusResponse { 2199 - has_passkeys, 2200 - has_totp, 2201 - has_password, 2202 - is_delegated, 2203 - did, 2204 - }) 2205 - .into_response() 2206 - } 2207 - 2208 - #[derive(Debug, Deserialize)] 2209 - pub struct PasskeyStartInput { 2210 - pub request_uri: String, 2211 - pub identifier: String, 2212 - pub delegated_did: Option<String>, 2213 - } 2214 - 2215 - #[derive(Debug, Serialize)] 2216 - #[serde(rename_all = "camelCase")] 2217 - pub struct PasskeyStartResponse { 2218 - pub options: serde_json::Value, 2219 - } 2220 - 2221 - pub async fn passkey_start( 2222 - State(state): State<AppState>, 2223 - _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 2224 - Json(form): Json<PasskeyStartInput>, 2225 - ) -> Response { 2226 - let passkey_start_request_id = RequestId::from(form.request_uri.clone()); 2227 - let request_data = match state 2228 - .oauth_repo 2229 - .get_authorization_request(&passkey_start_request_id) 2230 - .await 2231 - { 2232 - Ok(Some(data)) => data, 2233 - Ok(None) => { 2234 - return ( 2235 - StatusCode::BAD_REQUEST, 2236 - Json(serde_json::json!({ 2237 - "error": "invalid_request", 2238 - "error_description": "Invalid or expired request_uri." 2239 - })), 2240 - ) 2241 - .into_response(); 2242 - } 2243 - Err(_) => { 2244 - return ( 2245 - StatusCode::INTERNAL_SERVER_ERROR, 2246 - Json(serde_json::json!({ 2247 - "error": "server_error", 2248 - "error_description": "An error occurred." 2249 - })), 2250 - ) 2251 - .into_response(); 2252 - } 2253 - }; 2254 - 2255 - if request_data.expires_at < Utc::now() { 2256 - let _ = state 2257 - .oauth_repo 2258 - .delete_authorization_request(&passkey_start_request_id) 2259 - .await; 2260 - return ( 2261 - StatusCode::BAD_REQUEST, 2262 - Json(serde_json::json!({ 2263 - "error": "invalid_request", 2264 - "error_description": "Authorization request has expired." 2265 - })), 2266 - ) 2267 - .into_response(); 2268 - } 2269 - 2270 - let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 2271 - let normalized_username = 2272 - NormalizedLoginIdentifier::normalize(&form.identifier, hostname_for_handles); 2273 - 2274 - let user = match state 2275 - .user_repo 2276 - .get_login_info_by_handle_or_email(normalized_username.as_str()) 2277 - .await 2278 - { 2279 - Ok(Some(u)) => u, 2280 - Ok(None) => { 2281 - return ( 2282 - StatusCode::FORBIDDEN, 2283 - Json(serde_json::json!({ 2284 - "error": "access_denied", 2285 - "error_description": "User not found or has no passkeys." 2286 - })), 2287 - ) 2288 - .into_response(); 2289 - } 2290 - Err(_) => { 2291 - return ( 2292 - StatusCode::INTERNAL_SERVER_ERROR, 2293 - Json(serde_json::json!({ 2294 - "error": "server_error", 2295 - "error_description": "An error occurred." 2296 - })), 2297 - ) 2298 - .into_response(); 2299 - } 2300 - }; 2301 - 2302 - if user.deactivated_at.is_some() { 2303 - return ( 2304 - StatusCode::FORBIDDEN, 2305 - Json(serde_json::json!({ 2306 - "error": "access_denied", 2307 - "error_description": "This account has been deactivated." 2308 - })), 2309 - ) 2310 - .into_response(); 2311 - } 2312 - 2313 - if user.takedown_ref.is_some() { 2314 - return ( 2315 - StatusCode::FORBIDDEN, 2316 - Json(serde_json::json!({ 2317 - "error": "access_denied", 2318 - "error_description": "This account has been taken down." 2319 - })), 2320 - ) 2321 - .into_response(); 2322 - } 2323 - 2324 - let is_verified = user.channel_verification.has_any_verified(); 2325 - 2326 - if !is_verified { 2327 - let resend_info = tranquil_api::server::auto_resend_verification(&state, &user.did).await; 2328 - return ( 2329 - StatusCode::FORBIDDEN, 2330 - Json(serde_json::json!({ 2331 - "error": "account_not_verified", 2332 - "error_description": "Please verify your account before logging in.", 2333 - "did": user.did, 2334 - "handle": resend_info.as_ref().map(|r| r.handle.to_string()), 2335 - "channel": resend_info.as_ref().map(|r| r.channel.as_str()) 2336 - })), 2337 - ) 2338 - .into_response(); 2339 - } 2340 - 2341 - let stored_passkeys = match state.user_repo.get_passkeys_for_user(&user.did).await { 2342 - Ok(pks) => pks, 2343 - Err(e) => { 2344 - tracing::error!(error = %e, "Failed to get passkeys"); 2345 - return ( 2346 - StatusCode::INTERNAL_SERVER_ERROR, 2347 - Json(serde_json::json!({ 2348 - "error": "server_error", 2349 - "error_description": "An error occurred." 2350 - })), 2351 - ) 2352 - .into_response(); 2353 - } 2354 - }; 2355 - 2356 - if stored_passkeys.is_empty() { 2357 - return ( 2358 - StatusCode::FORBIDDEN, 2359 - Json(serde_json::json!({ 2360 - "error": "access_denied", 2361 - "error_description": "User not found or has no passkeys." 2362 - })), 2363 - ) 2364 - .into_response(); 2365 - } 2366 - 2367 - let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys 2368 - .iter() 2369 - .filter_map(|sp| serde_json::from_slice(&sp.public_key).ok()) 2370 - .collect(); 2371 - 2372 - if passkeys.is_empty() { 2373 - return ( 2374 - StatusCode::INTERNAL_SERVER_ERROR, 2375 - Json(serde_json::json!({ 2376 - "error": "server_error", 2377 - "error_description": "Failed to load passkeys." 2378 - })), 2379 - ) 2380 - .into_response(); 2381 - } 2382 - 2383 - let (rcr, auth_state) = match state.webauthn_config.start_authentication(passkeys) { 2384 - Ok(result) => result, 2385 - Err(e) => { 2386 - tracing::error!(error = %e, "Failed to start passkey authentication"); 2387 - return ( 2388 - StatusCode::INTERNAL_SERVER_ERROR, 2389 - Json(serde_json::json!({ 2390 - "error": "server_error", 2391 - "error_description": "Failed to start authentication." 2392 - })), 2393 - ) 2394 - .into_response(); 2395 - } 2396 - }; 2397 - 2398 - let state_json = match serde_json::to_string(&auth_state) { 2399 - Ok(j) => j, 2400 - Err(e) => { 2401 - tracing::error!(error = %e, "Failed to serialize authentication state"); 2402 - return ( 2403 - StatusCode::INTERNAL_SERVER_ERROR, 2404 - Json(serde_json::json!({ 2405 - "error": "server_error", 2406 - "error_description": "An error occurred." 2407 - })), 2408 - ) 2409 - .into_response(); 2410 - } 2411 - }; 2412 - 2413 - if let Err(e) = state 2414 - .user_repo 2415 - .save_webauthn_challenge( 2416 - &user.did, 2417 - WebauthnChallengeType::Authentication, 2418 - &state_json, 2419 - ) 2420 - .await 2421 - { 2422 - tracing::error!(error = %e, "Failed to save authentication state"); 2423 - return ( 2424 - StatusCode::INTERNAL_SERVER_ERROR, 2425 - Json(serde_json::json!({ 2426 - "error": "server_error", 2427 - "error_description": "An error occurred." 2428 - })), 2429 - ) 2430 - .into_response(); 2431 - } 2432 - 2433 - let delegation_from_param = match &form.delegated_did { 2434 - Some(delegated_did_str) => match delegated_did_str.parse::<tranquil_types::Did>() { 2435 - Ok(delegated_did) if delegated_did != user.did => { 2436 - match state 2437 - .delegation_repo 2438 - .get_delegation(&delegated_did, &user.did) 2439 - .await 2440 - { 2441 - Ok(Some(_)) => Some(delegated_did), 2442 - Ok(None) => None, 2443 - Err(e) => { 2444 - tracing::warn!( 2445 - error = %e, 2446 - delegated_did = %delegated_did, 2447 - controller_did = %user.did, 2448 - "Failed to verify delegation relationship" 2449 - ); 2450 - None 2451 - } 2452 - } 2453 - } 2454 - _ => None, 2455 - }, 2456 - None => None, 2457 - }; 2458 - 2459 - let is_delegation_flow = delegation_from_param.is_some() 2460 - || request_data.did.as_ref().is_some_and(|existing_did| { 2461 - existing_did 2462 - .parse::<tranquil_types::Did>() 2463 - .ok() 2464 - .is_some_and(|parsed| parsed != user.did) 2465 - }); 2466 - 2467 - if let Some(delegated_did) = delegation_from_param { 2468 - tracing::info!( 2469 - delegated_did = %delegated_did, 2470 - controller_did = %user.did, 2471 - "Passkey auth with delegated_did param - setting delegation flow" 2472 - ); 2473 - if state 2474 - .oauth_repo 2475 - .set_authorization_did(&passkey_start_request_id, &delegated_did, None) 2476 - .await 2477 - .is_err() 2478 - { 2479 - return OAuthError::ServerError("An error occurred.".into()).into_response(); 2480 - } 2481 - if state 2482 - .oauth_repo 2483 - .set_controller_did(&passkey_start_request_id, &user.did) 2484 - .await 2485 - .is_err() 2486 - { 2487 - return OAuthError::ServerError("An error occurred.".into()).into_response(); 2488 - } 2489 - } else if is_delegation_flow { 2490 - tracing::info!( 2491 - delegated_did = ?request_data.did, 2492 - controller_did = %user.did, 2493 - "Passkey auth in delegation flow - preserving delegated DID" 2494 - ); 2495 - if state 2496 - .oauth_repo 2497 - .set_controller_did(&passkey_start_request_id, &user.did) 2498 - .await 2499 - .is_err() 2500 - { 2501 - return OAuthError::ServerError("An error occurred.".into()).into_response(); 2502 - } 2503 - } else if state 2504 - .oauth_repo 2505 - .set_authorization_did(&passkey_start_request_id, &user.did, None) 2506 - .await 2507 - .is_err() 2508 - { 2509 - return OAuthError::ServerError("An error occurred.".into()).into_response(); 2510 - } 2511 - 2512 - let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 2513 - 2514 - Json(PasskeyStartResponse { options }).into_response() 2515 - } 2516 - 2517 - #[derive(Debug, Deserialize)] 2518 - pub struct PasskeyFinishInput { 2519 - pub request_uri: String, 2520 - pub credential: serde_json::Value, 2521 - } 2522 - 2523 - pub async fn passkey_finish( 2524 - State(state): State<AppState>, 2525 - headers: HeaderMap, 2526 - Json(form): Json<PasskeyFinishInput>, 2527 - ) -> Response { 2528 - let passkey_finish_request_id = RequestId::from(form.request_uri.clone()); 2529 - let request_data = match state 2530 - .oauth_repo 2531 - .get_authorization_request(&passkey_finish_request_id) 2532 - .await 2533 - { 2534 - Ok(Some(data)) => data, 2535 - Ok(None) => { 2536 - return ( 2537 - StatusCode::BAD_REQUEST, 2538 - Json(serde_json::json!({ 2539 - "error": "invalid_request", 2540 - "error_description": "Invalid or expired request_uri." 2541 - })), 2542 - ) 2543 - .into_response(); 2544 - } 2545 - Err(_) => { 2546 - return ( 2547 - StatusCode::INTERNAL_SERVER_ERROR, 2548 - Json(serde_json::json!({ 2549 - "error": "server_error", 2550 - "error_description": "An error occurred." 2551 - })), 2552 - ) 2553 - .into_response(); 2554 - } 2555 - }; 2556 - 2557 - if request_data.expires_at < Utc::now() { 2558 - let _ = state 2559 - .oauth_repo 2560 - .delete_authorization_request(&passkey_finish_request_id) 2561 - .await; 2562 - return ( 2563 - StatusCode::BAD_REQUEST, 2564 - Json(serde_json::json!({ 2565 - "error": "invalid_request", 2566 - "error_description": "Authorization request has expired." 2567 - })), 2568 - ) 2569 - .into_response(); 2570 - } 2571 - 2572 - let did_str = match request_data.did { 2573 - Some(d) => d, 2574 - None => { 2575 - return ( 2576 - StatusCode::BAD_REQUEST, 2577 - Json(serde_json::json!({ 2578 - "error": "invalid_request", 2579 - "error_description": "No passkey authentication in progress." 2580 - })), 2581 - ) 2582 - .into_response(); 2583 - } 2584 - }; 2585 - let did: tranquil_types::Did = match did_str.parse() { 2586 - Ok(d) => d, 2587 - Err(_) => { 2588 - return ( 2589 - StatusCode::BAD_REQUEST, 2590 - Json(serde_json::json!({ 2591 - "error": "invalid_request", 2592 - "error_description": "Invalid DID format." 2593 - })), 2594 - ) 2595 - .into_response(); 2596 - } 2597 - }; 2598 - 2599 - let controller_did: Option<tranquil_types::Did> = request_data 2600 - .controller_did 2601 - .as_ref() 2602 - .and_then(|s| s.parse().ok()); 2603 - let passkey_owner_did = controller_did.as_ref().unwrap_or(&did); 2604 - 2605 - let auth_state_json = match state 2606 - .user_repo 2607 - .load_webauthn_challenge(passkey_owner_did, WebauthnChallengeType::Authentication) 2608 - .await 2609 - { 2610 - Ok(Some(s)) => s, 2611 - Ok(None) => { 2612 - return ( 2613 - StatusCode::BAD_REQUEST, 2614 - Json(serde_json::json!({ 2615 - "error": "invalid_request", 2616 - "error_description": "No passkey authentication in progress or challenge expired." 2617 - })), 2618 - ) 2619 - .into_response(); 2620 - } 2621 - Err(e) => { 2622 - tracing::error!(error = %e, "Failed to load authentication state"); 2623 - return ( 2624 - StatusCode::INTERNAL_SERVER_ERROR, 2625 - Json(serde_json::json!({ 2626 - "error": "server_error", 2627 - "error_description": "An error occurred." 2628 - })), 2629 - ) 2630 - .into_response(); 2631 - } 2632 - }; 2633 - 2634 - let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = 2635 - match serde_json::from_str(&auth_state_json) { 2636 - Ok(s) => s, 2637 - Err(e) => { 2638 - tracing::error!(error = %e, "Failed to deserialize authentication state"); 2639 - return ( 2640 - StatusCode::INTERNAL_SERVER_ERROR, 2641 - Json(serde_json::json!({ 2642 - "error": "server_error", 2643 - "error_description": "An error occurred." 2644 - })), 2645 - ) 2646 - .into_response(); 2647 - } 2648 - }; 2649 - 2650 - let credential: webauthn_rs::prelude::PublicKeyCredential = 2651 - match serde_json::from_value(form.credential) { 2652 - Ok(c) => c, 2653 - Err(e) => { 2654 - tracing::warn!(error = %e, "Failed to parse credential"); 2655 - return ( 2656 - StatusCode::BAD_REQUEST, 2657 - Json(serde_json::json!({ 2658 - "error": "invalid_request", 2659 - "error_description": "Failed to parse credential response." 2660 - })), 2661 - ) 2662 - .into_response(); 2663 - } 2664 - }; 2665 - 2666 - let auth_result = match state 2667 - .webauthn_config 2668 - .finish_authentication(&credential, &auth_state) 2669 - { 2670 - Ok(r) => r, 2671 - Err(e) => { 2672 - tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication"); 2673 - return ( 2674 - StatusCode::FORBIDDEN, 2675 - Json(serde_json::json!({ 2676 - "error": "access_denied", 2677 - "error_description": "Passkey verification failed." 2678 - })), 2679 - ) 2680 - .into_response(); 2681 - } 2682 - }; 2683 - 2684 - if let Err(e) = state 2685 - .user_repo 2686 - .delete_webauthn_challenge(passkey_owner_did, WebauthnChallengeType::Authentication) 2687 - .await 2688 - { 2689 - tracing::warn!(error = %e, "Failed to delete authentication state"); 2690 - } 2691 - 2692 - if auth_result.needs_update() { 2693 - let cred_id_bytes = auth_result.cred_id().as_slice(); 2694 - match state 2695 - .user_repo 2696 - .update_passkey_counter( 2697 - cred_id_bytes, 2698 - i32::try_from(auth_result.counter()).unwrap_or(i32::MAX), 2699 - ) 2700 - .await 2701 - { 2702 - Ok(false) => { 2703 - tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key"); 2704 - return ( 2705 - StatusCode::FORBIDDEN, 2706 - Json(serde_json::json!({ 2707 - "error": "access_denied", 2708 - "error_description": "Security key counter anomaly detected. This may indicate a cloned key." 2709 - })), 2710 - ) 2711 - .into_response(); 2712 - } 2713 - Err(e) => { 2714 - tracing::warn!(error = %e, "Failed to update passkey counter"); 2715 - } 2716 - Ok(true) => {} 2717 - } 2718 - } 2719 - 2720 - tracing::info!(did = %did, "Passkey authentication successful"); 2721 - 2722 - let device_id = extract_device_cookie(&headers); 2723 - let requested_scope_str = request_data 2724 - .parameters 2725 - .scope 2726 - .as_deref() 2727 - .unwrap_or("atproto"); 2728 - let requested_scopes: Vec<String> = requested_scope_str 2729 - .split_whitespace() 2730 - .map(|s| s.to_string()) 2731 - .collect(); 2732 - 2733 - let passkey_finish_client_id = ClientId::from(request_data.parameters.client_id.clone()); 2734 - let needs_consent = should_show_consent( 2735 - state.oauth_repo.as_ref(), 2736 - &did, 2737 - &passkey_finish_client_id, 2738 - &requested_scopes, 2739 - ) 2740 - .await 2741 - .unwrap_or(true); 2742 - 2743 - if needs_consent { 2744 - let consent_url = format!( 2745 - "/app/oauth/consent?request_uri={}", 2746 - url_encode(&form.request_uri) 2747 - ); 2748 - return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 2749 - } 2750 - 2751 - let code = Code::generate(); 2752 - let passkey_final_device_id = device_id.clone(); 2753 - let passkey_final_code = AuthorizationCode::from(code.0.clone()); 2754 - if state 2755 - .oauth_repo 2756 - .update_authorization_request( 2757 - &passkey_finish_request_id, 2758 - &did, 2759 - passkey_final_device_id.as_ref(), 2760 - &passkey_final_code, 2761 - ) 2762 - .await 2763 - .is_err() 2764 - { 2765 - return ( 2766 - StatusCode::INTERNAL_SERVER_ERROR, 2767 - Json(serde_json::json!({ 2768 - "error": "server_error", 2769 - "error_description": "An error occurred." 2770 - })), 2771 - ) 2772 - .into_response(); 2773 - } 2774 - 2775 - let redirect_url = build_intermediate_redirect_url( 2776 - &request_data.parameters.redirect_uri, 2777 - &code.0, 2778 - request_data.parameters.state.as_deref(), 2779 - request_data.parameters.response_mode.map(|m| m.as_str()), 2780 - ); 2781 - 2782 - Json(serde_json::json!({ 2783 - "redirect_uri": redirect_url 2784 - })) 2785 - .into_response() 2786 - } 2787 - 2788 - #[derive(Debug, Deserialize)] 2789 - pub struct AuthorizePasskeyQuery { 2790 - pub request_uri: String, 2791 - } 2792 - 2793 - #[derive(Debug, Serialize)] 2794 - #[serde(rename_all = "camelCase")] 2795 - pub struct PasskeyAuthResponse { 2796 - pub options: serde_json::Value, 2797 - pub request_uri: String, 2798 - } 2799 - 2800 - pub async fn authorize_passkey_start( 2801 - State(state): State<AppState>, 2802 - Query(query): Query<AuthorizePasskeyQuery>, 2803 - ) -> Response { 2804 - let auth_passkey_start_request_id = RequestId::from(query.request_uri.clone()); 2805 - let request_data = match state 2806 - .oauth_repo 2807 - .get_authorization_request(&auth_passkey_start_request_id) 2808 - .await 2809 - { 2810 - Ok(Some(d)) => d, 2811 - Ok(None) => { 2812 - return ( 2813 - StatusCode::BAD_REQUEST, 2814 - Json(serde_json::json!({ 2815 - "error": "invalid_request", 2816 - "error_description": "Authorization request not found." 2817 - })), 2818 - ) 2819 - .into_response(); 2820 - } 2821 - Err(_) => { 2822 - return ( 2823 - StatusCode::INTERNAL_SERVER_ERROR, 2824 - Json(serde_json::json!({ 2825 - "error": "server_error", 2826 - "error_description": "An error occurred." 2827 - })), 2828 - ) 2829 - .into_response(); 2830 - } 2831 - }; 2832 - 2833 - if request_data.expires_at < Utc::now() { 2834 - let _ = state 2835 - .oauth_repo 2836 - .delete_authorization_request(&auth_passkey_start_request_id) 2837 - .await; 2838 - return ( 2839 - StatusCode::BAD_REQUEST, 2840 - Json(serde_json::json!({ 2841 - "error": "invalid_request", 2842 - "error_description": "Authorization request has expired." 2843 - })), 2844 - ) 2845 - .into_response(); 2846 - } 2847 - 2848 - let did_str = match &request_data.did { 2849 - Some(d) => d.clone(), 2850 - None => { 2851 - return ( 2852 - StatusCode::BAD_REQUEST, 2853 - Json(serde_json::json!({ 2854 - "error": "invalid_request", 2855 - "error_description": "User not authenticated yet." 2856 - })), 2857 - ) 2858 - .into_response(); 2859 - } 2860 - }; 2861 - 2862 - let did: tranquil_types::Did = match did_str.parse() { 2863 - Ok(d) => d, 2864 - Err(_) => { 2865 - return ( 2866 - StatusCode::BAD_REQUEST, 2867 - Json(serde_json::json!({ 2868 - "error": "invalid_request", 2869 - "error_description": "Invalid DID format." 2870 - })), 2871 - ) 2872 - .into_response(); 2873 - } 2874 - }; 2875 - 2876 - let stored_passkeys = match state.user_repo.get_passkeys_for_user(&did).await { 2877 - Ok(pks) => pks, 2878 - Err(e) => { 2879 - tracing::error!("Failed to get passkeys: {:?}", e); 2880 - return ( 2881 - StatusCode::INTERNAL_SERVER_ERROR, 2882 - Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2883 - ) 2884 - .into_response(); 2885 - } 2886 - }; 2887 - 2888 - if stored_passkeys.is_empty() { 2889 - return ( 2890 - StatusCode::BAD_REQUEST, 2891 - Json(serde_json::json!({ 2892 - "error": "invalid_request", 2893 - "error_description": "No passkeys registered for this account." 2894 - })), 2895 - ) 2896 - .into_response(); 2897 - } 2898 - 2899 - let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys 2900 - .iter() 2901 - .filter_map(|sp| serde_json::from_slice(&sp.public_key).ok()) 2902 - .collect(); 2903 - 2904 - if passkeys.is_empty() { 2905 - return ( 2906 - StatusCode::INTERNAL_SERVER_ERROR, 2907 - Json(serde_json::json!({"error": "server_error", "error_description": "Failed to load passkeys."})), 2908 - ) 2909 - .into_response(); 2910 - } 2911 - 2912 - let (rcr, auth_state) = match state.webauthn_config.start_authentication(passkeys) { 2913 - Ok(result) => result, 2914 - Err(e) => { 2915 - tracing::error!("Failed to start passkey authentication: {:?}", e); 2916 - return ( 2917 - StatusCode::INTERNAL_SERVER_ERROR, 2918 - Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2919 - ) 2920 - .into_response(); 2921 - } 2922 - }; 2923 - 2924 - let state_json = match serde_json::to_string(&auth_state) { 2925 - Ok(j) => j, 2926 - Err(e) => { 2927 - tracing::error!("Failed to serialize authentication state: {:?}", e); 2928 - return ( 2929 - StatusCode::INTERNAL_SERVER_ERROR, 2930 - Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2931 - ) 2932 - .into_response(); 2933 - } 2934 - }; 2935 - 2936 - if let Err(e) = state 2937 - .user_repo 2938 - .save_webauthn_challenge(&did, WebauthnChallengeType::Authentication, &state_json) 2939 - .await 2940 - { 2941 - tracing::error!("Failed to save authentication state: {:?}", e); 2942 - return ( 2943 - StatusCode::INTERNAL_SERVER_ERROR, 2944 - Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2945 - ) 2946 - .into_response(); 2947 - } 2948 - 2949 - let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 2950 - Json(PasskeyAuthResponse { 2951 - options, 2952 - request_uri: query.request_uri, 2953 - }) 2954 - .into_response() 2955 - } 2956 - 2957 - #[derive(Debug, Deserialize)] 2958 - #[serde(rename_all = "camelCase")] 2959 - pub struct AuthorizePasskeySubmit { 2960 - pub request_uri: String, 2961 - pub credential: serde_json::Value, 2962 - } 2963 - 2964 - pub async fn authorize_passkey_finish( 2965 - State(state): State<AppState>, 2966 - headers: HeaderMap, 2967 - Json(form): Json<AuthorizePasskeySubmit>, 2968 - ) -> Response { 2969 - let pds_hostname = &tranquil_config::get().server.hostname; 2970 - let passkey_finish_request_id = RequestId::from(form.request_uri.clone()); 2971 - 2972 - let request_data = match state 2973 - .oauth_repo 2974 - .get_authorization_request(&passkey_finish_request_id) 2975 - .await 2976 - { 2977 - Ok(Some(d)) => d, 2978 - Ok(None) => { 2979 - return ( 2980 - StatusCode::BAD_REQUEST, 2981 - Json(serde_json::json!({ 2982 - "error": "invalid_request", 2983 - "error_description": "Authorization request not found." 2984 - })), 2985 - ) 2986 - .into_response(); 2987 - } 2988 - Err(_) => { 2989 - return ( 2990 - StatusCode::INTERNAL_SERVER_ERROR, 2991 - Json(serde_json::json!({ 2992 - "error": "server_error", 2993 - "error_description": "An error occurred." 2994 - })), 2995 - ) 2996 - .into_response(); 2997 - } 2998 - }; 2999 - 3000 - if request_data.expires_at < Utc::now() { 3001 - let _ = state 3002 - .oauth_repo 3003 - .delete_authorization_request(&passkey_finish_request_id) 3004 - .await; 3005 - return ( 3006 - StatusCode::BAD_REQUEST, 3007 - Json(serde_json::json!({ 3008 - "error": "invalid_request", 3009 - "error_description": "Authorization request has expired." 3010 - })), 3011 - ) 3012 - .into_response(); 3013 - } 3014 - 3015 - let did_str = match &request_data.did { 3016 - Some(d) => d.clone(), 3017 - None => { 3018 - return ( 3019 - StatusCode::BAD_REQUEST, 3020 - Json(serde_json::json!({ 3021 - "error": "invalid_request", 3022 - "error_description": "User not authenticated yet." 3023 - })), 3024 - ) 3025 - .into_response(); 3026 - } 3027 - }; 3028 - 3029 - let did: tranquil_types::Did = match did_str.parse() { 3030 - Ok(d) => d, 3031 - Err(_) => { 3032 - return ( 3033 - StatusCode::BAD_REQUEST, 3034 - Json(serde_json::json!({ 3035 - "error": "invalid_request", 3036 - "error_description": "Invalid DID format." 3037 - })), 3038 - ) 3039 - .into_response(); 3040 - } 3041 - }; 3042 - 3043 - let auth_state_json = match state 3044 - .user_repo 3045 - .load_webauthn_challenge(&did, WebauthnChallengeType::Authentication) 3046 - .await 3047 - { 3048 - Ok(Some(s)) => s, 3049 - Ok(None) => { 3050 - return ( 3051 - StatusCode::BAD_REQUEST, 3052 - Json(serde_json::json!({ 3053 - "error": "invalid_request", 3054 - "error_description": "No passkey challenge found. Please start over." 3055 - })), 3056 - ) 3057 - .into_response(); 3058 - } 3059 - Err(e) => { 3060 - tracing::error!("Failed to load authentication state: {:?}", e); 3061 - return ( 3062 - StatusCode::INTERNAL_SERVER_ERROR, 3063 - Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 3064 - ) 3065 - .into_response(); 3066 - } 3067 - }; 3068 - 3069 - let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = match serde_json::from_str( 3070 - &auth_state_json, 3071 - ) { 3072 - Ok(s) => s, 3073 - Err(e) => { 3074 - tracing::error!("Failed to deserialize authentication state: {:?}", e); 3075 - return ( 3076 - StatusCode::INTERNAL_SERVER_ERROR, 3077 - Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 3078 - ) 3079 - .into_response(); 3080 - } 3081 - }; 3082 - 3083 - let credential: webauthn_rs::prelude::PublicKeyCredential = 3084 - match serde_json::from_value(form.credential.clone()) { 3085 - Ok(c) => c, 3086 - Err(e) => { 3087 - tracing::error!("Failed to parse credential: {:?}", e); 3088 - return ( 3089 - StatusCode::BAD_REQUEST, 3090 - Json(serde_json::json!({ 3091 - "error": "invalid_request", 3092 - "error_description": "Invalid credential format." 3093 - })), 3094 - ) 3095 - .into_response(); 3096 - } 3097 - }; 3098 - 3099 - let auth_result = match state 3100 - .webauthn_config 3101 - .finish_authentication(&credential, &auth_state) 3102 - { 3103 - Ok(r) => r, 3104 - Err(e) => { 3105 - tracing::warn!("Passkey authentication failed: {:?}", e); 3106 - return ( 3107 - StatusCode::FORBIDDEN, 3108 - Json(serde_json::json!({ 3109 - "error": "access_denied", 3110 - "error_description": "Passkey authentication failed." 3111 - })), 3112 - ) 3113 - .into_response(); 3114 - } 3115 - }; 3116 - 3117 - let _ = state 3118 - .user_repo 3119 - .delete_webauthn_challenge(&did, WebauthnChallengeType::Authentication) 3120 - .await; 3121 - 3122 - match state 3123 - .user_repo 3124 - .update_passkey_counter( 3125 - credential.id.as_ref(), 3126 - i32::try_from(auth_result.counter()).unwrap_or(i32::MAX), 3127 - ) 3128 - .await 3129 - { 3130 - Ok(false) => { 3131 - tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key"); 3132 - return ( 3133 - StatusCode::FORBIDDEN, 3134 - Json(serde_json::json!({ 3135 - "error": "access_denied", 3136 - "error_description": "Security key counter anomaly detected. This may indicate a cloned key." 3137 - })), 3138 - ) 3139 - .into_response(); 3140 - } 3141 - Err(e) => { 3142 - tracing::warn!("Failed to update passkey counter: {:?}", e); 3143 - } 3144 - Ok(true) => {} 3145 - } 3146 - 3147 - let has_totp = state 3148 - .user_repo 3149 - .has_totp_enabled(&did) 3150 - .await 3151 - .unwrap_or(false); 3152 - if has_totp { 3153 - let device_cookie = extract_device_cookie(&headers); 3154 - let device_is_trusted = if let Some(ref dev_id) = device_cookie { 3155 - tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &did).await 3156 - } else { 3157 - false 3158 - }; 3159 - 3160 - if device_is_trusted { 3161 - if let Some(ref dev_id) = device_cookie { 3162 - let _ = 3163 - tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id) 3164 - .await; 3165 - } 3166 - } else { 3167 - let user = match state.user_repo.get_2fa_status_by_did(&did).await { 3168 - Ok(Some(u)) => u, 3169 - _ => { 3170 - return ( 3171 - StatusCode::INTERNAL_SERVER_ERROR, 3172 - Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 3173 - ) 3174 - .into_response(); 3175 - } 3176 - }; 3177 - 3178 - let _ = state 3179 - .oauth_repo 3180 - .delete_2fa_challenge_by_request_uri(&passkey_finish_request_id) 3181 - .await; 3182 - match state 3183 - .oauth_repo 3184 - .create_2fa_challenge(&did, &passkey_finish_request_id) 3185 - .await 3186 - { 3187 - Ok(challenge) => { 3188 - if let Err(e) = enqueue_2fa_code( 3189 - state.user_repo.as_ref(), 3190 - state.infra_repo.as_ref(), 3191 - user.id, 3192 - &challenge.code, 3193 - pds_hostname, 3194 - ) 3195 - .await 3196 - { 3197 - tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); 3198 - } 3199 - let channel_name = user.preferred_comms_channel.display_name(); 3200 - let redirect_url = format!( 3201 - "/app/oauth/2fa?request_uri={}&channel={}", 3202 - url_encode(&form.request_uri), 3203 - url_encode(channel_name) 3204 - ); 3205 - return ( 3206 - StatusCode::OK, 3207 - Json(serde_json::json!({ 3208 - "next": "2fa", 3209 - "redirect": redirect_url 3210 - })), 3211 - ) 3212 - .into_response(); 3213 - } 3214 - Err(_) => { 3215 - return ( 3216 - StatusCode::INTERNAL_SERVER_ERROR, 3217 - Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 3218 - ) 3219 - .into_response(); 3220 - } 3221 - } 3222 - } 3223 - } 3224 - 3225 - let redirect_url = format!( 3226 - "/app/oauth/consent?request_uri={}", 3227 - url_encode(&form.request_uri) 3228 - ); 3229 - ( 3230 - StatusCode::OK, 3231 - Json(serde_json::json!({ 3232 - "next": "consent", 3233 - "redirect": redirect_url 3234 - })), 3235 - ) 3236 - .into_response() 3237 - } 3238 - 3239 - #[derive(Debug, Deserialize)] 3240 - pub struct RegisterCompleteInput { 3241 - pub request_uri: String, 3242 - pub did: String, 3243 - pub app_password: String, 3244 - } 3245 - 3246 - pub async fn register_complete( 3247 - State(state): State<AppState>, 3248 - _rate_limit: OAuthRateLimited<OAuthRegisterCompleteLimit>, 3249 - Json(form): Json<RegisterCompleteInput>, 3250 - ) -> Response { 3251 - let did = Did::from(form.did.clone()); 3252 - 3253 - let request_id = RequestId::from(form.request_uri.clone()); 3254 - let request_data = match state 3255 - .oauth_repo 3256 - .get_authorization_request(&request_id) 3257 - .await 3258 - { 3259 - Ok(Some(data)) => data, 3260 - Ok(None) => { 3261 - return ( 3262 - StatusCode::BAD_REQUEST, 3263 - Json(serde_json::json!({ 3264 - "error": "invalid_request", 3265 - "error_description": "Invalid or expired request_uri." 3266 - })), 3267 - ) 3268 - .into_response(); 3269 - } 3270 - Err(e) => { 3271 - tracing::error!( 3272 - request_uri = %form.request_uri, 3273 - error = ?e, 3274 - "register_complete: failed to fetch authorization request" 3275 - ); 3276 - return ( 3277 - StatusCode::INTERNAL_SERVER_ERROR, 3278 - Json(serde_json::json!({ 3279 - "error": "server_error", 3280 - "error_description": "An error occurred." 3281 - })), 3282 - ) 3283 - .into_response(); 3284 - } 3285 - }; 3286 - 3287 - if request_data.expires_at < Utc::now() { 3288 - let _ = state 3289 - .oauth_repo 3290 - .delete_authorization_request(&request_id) 3291 - .await; 3292 - return ( 3293 - StatusCode::BAD_REQUEST, 3294 - Json(serde_json::json!({ 3295 - "error": "invalid_request", 3296 - "error_description": "Authorization request has expired." 3297 - })), 3298 - ) 3299 - .into_response(); 3300 - } 3301 - 3302 - if request_data.parameters.prompt != Some(Prompt::Create) { 3303 - tracing::warn!( 3304 - request_uri = %form.request_uri, 3305 - prompt = ?request_data.parameters.prompt, 3306 - "register_complete called on non-registration OAuth flow" 3307 - ); 3308 - return ( 3309 - StatusCode::BAD_REQUEST, 3310 - Json(serde_json::json!({ 3311 - "error": "invalid_request", 3312 - "error_description": "This endpoint is only for registration flows." 3313 - })), 3314 - ) 3315 - .into_response(); 3316 - } 3317 - 3318 - if request_data.code.is_some() { 3319 - tracing::warn!( 3320 - request_uri = %form.request_uri, 3321 - "register_complete called on already-completed OAuth flow" 3322 - ); 3323 - return ( 3324 - StatusCode::BAD_REQUEST, 3325 - Json(serde_json::json!({ 3326 - "error": "invalid_request", 3327 - "error_description": "Authorization has already been completed." 3328 - })), 3329 - ) 3330 - .into_response(); 3331 - } 3332 - 3333 - if let Some(existing_did) = &request_data.did 3334 - && existing_did != &form.did 3335 - { 3336 - tracing::warn!( 3337 - request_uri = %form.request_uri, 3338 - existing_did = %existing_did, 3339 - attempted_did = %form.did, 3340 - "register_complete attempted with different DID than already bound" 3341 - ); 3342 - return ( 3343 - StatusCode::BAD_REQUEST, 3344 - Json(serde_json::json!({ 3345 - "error": "invalid_request", 3346 - "error_description": "Authorization request is already bound to a different account." 3347 - })), 3348 - ) 3349 - .into_response(); 3350 - } 3351 - 3352 - let password_hashes = match state 3353 - .session_repo 3354 - .get_app_password_hashes_by_did(&did) 3355 - .await 3356 - { 3357 - Ok(hashes) => hashes, 3358 - Err(e) => { 3359 - tracing::error!( 3360 - did = %did, 3361 - error = ?e, 3362 - "register_complete: failed to fetch app password hashes" 3363 - ); 3364 - return ( 3365 - StatusCode::INTERNAL_SERVER_ERROR, 3366 - Json(serde_json::json!({ 3367 - "error": "server_error", 3368 - "error_description": "An error occurred." 3369 - })), 3370 - ) 3371 - .into_response(); 3372 - } 3373 - }; 3374 - 3375 - let mut password_valid = password_hashes.iter().fold(false, |acc, hash| { 3376 - acc | bcrypt::verify(&form.app_password, hash).unwrap_or(false) 3377 - }); 3378 - 3379 - if !password_valid 3380 - && let Ok(Some(account_hash)) = state.user_repo.get_password_hash_by_did(&did).await 3381 - { 3382 - password_valid = bcrypt::verify(&form.app_password, &account_hash).unwrap_or(false); 3383 - } 3384 - 3385 - if !password_valid { 3386 - return ( 3387 - StatusCode::FORBIDDEN, 3388 - Json(serde_json::json!({ 3389 - "error": "access_denied", 3390 - "error_description": "Invalid credentials." 3391 - })), 3392 - ) 3393 - .into_response(); 3394 - } 3395 - 3396 - let is_verified = match state.user_repo.get_session_info_by_did(&did).await { 3397 - Ok(Some(info)) => info.channel_verification.has_any_verified(), 3398 - Ok(None) => { 3399 - return ( 3400 - StatusCode::FORBIDDEN, 3401 - Json(serde_json::json!({ 3402 - "error": "access_denied", 3403 - "error_description": "Account not found." 3404 - })), 3405 - ) 3406 - .into_response(); 3407 - } 3408 - Err(e) => { 3409 - tracing::error!( 3410 - did = %did, 3411 - error = ?e, 3412 - "register_complete: failed to fetch session info" 3413 - ); 3414 - return ( 3415 - StatusCode::INTERNAL_SERVER_ERROR, 3416 - Json(serde_json::json!({ 3417 - "error": "server_error", 3418 - "error_description": "An error occurred." 3419 - })), 3420 - ) 3421 - .into_response(); 3422 - } 3423 - }; 3424 - 3425 - if !is_verified { 3426 - let resend_info = tranquil_api::server::auto_resend_verification(&state, &did).await; 3427 - return ( 3428 - StatusCode::FORBIDDEN, 3429 - Json(serde_json::json!({ 3430 - "error": "account_not_verified", 3431 - "error_description": "Please verify your account before continuing.", 3432 - "did": did, 3433 - "handle": resend_info.as_ref().map(|r| r.handle.to_string()), 3434 - "channel": resend_info.as_ref().map(|r| r.channel.as_str()) 3435 - })), 3436 - ) 3437 - .into_response(); 3438 - } 3439 - 3440 - if let Err(e) = state 3441 - .oauth_repo 3442 - .set_authorization_did(&request_id, &did, None) 3443 - .await 3444 - { 3445 - tracing::error!( 3446 - request_uri = %form.request_uri, 3447 - did = %did, 3448 - error = ?e, 3449 - "register_complete: failed to set authorization DID" 3450 - ); 3451 - return ( 3452 - StatusCode::INTERNAL_SERVER_ERROR, 3453 - Json(serde_json::json!({ 3454 - "error": "server_error", 3455 - "error_description": "An error occurred." 3456 - })), 3457 - ) 3458 - .into_response(); 3459 - } 3460 - 3461 - let requested_scope_str = request_data 3462 - .parameters 3463 - .scope 3464 - .as_deref() 3465 - .unwrap_or("atproto"); 3466 - let requested_scopes: Vec<String> = requested_scope_str 3467 - .split_whitespace() 3468 - .map(|s| s.to_string()) 3469 - .collect(); 3470 - let client_id_typed = ClientId::from(request_data.parameters.client_id.clone()); 3471 - let needs_consent = should_show_consent( 3472 - state.oauth_repo.as_ref(), 3473 - &did, 3474 - &client_id_typed, 3475 - &requested_scopes, 3476 - ) 3477 - .await 3478 - .unwrap_or(true); 3479 - 3480 - if needs_consent { 3481 - tracing::info!( 3482 - did = %did, 3483 - client_id = %request_data.parameters.client_id, 3484 - "OAuth registration complete, redirecting to consent" 3485 - ); 3486 - let consent_url = format!( 3487 - "/app/oauth/consent?request_uri={}", 3488 - url_encode(&form.request_uri) 3489 - ); 3490 - return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 3491 - } 3492 - 3493 - let code = Code::generate(); 3494 - let auth_code = AuthorizationCode::from(code.0.clone()); 3495 - if let Err(e) = state 3496 - .oauth_repo 3497 - .update_authorization_request(&request_id, &did, None, &auth_code) 3498 - .await 3499 - { 3500 - tracing::error!( 3501 - request_uri = %form.request_uri, 3502 - did = %did, 3503 - error = ?e, 3504 - "register_complete: failed to update authorization request with code" 3505 - ); 3506 - return ( 3507 - StatusCode::INTERNAL_SERVER_ERROR, 3508 - Json(serde_json::json!({ 3509 - "error": "server_error", 3510 - "error_description": "An error occurred." 3511 - })), 3512 - ) 3513 - .into_response(); 3514 - } 3515 - 3516 - tracing::info!( 3517 - did = %did, 3518 - client_id = %request_data.parameters.client_id, 3519 - "OAuth registration flow completed successfully" 3520 - ); 3521 - 3522 - let redirect_url = build_intermediate_redirect_url( 3523 - &request_data.parameters.redirect_uri, 3524 - &code.0, 3525 - request_data.parameters.state.as_deref(), 3526 - request_data.parameters.response_mode.map(|m| m.as_str()), 3527 - ); 3528 - Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 3529 - } 3530 - 3531 - pub async fn establish_session( 3532 - State(state): State<AppState>, 3533 - headers: HeaderMap, 3534 - auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>, 3535 - ) -> Response { 3536 - let did = &auth.did; 3537 - 3538 - let existing_device = extract_device_cookie(&headers); 3539 - 3540 - let (device_id, new_cookie) = match existing_device { 3541 - Some(id) => { 3542 - let _ = state.oauth_repo.upsert_account_device(did, &id).await; 3543 - (id, None) 3544 - } 3545 - None => { 3546 - let new_id = DeviceId::generate(); 3547 - let device_typed = DeviceIdType::new(new_id.0.clone()); 3548 - let device_data = DeviceData { 3549 - session_id: SessionId::generate(), 3550 - user_agent: extract_user_agent(&headers), 3551 - ip_address: extract_client_ip(&headers, None), 3552 - last_seen_at: Utc::now(), 3553 - }; 3554 - 3555 - if let Err(e) = state 3556 - .oauth_repo 3557 - .create_device(&device_typed, &device_data) 3558 - .await 3559 - { 3560 - tracing::error!(error = ?e, "Failed to create device"); 3561 - return ( 3562 - StatusCode::INTERNAL_SERVER_ERROR, 3563 - Json(serde_json::json!({ 3564 - "error": "server_error", 3565 - "error_description": "Failed to establish session" 3566 - })), 3567 - ) 3568 - .into_response(); 3569 - } 3570 - 3571 - if let Err(e) = state 3572 - .oauth_repo 3573 - .upsert_account_device(did, &device_typed) 3574 - .await 3575 - { 3576 - tracing::error!(error = ?e, "Failed to link device to account"); 3577 - return ( 3578 - StatusCode::INTERNAL_SERVER_ERROR, 3579 - Json(serde_json::json!({ 3580 - "error": "server_error", 3581 - "error_description": "Failed to establish session" 3582 - })), 3583 - ) 3584 - .into_response(); 3585 - } 3586 - 3587 - let cookie = make_device_cookie(&device_typed); 3588 - (device_typed, Some(cookie)) 3589 - } 3590 - }; 3591 - 3592 - tracing::info!(did = %did, device_id = %device_id, "Device session established"); 3593 - 3594 - match new_cookie { 3595 - Some(cookie) => ( 3596 - StatusCode::OK, 3597 - [(SET_COOKIE, cookie)], 3598 - Json(serde_json::json!({ 3599 - "success": true, 3600 - "device_id": device_id 3601 - })), 3602 - ) 3603 - .into_response(), 3604 - None => Json(serde_json::json!({ 3605 - "success": true, 3606 - "device_id": device_id 3607 - })) 3608 - .into_response(), 3609 - } 3610 - }
+534
crates/tranquil-oauth-server/src/endpoints/authorize/consent.rs
··· 1 + use super::*; 2 + 3 + #[derive(Debug, Serialize)] 4 + pub struct ScopeInfo { 5 + pub scope: String, 6 + pub category: String, 7 + pub required: bool, 8 + pub description: String, 9 + pub display_name: String, 10 + pub granted: Option<bool>, 11 + } 12 + 13 + #[derive(Debug, Serialize)] 14 + pub struct ConsentResponse { 15 + pub request_uri: String, 16 + pub client_id: String, 17 + pub client_name: Option<String>, 18 + pub client_uri: Option<String>, 19 + pub logo_uri: Option<String>, 20 + pub scopes: Vec<ScopeInfo>, 21 + pub show_consent: bool, 22 + pub did: String, 23 + #[serde(skip_serializing_if = "Option::is_none")] 24 + pub handle: Option<String>, 25 + #[serde(skip_serializing_if = "Option::is_none")] 26 + pub is_delegation: Option<bool>, 27 + #[serde(skip_serializing_if = "Option::is_none")] 28 + pub controller_did: Option<String>, 29 + #[serde(skip_serializing_if = "Option::is_none")] 30 + pub controller_handle: Option<String>, 31 + #[serde(skip_serializing_if = "Option::is_none")] 32 + pub delegation_level: Option<String>, 33 + } 34 + 35 + #[derive(Debug, Deserialize)] 36 + pub struct ConsentQuery { 37 + pub request_uri: String, 38 + } 39 + 40 + #[derive(Debug, Deserialize)] 41 + pub struct ConsentSubmit { 42 + pub request_uri: String, 43 + pub approved_scopes: Vec<String>, 44 + pub remember: bool, 45 + } 46 + 47 + pub async fn consent_get( 48 + State(state): State<AppState>, 49 + Query(query): Query<ConsentQuery>, 50 + ) -> Response { 51 + let consent_request_id = RequestId::from(query.request_uri.clone()); 52 + let request_data = match state 53 + .repos.oauth 54 + .get_authorization_request(&consent_request_id) 55 + .await 56 + { 57 + Ok(Some(data)) => data, 58 + Ok(None) => { 59 + return json_error( 60 + StatusCode::BAD_REQUEST, 61 + "invalid_request", 62 + "Invalid or expired request_uri", 63 + ); 64 + } 65 + Err(e) => { 66 + return json_error( 67 + StatusCode::INTERNAL_SERVER_ERROR, 68 + "server_error", 69 + &format!("Database error: {:?}", e), 70 + ); 71 + } 72 + }; 73 + let flow_with_user = match AuthFlow::from_request_data(request_data.clone()) { 74 + Ok(flow) => match flow.require_user() { 75 + Ok(u) => u, 76 + Err(_) => { 77 + return json_error(StatusCode::FORBIDDEN, "access_denied", "Not authenticated"); 78 + } 79 + }, 80 + Err(_) => { 81 + return json_error( 82 + StatusCode::BAD_REQUEST, 83 + "expired_request", 84 + "Authorization request has expired", 85 + ); 86 + } 87 + }; 88 + 89 + let did = flow_with_user.did().clone(); 90 + let client_cache = ClientMetadataCache::new(3600); 91 + let client_metadata = client_cache 92 + .get(&request_data.parameters.client_id) 93 + .await 94 + .ok(); 95 + let requested_scope_str = request_data 96 + .parameters 97 + .scope 98 + .as_deref() 99 + .filter(|s| !s.trim().is_empty()) 100 + .unwrap_or("atproto"); 101 + 102 + let controller_did_parsed: Option<Did> = request_data 103 + .controller_did 104 + .as_ref() 105 + .and_then(|s| s.parse().ok()); 106 + let delegation_grant = if let Some(ref ctrl_did) = controller_did_parsed { 107 + state 108 + .repos.delegation 109 + .get_delegation(&did, ctrl_did) 110 + .await 111 + .ok() 112 + .flatten() 113 + } else { 114 + None 115 + }; 116 + 117 + let effective_scope_str = if let Some(ref grant) = delegation_grant { 118 + tranquil_pds::delegation::intersect_scopes( 119 + requested_scope_str, 120 + grant.granted_scopes.as_str(), 121 + ) 122 + } else { 123 + requested_scope_str.to_string() 124 + }; 125 + 126 + let expanded_scope_str = match expand_include_scopes(&effective_scope_str).await { 127 + Ok(s) => s, 128 + Err(e) => { 129 + return json_error( 130 + StatusCode::BAD_REQUEST, 131 + "invalid_scope", 132 + &format!("Failed to expand permission set: {e}"), 133 + ); 134 + } 135 + }; 136 + let requested_scopes: Vec<&str> = expanded_scope_str.split_whitespace().collect(); 137 + let consent_client_id = ClientId::from(request_data.parameters.client_id.clone()); 138 + let preferences = state 139 + .repos.oauth 140 + .get_scope_preferences(&did, &consent_client_id) 141 + .await 142 + .unwrap_or_default(); 143 + let pref_map: std::collections::HashMap<_, _> = preferences 144 + .iter() 145 + .map(|p| (p.scope.as_str(), p.granted)) 146 + .collect(); 147 + let requested_scope_strings: Vec<String> = 148 + requested_scopes.iter().map(|s| s.to_string()).collect(); 149 + let show_consent = should_show_consent( 150 + state.repos.oauth.as_ref(), 151 + &did, 152 + &consent_client_id, 153 + &requested_scope_strings, 154 + ) 155 + .await 156 + .unwrap_or(true); 157 + let has_granular_scopes = requested_scopes.iter().any(|s| is_granular_scope(s)); 158 + let scopes: Vec<ScopeInfo> = requested_scopes 159 + .iter() 160 + .map(|scope| { 161 + let (category, required, description, display_name) = if let Some(def) = 162 + tranquil_pds::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) 163 + { 164 + let desc = if *scope == "atproto" && has_granular_scopes { 165 + "AT Protocol baseline scope (permissions determined by selected options below)" 166 + .to_string() 167 + } else { 168 + def.description.to_string() 169 + }; 170 + let name = if *scope == "atproto" && has_granular_scopes { 171 + "AT Protocol Access".to_string() 172 + } else { 173 + def.display_name.to_string() 174 + }; 175 + ( 176 + def.category.display_name().to_string(), 177 + def.required, 178 + desc, 179 + name, 180 + ) 181 + } else if scope.starts_with("ref:") { 182 + ( 183 + "Reference".to_string(), 184 + false, 185 + "Referenced scope".to_string(), 186 + scope.to_string(), 187 + ) 188 + } else { 189 + ( 190 + "Other".to_string(), 191 + false, 192 + format!("Access to {}", scope), 193 + scope.to_string(), 194 + ) 195 + }; 196 + let granted = pref_map.get(*scope).copied(); 197 + ScopeInfo { 198 + scope: scope.to_string(), 199 + category, 200 + required, 201 + description, 202 + display_name, 203 + granted, 204 + } 205 + }) 206 + .collect(); 207 + 208 + let account_handle = state 209 + .repos.user 210 + .get_handle_by_did(&did) 211 + .await 212 + .ok() 213 + .flatten() 214 + .map(|h| h.to_string()); 215 + 216 + let (is_delegation, controller_did_resp, controller_handle, delegation_level) = 217 + if let Some(ref ctrl_did) = controller_did_parsed { 218 + let ctrl_handle = state 219 + .repos.user 220 + .get_handle_by_did(ctrl_did) 221 + .await 222 + .ok() 223 + .flatten() 224 + .map(|h| h.to_string()); 225 + 226 + let level = if let Some(ref grant) = delegation_grant { 227 + let preset = tranquil_pds::delegation::SCOPE_PRESETS 228 + .iter() 229 + .find(|p| p.scopes == grant.granted_scopes.as_str()); 230 + preset 231 + .map(|p| p.label.to_string()) 232 + .unwrap_or_else(|| "Custom".to_string()) 233 + } else { 234 + "Unknown".to_string() 235 + }; 236 + 237 + ( 238 + Some(true), 239 + Some(ctrl_did.to_string()), 240 + ctrl_handle, 241 + Some(level), 242 + ) 243 + } else { 244 + (None, None, None, None) 245 + }; 246 + 247 + Json(ConsentResponse { 248 + request_uri: query.request_uri.clone(), 249 + client_id: request_data.parameters.client_id.clone(), 250 + client_name: client_metadata.as_ref().and_then(|m| m.client_name.clone()), 251 + client_uri: client_metadata.as_ref().and_then(|m| m.client_uri.clone()), 252 + logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()), 253 + scopes, 254 + show_consent, 255 + did: did.to_string(), 256 + handle: account_handle, 257 + is_delegation, 258 + controller_did: controller_did_resp, 259 + controller_handle, 260 + delegation_level, 261 + }) 262 + .into_response() 263 + } 264 + 265 + pub async fn consent_post( 266 + State(state): State<AppState>, 267 + Json(form): Json<ConsentSubmit>, 268 + ) -> Response { 269 + tracing::info!( 270 + "consent_post: approved_scopes={:?}, remember={}", 271 + form.approved_scopes, 272 + form.remember 273 + ); 274 + let consent_post_request_id = RequestId::from(form.request_uri.clone()); 275 + let request_data = match state 276 + .repos.oauth 277 + .get_authorization_request(&consent_post_request_id) 278 + .await 279 + { 280 + Ok(Some(data)) => data, 281 + Ok(None) => { 282 + return json_error( 283 + StatusCode::BAD_REQUEST, 284 + "invalid_request", 285 + "Invalid or expired request_uri", 286 + ); 287 + } 288 + Err(e) => { 289 + return json_error( 290 + StatusCode::INTERNAL_SERVER_ERROR, 291 + "server_error", 292 + &format!("Database error: {:?}", e), 293 + ); 294 + } 295 + }; 296 + let flow_with_user = match AuthFlow::from_request_data(request_data.clone()) { 297 + Ok(flow) => match flow.require_user() { 298 + Ok(u) => u, 299 + Err(_) => { 300 + return json_error(StatusCode::FORBIDDEN, "access_denied", "Not authenticated"); 301 + } 302 + }, 303 + Err(_) => { 304 + let _ = state 305 + .repos.oauth 306 + .delete_authorization_request(&consent_post_request_id) 307 + .await; 308 + return json_error( 309 + StatusCode::BAD_REQUEST, 310 + "invalid_request", 311 + "Authorization request has expired", 312 + ); 313 + } 314 + }; 315 + 316 + let did = flow_with_user.did().clone(); 317 + let original_scope_str = request_data 318 + .parameters 319 + .scope 320 + .as_deref() 321 + .unwrap_or("atproto"); 322 + 323 + let controller_did_parsed: Option<Did> = request_data 324 + .controller_did 325 + .as_ref() 326 + .and_then(|s| s.parse().ok()); 327 + 328 + let delegation_grant = match controller_did_parsed.as_ref() { 329 + Some(ctrl_did) => state 330 + .repos.delegation 331 + .get_delegation(&did, ctrl_did) 332 + .await 333 + .ok() 334 + .flatten(), 335 + None => None, 336 + }; 337 + 338 + let effective_scope_str = if let Some(ref grant) = delegation_grant { 339 + tranquil_pds::delegation::intersect_scopes( 340 + original_scope_str, 341 + grant.granted_scopes.as_str(), 342 + ) 343 + } else { 344 + original_scope_str.to_string() 345 + }; 346 + 347 + let requested_scopes: Vec<&str> = effective_scope_str.split_whitespace().collect(); 348 + let has_granular_scopes = requested_scopes.iter().any(|s| is_granular_scope(s)); 349 + let user_denied_some_granular = has_granular_scopes 350 + && requested_scopes 351 + .iter() 352 + .filter(|s| is_granular_scope(s)) 353 + .any(|s| !form.approved_scopes.contains(&s.to_string())); 354 + let atproto_was_requested = requested_scopes.contains(&"atproto"); 355 + if atproto_was_requested 356 + && !has_granular_scopes 357 + && !form.approved_scopes.contains(&"atproto".to_string()) 358 + { 359 + return json_error( 360 + StatusCode::BAD_REQUEST, 361 + "invalid_request", 362 + "The atproto scope was requested and must be approved", 363 + ); 364 + } 365 + let final_approved: Vec<String> = if user_denied_some_granular { 366 + form.approved_scopes 367 + .iter() 368 + .filter(|s| *s != "atproto") 369 + .cloned() 370 + .collect() 371 + } else { 372 + form.approved_scopes.clone() 373 + }; 374 + if final_approved.is_empty() { 375 + return json_error( 376 + StatusCode::BAD_REQUEST, 377 + "invalid_request", 378 + "At least one scope must be approved", 379 + ); 380 + } 381 + let approved_scope_str = final_approved.join(" "); 382 + let has_valid_scope = final_approved.iter().all(|s| is_valid_scope(s)); 383 + if !has_valid_scope { 384 + return json_error( 385 + StatusCode::BAD_REQUEST, 386 + "invalid_request", 387 + "Invalid scope format", 388 + ); 389 + } 390 + if form.remember { 391 + let preferences: Vec<ScopePreference> = requested_scopes 392 + .iter() 393 + .map(|s| ScopePreference { 394 + scope: s.to_string(), 395 + granted: form.approved_scopes.contains(&s.to_string()), 396 + }) 397 + .collect(); 398 + let consent_post_client_id = ClientId::from(request_data.parameters.client_id.clone()); 399 + let _ = state 400 + .repos.oauth 401 + .upsert_scope_preferences(&did, &consent_post_client_id, &preferences) 402 + .await; 403 + } 404 + if let Err(e) = state 405 + .repos.oauth 406 + .update_request_scope(&consent_post_request_id, &approved_scope_str) 407 + .await 408 + { 409 + tracing::warn!("Failed to update request scope: {:?}", e); 410 + } 411 + let code = Code::generate(); 412 + let consent_post_device_id = request_data 413 + .device_id 414 + .as_ref() 415 + .map(|d| DeviceIdType::new(d.0.clone())); 416 + let consent_post_code = AuthorizationCode::from(code.0.clone()); 417 + if state 418 + .repos.oauth 419 + .update_authorization_request( 420 + &consent_post_request_id, 421 + &did, 422 + consent_post_device_id.as_ref(), 423 + &consent_post_code, 424 + ) 425 + .await 426 + .is_err() 427 + { 428 + return json_error( 429 + StatusCode::INTERNAL_SERVER_ERROR, 430 + "server_error", 431 + "Failed to complete authorization", 432 + ); 433 + } 434 + let redirect_uri = &request_data.parameters.redirect_uri; 435 + let intermediate_url = build_intermediate_redirect_url( 436 + redirect_uri, 437 + &code.0, 438 + request_data.parameters.state.as_deref(), 439 + request_data.parameters.response_mode.map(|m| m.as_str()), 440 + ); 441 + tracing::info!( 442 + intermediate_url = %intermediate_url, 443 + client_redirect = %redirect_uri, 444 + "consent_post returning JSON with intermediate URL (for 303 redirect)" 445 + ); 446 + Json(serde_json::json!({ "redirect_uri": intermediate_url })).into_response() 447 + } 448 + 449 + #[derive(Debug, Deserialize)] 450 + pub struct RenewRequest { 451 + pub request_uri: String, 452 + } 453 + 454 + pub async fn authorize_renew( 455 + State(state): State<AppState>, 456 + _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 457 + Json(form): Json<RenewRequest>, 458 + ) -> Response { 459 + let request_id = RequestId::from(form.request_uri.clone()); 460 + let request_data = match state 461 + .repos.oauth 462 + .get_authorization_request(&request_id) 463 + .await 464 + { 465 + Ok(Some(data)) => data, 466 + Ok(None) => { 467 + return json_error( 468 + StatusCode::BAD_REQUEST, 469 + "invalid_request", 470 + "Unknown authorization request", 471 + ); 472 + } 473 + Err(_) => { 474 + return json_error( 475 + StatusCode::INTERNAL_SERVER_ERROR, 476 + "server_error", 477 + "Database error", 478 + ); 479 + } 480 + }; 481 + 482 + if request_data.did.is_none() { 483 + return json_error( 484 + StatusCode::BAD_REQUEST, 485 + "invalid_request", 486 + "Authorization request not yet authenticated", 487 + ); 488 + } 489 + 490 + let now = Utc::now(); 491 + if request_data.expires_at >= now { 492 + return Json(serde_json::json!({ 493 + "request_uri": form.request_uri, 494 + "renewed": false 495 + })) 496 + .into_response(); 497 + } 498 + 499 + let staleness = now - request_data.expires_at; 500 + if staleness.num_seconds() > MAX_RENEWAL_STALENESS_SECONDS { 501 + let _ = state 502 + .repos.oauth 503 + .delete_authorization_request(&request_id) 504 + .await; 505 + return json_error( 506 + StatusCode::BAD_REQUEST, 507 + "invalid_request", 508 + "Authorization request expired too long ago to renew", 509 + ); 510 + } 511 + 512 + let new_expires_at = now + chrono::Duration::seconds(RENEW_EXPIRY_SECONDS); 513 + match state 514 + .repos.oauth 515 + .extend_authorization_request_expiry(&request_id, new_expires_at) 516 + .await 517 + { 518 + Ok(true) => Json(serde_json::json!({ 519 + "request_uri": form.request_uri, 520 + "renewed": true 521 + })) 522 + .into_response(), 523 + Ok(false) => json_error( 524 + StatusCode::BAD_REQUEST, 525 + "invalid_request", 526 + "Authorization request could not be renewed", 527 + ), 528 + Err(_) => json_error( 529 + StatusCode::INTERNAL_SERVER_ERROR, 530 + "server_error", 531 + "Database error", 532 + ), 533 + } 534 + }
+939
crates/tranquil-oauth-server/src/endpoints/authorize/login.rs
··· 1 + use super::*; 2 + 3 + pub async fn authorize_get( 4 + State(state): State<AppState>, 5 + headers: HeaderMap, 6 + Query(query): Query<AuthorizeQuery>, 7 + ) -> Response { 8 + let request_uri = match query.request_uri { 9 + Some(uri) => uri, 10 + None => { 11 + if wants_json(&headers) { 12 + return ( 13 + StatusCode::BAD_REQUEST, 14 + Json(serde_json::json!({ 15 + "error": "invalid_request", 16 + "error_description": "Missing request_uri parameter. Use PAR to initiate authorization." 17 + })), 18 + ).into_response(); 19 + } 20 + return redirect_to_frontend_error( 21 + "invalid_request", 22 + "Missing request_uri parameter. Use PAR to initiate authorization.", 23 + ); 24 + } 25 + }; 26 + let request_id = RequestId::from(request_uri.clone()); 27 + let request_data = match state 28 + .repos.oauth 29 + .get_authorization_request(&request_id) 30 + .await 31 + { 32 + Ok(Some(data)) => data, 33 + Ok(None) => { 34 + if wants_json(&headers) { 35 + return ( 36 + StatusCode::BAD_REQUEST, 37 + Json(serde_json::json!({ 38 + "error": "invalid_request", 39 + "error_description": "Invalid or expired request_uri. Please start a new authorization request." 40 + })), 41 + ).into_response(); 42 + } 43 + return redirect_to_frontend_error( 44 + "invalid_request", 45 + "Invalid or expired request_uri. Please start a new authorization request.", 46 + ); 47 + } 48 + Err(e) => { 49 + if wants_json(&headers) { 50 + return ( 51 + StatusCode::INTERNAL_SERVER_ERROR, 52 + Json(serde_json::json!({ 53 + "error": "server_error", 54 + "error_description": format!("Database error: {:?}", e) 55 + })), 56 + ) 57 + .into_response(); 58 + } 59 + return redirect_to_frontend_error("server_error", "A database error occurred."); 60 + } 61 + }; 62 + if request_data.expires_at < Utc::now() { 63 + let _ = state 64 + .repos.oauth 65 + .delete_authorization_request(&request_id) 66 + .await; 67 + if wants_json(&headers) { 68 + return ( 69 + StatusCode::BAD_REQUEST, 70 + Json(serde_json::json!({ 71 + "error": "invalid_request", 72 + "error_description": "Authorization request has expired. Please start a new request." 73 + })), 74 + ).into_response(); 75 + } 76 + return redirect_to_frontend_error( 77 + "invalid_request", 78 + "Authorization request has expired. Please start a new request.", 79 + ); 80 + } 81 + let client_cache = ClientMetadataCache::new(3600); 82 + let client_name = client_cache 83 + .get(&request_data.parameters.client_id) 84 + .await 85 + .ok() 86 + .and_then(|m| m.client_name); 87 + if wants_json(&headers) { 88 + return Json(AuthorizeResponse { 89 + client_id: request_data.parameters.client_id.clone(), 90 + client_name: client_name.clone(), 91 + scope: request_data.parameters.scope.clone(), 92 + redirect_uri: request_data.parameters.redirect_uri.clone(), 93 + state: request_data.parameters.state.clone(), 94 + login_hint: request_data.parameters.login_hint.clone(), 95 + }) 96 + .into_response(); 97 + } 98 + let force_new_account = query.new_account.unwrap_or(false); 99 + 100 + if let Some(ref login_hint) = request_data.parameters.login_hint { 101 + tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation"); 102 + let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 103 + let normalized = NormalizedLoginIdentifier::normalize(login_hint, hostname_for_handles); 104 + tracing::info!(normalized = %normalized, "Normalized login_hint"); 105 + 106 + match state 107 + .repos.user 108 + .get_login_check_by_handle_or_email(normalized.as_str()) 109 + .await 110 + { 111 + Ok(Some(user)) => { 112 + tracing::info!(did = %user.did, has_password = user.password_hash.is_some(), "Found user for login_hint"); 113 + let is_delegated = state 114 + .repos.delegation 115 + .is_delegated_account(&user.did) 116 + .await 117 + .unwrap_or(false); 118 + let has_password = user.password_hash.is_some(); 119 + tracing::info!(is_delegated = %is_delegated, has_password = %has_password, "Delegation check"); 120 + 121 + if is_delegated { 122 + tracing::info!("Redirecting to delegation auth"); 123 + if let Err(e) = state 124 + .repos.oauth 125 + .set_request_did(&request_id, &user.did) 126 + .await 127 + { 128 + tracing::error!(error = %e, "Failed to set delegated DID on authorization request"); 129 + return redirect_to_frontend_error( 130 + "server_error", 131 + "Failed to initialize delegation flow", 132 + ); 133 + } 134 + return redirect_see_other(&format!( 135 + "/app/oauth/delegation?request_uri={}&delegated_did={}", 136 + url_encode(&request_uri), 137 + url_encode(&user.did) 138 + )); 139 + } 140 + } 141 + Ok(None) => { 142 + tracing::info!(normalized = %normalized, "No user found for login_hint"); 143 + } 144 + Err(e) => { 145 + tracing::error!(error = %e, "Error looking up user for login_hint"); 146 + } 147 + } 148 + } else { 149 + tracing::info!("No login_hint in request"); 150 + } 151 + 152 + if request_data.parameters.prompt == Some(Prompt::Create) { 153 + return redirect_see_other(&format!( 154 + "/app/oauth/register?request_uri={}", 155 + url_encode(&request_uri) 156 + )); 157 + } 158 + 159 + if !force_new_account 160 + && let Some(device_id) = extract_device_cookie(&headers) 161 + && let Ok(accounts) = state 162 + .repos.oauth 163 + .get_device_accounts(&device_id.clone()) 164 + .await 165 + && !accounts.is_empty() 166 + { 167 + let login_hint_param = request_data 168 + .parameters 169 + .login_hint 170 + .as_ref() 171 + .map(|h| format!("&login_hint={}", url_encode(h))) 172 + .unwrap_or_default(); 173 + return redirect_see_other(&format!( 174 + "/app/oauth/accounts?request_uri={}{}", 175 + url_encode(&request_uri), 176 + login_hint_param 177 + )); 178 + } 179 + redirect_see_other(&format!( 180 + "/app/oauth/login?request_uri={}", 181 + url_encode(&request_uri) 182 + )) 183 + } 184 + 185 + pub async fn authorize_get_json( 186 + State(state): State<AppState>, 187 + Query(query): Query<AuthorizeQuery>, 188 + ) -> Result<Json<AuthorizeResponse>, OAuthError> { 189 + let request_uri = query 190 + .request_uri 191 + .ok_or_else(|| OAuthError::InvalidRequest("request_uri is required".to_string()))?; 192 + let request_id_json = RequestId::from(request_uri.clone()); 193 + let request_data = state 194 + .repos.oauth 195 + .get_authorization_request(&request_id_json) 196 + .await 197 + .map_err(tranquil_pds::oauth::db_err_to_oauth)? 198 + .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?; 199 + if request_data.expires_at < Utc::now() { 200 + let _ = state 201 + .repos.oauth 202 + .delete_authorization_request(&request_id_json) 203 + .await; 204 + return Err(OAuthError::InvalidRequest( 205 + "request_uri has expired".to_string(), 206 + )); 207 + } 208 + Ok(Json(AuthorizeResponse { 209 + client_id: request_data.parameters.client_id.clone(), 210 + client_name: None, 211 + scope: request_data.parameters.scope.clone(), 212 + redirect_uri: request_data.parameters.redirect_uri.clone(), 213 + state: request_data.parameters.state.clone(), 214 + login_hint: request_data.parameters.login_hint.clone(), 215 + })) 216 + } 217 + 218 + #[derive(Debug, Serialize)] 219 + pub struct AccountInfo { 220 + pub did: String, 221 + pub handle: Handle, 222 + #[serde(skip_serializing_if = "Option::is_none")] 223 + pub email: Option<String>, 224 + } 225 + 226 + #[derive(Debug, Serialize)] 227 + pub struct AccountsResponse { 228 + pub accounts: Vec<AccountInfo>, 229 + pub request_uri: String, 230 + } 231 + 232 + fn mask_email(email: &str) -> String { 233 + if let Some(at_pos) = email.find('@') { 234 + let local = &email[..at_pos]; 235 + let domain = &email[at_pos..]; 236 + if local.len() <= 2 { 237 + format!("{}***{}", local.chars().next().unwrap_or('*'), domain) 238 + } else { 239 + let first = local.chars().next().unwrap_or('*'); 240 + let last = local.chars().last().unwrap_or('*'); 241 + format!("{}***{}{}", first, last, domain) 242 + } 243 + } else { 244 + "***".to_string() 245 + } 246 + } 247 + 248 + pub async fn authorize_accounts( 249 + State(state): State<AppState>, 250 + headers: HeaderMap, 251 + Query(query): Query<AuthorizeQuery>, 252 + ) -> Response { 253 + let request_uri = match query.request_uri { 254 + Some(uri) => uri, 255 + None => { 256 + return ( 257 + StatusCode::BAD_REQUEST, 258 + Json(serde_json::json!({ 259 + "error": "invalid_request", 260 + "error_description": "Missing request_uri parameter" 261 + })), 262 + ) 263 + .into_response(); 264 + } 265 + }; 266 + let device_id = match extract_device_cookie(&headers) { 267 + Some(id) => id, 268 + None => { 269 + return Json(AccountsResponse { 270 + accounts: vec![], 271 + request_uri, 272 + }) 273 + .into_response(); 274 + } 275 + }; 276 + let accounts = match state.repos.oauth.get_device_accounts(&device_id).await { 277 + Ok(accts) => accts, 278 + Err(_) => { 279 + return Json(AccountsResponse { 280 + accounts: vec![], 281 + request_uri, 282 + }) 283 + .into_response(); 284 + } 285 + }; 286 + let account_infos: Vec<AccountInfo> = accounts 287 + .into_iter() 288 + .map(|row| AccountInfo { 289 + did: row.did.to_string(), 290 + handle: row.handle, 291 + email: row.email.map(|e| mask_email(&e)), 292 + }) 293 + .collect(); 294 + Json(AccountsResponse { 295 + accounts: account_infos, 296 + request_uri, 297 + }) 298 + .into_response() 299 + } 300 + 301 + pub async fn authorize_post( 302 + State(state): State<AppState>, 303 + _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 304 + headers: HeaderMap, 305 + Json(form): Json<AuthorizeSubmit>, 306 + ) -> Response { 307 + let json_response = wants_json(&headers); 308 + let form_request_id = RequestId::from(form.request_uri.clone()); 309 + let request_data = match state 310 + .repos.oauth 311 + .get_authorization_request(&form_request_id) 312 + .await 313 + { 314 + Ok(Some(data)) => data, 315 + Ok(None) => { 316 + if json_response { 317 + return ( 318 + axum::http::StatusCode::BAD_REQUEST, 319 + Json(serde_json::json!({ 320 + "error": "invalid_request", 321 + "error_description": "Invalid or expired request_uri." 322 + })), 323 + ) 324 + .into_response(); 325 + } 326 + return redirect_to_frontend_error( 327 + "invalid_request", 328 + "Invalid or expired request_uri. Please start a new authorization request.", 329 + ); 330 + } 331 + Err(e) => { 332 + if json_response { 333 + return ( 334 + axum::http::StatusCode::INTERNAL_SERVER_ERROR, 335 + Json(serde_json::json!({ 336 + "error": "server_error", 337 + "error_description": format!("Database error: {:?}", e) 338 + })), 339 + ) 340 + .into_response(); 341 + } 342 + return redirect_to_frontend_error("server_error", &format!("Database error: {:?}", e)); 343 + } 344 + }; 345 + if request_data.expires_at < Utc::now() { 346 + let _ = state 347 + .repos.oauth 348 + .delete_authorization_request(&form_request_id) 349 + .await; 350 + if json_response { 351 + return ( 352 + axum::http::StatusCode::BAD_REQUEST, 353 + Json(serde_json::json!({ 354 + "error": "invalid_request", 355 + "error_description": "Authorization request has expired." 356 + })), 357 + ) 358 + .into_response(); 359 + } 360 + return redirect_to_frontend_error( 361 + "invalid_request", 362 + "Authorization request has expired. Please start a new request.", 363 + ); 364 + } 365 + let show_login_error = |error_msg: &str, json: bool| -> Response { 366 + if json { 367 + return ( 368 + axum::http::StatusCode::FORBIDDEN, 369 + Json(serde_json::json!({ 370 + "error": "access_denied", 371 + "error_description": error_msg 372 + })), 373 + ) 374 + .into_response(); 375 + } 376 + redirect_see_other(&format!( 377 + "/app/oauth/login?request_uri={}&error={}", 378 + url_encode(&form.request_uri), 379 + url_encode(error_msg) 380 + )) 381 + }; 382 + let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 383 + let normalized_username = 384 + NormalizedLoginIdentifier::normalize(&form.username, hostname_for_handles); 385 + tracing::debug!( 386 + original_username = %form.username, 387 + normalized_username = %normalized_username, 388 + pds_hostname = %tranquil_config::get().server.hostname, 389 + "Normalized username for lookup" 390 + ); 391 + let user = match state 392 + .repos.user 393 + .get_login_info_by_handle_or_email(normalized_username.as_str()) 394 + .await 395 + { 396 + Ok(Some(u)) => u, 397 + Ok(None) => { 398 + let _ = bcrypt::verify( 399 + &form.password, 400 + "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK", 401 + ); 402 + return show_login_error("Invalid handle/email or password.", json_response); 403 + } 404 + Err(_) => return show_login_error("An error occurred. Please try again.", json_response), 405 + }; 406 + if user.deactivated_at.is_some() { 407 + return show_login_error("This account has been deactivated.", json_response); 408 + } 409 + if user.takedown_ref.is_some() { 410 + return show_login_error("This account has been taken down.", json_response); 411 + } 412 + if user.account_type.is_delegated() { 413 + if state 414 + .repos.oauth 415 + .set_authorization_did(&form_request_id, &user.did, None) 416 + .await 417 + .is_err() 418 + { 419 + return show_login_error("An error occurred. Please try again.", json_response); 420 + } 421 + let redirect_url = format!( 422 + "/app/oauth/delegation?request_uri={}&delegated_did={}", 423 + url_encode(&form.request_uri), 424 + url_encode(&user.did) 425 + ); 426 + if json_response { 427 + return ( 428 + StatusCode::OK, 429 + Json(serde_json::json!({ 430 + "next": "delegation", 431 + "delegated_did": user.did, 432 + "redirect": redirect_url 433 + })), 434 + ) 435 + .into_response(); 436 + } 437 + return redirect_see_other(&redirect_url); 438 + } 439 + 440 + if !user.password_required { 441 + if state 442 + .repos.oauth 443 + .set_authorization_did(&form_request_id, &user.did, None) 444 + .await 445 + .is_err() 446 + { 447 + return show_login_error("An error occurred. Please try again.", json_response); 448 + } 449 + let redirect_url = format!( 450 + "/app/oauth/passkey?request_uri={}", 451 + url_encode(&form.request_uri) 452 + ); 453 + if json_response { 454 + return ( 455 + StatusCode::OK, 456 + Json(serde_json::json!({ 457 + "next": "passkey", 458 + "redirect": redirect_url 459 + })), 460 + ) 461 + .into_response(); 462 + } 463 + return redirect_see_other(&redirect_url); 464 + } 465 + 466 + let password_valid = match &user.password_hash { 467 + Some(hash) => match bcrypt::verify(&form.password, hash) { 468 + Ok(valid) => valid, 469 + Err(_) => { 470 + return show_login_error("An error occurred. Please try again.", json_response); 471 + } 472 + }, 473 + None => false, 474 + }; 475 + if !password_valid { 476 + return show_login_error("Invalid handle/email or password.", json_response); 477 + } 478 + let is_verified = user.channel_verification.has_any_verified(); 479 + if !is_verified { 480 + let resend_info = tranquil_api::server::auto_resend_verification(&state, &user.did).await; 481 + let handle = resend_info 482 + .as_ref() 483 + .map(|r| r.handle.to_string()) 484 + .unwrap_or_else(|| form.username.clone()); 485 + let channel = resend_info 486 + .map(|r| r.channel.as_str().to_owned()) 487 + .unwrap_or_else(|| user.preferred_comms_channel.as_str().to_owned()); 488 + if json_response { 489 + return ( 490 + axum::http::StatusCode::FORBIDDEN, 491 + Json(serde_json::json!({ 492 + "error": "account_not_verified", 493 + "error_description": "Please verify your account before logging in.", 494 + "did": user.did, 495 + "handle": handle, 496 + "channel": channel 497 + })), 498 + ) 499 + .into_response(); 500 + } 501 + return redirect_see_other(&format!( 502 + "/app/oauth/login?request_uri={}&error={}", 503 + url_encode(&form.request_uri), 504 + url_encode("account_not_verified") 505 + )); 506 + } 507 + let has_totp = tranquil_api::server::has_totp_enabled(&state, &user.did).await; 508 + if has_totp { 509 + let device_cookie = extract_device_cookie(&headers); 510 + let device_is_trusted = if let Some(ref dev_id) = device_cookie { 511 + tranquil_api::server::is_device_trusted(state.repos.oauth.as_ref(), dev_id, &user.did) 512 + .await 513 + } else { 514 + false 515 + }; 516 + 517 + if device_is_trusted { 518 + if let Some(ref dev_id) = device_cookie { 519 + let _ = 520 + tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), dev_id) 521 + .await; 522 + } 523 + } else { 524 + if state 525 + .repos.oauth 526 + .set_authorization_did(&form_request_id, &user.did, None) 527 + .await 528 + .is_err() 529 + { 530 + return show_login_error("An error occurred. Please try again.", json_response); 531 + } 532 + if json_response { 533 + return Json(serde_json::json!({ 534 + "needs_totp": true 535 + })) 536 + .into_response(); 537 + } 538 + return redirect_see_other(&format!( 539 + "/app/oauth/totp?request_uri={}", 540 + url_encode(&form.request_uri) 541 + )); 542 + } 543 + } 544 + if user.two_factor_enabled { 545 + let _ = state 546 + .repos.oauth 547 + .delete_2fa_challenge_by_request_uri(&form_request_id) 548 + .await; 549 + match state 550 + .repos.oauth 551 + .create_2fa_challenge(&user.did, &form_request_id) 552 + .await 553 + { 554 + Ok(challenge) => { 555 + let hostname = &tranquil_config::get().server.hostname; 556 + if let Err(e) = enqueue_2fa_code( 557 + state.repos.user.as_ref(), 558 + state.repos.infra.as_ref(), 559 + user.id, 560 + &challenge.code, 561 + hostname, 562 + ) 563 + .await 564 + { 565 + tracing::warn!( 566 + did = %user.did, 567 + error = %e, 568 + "Failed to enqueue 2FA notification" 569 + ); 570 + } 571 + let channel_name = user.preferred_comms_channel.display_name(); 572 + if json_response { 573 + return Json(serde_json::json!({ 574 + "needs_2fa": true, 575 + "channel": channel_name 576 + })) 577 + .into_response(); 578 + } 579 + return redirect_see_other(&format!( 580 + "/app/oauth/2fa?request_uri={}&channel={}", 581 + url_encode(&form.request_uri), 582 + url_encode(channel_name) 583 + )); 584 + } 585 + Err(_) => { 586 + return show_login_error("An error occurred. Please try again.", json_response); 587 + } 588 + } 589 + } 590 + let mut device_id: Option<DeviceIdType> = extract_device_cookie(&headers); 591 + let mut new_cookie: Option<String> = None; 592 + if form.remember_device { 593 + let final_device_id = if let Some(existing_id) = &device_id { 594 + existing_id.clone() 595 + } else { 596 + let new_id = DeviceId::generate(); 597 + let new_device_id_typed = DeviceIdType::new(new_id.0.clone()); 598 + let device_data = DeviceData { 599 + session_id: SessionId::generate(), 600 + user_agent: extract_user_agent(&headers), 601 + ip_address: extract_client_ip(&headers, None), 602 + last_seen_at: Utc::now(), 603 + }; 604 + if state 605 + .repos.oauth 606 + .create_device(&new_device_id_typed, &device_data) 607 + .await 608 + .is_ok() 609 + { 610 + new_cookie = Some(make_device_cookie(&new_device_id_typed)); 611 + device_id = Some(new_device_id_typed.clone()); 612 + } 613 + new_device_id_typed 614 + }; 615 + let _ = state 616 + .repos.oauth 617 + .upsert_account_device(&user.did, &final_device_id) 618 + .await; 619 + } 620 + let set_auth_device_id = device_id.clone(); 621 + if state 622 + .repos.oauth 623 + .set_authorization_did(&form_request_id, &user.did, set_auth_device_id.as_ref()) 624 + .await 625 + .is_err() 626 + { 627 + return show_login_error("An error occurred. Please try again.", json_response); 628 + } 629 + let requested_scope_str = request_data 630 + .parameters 631 + .scope 632 + .as_deref() 633 + .unwrap_or("atproto"); 634 + let requested_scopes: Vec<String> = requested_scope_str 635 + .split_whitespace() 636 + .map(|s| s.to_string()) 637 + .collect(); 638 + let client_id_typed = ClientId::from(request_data.parameters.client_id.clone()); 639 + let needs_consent = should_show_consent( 640 + state.repos.oauth.as_ref(), 641 + &user.did, 642 + &client_id_typed, 643 + &requested_scopes, 644 + ) 645 + .await 646 + .unwrap_or(true); 647 + if needs_consent { 648 + let consent_url = format!( 649 + "/app/oauth/consent?request_uri={}", 650 + url_encode(&form.request_uri) 651 + ); 652 + if json_response { 653 + if let Some(cookie) = new_cookie { 654 + return ( 655 + StatusCode::OK, 656 + [(SET_COOKIE, cookie)], 657 + Json(serde_json::json!({"redirect_uri": consent_url})), 658 + ) 659 + .into_response(); 660 + } 661 + return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 662 + } 663 + if let Some(cookie) = new_cookie { 664 + return ( 665 + StatusCode::SEE_OTHER, 666 + [(SET_COOKIE, cookie), (LOCATION, consent_url)], 667 + ) 668 + .into_response(); 669 + } 670 + return redirect_see_other(&consent_url); 671 + } 672 + let code = Code::generate(); 673 + let auth_post_device_id = device_id.clone(); 674 + let auth_post_code = AuthorizationCode::from(code.0.clone()); 675 + if state 676 + .repos.oauth 677 + .update_authorization_request( 678 + &form_request_id, 679 + &user.did, 680 + auth_post_device_id.as_ref(), 681 + &auth_post_code, 682 + ) 683 + .await 684 + .is_err() 685 + { 686 + return show_login_error("An error occurred. Please try again.", json_response); 687 + } 688 + if json_response { 689 + let redirect_url = build_intermediate_redirect_url( 690 + &request_data.parameters.redirect_uri, 691 + &code.0, 692 + request_data.parameters.state.as_deref(), 693 + request_data.parameters.response_mode.map(|m| m.as_str()), 694 + ); 695 + if let Some(cookie) = new_cookie { 696 + ( 697 + StatusCode::OK, 698 + [(SET_COOKIE, cookie)], 699 + Json(serde_json::json!({"redirect_uri": redirect_url})), 700 + ) 701 + .into_response() 702 + } else { 703 + Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 704 + } 705 + } else { 706 + let redirect_url = build_success_redirect( 707 + &request_data.parameters.redirect_uri, 708 + &code.0, 709 + request_data.parameters.state.as_deref(), 710 + request_data.parameters.response_mode.map(|m| m.as_str()), 711 + ); 712 + if let Some(cookie) = new_cookie { 713 + ( 714 + StatusCode::SEE_OTHER, 715 + [(SET_COOKIE, cookie), (LOCATION, redirect_url)], 716 + ) 717 + .into_response() 718 + } else { 719 + redirect_see_other(&redirect_url) 720 + } 721 + } 722 + } 723 + 724 + pub async fn authorize_select( 725 + State(state): State<AppState>, 726 + headers: HeaderMap, 727 + Json(form): Json<AuthorizeSelectSubmit>, 728 + ) -> Response { 729 + let json_error = |status: StatusCode, error: &str, description: &str| -> Response { 730 + ( 731 + status, 732 + Json(serde_json::json!({ 733 + "error": error, 734 + "error_description": description 735 + })), 736 + ) 737 + .into_response() 738 + }; 739 + let select_request_id = RequestId::from(form.request_uri.clone()); 740 + let request_data = match state 741 + .repos.oauth 742 + .get_authorization_request(&select_request_id) 743 + .await 744 + { 745 + Ok(Some(data)) => data, 746 + Ok(None) => { 747 + return json_error( 748 + StatusCode::BAD_REQUEST, 749 + "invalid_request", 750 + "Invalid or expired request_uri. Please start a new authorization request.", 751 + ); 752 + } 753 + Err(_) => { 754 + return json_error( 755 + StatusCode::INTERNAL_SERVER_ERROR, 756 + "server_error", 757 + "An error occurred. Please try again.", 758 + ); 759 + } 760 + }; 761 + if request_data.expires_at < Utc::now() { 762 + let _ = state 763 + .repos.oauth 764 + .delete_authorization_request(&select_request_id) 765 + .await; 766 + return json_error( 767 + StatusCode::BAD_REQUEST, 768 + "invalid_request", 769 + "Authorization request has expired. Please start a new request.", 770 + ); 771 + } 772 + let device_id = match extract_device_cookie(&headers) { 773 + Some(id) => id, 774 + None => { 775 + return json_error( 776 + StatusCode::BAD_REQUEST, 777 + "invalid_request", 778 + "No device session found. Please sign in.", 779 + ); 780 + } 781 + }; 782 + let did: Did = match form.did.parse() { 783 + Ok(d) => d, 784 + Err(_) => { 785 + return json_error( 786 + StatusCode::BAD_REQUEST, 787 + "invalid_request", 788 + "Invalid DID format.", 789 + ); 790 + } 791 + }; 792 + let verify_device_id = device_id.clone(); 793 + let account_valid = match state 794 + .repos.oauth 795 + .verify_account_on_device(&verify_device_id, &did) 796 + .await 797 + { 798 + Ok(valid) => valid, 799 + Err(_) => { 800 + return json_error( 801 + StatusCode::INTERNAL_SERVER_ERROR, 802 + "server_error", 803 + "An error occurred. Please try again.", 804 + ); 805 + } 806 + }; 807 + if !account_valid { 808 + return json_error( 809 + StatusCode::FORBIDDEN, 810 + "access_denied", 811 + "This account is not available on this device. Please sign in.", 812 + ); 813 + } 814 + let user = match state.repos.user.get_2fa_status_by_did(&did).await { 815 + Ok(Some(u)) => u, 816 + Ok(None) => { 817 + return json_error( 818 + StatusCode::FORBIDDEN, 819 + "access_denied", 820 + "Account not found. Please sign in.", 821 + ); 822 + } 823 + Err(_) => { 824 + return json_error( 825 + StatusCode::INTERNAL_SERVER_ERROR, 826 + "server_error", 827 + "An error occurred. Please try again.", 828 + ); 829 + } 830 + }; 831 + let is_verified = user.channel_verification.has_any_verified(); 832 + if !is_verified { 833 + let resend_info = tranquil_api::server::auto_resend_verification(&state, &did).await; 834 + return ( 835 + StatusCode::FORBIDDEN, 836 + Json(serde_json::json!({ 837 + "error": "account_not_verified", 838 + "error_description": "Please verify your account before logging in.", 839 + "did": did, 840 + "handle": resend_info.as_ref().map(|r| r.handle.to_string()), 841 + "channel": resend_info.as_ref().map(|r| r.channel.as_str()) 842 + })), 843 + ) 844 + .into_response(); 845 + } 846 + let has_totp = tranquil_api::server::has_totp_enabled(&state, &did).await; 847 + let select_early_device_typed = device_id.clone(); 848 + if has_totp { 849 + let device_is_trusted = 850 + tranquil_api::server::is_device_trusted(state.repos.oauth.as_ref(), &device_id, &did) 851 + .await; 852 + if !device_is_trusted { 853 + if state 854 + .repos.oauth 855 + .set_authorization_did(&select_request_id, &did, Some(&select_early_device_typed)) 856 + .await 857 + .is_err() 858 + { 859 + return json_error( 860 + StatusCode::INTERNAL_SERVER_ERROR, 861 + "server_error", 862 + "An error occurred. Please try again.", 863 + ); 864 + } 865 + return Json(serde_json::json!({ 866 + "needs_totp": true 867 + })) 868 + .into_response(); 869 + } 870 + let _ = 871 + tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), &device_id).await; 872 + } 873 + if user.two_factor_enabled { 874 + let _ = state 875 + .repos.oauth 876 + .delete_2fa_challenge_by_request_uri(&select_request_id) 877 + .await; 878 + match state 879 + .repos.oauth 880 + .create_2fa_challenge(&did, &select_request_id) 881 + .await 882 + { 883 + Ok(challenge) => { 884 + let hostname = &tranquil_config::get().server.hostname; 885 + if let Err(e) = enqueue_2fa_code( 886 + state.repos.user.as_ref(), 887 + state.repos.infra.as_ref(), 888 + user.id, 889 + &challenge.code, 890 + hostname, 891 + ) 892 + .await 893 + { 894 + tracing::warn!( 895 + did = %form.did, 896 + error = %e, 897 + "Failed to enqueue 2FA notification" 898 + ); 899 + } 900 + let channel_name = user.preferred_comms_channel.display_name(); 901 + return Json(serde_json::json!({ 902 + "needs_2fa": true, 903 + "channel": channel_name 904 + })) 905 + .into_response(); 906 + } 907 + Err(_) => { 908 + return json_error( 909 + StatusCode::INTERNAL_SERVER_ERROR, 910 + "server_error", 911 + "An error occurred. Please try again.", 912 + ); 913 + } 914 + } 915 + } 916 + let select_device_typed = device_id.clone(); 917 + let _ = state 918 + .repos.oauth 919 + .upsert_account_device(&did, &select_device_typed) 920 + .await; 921 + 922 + if state 923 + .repos.oauth 924 + .set_authorization_did(&select_request_id, &did, Some(&select_device_typed)) 925 + .await 926 + .is_err() 927 + { 928 + return json_error( 929 + StatusCode::INTERNAL_SERVER_ERROR, 930 + "server_error", 931 + "An error occurred. Please try again.", 932 + ); 933 + } 934 + let consent_url = format!( 935 + "/app/oauth/consent?request_uri={}", 936 + url_encode(&form.request_uri) 937 + ); 938 + Json(serde_json::json!({"redirect_uri": consent_url})).into_response() 939 + }
+309
crates/tranquil-oauth-server/src/endpoints/authorize/mod.rs
··· 1 + use axum::{ 2 + Json, 3 + extract::{Query, State}, 4 + http::{ 5 + HeaderMap, StatusCode, 6 + header::{LOCATION, SET_COOKIE}, 7 + }, 8 + response::{IntoResponse, Response}, 9 + }; 10 + use chrono::Utc; 11 + use serde::{Deserialize, Serialize}; 12 + use subtle::ConstantTimeEq; 13 + use tranquil_db_traits::{ScopePreference, WebauthnChallengeType}; 14 + use tranquil_pds::auth::{BareLoginIdentifier, NormalizedLoginIdentifier}; 15 + use tranquil_pds::comms::comms_repo::enqueue_2fa_code; 16 + use tranquil_pds::oauth::{ 17 + AuthFlow, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, Prompt, SessionId, 18 + db::should_show_consent, scopes::expand_include_scopes, 19 + }; 20 + use tranquil_pds::rate_limit::{ 21 + OAuthAuthorizeLimit, OAuthRateLimited, OAuthRegisterCompleteLimit, TotpVerifyLimit, 22 + check_user_rate_limit, 23 + }; 24 + use tranquil_pds::state::AppState; 25 + use tranquil_pds::types::{Did, Handle, PlainPassword}; 26 + use tranquil_pds::util::extract_client_ip; 27 + use tranquil_types::{AuthorizationCode, ClientId, DeviceId as DeviceIdType, RequestId}; 28 + use urlencoding::encode as url_encode; 29 + 30 + const DEVICE_COOKIE_NAME: &str = "oauth_device_id"; 31 + const RENEW_EXPIRY_SECONDS: i64 = 600; 32 + const MAX_RENEWAL_STALENESS_SECONDS: i64 = 3600; 33 + 34 + fn redirect_see_other(uri: &str) -> Response { 35 + ( 36 + StatusCode::SEE_OTHER, 37 + [ 38 + (LOCATION, uri.to_string()), 39 + (axum::http::header::CACHE_CONTROL, "no-store".to_string()), 40 + ( 41 + SET_COOKIE, 42 + "bfCacheBypass=foo; max-age=1; SameSite=Lax".to_string(), 43 + ), 44 + ], 45 + ) 46 + .into_response() 47 + } 48 + 49 + fn redirect_to_frontend_error(error: &str, description: &str) -> Response { 50 + redirect_see_other(&format!( 51 + "/app/oauth/error?error={}&error_description={}", 52 + url_encode(error), 53 + url_encode(description) 54 + )) 55 + } 56 + 57 + fn json_error(status: StatusCode, error: &str, description: &str) -> Response { 58 + ( 59 + status, 60 + Json(serde_json::json!({ 61 + "error": error, 62 + "error_description": description 63 + })), 64 + ) 65 + .into_response() 66 + } 67 + 68 + fn is_granular_scope(s: &str) -> bool { 69 + s.starts_with("repo:") 70 + || s.starts_with("repo?") 71 + || s == "repo" 72 + || s.starts_with("blob:") 73 + || s.starts_with("blob?") 74 + || s == "blob" 75 + || s.starts_with("rpc:") 76 + || s.starts_with("rpc?") 77 + || s.starts_with("account:") 78 + || s.starts_with("identity:") 79 + } 80 + 81 + fn is_valid_scope(s: &str) -> bool { 82 + s == "atproto" 83 + || s == "transition:generic" 84 + || s == "transition:chat.bsky" 85 + || s == "transition:email" 86 + || is_granular_scope(s) 87 + || s.starts_with("include:") 88 + } 89 + 90 + fn extract_device_cookie(headers: &HeaderMap) -> Option<tranquil_types::DeviceId> { 91 + headers 92 + .get("cookie") 93 + .and_then(|v| v.to_str().ok()) 94 + .and_then(|cookie_str| { 95 + cookie_str.split(';').map(|c| c.trim()).find_map(|cookie| { 96 + cookie 97 + .strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME)) 98 + .and_then(|value| { 99 + tranquil_pds::config::AuthConfig::get().verify_device_cookie(value) 100 + }) 101 + .map(tranquil_types::DeviceId::new) 102 + }) 103 + }) 104 + } 105 + 106 + fn extract_user_agent(headers: &HeaderMap) -> Option<String> { 107 + headers 108 + .get("user-agent") 109 + .and_then(|v| v.to_str().ok()) 110 + .map(|s| s.to_string()) 111 + } 112 + 113 + fn make_device_cookie(device_id: &tranquil_types::DeviceId) -> String { 114 + let signed_value = 115 + tranquil_pds::config::AuthConfig::get().sign_device_cookie(device_id.as_str()); 116 + format!( 117 + "{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000", 118 + DEVICE_COOKIE_NAME, signed_value 119 + ) 120 + } 121 + 122 + #[derive(Debug, Deserialize)] 123 + pub struct AuthorizeQuery { 124 + pub request_uri: Option<String>, 125 + pub client_id: Option<String>, 126 + pub new_account: Option<bool>, 127 + } 128 + 129 + #[derive(Debug, Serialize)] 130 + pub struct AuthorizeResponse { 131 + pub client_id: String, 132 + pub client_name: Option<String>, 133 + pub scope: Option<String>, 134 + pub redirect_uri: String, 135 + pub state: Option<String>, 136 + pub login_hint: Option<String>, 137 + } 138 + 139 + #[derive(Debug, Deserialize)] 140 + pub struct AuthorizeSubmit { 141 + pub request_uri: String, 142 + pub username: String, 143 + pub password: PlainPassword, 144 + #[serde(default)] 145 + pub remember_device: bool, 146 + } 147 + 148 + #[derive(Debug, Deserialize)] 149 + pub struct AuthorizeSelectSubmit { 150 + pub request_uri: String, 151 + pub did: String, 152 + } 153 + 154 + fn wants_json(headers: &HeaderMap) -> bool { 155 + headers 156 + .get("accept") 157 + .and_then(|v| v.to_str().ok()) 158 + .map(|accept| accept.contains("application/json")) 159 + .unwrap_or(false) 160 + } 161 + 162 + fn build_success_redirect( 163 + redirect_uri: &str, 164 + code: &str, 165 + state: Option<&str>, 166 + response_mode: Option<&str>, 167 + ) -> String { 168 + let mut redirect_url = redirect_uri.to_string(); 169 + let use_fragment = response_mode == Some("fragment"); 170 + let separator = if use_fragment { 171 + '#' 172 + } else if redirect_url.contains('?') { 173 + '&' 174 + } else { 175 + '?' 176 + }; 177 + redirect_url.push(separator); 178 + let pds_host = &tranquil_config::get().server.hostname; 179 + redirect_url.push_str(&format!( 180 + "iss={}", 181 + url_encode(&format!("https://{}", pds_host)) 182 + )); 183 + if let Some(req_state) = state { 184 + redirect_url.push_str(&format!("&state={}", url_encode(req_state))); 185 + } 186 + redirect_url.push_str(&format!("&code={}", url_encode(code))); 187 + redirect_url 188 + } 189 + 190 + fn build_intermediate_redirect_url( 191 + redirect_uri: &str, 192 + code: &str, 193 + state: Option<&str>, 194 + response_mode: Option<&str>, 195 + ) -> String { 196 + let pds_host = &tranquil_config::get().server.hostname; 197 + let mut url = format!( 198 + "https://{}/oauth/authorize/redirect?redirect_uri={}&code={}", 199 + pds_host, 200 + url_encode(redirect_uri), 201 + url_encode(code) 202 + ); 203 + if let Some(s) = state { 204 + url.push_str(&format!("&state={}", url_encode(s))); 205 + } 206 + if let Some(rm) = response_mode { 207 + url.push_str(&format!("&response_mode={}", url_encode(rm))); 208 + } 209 + url 210 + } 211 + 212 + #[derive(Debug, Deserialize)] 213 + pub struct AuthorizeRedirectParams { 214 + redirect_uri: String, 215 + code: String, 216 + state: Option<String>, 217 + response_mode: Option<String>, 218 + } 219 + 220 + pub async fn authorize_redirect(Query(params): Query<AuthorizeRedirectParams>) -> Response { 221 + let final_url = build_success_redirect( 222 + &params.redirect_uri, 223 + &params.code, 224 + params.state.as_deref(), 225 + params.response_mode.as_deref(), 226 + ); 227 + tracing::info!( 228 + final_url = %final_url, 229 + client_redirect = %params.redirect_uri, 230 + "authorize_redirect performing 303 redirect" 231 + ); 232 + ( 233 + StatusCode::SEE_OTHER, 234 + [ 235 + (axum::http::header::LOCATION, final_url), 236 + (axum::http::header::CACHE_CONTROL, "no-store".to_string()), 237 + ], 238 + ) 239 + .into_response() 240 + } 241 + 242 + pub async fn authorize_deny( 243 + State(state): State<AppState>, 244 + Json(form): Json<AuthorizeDenyForm>, 245 + ) -> Response { 246 + let deny_request_id = RequestId::from(form.request_uri.clone()); 247 + let request_data = match state 248 + .repos.oauth 249 + .get_authorization_request(&deny_request_id) 250 + .await 251 + { 252 + Ok(Some(data)) => data, 253 + Ok(None) => { 254 + return ( 255 + StatusCode::BAD_REQUEST, 256 + Json(serde_json::json!({ 257 + "error": "invalid_request", 258 + "error_description": "Invalid request_uri" 259 + })), 260 + ) 261 + .into_response(); 262 + } 263 + Err(_) => { 264 + return ( 265 + StatusCode::INTERNAL_SERVER_ERROR, 266 + Json(serde_json::json!({ 267 + "error": "server_error", 268 + "error_description": "An error occurred" 269 + })), 270 + ) 271 + .into_response(); 272 + } 273 + }; 274 + let _ = state 275 + .repos.oauth 276 + .delete_authorization_request(&deny_request_id) 277 + .await; 278 + let redirect_uri = &request_data.parameters.redirect_uri; 279 + let mut redirect_url = redirect_uri.to_string(); 280 + let separator = if redirect_url.contains('?') { '&' } else { '?' }; 281 + redirect_url.push(separator); 282 + redirect_url.push_str("error=access_denied"); 283 + redirect_url.push_str("&error_description=User%20denied%20the%20request"); 284 + if let Some(state) = &request_data.parameters.state { 285 + redirect_url.push_str(&format!("&state={}", url_encode(state))); 286 + } 287 + Json(serde_json::json!({ 288 + "redirect_uri": redirect_url 289 + })) 290 + .into_response() 291 + } 292 + 293 + #[derive(Debug, Deserialize)] 294 + pub struct AuthorizeDenyForm { 295 + pub request_uri: String, 296 + } 297 + 298 + 299 + mod consent; 300 + mod login; 301 + mod passkey; 302 + mod registration; 303 + mod two_factor; 304 + 305 + pub use consent::*; 306 + pub use login::*; 307 + pub use passkey::*; 308 + pub use registration::*; 309 + pub use two_factor::*;
+1125
crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs
··· 1 + use super::*; 2 + 3 + #[derive(Debug, Deserialize)] 4 + pub struct CheckPasskeysQuery { 5 + pub identifier: String, 6 + } 7 + 8 + #[derive(Debug, Serialize)] 9 + #[serde(rename_all = "camelCase")] 10 + pub struct CheckPasskeysResponse { 11 + pub has_passkeys: bool, 12 + } 13 + 14 + pub async fn check_user_has_passkeys( 15 + State(state): State<AppState>, 16 + Query(query): Query<CheckPasskeysQuery>, 17 + ) -> Response { 18 + let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 19 + let bare_identifier = 20 + BareLoginIdentifier::from_identifier(&query.identifier, hostname_for_handles); 21 + 22 + let user = state 23 + .repos.user 24 + .get_login_check_by_handle_or_email(bare_identifier.as_str()) 25 + .await; 26 + 27 + let has_passkeys = match user { 28 + Ok(Some(u)) => tranquil_api::server::has_passkeys_for_user(&state, &u.did).await, 29 + _ => false, 30 + }; 31 + 32 + Json(CheckPasskeysResponse { has_passkeys }).into_response() 33 + } 34 + 35 + #[derive(Debug, Serialize)] 36 + #[serde(rename_all = "camelCase")] 37 + pub struct SecurityStatusResponse { 38 + pub has_passkeys: bool, 39 + pub has_totp: bool, 40 + pub has_password: bool, 41 + pub is_delegated: bool, 42 + #[serde(skip_serializing_if = "Option::is_none")] 43 + pub did: Option<String>, 44 + } 45 + 46 + pub async fn check_user_security_status( 47 + State(state): State<AppState>, 48 + Query(query): Query<CheckPasskeysQuery>, 49 + ) -> Response { 50 + let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 51 + let normalized_identifier = 52 + NormalizedLoginIdentifier::normalize(&query.identifier, hostname_for_handles); 53 + 54 + let user = state 55 + .repos.user 56 + .get_login_check_by_handle_or_email(normalized_identifier.as_str()) 57 + .await; 58 + 59 + let (has_passkeys, has_totp, has_password, is_delegated, did): ( 60 + bool, 61 + bool, 62 + bool, 63 + bool, 64 + Option<String>, 65 + ) = match user { 66 + Ok(Some(u)) => { 67 + let passkeys = tranquil_api::server::has_passkeys_for_user(&state, &u.did).await; 68 + let totp = tranquil_api::server::has_totp_enabled(&state, &u.did).await; 69 + let has_pw = u.password_hash.is_some(); 70 + let has_controllers = state 71 + .repos.delegation 72 + .is_delegated_account(&u.did) 73 + .await 74 + .unwrap_or(false); 75 + ( 76 + passkeys, 77 + totp, 78 + has_pw, 79 + has_controllers, 80 + Some(u.did.to_string()), 81 + ) 82 + } 83 + _ => (false, false, false, false, None), 84 + }; 85 + 86 + Json(SecurityStatusResponse { 87 + has_passkeys, 88 + has_totp, 89 + has_password, 90 + is_delegated, 91 + did, 92 + }) 93 + .into_response() 94 + } 95 + 96 + #[derive(Debug, Deserialize)] 97 + pub struct PasskeyStartInput { 98 + pub request_uri: String, 99 + pub identifier: String, 100 + pub delegated_did: Option<String>, 101 + } 102 + 103 + #[derive(Debug, Serialize)] 104 + #[serde(rename_all = "camelCase")] 105 + pub struct PasskeyStartResponse { 106 + pub options: serde_json::Value, 107 + } 108 + 109 + pub async fn passkey_start( 110 + State(state): State<AppState>, 111 + _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 112 + Json(form): Json<PasskeyStartInput>, 113 + ) -> Response { 114 + let passkey_start_request_id = RequestId::from(form.request_uri.clone()); 115 + let request_data = match state 116 + .repos.oauth 117 + .get_authorization_request(&passkey_start_request_id) 118 + .await 119 + { 120 + Ok(Some(data)) => data, 121 + Ok(None) => { 122 + return ( 123 + StatusCode::BAD_REQUEST, 124 + Json(serde_json::json!({ 125 + "error": "invalid_request", 126 + "error_description": "Invalid or expired request_uri." 127 + })), 128 + ) 129 + .into_response(); 130 + } 131 + Err(_) => { 132 + return ( 133 + StatusCode::INTERNAL_SERVER_ERROR, 134 + Json(serde_json::json!({ 135 + "error": "server_error", 136 + "error_description": "An error occurred." 137 + })), 138 + ) 139 + .into_response(); 140 + } 141 + }; 142 + 143 + if request_data.expires_at < Utc::now() { 144 + let _ = state 145 + .repos.oauth 146 + .delete_authorization_request(&passkey_start_request_id) 147 + .await; 148 + return ( 149 + StatusCode::BAD_REQUEST, 150 + Json(serde_json::json!({ 151 + "error": "invalid_request", 152 + "error_description": "Authorization request has expired." 153 + })), 154 + ) 155 + .into_response(); 156 + } 157 + 158 + let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 159 + let normalized_username = 160 + NormalizedLoginIdentifier::normalize(&form.identifier, hostname_for_handles); 161 + 162 + let user = match state 163 + .repos.user 164 + .get_login_info_by_handle_or_email(normalized_username.as_str()) 165 + .await 166 + { 167 + Ok(Some(u)) => u, 168 + Ok(None) => { 169 + return ( 170 + StatusCode::FORBIDDEN, 171 + Json(serde_json::json!({ 172 + "error": "access_denied", 173 + "error_description": "User not found or has no passkeys." 174 + })), 175 + ) 176 + .into_response(); 177 + } 178 + Err(_) => { 179 + return ( 180 + StatusCode::INTERNAL_SERVER_ERROR, 181 + Json(serde_json::json!({ 182 + "error": "server_error", 183 + "error_description": "An error occurred." 184 + })), 185 + ) 186 + .into_response(); 187 + } 188 + }; 189 + 190 + if user.deactivated_at.is_some() { 191 + return ( 192 + StatusCode::FORBIDDEN, 193 + Json(serde_json::json!({ 194 + "error": "access_denied", 195 + "error_description": "This account has been deactivated." 196 + })), 197 + ) 198 + .into_response(); 199 + } 200 + 201 + if user.takedown_ref.is_some() { 202 + return ( 203 + StatusCode::FORBIDDEN, 204 + Json(serde_json::json!({ 205 + "error": "access_denied", 206 + "error_description": "This account has been taken down." 207 + })), 208 + ) 209 + .into_response(); 210 + } 211 + 212 + let is_verified = user.channel_verification.has_any_verified(); 213 + 214 + if !is_verified { 215 + let resend_info = tranquil_api::server::auto_resend_verification(&state, &user.did).await; 216 + return ( 217 + StatusCode::FORBIDDEN, 218 + Json(serde_json::json!({ 219 + "error": "account_not_verified", 220 + "error_description": "Please verify your account before logging in.", 221 + "did": user.did, 222 + "handle": resend_info.as_ref().map(|r| r.handle.to_string()), 223 + "channel": resend_info.as_ref().map(|r| r.channel.as_str()) 224 + })), 225 + ) 226 + .into_response(); 227 + } 228 + 229 + let stored_passkeys = match state.repos.user.get_passkeys_for_user(&user.did).await { 230 + Ok(pks) => pks, 231 + Err(e) => { 232 + tracing::error!(error = %e, "Failed to get passkeys"); 233 + return ( 234 + StatusCode::INTERNAL_SERVER_ERROR, 235 + Json(serde_json::json!({ 236 + "error": "server_error", 237 + "error_description": "An error occurred." 238 + })), 239 + ) 240 + .into_response(); 241 + } 242 + }; 243 + 244 + if stored_passkeys.is_empty() { 245 + return ( 246 + StatusCode::FORBIDDEN, 247 + Json(serde_json::json!({ 248 + "error": "access_denied", 249 + "error_description": "User not found or has no passkeys." 250 + })), 251 + ) 252 + .into_response(); 253 + } 254 + 255 + let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys 256 + .iter() 257 + .filter_map(|sp| serde_json::from_slice(&sp.public_key).ok()) 258 + .collect(); 259 + 260 + if passkeys.is_empty() { 261 + return ( 262 + StatusCode::INTERNAL_SERVER_ERROR, 263 + Json(serde_json::json!({ 264 + "error": "server_error", 265 + "error_description": "Failed to load passkeys." 266 + })), 267 + ) 268 + .into_response(); 269 + } 270 + 271 + let (rcr, auth_state) = match state.webauthn_config.start_authentication(passkeys) { 272 + Ok(result) => result, 273 + Err(e) => { 274 + tracing::error!(error = %e, "Failed to start passkey authentication"); 275 + return ( 276 + StatusCode::INTERNAL_SERVER_ERROR, 277 + Json(serde_json::json!({ 278 + "error": "server_error", 279 + "error_description": "Failed to start authentication." 280 + })), 281 + ) 282 + .into_response(); 283 + } 284 + }; 285 + 286 + let state_json = match serde_json::to_string(&auth_state) { 287 + Ok(j) => j, 288 + Err(e) => { 289 + tracing::error!(error = %e, "Failed to serialize authentication state"); 290 + return ( 291 + StatusCode::INTERNAL_SERVER_ERROR, 292 + Json(serde_json::json!({ 293 + "error": "server_error", 294 + "error_description": "An error occurred." 295 + })), 296 + ) 297 + .into_response(); 298 + } 299 + }; 300 + 301 + if let Err(e) = state 302 + .repos.user 303 + .save_webauthn_challenge( 304 + &user.did, 305 + WebauthnChallengeType::Authentication, 306 + &state_json, 307 + ) 308 + .await 309 + { 310 + tracing::error!(error = %e, "Failed to save authentication state"); 311 + return ( 312 + StatusCode::INTERNAL_SERVER_ERROR, 313 + Json(serde_json::json!({ 314 + "error": "server_error", 315 + "error_description": "An error occurred." 316 + })), 317 + ) 318 + .into_response(); 319 + } 320 + 321 + let delegation_from_param = match &form.delegated_did { 322 + Some(delegated_did_str) => match delegated_did_str.parse::<tranquil_types::Did>() { 323 + Ok(delegated_did) if delegated_did != user.did => { 324 + match state 325 + .repos.delegation 326 + .get_delegation(&delegated_did, &user.did) 327 + .await 328 + { 329 + Ok(Some(_)) => Some(delegated_did), 330 + Ok(None) => None, 331 + Err(e) => { 332 + tracing::warn!( 333 + error = %e, 334 + delegated_did = %delegated_did, 335 + controller_did = %user.did, 336 + "Failed to verify delegation relationship" 337 + ); 338 + None 339 + } 340 + } 341 + } 342 + _ => None, 343 + }, 344 + None => None, 345 + }; 346 + 347 + let is_delegation_flow = delegation_from_param.is_some() 348 + || request_data.did.as_ref().is_some_and(|existing_did| { 349 + existing_did 350 + .parse::<tranquil_types::Did>() 351 + .ok() 352 + .is_some_and(|parsed| parsed != user.did) 353 + }); 354 + 355 + if let Some(delegated_did) = delegation_from_param { 356 + tracing::info!( 357 + delegated_did = %delegated_did, 358 + controller_did = %user.did, 359 + "Passkey auth with delegated_did param - setting delegation flow" 360 + ); 361 + if state 362 + .repos.oauth 363 + .set_authorization_did(&passkey_start_request_id, &delegated_did, None) 364 + .await 365 + .is_err() 366 + { 367 + return OAuthError::ServerError("An error occurred.".into()).into_response(); 368 + } 369 + if state 370 + .repos.oauth 371 + .set_controller_did(&passkey_start_request_id, &user.did) 372 + .await 373 + .is_err() 374 + { 375 + return OAuthError::ServerError("An error occurred.".into()).into_response(); 376 + } 377 + } else if is_delegation_flow { 378 + tracing::info!( 379 + delegated_did = ?request_data.did, 380 + controller_did = %user.did, 381 + "Passkey auth in delegation flow - preserving delegated DID" 382 + ); 383 + if state 384 + .repos.oauth 385 + .set_controller_did(&passkey_start_request_id, &user.did) 386 + .await 387 + .is_err() 388 + { 389 + return OAuthError::ServerError("An error occurred.".into()).into_response(); 390 + } 391 + } else if state 392 + .repos.oauth 393 + .set_authorization_did(&passkey_start_request_id, &user.did, None) 394 + .await 395 + .is_err() 396 + { 397 + return OAuthError::ServerError("An error occurred.".into()).into_response(); 398 + } 399 + 400 + let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 401 + 402 + Json(PasskeyStartResponse { options }).into_response() 403 + } 404 + 405 + #[derive(Debug, Deserialize)] 406 + pub struct PasskeyFinishInput { 407 + pub request_uri: String, 408 + pub credential: serde_json::Value, 409 + } 410 + 411 + pub async fn passkey_finish( 412 + State(state): State<AppState>, 413 + headers: HeaderMap, 414 + Json(form): Json<PasskeyFinishInput>, 415 + ) -> Response { 416 + let passkey_finish_request_id = RequestId::from(form.request_uri.clone()); 417 + let request_data = match state 418 + .repos.oauth 419 + .get_authorization_request(&passkey_finish_request_id) 420 + .await 421 + { 422 + Ok(Some(data)) => data, 423 + Ok(None) => { 424 + return ( 425 + StatusCode::BAD_REQUEST, 426 + Json(serde_json::json!({ 427 + "error": "invalid_request", 428 + "error_description": "Invalid or expired request_uri." 429 + })), 430 + ) 431 + .into_response(); 432 + } 433 + Err(_) => { 434 + return ( 435 + StatusCode::INTERNAL_SERVER_ERROR, 436 + Json(serde_json::json!({ 437 + "error": "server_error", 438 + "error_description": "An error occurred." 439 + })), 440 + ) 441 + .into_response(); 442 + } 443 + }; 444 + 445 + if request_data.expires_at < Utc::now() { 446 + let _ = state 447 + .repos.oauth 448 + .delete_authorization_request(&passkey_finish_request_id) 449 + .await; 450 + return ( 451 + StatusCode::BAD_REQUEST, 452 + Json(serde_json::json!({ 453 + "error": "invalid_request", 454 + "error_description": "Authorization request has expired." 455 + })), 456 + ) 457 + .into_response(); 458 + } 459 + 460 + let did_str = match request_data.did { 461 + Some(d) => d, 462 + None => { 463 + return ( 464 + StatusCode::BAD_REQUEST, 465 + Json(serde_json::json!({ 466 + "error": "invalid_request", 467 + "error_description": "No passkey authentication in progress." 468 + })), 469 + ) 470 + .into_response(); 471 + } 472 + }; 473 + let did: tranquil_types::Did = match did_str.parse() { 474 + Ok(d) => d, 475 + Err(_) => { 476 + return ( 477 + StatusCode::BAD_REQUEST, 478 + Json(serde_json::json!({ 479 + "error": "invalid_request", 480 + "error_description": "Invalid DID format." 481 + })), 482 + ) 483 + .into_response(); 484 + } 485 + }; 486 + 487 + let controller_did: Option<tranquil_types::Did> = request_data 488 + .controller_did 489 + .as_ref() 490 + .and_then(|s| s.parse().ok()); 491 + let passkey_owner_did = controller_did.as_ref().unwrap_or(&did); 492 + 493 + let auth_state_json = match state 494 + .repos.user 495 + .load_webauthn_challenge(passkey_owner_did, WebauthnChallengeType::Authentication) 496 + .await 497 + { 498 + Ok(Some(s)) => s, 499 + Ok(None) => { 500 + return ( 501 + StatusCode::BAD_REQUEST, 502 + Json(serde_json::json!({ 503 + "error": "invalid_request", 504 + "error_description": "No passkey authentication in progress or challenge expired." 505 + })), 506 + ) 507 + .into_response(); 508 + } 509 + Err(e) => { 510 + tracing::error!(error = %e, "Failed to load authentication state"); 511 + return ( 512 + StatusCode::INTERNAL_SERVER_ERROR, 513 + Json(serde_json::json!({ 514 + "error": "server_error", 515 + "error_description": "An error occurred." 516 + })), 517 + ) 518 + .into_response(); 519 + } 520 + }; 521 + 522 + let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = 523 + match serde_json::from_str(&auth_state_json) { 524 + Ok(s) => s, 525 + Err(e) => { 526 + tracing::error!(error = %e, "Failed to deserialize authentication state"); 527 + return ( 528 + StatusCode::INTERNAL_SERVER_ERROR, 529 + Json(serde_json::json!({ 530 + "error": "server_error", 531 + "error_description": "An error occurred." 532 + })), 533 + ) 534 + .into_response(); 535 + } 536 + }; 537 + 538 + let credential: webauthn_rs::prelude::PublicKeyCredential = 539 + match serde_json::from_value(form.credential) { 540 + Ok(c) => c, 541 + Err(e) => { 542 + tracing::warn!(error = %e, "Failed to parse credential"); 543 + return ( 544 + StatusCode::BAD_REQUEST, 545 + Json(serde_json::json!({ 546 + "error": "invalid_request", 547 + "error_description": "Failed to parse credential response." 548 + })), 549 + ) 550 + .into_response(); 551 + } 552 + }; 553 + 554 + let auth_result = match state 555 + .webauthn_config 556 + .finish_authentication(&credential, &auth_state) 557 + { 558 + Ok(r) => r, 559 + Err(e) => { 560 + tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication"); 561 + return ( 562 + StatusCode::FORBIDDEN, 563 + Json(serde_json::json!({ 564 + "error": "access_denied", 565 + "error_description": "Passkey verification failed." 566 + })), 567 + ) 568 + .into_response(); 569 + } 570 + }; 571 + 572 + if let Err(e) = state 573 + .repos.user 574 + .delete_webauthn_challenge(passkey_owner_did, WebauthnChallengeType::Authentication) 575 + .await 576 + { 577 + tracing::warn!(error = %e, "Failed to delete authentication state"); 578 + } 579 + 580 + if auth_result.needs_update() { 581 + let cred_id_bytes = auth_result.cred_id().as_slice(); 582 + match state 583 + .repos.user 584 + .update_passkey_counter( 585 + cred_id_bytes, 586 + i32::try_from(auth_result.counter()).unwrap_or(i32::MAX), 587 + ) 588 + .await 589 + { 590 + Ok(false) => { 591 + tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key"); 592 + return ( 593 + StatusCode::FORBIDDEN, 594 + Json(serde_json::json!({ 595 + "error": "access_denied", 596 + "error_description": "Security key counter anomaly detected. This may indicate a cloned key." 597 + })), 598 + ) 599 + .into_response(); 600 + } 601 + Err(e) => { 602 + tracing::warn!(error = %e, "Failed to update passkey counter"); 603 + } 604 + Ok(true) => {} 605 + } 606 + } 607 + 608 + tracing::info!(did = %did, "Passkey authentication successful"); 609 + 610 + let device_id = extract_device_cookie(&headers); 611 + let requested_scope_str = request_data 612 + .parameters 613 + .scope 614 + .as_deref() 615 + .unwrap_or("atproto"); 616 + let requested_scopes: Vec<String> = requested_scope_str 617 + .split_whitespace() 618 + .map(|s| s.to_string()) 619 + .collect(); 620 + 621 + let passkey_finish_client_id = ClientId::from(request_data.parameters.client_id.clone()); 622 + let needs_consent = should_show_consent( 623 + state.repos.oauth.as_ref(), 624 + &did, 625 + &passkey_finish_client_id, 626 + &requested_scopes, 627 + ) 628 + .await 629 + .unwrap_or(true); 630 + 631 + if needs_consent { 632 + let consent_url = format!( 633 + "/app/oauth/consent?request_uri={}", 634 + url_encode(&form.request_uri) 635 + ); 636 + return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 637 + } 638 + 639 + let code = Code::generate(); 640 + let passkey_final_device_id = device_id.clone(); 641 + let passkey_final_code = AuthorizationCode::from(code.0.clone()); 642 + if state 643 + .repos.oauth 644 + .update_authorization_request( 645 + &passkey_finish_request_id, 646 + &did, 647 + passkey_final_device_id.as_ref(), 648 + &passkey_final_code, 649 + ) 650 + .await 651 + .is_err() 652 + { 653 + return ( 654 + StatusCode::INTERNAL_SERVER_ERROR, 655 + Json(serde_json::json!({ 656 + "error": "server_error", 657 + "error_description": "An error occurred." 658 + })), 659 + ) 660 + .into_response(); 661 + } 662 + 663 + let redirect_url = build_intermediate_redirect_url( 664 + &request_data.parameters.redirect_uri, 665 + &code.0, 666 + request_data.parameters.state.as_deref(), 667 + request_data.parameters.response_mode.map(|m| m.as_str()), 668 + ); 669 + 670 + Json(serde_json::json!({ 671 + "redirect_uri": redirect_url 672 + })) 673 + .into_response() 674 + } 675 + 676 + #[derive(Debug, Deserialize)] 677 + pub struct AuthorizePasskeyQuery { 678 + pub request_uri: String, 679 + } 680 + 681 + #[derive(Debug, Serialize)] 682 + #[serde(rename_all = "camelCase")] 683 + pub struct PasskeyAuthResponse { 684 + pub options: serde_json::Value, 685 + pub request_uri: String, 686 + } 687 + 688 + pub async fn authorize_passkey_start( 689 + State(state): State<AppState>, 690 + Query(query): Query<AuthorizePasskeyQuery>, 691 + ) -> Response { 692 + let auth_passkey_start_request_id = RequestId::from(query.request_uri.clone()); 693 + let request_data = match state 694 + .repos.oauth 695 + .get_authorization_request(&auth_passkey_start_request_id) 696 + .await 697 + { 698 + Ok(Some(d)) => d, 699 + Ok(None) => { 700 + return ( 701 + StatusCode::BAD_REQUEST, 702 + Json(serde_json::json!({ 703 + "error": "invalid_request", 704 + "error_description": "Authorization request not found." 705 + })), 706 + ) 707 + .into_response(); 708 + } 709 + Err(_) => { 710 + return ( 711 + StatusCode::INTERNAL_SERVER_ERROR, 712 + Json(serde_json::json!({ 713 + "error": "server_error", 714 + "error_description": "An error occurred." 715 + })), 716 + ) 717 + .into_response(); 718 + } 719 + }; 720 + 721 + if request_data.expires_at < Utc::now() { 722 + let _ = state 723 + .repos.oauth 724 + .delete_authorization_request(&auth_passkey_start_request_id) 725 + .await; 726 + return ( 727 + StatusCode::BAD_REQUEST, 728 + Json(serde_json::json!({ 729 + "error": "invalid_request", 730 + "error_description": "Authorization request has expired." 731 + })), 732 + ) 733 + .into_response(); 734 + } 735 + 736 + let did_str = match &request_data.did { 737 + Some(d) => d.clone(), 738 + None => { 739 + return ( 740 + StatusCode::BAD_REQUEST, 741 + Json(serde_json::json!({ 742 + "error": "invalid_request", 743 + "error_description": "User not authenticated yet." 744 + })), 745 + ) 746 + .into_response(); 747 + } 748 + }; 749 + 750 + let did: tranquil_types::Did = match did_str.parse() { 751 + Ok(d) => d, 752 + Err(_) => { 753 + return ( 754 + StatusCode::BAD_REQUEST, 755 + Json(serde_json::json!({ 756 + "error": "invalid_request", 757 + "error_description": "Invalid DID format." 758 + })), 759 + ) 760 + .into_response(); 761 + } 762 + }; 763 + 764 + let stored_passkeys = match state.repos.user.get_passkeys_for_user(&did).await { 765 + Ok(pks) => pks, 766 + Err(e) => { 767 + tracing::error!("Failed to get passkeys: {:?}", e); 768 + return ( 769 + StatusCode::INTERNAL_SERVER_ERROR, 770 + Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 771 + ) 772 + .into_response(); 773 + } 774 + }; 775 + 776 + if stored_passkeys.is_empty() { 777 + return ( 778 + StatusCode::BAD_REQUEST, 779 + Json(serde_json::json!({ 780 + "error": "invalid_request", 781 + "error_description": "No passkeys registered for this account." 782 + })), 783 + ) 784 + .into_response(); 785 + } 786 + 787 + let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys 788 + .iter() 789 + .filter_map(|sp| serde_json::from_slice(&sp.public_key).ok()) 790 + .collect(); 791 + 792 + if passkeys.is_empty() { 793 + return ( 794 + StatusCode::INTERNAL_SERVER_ERROR, 795 + Json(serde_json::json!({"error": "server_error", "error_description": "Failed to load passkeys."})), 796 + ) 797 + .into_response(); 798 + } 799 + 800 + let (rcr, auth_state) = match state.webauthn_config.start_authentication(passkeys) { 801 + Ok(result) => result, 802 + Err(e) => { 803 + tracing::error!("Failed to start passkey authentication: {:?}", e); 804 + return ( 805 + StatusCode::INTERNAL_SERVER_ERROR, 806 + Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 807 + ) 808 + .into_response(); 809 + } 810 + }; 811 + 812 + let state_json = match serde_json::to_string(&auth_state) { 813 + Ok(j) => j, 814 + Err(e) => { 815 + tracing::error!("Failed to serialize authentication state: {:?}", e); 816 + return ( 817 + StatusCode::INTERNAL_SERVER_ERROR, 818 + Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 819 + ) 820 + .into_response(); 821 + } 822 + }; 823 + 824 + if let Err(e) = state 825 + .repos.user 826 + .save_webauthn_challenge(&did, WebauthnChallengeType::Authentication, &state_json) 827 + .await 828 + { 829 + tracing::error!("Failed to save authentication state: {:?}", e); 830 + return ( 831 + StatusCode::INTERNAL_SERVER_ERROR, 832 + Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 833 + ) 834 + .into_response(); 835 + } 836 + 837 + let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 838 + Json(PasskeyAuthResponse { 839 + options, 840 + request_uri: query.request_uri, 841 + }) 842 + .into_response() 843 + } 844 + 845 + #[derive(Debug, Deserialize)] 846 + #[serde(rename_all = "camelCase")] 847 + pub struct AuthorizePasskeySubmit { 848 + pub request_uri: String, 849 + pub credential: serde_json::Value, 850 + } 851 + 852 + pub async fn authorize_passkey_finish( 853 + State(state): State<AppState>, 854 + headers: HeaderMap, 855 + Json(form): Json<AuthorizePasskeySubmit>, 856 + ) -> Response { 857 + let pds_hostname = &tranquil_config::get().server.hostname; 858 + let passkey_finish_request_id = RequestId::from(form.request_uri.clone()); 859 + 860 + let request_data = match state 861 + .repos.oauth 862 + .get_authorization_request(&passkey_finish_request_id) 863 + .await 864 + { 865 + Ok(Some(d)) => d, 866 + Ok(None) => { 867 + return ( 868 + StatusCode::BAD_REQUEST, 869 + Json(serde_json::json!({ 870 + "error": "invalid_request", 871 + "error_description": "Authorization request not found." 872 + })), 873 + ) 874 + .into_response(); 875 + } 876 + Err(_) => { 877 + return ( 878 + StatusCode::INTERNAL_SERVER_ERROR, 879 + Json(serde_json::json!({ 880 + "error": "server_error", 881 + "error_description": "An error occurred." 882 + })), 883 + ) 884 + .into_response(); 885 + } 886 + }; 887 + 888 + if request_data.expires_at < Utc::now() { 889 + let _ = state 890 + .repos.oauth 891 + .delete_authorization_request(&passkey_finish_request_id) 892 + .await; 893 + return ( 894 + StatusCode::BAD_REQUEST, 895 + Json(serde_json::json!({ 896 + "error": "invalid_request", 897 + "error_description": "Authorization request has expired." 898 + })), 899 + ) 900 + .into_response(); 901 + } 902 + 903 + let did_str = match &request_data.did { 904 + Some(d) => d.clone(), 905 + None => { 906 + return ( 907 + StatusCode::BAD_REQUEST, 908 + Json(serde_json::json!({ 909 + "error": "invalid_request", 910 + "error_description": "User not authenticated yet." 911 + })), 912 + ) 913 + .into_response(); 914 + } 915 + }; 916 + 917 + let did: tranquil_types::Did = match did_str.parse() { 918 + Ok(d) => d, 919 + Err(_) => { 920 + return ( 921 + StatusCode::BAD_REQUEST, 922 + Json(serde_json::json!({ 923 + "error": "invalid_request", 924 + "error_description": "Invalid DID format." 925 + })), 926 + ) 927 + .into_response(); 928 + } 929 + }; 930 + 931 + let auth_state_json = match state 932 + .repos.user 933 + .load_webauthn_challenge(&did, WebauthnChallengeType::Authentication) 934 + .await 935 + { 936 + Ok(Some(s)) => s, 937 + Ok(None) => { 938 + return ( 939 + StatusCode::BAD_REQUEST, 940 + Json(serde_json::json!({ 941 + "error": "invalid_request", 942 + "error_description": "No passkey challenge found. Please start over." 943 + })), 944 + ) 945 + .into_response(); 946 + } 947 + Err(e) => { 948 + tracing::error!("Failed to load authentication state: {:?}", e); 949 + return ( 950 + StatusCode::INTERNAL_SERVER_ERROR, 951 + Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 952 + ) 953 + .into_response(); 954 + } 955 + }; 956 + 957 + let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = match serde_json::from_str( 958 + &auth_state_json, 959 + ) { 960 + Ok(s) => s, 961 + Err(e) => { 962 + tracing::error!("Failed to deserialize authentication state: {:?}", e); 963 + return ( 964 + StatusCode::INTERNAL_SERVER_ERROR, 965 + Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 966 + ) 967 + .into_response(); 968 + } 969 + }; 970 + 971 + let credential: webauthn_rs::prelude::PublicKeyCredential = 972 + match serde_json::from_value(form.credential.clone()) { 973 + Ok(c) => c, 974 + Err(e) => { 975 + tracing::error!("Failed to parse credential: {:?}", e); 976 + return ( 977 + StatusCode::BAD_REQUEST, 978 + Json(serde_json::json!({ 979 + "error": "invalid_request", 980 + "error_description": "Invalid credential format." 981 + })), 982 + ) 983 + .into_response(); 984 + } 985 + }; 986 + 987 + let auth_result = match state 988 + .webauthn_config 989 + .finish_authentication(&credential, &auth_state) 990 + { 991 + Ok(r) => r, 992 + Err(e) => { 993 + tracing::warn!("Passkey authentication failed: {:?}", e); 994 + return ( 995 + StatusCode::FORBIDDEN, 996 + Json(serde_json::json!({ 997 + "error": "access_denied", 998 + "error_description": "Passkey authentication failed." 999 + })), 1000 + ) 1001 + .into_response(); 1002 + } 1003 + }; 1004 + 1005 + let _ = state 1006 + .repos.user 1007 + .delete_webauthn_challenge(&did, WebauthnChallengeType::Authentication) 1008 + .await; 1009 + 1010 + match state 1011 + .repos.user 1012 + .update_passkey_counter( 1013 + credential.id.as_ref(), 1014 + i32::try_from(auth_result.counter()).unwrap_or(i32::MAX), 1015 + ) 1016 + .await 1017 + { 1018 + Ok(false) => { 1019 + tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key"); 1020 + return ( 1021 + StatusCode::FORBIDDEN, 1022 + Json(serde_json::json!({ 1023 + "error": "access_denied", 1024 + "error_description": "Security key counter anomaly detected. This may indicate a cloned key." 1025 + })), 1026 + ) 1027 + .into_response(); 1028 + } 1029 + Err(e) => { 1030 + tracing::warn!("Failed to update passkey counter: {:?}", e); 1031 + } 1032 + Ok(true) => {} 1033 + } 1034 + 1035 + let has_totp = state 1036 + .repos.user 1037 + .has_totp_enabled(&did) 1038 + .await 1039 + .unwrap_or(false); 1040 + if has_totp { 1041 + let device_cookie = extract_device_cookie(&headers); 1042 + let device_is_trusted = if let Some(ref dev_id) = device_cookie { 1043 + tranquil_api::server::is_device_trusted(state.repos.oauth.as_ref(), dev_id, &did).await 1044 + } else { 1045 + false 1046 + }; 1047 + 1048 + if device_is_trusted { 1049 + if let Some(ref dev_id) = device_cookie { 1050 + let _ = 1051 + tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), dev_id) 1052 + .await; 1053 + } 1054 + } else { 1055 + let user = match state.repos.user.get_2fa_status_by_did(&did).await { 1056 + Ok(Some(u)) => u, 1057 + _ => { 1058 + return ( 1059 + StatusCode::INTERNAL_SERVER_ERROR, 1060 + Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 1061 + ) 1062 + .into_response(); 1063 + } 1064 + }; 1065 + 1066 + let _ = state 1067 + .repos.oauth 1068 + .delete_2fa_challenge_by_request_uri(&passkey_finish_request_id) 1069 + .await; 1070 + match state 1071 + .repos.oauth 1072 + .create_2fa_challenge(&did, &passkey_finish_request_id) 1073 + .await 1074 + { 1075 + Ok(challenge) => { 1076 + if let Err(e) = enqueue_2fa_code( 1077 + state.repos.user.as_ref(), 1078 + state.repos.infra.as_ref(), 1079 + user.id, 1080 + &challenge.code, 1081 + pds_hostname, 1082 + ) 1083 + .await 1084 + { 1085 + tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); 1086 + } 1087 + let channel_name = user.preferred_comms_channel.display_name(); 1088 + let redirect_url = format!( 1089 + "/app/oauth/2fa?request_uri={}&channel={}", 1090 + url_encode(&form.request_uri), 1091 + url_encode(channel_name) 1092 + ); 1093 + return ( 1094 + StatusCode::OK, 1095 + Json(serde_json::json!({ 1096 + "next": "2fa", 1097 + "redirect": redirect_url 1098 + })), 1099 + ) 1100 + .into_response(); 1101 + } 1102 + Err(_) => { 1103 + return ( 1104 + StatusCode::INTERNAL_SERVER_ERROR, 1105 + Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 1106 + ) 1107 + .into_response(); 1108 + } 1109 + } 1110 + } 1111 + } 1112 + 1113 + let redirect_url = format!( 1114 + "/app/oauth/consent?request_uri={}", 1115 + url_encode(&form.request_uri) 1116 + ); 1117 + ( 1118 + StatusCode::OK, 1119 + Json(serde_json::json!({ 1120 + "next": "consent", 1121 + "redirect": redirect_url 1122 + })), 1123 + ) 1124 + .into_response() 1125 + }
+374
crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs
··· 1 + use super::*; 2 + 3 + #[derive(Debug, Deserialize)] 4 + pub struct RegisterCompleteInput { 5 + pub request_uri: String, 6 + pub did: String, 7 + pub app_password: String, 8 + } 9 + 10 + pub async fn register_complete( 11 + State(state): State<AppState>, 12 + _rate_limit: OAuthRateLimited<OAuthRegisterCompleteLimit>, 13 + Json(form): Json<RegisterCompleteInput>, 14 + ) -> Response { 15 + let did = Did::from(form.did.clone()); 16 + 17 + let request_id = RequestId::from(form.request_uri.clone()); 18 + let request_data = match state 19 + .repos.oauth 20 + .get_authorization_request(&request_id) 21 + .await 22 + { 23 + Ok(Some(data)) => data, 24 + Ok(None) => { 25 + return ( 26 + StatusCode::BAD_REQUEST, 27 + Json(serde_json::json!({ 28 + "error": "invalid_request", 29 + "error_description": "Invalid or expired request_uri." 30 + })), 31 + ) 32 + .into_response(); 33 + } 34 + Err(e) => { 35 + tracing::error!( 36 + request_uri = %form.request_uri, 37 + error = ?e, 38 + "register_complete: failed to fetch authorization request" 39 + ); 40 + return ( 41 + StatusCode::INTERNAL_SERVER_ERROR, 42 + Json(serde_json::json!({ 43 + "error": "server_error", 44 + "error_description": "An error occurred." 45 + })), 46 + ) 47 + .into_response(); 48 + } 49 + }; 50 + 51 + if request_data.expires_at < Utc::now() { 52 + let _ = state 53 + .repos.oauth 54 + .delete_authorization_request(&request_id) 55 + .await; 56 + return ( 57 + StatusCode::BAD_REQUEST, 58 + Json(serde_json::json!({ 59 + "error": "invalid_request", 60 + "error_description": "Authorization request has expired." 61 + })), 62 + ) 63 + .into_response(); 64 + } 65 + 66 + if request_data.parameters.prompt != Some(Prompt::Create) { 67 + tracing::warn!( 68 + request_uri = %form.request_uri, 69 + prompt = ?request_data.parameters.prompt, 70 + "register_complete called on non-registration OAuth flow" 71 + ); 72 + return ( 73 + StatusCode::BAD_REQUEST, 74 + Json(serde_json::json!({ 75 + "error": "invalid_request", 76 + "error_description": "This endpoint is only for registration flows." 77 + })), 78 + ) 79 + .into_response(); 80 + } 81 + 82 + if request_data.code.is_some() { 83 + tracing::warn!( 84 + request_uri = %form.request_uri, 85 + "register_complete called on already-completed OAuth flow" 86 + ); 87 + return ( 88 + StatusCode::BAD_REQUEST, 89 + Json(serde_json::json!({ 90 + "error": "invalid_request", 91 + "error_description": "Authorization has already been completed." 92 + })), 93 + ) 94 + .into_response(); 95 + } 96 + 97 + if let Some(existing_did) = &request_data.did 98 + && existing_did != &form.did 99 + { 100 + tracing::warn!( 101 + request_uri = %form.request_uri, 102 + existing_did = %existing_did, 103 + attempted_did = %form.did, 104 + "register_complete attempted with different DID than already bound" 105 + ); 106 + return ( 107 + StatusCode::BAD_REQUEST, 108 + Json(serde_json::json!({ 109 + "error": "invalid_request", 110 + "error_description": "Authorization request is already bound to a different account." 111 + })), 112 + ) 113 + .into_response(); 114 + } 115 + 116 + let password_hashes = match state 117 + .repos.session 118 + .get_app_password_hashes_by_did(&did) 119 + .await 120 + { 121 + Ok(hashes) => hashes, 122 + Err(e) => { 123 + tracing::error!( 124 + did = %did, 125 + error = ?e, 126 + "register_complete: failed to fetch app password hashes" 127 + ); 128 + return ( 129 + StatusCode::INTERNAL_SERVER_ERROR, 130 + Json(serde_json::json!({ 131 + "error": "server_error", 132 + "error_description": "An error occurred." 133 + })), 134 + ) 135 + .into_response(); 136 + } 137 + }; 138 + 139 + let mut password_valid = password_hashes.iter().fold(false, |acc, hash| { 140 + acc | bcrypt::verify(&form.app_password, hash).unwrap_or(false) 141 + }); 142 + 143 + if !password_valid 144 + && let Ok(Some(account_hash)) = state.repos.user.get_password_hash_by_did(&did).await 145 + { 146 + password_valid = bcrypt::verify(&form.app_password, &account_hash).unwrap_or(false); 147 + } 148 + 149 + if !password_valid { 150 + return ( 151 + StatusCode::FORBIDDEN, 152 + Json(serde_json::json!({ 153 + "error": "access_denied", 154 + "error_description": "Invalid credentials." 155 + })), 156 + ) 157 + .into_response(); 158 + } 159 + 160 + let is_verified = match state.repos.user.get_session_info_by_did(&did).await { 161 + Ok(Some(info)) => info.channel_verification.has_any_verified(), 162 + Ok(None) => { 163 + return ( 164 + StatusCode::FORBIDDEN, 165 + Json(serde_json::json!({ 166 + "error": "access_denied", 167 + "error_description": "Account not found." 168 + })), 169 + ) 170 + .into_response(); 171 + } 172 + Err(e) => { 173 + tracing::error!( 174 + did = %did, 175 + error = ?e, 176 + "register_complete: failed to fetch session info" 177 + ); 178 + return ( 179 + StatusCode::INTERNAL_SERVER_ERROR, 180 + Json(serde_json::json!({ 181 + "error": "server_error", 182 + "error_description": "An error occurred." 183 + })), 184 + ) 185 + .into_response(); 186 + } 187 + }; 188 + 189 + if !is_verified { 190 + let resend_info = tranquil_api::server::auto_resend_verification(&state, &did).await; 191 + return ( 192 + StatusCode::FORBIDDEN, 193 + Json(serde_json::json!({ 194 + "error": "account_not_verified", 195 + "error_description": "Please verify your account before continuing.", 196 + "did": did, 197 + "handle": resend_info.as_ref().map(|r| r.handle.to_string()), 198 + "channel": resend_info.as_ref().map(|r| r.channel.as_str()) 199 + })), 200 + ) 201 + .into_response(); 202 + } 203 + 204 + if let Err(e) = state 205 + .repos.oauth 206 + .set_authorization_did(&request_id, &did, None) 207 + .await 208 + { 209 + tracing::error!( 210 + request_uri = %form.request_uri, 211 + did = %did, 212 + error = ?e, 213 + "register_complete: failed to set authorization DID" 214 + ); 215 + return ( 216 + StatusCode::INTERNAL_SERVER_ERROR, 217 + Json(serde_json::json!({ 218 + "error": "server_error", 219 + "error_description": "An error occurred." 220 + })), 221 + ) 222 + .into_response(); 223 + } 224 + 225 + let requested_scope_str = request_data 226 + .parameters 227 + .scope 228 + .as_deref() 229 + .unwrap_or("atproto"); 230 + let requested_scopes: Vec<String> = requested_scope_str 231 + .split_whitespace() 232 + .map(|s| s.to_string()) 233 + .collect(); 234 + let client_id_typed = ClientId::from(request_data.parameters.client_id.clone()); 235 + let needs_consent = should_show_consent( 236 + state.repos.oauth.as_ref(), 237 + &did, 238 + &client_id_typed, 239 + &requested_scopes, 240 + ) 241 + .await 242 + .unwrap_or(true); 243 + 244 + if needs_consent { 245 + tracing::info!( 246 + did = %did, 247 + client_id = %request_data.parameters.client_id, 248 + "OAuth registration complete, redirecting to consent" 249 + ); 250 + let consent_url = format!( 251 + "/app/oauth/consent?request_uri={}", 252 + url_encode(&form.request_uri) 253 + ); 254 + return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 255 + } 256 + 257 + let code = Code::generate(); 258 + let auth_code = AuthorizationCode::from(code.0.clone()); 259 + if let Err(e) = state 260 + .repos.oauth 261 + .update_authorization_request(&request_id, &did, None, &auth_code) 262 + .await 263 + { 264 + tracing::error!( 265 + request_uri = %form.request_uri, 266 + did = %did, 267 + error = ?e, 268 + "register_complete: failed to update authorization request with code" 269 + ); 270 + return ( 271 + StatusCode::INTERNAL_SERVER_ERROR, 272 + Json(serde_json::json!({ 273 + "error": "server_error", 274 + "error_description": "An error occurred." 275 + })), 276 + ) 277 + .into_response(); 278 + } 279 + 280 + tracing::info!( 281 + did = %did, 282 + client_id = %request_data.parameters.client_id, 283 + "OAuth registration flow completed successfully" 284 + ); 285 + 286 + let redirect_url = build_intermediate_redirect_url( 287 + &request_data.parameters.redirect_uri, 288 + &code.0, 289 + request_data.parameters.state.as_deref(), 290 + request_data.parameters.response_mode.map(|m| m.as_str()), 291 + ); 292 + Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 293 + } 294 + 295 + pub async fn establish_session( 296 + State(state): State<AppState>, 297 + headers: HeaderMap, 298 + auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>, 299 + ) -> Response { 300 + let did = &auth.did; 301 + 302 + let existing_device = extract_device_cookie(&headers); 303 + 304 + let (device_id, new_cookie) = match existing_device { 305 + Some(id) => { 306 + let _ = state.repos.oauth.upsert_account_device(did, &id).await; 307 + (id, None) 308 + } 309 + None => { 310 + let new_id = DeviceId::generate(); 311 + let device_typed = DeviceIdType::new(new_id.0.clone()); 312 + let device_data = DeviceData { 313 + session_id: SessionId::generate(), 314 + user_agent: extract_user_agent(&headers), 315 + ip_address: extract_client_ip(&headers, None), 316 + last_seen_at: Utc::now(), 317 + }; 318 + 319 + if let Err(e) = state 320 + .repos.oauth 321 + .create_device(&device_typed, &device_data) 322 + .await 323 + { 324 + tracing::error!(error = ?e, "Failed to create device"); 325 + return ( 326 + StatusCode::INTERNAL_SERVER_ERROR, 327 + Json(serde_json::json!({ 328 + "error": "server_error", 329 + "error_description": "Failed to establish session" 330 + })), 331 + ) 332 + .into_response(); 333 + } 334 + 335 + if let Err(e) = state 336 + .repos.oauth 337 + .upsert_account_device(did, &device_typed) 338 + .await 339 + { 340 + tracing::error!(error = ?e, "Failed to link device to account"); 341 + return ( 342 + StatusCode::INTERNAL_SERVER_ERROR, 343 + Json(serde_json::json!({ 344 + "error": "server_error", 345 + "error_description": "Failed to establish session" 346 + })), 347 + ) 348 + .into_response(); 349 + } 350 + 351 + let cookie = make_device_cookie(&device_typed); 352 + (device_typed, Some(cookie)) 353 + } 354 + }; 355 + 356 + tracing::info!(did = %did, device_id = %device_id, "Device session established"); 357 + 358 + match new_cookie { 359 + Some(cookie) => ( 360 + StatusCode::OK, 361 + [(SET_COOKIE, cookie)], 362 + Json(serde_json::json!({ 363 + "success": true, 364 + "device_id": device_id 365 + })), 366 + ) 367 + .into_response(), 368 + None => Json(serde_json::json!({ 369 + "success": true, 370 + "device_id": device_id 371 + })) 372 + .into_response(), 373 + } 374 + }
+340
crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs
··· 1 + use super::*; 2 + 3 + #[derive(Debug, Deserialize)] 4 + pub struct Authorize2faQuery { 5 + pub request_uri: String, 6 + pub channel: Option<String>, 7 + } 8 + 9 + #[derive(Debug, Deserialize)] 10 + pub struct Authorize2faSubmit { 11 + pub request_uri: String, 12 + pub code: String, 13 + #[serde(default)] 14 + pub trust_device: bool, 15 + } 16 + 17 + const MAX_2FA_ATTEMPTS: i32 = 5; 18 + 19 + pub async fn authorize_2fa_get( 20 + State(state): State<AppState>, 21 + Query(query): Query<Authorize2faQuery>, 22 + ) -> Response { 23 + let twofa_request_id = RequestId::from(query.request_uri.clone()); 24 + let challenge = match state.repos.oauth.get_2fa_challenge(&twofa_request_id).await { 25 + Ok(Some(c)) => c, 26 + Ok(None) => { 27 + return redirect_to_frontend_error( 28 + "invalid_request", 29 + "No 2FA challenge found. Please start over.", 30 + ); 31 + } 32 + Err(_) => { 33 + return redirect_to_frontend_error( 34 + "server_error", 35 + "An error occurred. Please try again.", 36 + ); 37 + } 38 + }; 39 + if challenge.expires_at < Utc::now() { 40 + let _ = state.repos.oauth.delete_2fa_challenge(challenge.id).await; 41 + return redirect_to_frontend_error( 42 + "invalid_request", 43 + "2FA code has expired. Please start over.", 44 + ); 45 + } 46 + let _request_data = match state 47 + .repos.oauth 48 + .get_authorization_request(&twofa_request_id) 49 + .await 50 + { 51 + Ok(Some(d)) => d, 52 + Ok(None) => { 53 + return redirect_to_frontend_error( 54 + "invalid_request", 55 + "Authorization request not found. Please start over.", 56 + ); 57 + } 58 + Err(_) => { 59 + return redirect_to_frontend_error( 60 + "server_error", 61 + "An error occurred. Please try again.", 62 + ); 63 + } 64 + }; 65 + let channel = query.channel.as_deref().unwrap_or("email"); 66 + redirect_see_other(&format!( 67 + "/app/oauth/2fa?request_uri={}&channel={}", 68 + url_encode(&query.request_uri), 69 + url_encode(channel) 70 + )) 71 + } 72 + 73 + pub async fn authorize_2fa_post( 74 + State(state): State<AppState>, 75 + _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 76 + headers: HeaderMap, 77 + Json(form): Json<Authorize2faSubmit>, 78 + ) -> Response { 79 + let json_error = |status: StatusCode, error: &str, description: &str| -> Response { 80 + ( 81 + status, 82 + Json(serde_json::json!({ 83 + "error": error, 84 + "error_description": description 85 + })), 86 + ) 87 + .into_response() 88 + }; 89 + let twofa_post_request_id = RequestId::from(form.request_uri.clone()); 90 + let request_data = match state 91 + .repos.oauth 92 + .get_authorization_request(&twofa_post_request_id) 93 + .await 94 + { 95 + Ok(Some(d)) => d, 96 + Ok(None) => { 97 + return json_error( 98 + StatusCode::BAD_REQUEST, 99 + "invalid_request", 100 + "Authorization request not found.", 101 + ); 102 + } 103 + Err(_) => { 104 + return json_error( 105 + StatusCode::INTERNAL_SERVER_ERROR, 106 + "server_error", 107 + "An error occurred.", 108 + ); 109 + } 110 + }; 111 + if request_data.expires_at < Utc::now() { 112 + let _ = state 113 + .repos.oauth 114 + .delete_authorization_request(&twofa_post_request_id) 115 + .await; 116 + return json_error( 117 + StatusCode::BAD_REQUEST, 118 + "invalid_request", 119 + "Authorization request has expired.", 120 + ); 121 + } 122 + let challenge = state 123 + .repos.oauth 124 + .get_2fa_challenge(&twofa_post_request_id) 125 + .await 126 + .ok() 127 + .flatten(); 128 + if let Some(challenge) = challenge { 129 + if challenge.expires_at < Utc::now() { 130 + let _ = state.repos.oauth.delete_2fa_challenge(challenge.id).await; 131 + return json_error( 132 + StatusCode::BAD_REQUEST, 133 + "invalid_request", 134 + "2FA code has expired. Please start over.", 135 + ); 136 + } 137 + if challenge.attempts >= MAX_2FA_ATTEMPTS { 138 + let _ = state.repos.oauth.delete_2fa_challenge(challenge.id).await; 139 + return json_error( 140 + StatusCode::FORBIDDEN, 141 + "access_denied", 142 + "Too many failed attempts. Please start over.", 143 + ); 144 + } 145 + let code_valid: bool = form 146 + .code 147 + .trim() 148 + .as_bytes() 149 + .ct_eq(challenge.code.as_bytes()) 150 + .into(); 151 + if !code_valid { 152 + let _ = state.repos.oauth.increment_2fa_attempts(challenge.id).await; 153 + return json_error( 154 + StatusCode::FORBIDDEN, 155 + "invalid_code", 156 + "Invalid verification code. Please try again.", 157 + ); 158 + } 159 + let _ = state.repos.oauth.delete_2fa_challenge(challenge.id).await; 160 + let code = Code::generate(); 161 + let device_id = extract_device_cookie(&headers); 162 + let twofa_totp_device_id = device_id.clone(); 163 + let twofa_totp_code = AuthorizationCode::from(code.0.clone()); 164 + if state 165 + .repos.oauth 166 + .update_authorization_request( 167 + &twofa_post_request_id, 168 + &challenge.did, 169 + twofa_totp_device_id.as_ref(), 170 + &twofa_totp_code, 171 + ) 172 + .await 173 + .is_err() 174 + { 175 + return json_error( 176 + StatusCode::INTERNAL_SERVER_ERROR, 177 + "server_error", 178 + "An error occurred. Please try again.", 179 + ); 180 + } 181 + let redirect_url = build_intermediate_redirect_url( 182 + &request_data.parameters.redirect_uri, 183 + &code.0, 184 + request_data.parameters.state.as_deref(), 185 + request_data.parameters.response_mode.map(|m| m.as_str()), 186 + ); 187 + return Json(serde_json::json!({ 188 + "redirect_uri": redirect_url 189 + })) 190 + .into_response(); 191 + } 192 + let did_str = match &request_data.did { 193 + Some(d) => d.clone(), 194 + None => { 195 + return json_error( 196 + StatusCode::BAD_REQUEST, 197 + "invalid_request", 198 + "No 2FA challenge found. Please start over.", 199 + ); 200 + } 201 + }; 202 + let did: tranquil_types::Did = match did_str.parse() { 203 + Ok(d) => d, 204 + Err(_) => { 205 + return json_error( 206 + StatusCode::BAD_REQUEST, 207 + "invalid_request", 208 + "Invalid DID format.", 209 + ); 210 + } 211 + }; 212 + if !tranquil_api::server::has_totp_enabled(&state, &did).await { 213 + return json_error( 214 + StatusCode::BAD_REQUEST, 215 + "invalid_request", 216 + "No 2FA challenge found. Please start over.", 217 + ); 218 + } 219 + let _rate_proof = match check_user_rate_limit::<TotpVerifyLimit>(&state, &did).await { 220 + Ok(proof) => proof, 221 + Err(_) => { 222 + return json_error( 223 + StatusCode::TOO_MANY_REQUESTS, 224 + "RateLimitExceeded", 225 + "Too many verification attempts. Please try again in a few minutes.", 226 + ); 227 + } 228 + }; 229 + let totp_valid = 230 + tranquil_api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await; 231 + if !totp_valid { 232 + return json_error( 233 + StatusCode::FORBIDDEN, 234 + "invalid_code", 235 + "Invalid verification code. Please try again.", 236 + ); 237 + } 238 + let mut device_id = extract_device_cookie(&headers); 239 + let mut new_cookie: Option<String> = None; 240 + if form.trust_device { 241 + let trust_device_id = match &device_id { 242 + Some(existing_id) => existing_id.clone(), 243 + None => { 244 + let new_id = DeviceId::generate(); 245 + let new_device_id_typed = DeviceIdType::new(new_id.0.clone()); 246 + let device_data = DeviceData { 247 + session_id: SessionId::generate(), 248 + user_agent: extract_user_agent(&headers), 249 + ip_address: extract_client_ip(&headers, None), 250 + last_seen_at: Utc::now(), 251 + }; 252 + if state 253 + .repos.oauth 254 + .create_device(&new_device_id_typed, &device_data) 255 + .await 256 + .is_ok() 257 + { 258 + new_cookie = Some(make_device_cookie(&new_device_id_typed)); 259 + device_id = Some(new_device_id_typed.clone()); 260 + } 261 + new_device_id_typed 262 + } 263 + }; 264 + let _ = state 265 + .repos.oauth 266 + .upsert_account_device(&did, &trust_device_id) 267 + .await; 268 + let _ = 269 + tranquil_api::server::trust_device(state.repos.oauth.as_ref(), &trust_device_id).await; 270 + } 271 + let requested_scope_str = request_data 272 + .parameters 273 + .scope 274 + .as_deref() 275 + .unwrap_or("atproto"); 276 + let requested_scopes: Vec<String> = requested_scope_str 277 + .split_whitespace() 278 + .map(|s| s.to_string()) 279 + .collect(); 280 + let twofa_post_client_id = ClientId::from(request_data.parameters.client_id.clone()); 281 + let needs_consent = should_show_consent( 282 + state.repos.oauth.as_ref(), 283 + &did, 284 + &twofa_post_client_id, 285 + &requested_scopes, 286 + ) 287 + .await 288 + .unwrap_or(true); 289 + if needs_consent { 290 + let consent_url = format!( 291 + "/app/oauth/consent?request_uri={}", 292 + url_encode(&form.request_uri) 293 + ); 294 + if let Some(cookie) = new_cookie { 295 + return ( 296 + StatusCode::OK, 297 + [(SET_COOKIE, cookie)], 298 + Json(serde_json::json!({"redirect_uri": consent_url})), 299 + ) 300 + .into_response(); 301 + } 302 + return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 303 + } 304 + let code = Code::generate(); 305 + let twofa_final_device_id = device_id.clone(); 306 + let twofa_final_code = AuthorizationCode::from(code.0.clone()); 307 + if state 308 + .repos.oauth 309 + .update_authorization_request( 310 + &twofa_post_request_id, 311 + &did, 312 + twofa_final_device_id.as_ref(), 313 + &twofa_final_code, 314 + ) 315 + .await 316 + .is_err() 317 + { 318 + return json_error( 319 + StatusCode::INTERNAL_SERVER_ERROR, 320 + "server_error", 321 + "An error occurred. Please try again.", 322 + ); 323 + } 324 + let redirect_url = build_intermediate_redirect_url( 325 + &request_data.parameters.redirect_uri, 326 + &code.0, 327 + request_data.parameters.state.as_deref(), 328 + request_data.parameters.response_mode.map(|m| m.as_str()), 329 + ); 330 + if let Some(cookie) = new_cookie { 331 + ( 332 + StatusCode::OK, 333 + [(SET_COOKIE, cookie)], 334 + Json(serde_json::json!({"redirect_uri": redirect_url})), 335 + ) 336 + .into_response() 337 + } else { 338 + Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 339 + } 340 + }