Our Personal Data Server from scratch!
0

Configure Feed

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

tranquil-pds / crates / tranquil-oauth-server / src / sso_endpoints.rs
46 kB 1408 lines
1use axum::{ 2 Form, Json, 3 extract::{Query, State}, 4 http::HeaderMap, 5 response::{IntoResponse, Redirect, Response}, 6}; 7use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 8use serde::{Deserialize, Serialize}; 9use tranquil_db_traits::{SsoAction, SsoProviderType}; 10use tranquil_types::RequestId; 11 12use tranquil_pds::api::error::ApiError; 13use tranquil_pds::auth::extractor::extract_auth_token_from_header; 14use tranquil_pds::auth::{generate_app_password, validate_bearer_token_cached}; 15use tranquil_pds::rate_limit::{ 16 AccountCreationLimit, RateLimited, SsoCallbackLimit, SsoInitiateLimit, SsoUnlinkLimit, 17 check_user_rate_limit_with_message, 18}; 19use tranquil_pds::sso::SsoConfig; 20use tranquil_pds::state::AppState; 21 22fn generate_nonce() -> String { 23 use rand::RngCore; 24 let mut bytes = [0u8; 16]; 25 rand::thread_rng().fill_bytes(&mut bytes); 26 URL_SAFE_NO_PAD.encode(bytes) 27} 28 29#[derive(Debug, Serialize)] 30pub struct SsoProviderInfo { 31 pub provider: SsoProviderType, 32 pub name: String, 33 pub icon: String, 34} 35 36#[derive(Debug, Serialize)] 37pub struct SsoProvidersResponse { 38 pub providers: Vec<SsoProviderInfo>, 39} 40 41pub async fn get_sso_providers(State(state): State<AppState>) -> Json<SsoProvidersResponse> { 42 let providers = state 43 .sso_manager 44 .enabled_providers() 45 .iter() 46 .map(|(t, name, icon)| SsoProviderInfo { 47 provider: *t, 48 name: name.to_string(), 49 icon: icon.to_string(), 50 }) 51 .collect(); 52 53 Json(SsoProvidersResponse { providers }) 54} 55 56#[derive(Debug, Deserialize)] 57pub struct SsoInitiateRequest { 58 pub provider: SsoProviderType, 59 pub request_uri: Option<String>, 60 pub action: Option<SsoAction>, 61} 62 63#[derive(Debug, Serialize)] 64pub struct SsoInitiateResponse { 65 pub redirect_url: String, 66} 67 68pub async fn sso_initiate( 69 State(state): State<AppState>, 70 _rate_limit: RateLimited<SsoInitiateLimit>, 71 headers: HeaderMap, 72 Json(input): Json<SsoInitiateRequest>, 73) -> Result<Json<SsoInitiateResponse>, ApiError> { 74 if let Some(ref uri) = input.request_uri 75 && uri.len() > 500 76 { 77 return Err(ApiError::InvalidRequest("Request URI too long".into())); 78 } 79 80 let provider_type = input.provider; 81 82 let provider = state 83 .sso_manager 84 .get_provider(provider_type) 85 .ok_or(ApiError::SsoProviderNotEnabled)?; 86 87 let action = input.action.unwrap_or(SsoAction::Login); 88 89 let is_standalone = action == SsoAction::Register && input.request_uri.is_none(); 90 let request_uri = input 91 .request_uri 92 .clone() 93 .unwrap_or_else(|| "standalone".to_string()); 94 95 let auth_did = match action { 96 SsoAction::Link => { 97 let auth_header = headers 98 .get(axum::http::header::AUTHORIZATION) 99 .and_then(|v| v.to_str().ok()); 100 let extracted = 101 extract_auth_token_from_header(auth_header).ok_or(ApiError::SsoNotAuthenticated)?; 102 let auth_user = validate_bearer_token_cached( 103 state.user_repo.as_ref(), 104 state.cache.as_ref(), 105 &extracted.token, 106 ) 107 .await 108 .map_err(|_| ApiError::SsoNotAuthenticated)?; 109 Some(auth_user.did) 110 } 111 SsoAction::Register if is_standalone => None, 112 _ => { 113 let request_id = RequestId::new(request_uri.clone()); 114 let _request_data = state 115 .oauth_repo 116 .get_authorization_request(&request_id) 117 .await? 118 .ok_or(ApiError::InvalidRequest( 119 "Authorization request not found or expired".into(), 120 ))?; 121 None 122 } 123 }; 124 125 let sso_state = tranquil_pds::util::generate_random_token(); 126 let nonce = generate_nonce(); 127 let redirect_uri = SsoConfig::get_redirect_uri(); 128 129 let auth_result = provider 130 .build_auth_url(&sso_state, redirect_uri, Some(&nonce)) 131 .await 132 .map_err(|e| { 133 tracing::error!("Failed to build auth URL: {:?}", e); 134 ApiError::InternalError(Some("Failed to build authorization URL".into())) 135 })?; 136 137 state 138 .sso_repo 139 .create_sso_auth_state( 140 &sso_state, 141 &request_uri, 142 provider_type, 143 action, 144 Some(&nonce), 145 auth_result.code_verifier.as_deref(), 146 auth_did.as_ref(), 147 ) 148 .await?; 149 150 tracing::debug!( 151 provider = %provider_type.as_str(), 152 action = %action, 153 "SSO flow initiated" 154 ); 155 156 Ok(Json(SsoInitiateResponse { 157 redirect_url: auth_result.url, 158 })) 159} 160 161#[derive(Debug, Deserialize)] 162pub struct SsoCallbackQuery { 163 pub code: Option<String>, 164 pub state: Option<String>, 165 pub error: Option<String>, 166 pub error_description: Option<String>, 167} 168 169#[derive(Debug, Deserialize)] 170pub struct SsoCallbackForm { 171 pub code: Option<String>, 172 pub state: Option<String>, 173 pub error: Option<String>, 174 pub error_description: Option<String>, 175 #[serde(default)] 176 pub user: Option<String>, 177} 178 179fn redirect_to_error(message: &str) -> Response { 180 let encoded = urlencoding::encode(message); 181 Redirect::to(&format!("/app/oauth/error?error={}", encoded)).into_response() 182} 183 184fn redirect_to_login_with_error(request_uri: &str, message: &str) -> Response { 185 let uri_encoded = urlencoding::encode(request_uri); 186 let msg_encoded = urlencoding::encode(message); 187 Redirect::to(&format!( 188 "/app/oauth/login?request_uri={}&error={}", 189 uri_encoded, msg_encoded 190 )) 191 .into_response() 192} 193 194pub async fn sso_callback( 195 State(state): State<AppState>, 196 _rate_limit: RateLimited<SsoCallbackLimit>, 197 Query(query): Query<SsoCallbackQuery>, 198) -> Response { 199 sso_callback_internal(&state, query).await 200} 201 202async fn sso_callback_internal(state: &AppState, query: SsoCallbackQuery) -> Response { 203 tracing::debug!( 204 has_code = query.code.is_some(), 205 has_state = query.state.is_some(), 206 has_error = query.error.is_some(), 207 "SSO callback received" 208 ); 209 210 if let Some(ref error) = query.error { 211 tracing::warn!( 212 error = %error, 213 error_description = ?query.error_description, 214 "SSO provider returned error" 215 ); 216 if error.len() > 100 { 217 return redirect_to_error("Invalid error response"); 218 } 219 let desc = query 220 .error_description 221 .as_ref() 222 .map(|d| if d.len() > 500 { "Error" } else { d.as_str() }) 223 .unwrap_or_default(); 224 return redirect_to_error(&format!("{}: {}", error, desc)); 225 } 226 227 let (code, sso_state) = match (&query.code, &query.state) { 228 (Some(c), Some(s)) if c.len() <= 2000 && s.len() <= 100 => (c.clone(), s.clone()), 229 (Some(_), Some(_)) => return redirect_to_error("Invalid callback parameters"), 230 _ => return redirect_to_error("Missing code or state parameter"), 231 }; 232 233 let auth_state = match state.sso_repo.consume_sso_auth_state(&sso_state).await { 234 Ok(Some(s)) => s, 235 Ok(None) => return redirect_to_error("SSO session expired or invalid"), 236 Err(e) => { 237 tracing::error!("SSO state lookup failed: {:?}", e); 238 return redirect_to_error("Database error"); 239 } 240 }; 241 242 tracing::debug!( 243 provider = %auth_state.provider.as_str(), 244 action = %auth_state.action, 245 request_uri = %auth_state.request_uri, 246 "SSO auth state retrieved" 247 ); 248 249 let is_standalone = auth_state.request_uri == "standalone"; 250 251 let provider = match state.sso_manager.get_provider(auth_state.provider) { 252 Some(p) => p, 253 None => return redirect_to_error("Provider no longer available"), 254 }; 255 256 let redirect_uri = SsoConfig::get_redirect_uri(); 257 258 let token_resp = match provider 259 .exchange_code(&code, redirect_uri, auth_state.code_verifier.as_deref()) 260 .await 261 { 262 Ok(t) => t, 263 Err(e) => { 264 tracing::error!("SSO token exchange failed: {:?}", e); 265 if is_standalone { 266 return redirect_to_error( 267 "Failed to exchange authorization code. Please try again.", 268 ); 269 } 270 return redirect_to_login_with_error( 271 &auth_state.request_uri, 272 "Failed to exchange authorization code", 273 ); 274 } 275 }; 276 277 let user_info = match provider 278 .get_user_info( 279 &token_resp.access_token, 280 token_resp.id_token.as_deref(), 281 auth_state.nonce.as_deref(), 282 ) 283 .await 284 { 285 Ok(u) => u, 286 Err(e) => { 287 tracing::error!("SSO user info fetch failed: {:?}", e); 288 if is_standalone { 289 return redirect_to_error( 290 "Failed to get user information from provider. Please try again.", 291 ); 292 } 293 return redirect_to_login_with_error( 294 &auth_state.request_uri, 295 "Failed to get user information from provider", 296 ); 297 } 298 }; 299 300 match auth_state.action { 301 SsoAction::Login => { 302 handle_sso_login( 303 state, 304 &auth_state.request_uri, 305 auth_state.provider, 306 &user_info, 307 ) 308 .await 309 } 310 SsoAction::Link => { 311 let did = match auth_state.did { 312 Some(d) => d, 313 None => return redirect_to_error("Not authenticated"), 314 }; 315 handle_sso_link(state, did, auth_state.provider, &user_info).await 316 } 317 SsoAction::Register => { 318 handle_sso_register( 319 state, 320 &auth_state.request_uri, 321 auth_state.provider, 322 &user_info, 323 ) 324 .await 325 } 326 } 327} 328 329pub async fn sso_callback_post( 330 State(state): State<AppState>, 331 _rate_limit: RateLimited<SsoCallbackLimit>, 332 Form(form): Form<SsoCallbackForm>, 333) -> Response { 334 tracing::debug!( 335 has_code = form.code.is_some(), 336 has_state = form.state.is_some(), 337 has_error = form.error.is_some(), 338 has_user = form.user.is_some(), 339 "SSO callback (POST/form_post) received" 340 ); 341 342 let query = SsoCallbackQuery { 343 code: form.code, 344 state: form.state, 345 error: form.error, 346 error_description: form.error_description, 347 }; 348 349 sso_callback_internal(&state, query).await 350} 351 352fn generate_registration_token() -> String { 353 use rand::RngCore; 354 let mut bytes = [0u8; 32]; 355 rand::thread_rng().fill_bytes(&mut bytes); 356 URL_SAFE_NO_PAD.encode(bytes) 357} 358 359async fn handle_sso_login( 360 state: &AppState, 361 request_uri: &str, 362 provider: SsoProviderType, 363 user_info: &tranquil_pds::sso::providers::SsoUserInfo, 364) -> Response { 365 let identity = match state 366 .sso_repo 367 .get_external_identity_by_provider(provider, &user_info.provider_user_id) 368 .await 369 { 370 Ok(Some(id)) => id, 371 Ok(None) => { 372 let token = generate_registration_token(); 373 if let Err(e) = state 374 .sso_repo 375 .create_pending_registration( 376 &token, 377 request_uri, 378 provider, 379 &user_info.provider_user_id, 380 user_info.username.as_deref(), 381 user_info.email.as_deref(), 382 user_info.email_verified.unwrap_or(false), 383 ) 384 .await 385 { 386 tracing::error!("Failed to create pending registration: {:?}", e); 387 return redirect_to_error("Database error"); 388 } 389 return Redirect::to(&format!( 390 "/app/oauth/sso-register?token={}", 391 urlencoding::encode(&token), 392 )) 393 .into_response(); 394 } 395 Err(e) => { 396 tracing::error!("SSO identity lookup failed: {:?}", e); 397 return redirect_to_error("Database error"); 398 } 399 }; 400 401 let is_verified = match state.user_repo.get_session_info_by_did(&identity.did).await { 402 Ok(Some(info)) => info.channel_verification.has_any_verified(), 403 Ok(None) => { 404 tracing::error!("User not found for SSO login: {}", identity.did); 405 return redirect_to_error("Account not found"); 406 } 407 Err(e) => { 408 tracing::error!("Database error checking verification status: {:?}", e); 409 return redirect_to_error("Database error"); 410 } 411 }; 412 413 if !is_verified { 414 tracing::warn!( 415 did = %identity.did, 416 provider = %provider.as_str(), 417 "SSO login attempt for unverified account" 418 ); 419 return redirect_to_login_with_error( 420 request_uri, 421 "Please verify your account before logging in", 422 ); 423 } 424 425 if let Err(e) = state 426 .sso_repo 427 .update_external_identity_login( 428 identity.id, 429 user_info.username.as_deref(), 430 user_info.email.as_deref(), 431 ) 432 .await 433 { 434 tracing::warn!("Failed to update external identity last login: {:?}", e); 435 } 436 437 let request_id = RequestId::new(request_uri.to_string()); 438 if let Err(e) = state 439 .oauth_repo 440 .set_authorization_did(&request_id, &identity.did, None) 441 .await 442 { 443 tracing::error!("Failed to set authorization DID: {:?}", e); 444 return redirect_to_error("Failed to authenticate"); 445 } 446 447 tracing::info!( 448 did = %identity.did, 449 provider = %provider.as_str(), 450 provider_user_id = %user_info.provider_user_id, 451 "SSO login successful" 452 ); 453 454 let has_totp = matches!( 455 state.user_repo.get_totp_record_state(&identity.did).await, 456 Ok(Some(tranquil_db_traits::TotpRecordState::Verified(_))) 457 ); 458 459 if has_totp { 460 return Redirect::to(&format!( 461 "/app/oauth/totp?request_uri={}", 462 urlencoding::encode(request_uri) 463 )) 464 .into_response(); 465 } 466 467 Redirect::to(&format!( 468 "/app/oauth/consent?request_uri={}", 469 urlencoding::encode(request_uri) 470 )) 471 .into_response() 472} 473 474async fn handle_sso_link( 475 state: &AppState, 476 did: tranquil_types::Did, 477 provider: SsoProviderType, 478 user_info: &tranquil_pds::sso::providers::SsoUserInfo, 479) -> Response { 480 let existing = state 481 .sso_repo 482 .get_external_identity_by_provider(provider, &user_info.provider_user_id) 483 .await; 484 485 match existing { 486 Ok(Some(existing_id)) => { 487 if existing_id.did != did { 488 tracing::warn!( 489 provider = %provider.as_str(), 490 provider_user_id = %user_info.provider_user_id, 491 existing_did = %existing_id.did, 492 requested_did = %did, 493 "SSO account already linked to different user" 494 ); 495 return Redirect::to(&format!( 496 "/app/security?error={}", 497 urlencoding::encode("This SSO account is already linked to a different user") 498 )) 499 .into_response(); 500 } 501 tracing::info!( 502 did = %did, 503 provider = %provider.as_str(), 504 "SSO account already linked to this user" 505 ); 506 return Redirect::to("/app/security?sso_linked=true").into_response(); 507 } 508 Ok(None) => {} 509 Err(e) => { 510 tracing::error!("Failed to check existing identity: {:?}", e); 511 return Redirect::to(&format!( 512 "/app/security?error={}", 513 urlencoding::encode("Database error") 514 )) 515 .into_response(); 516 } 517 } 518 519 if let Err(e) = state 520 .sso_repo 521 .create_external_identity( 522 &did, 523 provider, 524 &user_info.provider_user_id, 525 user_info.username.as_deref(), 526 user_info.email.as_deref(), 527 ) 528 .await 529 { 530 tracing::error!("Failed to create external identity: {:?}", e); 531 return Redirect::to(&format!( 532 "/app/security?error={}", 533 urlencoding::encode("Failed to link account") 534 )) 535 .into_response(); 536 } 537 538 tracing::info!( 539 did = %did, 540 provider = %provider.as_str(), 541 provider_user_id = %user_info.provider_user_id, 542 "Successfully linked SSO account" 543 ); 544 Redirect::to("/app/security?sso_linked=true").into_response() 545} 546 547async fn handle_sso_register( 548 state: &AppState, 549 request_uri: &str, 550 provider: SsoProviderType, 551 user_info: &tranquil_pds::sso::providers::SsoUserInfo, 552) -> Response { 553 match state 554 .sso_repo 555 .get_external_identity_by_provider(provider, &user_info.provider_user_id) 556 .await 557 { 558 Ok(Some(_)) => { 559 return redirect_to_error( 560 "This account is already linked to an existing user. Please sign in instead.", 561 ); 562 } 563 Ok(None) => {} 564 Err(e) => { 565 tracing::error!("SSO identity lookup failed: {:?}", e); 566 return redirect_to_error("Database error"); 567 } 568 } 569 570 let token = generate_registration_token(); 571 if let Err(e) = state 572 .sso_repo 573 .create_pending_registration( 574 &token, 575 request_uri, 576 provider, 577 &user_info.provider_user_id, 578 user_info.username.as_deref(), 579 user_info.email.as_deref(), 580 user_info.email_verified.unwrap_or(false), 581 ) 582 .await 583 { 584 tracing::error!("Failed to create pending registration: {:?}", e); 585 return redirect_to_error("Database error"); 586 } 587 Redirect::to(&format!( 588 "/app/oauth/sso-register?token={}", 589 urlencoding::encode(&token), 590 )) 591 .into_response() 592} 593 594#[derive(Debug, Serialize)] 595pub struct LinkedAccountInfo { 596 pub id: String, 597 pub provider: SsoProviderType, 598 pub provider_name: String, 599 pub provider_username: Option<String>, 600 pub provider_email: Option<String>, 601 pub created_at: String, 602 pub last_login_at: Option<String>, 603} 604 605#[derive(Debug, Serialize)] 606pub struct LinkedAccountsResponse { 607 pub accounts: Vec<LinkedAccountInfo>, 608} 609 610pub async fn get_linked_accounts( 611 State(state): State<AppState>, 612 auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>, 613) -> Result<Json<LinkedAccountsResponse>, ApiError> { 614 let identities = state 615 .sso_repo 616 .get_external_identities_by_did(&auth.did) 617 .await?; 618 619 let accounts = identities 620 .into_iter() 621 .map(|id| LinkedAccountInfo { 622 id: id.id.to_string(), 623 provider: id.provider, 624 provider_name: id.provider.display_name().to_string(), 625 provider_username: id.provider_username.map(|u| u.into_inner()), 626 provider_email: id.provider_email.map(|e| e.into_inner()), 627 created_at: id.created_at.to_rfc3339(), 628 last_login_at: id.last_login_at.map(|t| t.to_rfc3339()), 629 }) 630 .collect(); 631 632 Ok(Json(LinkedAccountsResponse { accounts })) 633} 634 635#[derive(Debug, Deserialize)] 636pub struct UnlinkAccountRequest { 637 pub id: String, 638} 639 640#[derive(Debug, Serialize)] 641pub struct UnlinkAccountResponse { 642 pub success: bool, 643} 644 645pub async fn unlink_account( 646 State(state): State<AppState>, 647 auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>, 648 Json(input): Json<UnlinkAccountRequest>, 649) -> Result<Json<UnlinkAccountResponse>, ApiError> { 650 let _rate_limit = check_user_rate_limit_with_message::<SsoUnlinkLimit>( 651 &state, 652 auth.did.as_str(), 653 "Too many unlink attempts. Please try again later.", 654 ) 655 .await?; 656 657 let id = uuid::Uuid::parse_str(&input.id).map_err(|_| ApiError::InvalidId)?; 658 659 let has_password = state 660 .user_repo 661 .has_password_by_did(&auth.did) 662 .await? 663 .unwrap_or(false); 664 665 let passkeys = state.user_repo.get_passkeys_for_user(&auth.did).await?; 666 let has_passkeys = !passkeys.is_empty(); 667 668 if !has_password && !has_passkeys { 669 let identities = state 670 .sso_repo 671 .get_external_identities_by_did(&auth.did) 672 .await?; 673 674 if identities.len() <= 1 { 675 return Err(ApiError::InvalidRequest( 676 "Cannot unlink your only login method. Add a password or passkey first." 677 .to_string(), 678 )); 679 } 680 } 681 682 let deleted = state 683 .sso_repo 684 .delete_external_identity(id, &auth.did) 685 .await?; 686 687 if !deleted { 688 return Err(ApiError::SsoLinkNotFound); 689 } 690 691 tracing::info!(did = %auth.did, identity_id = %id, "SSO account unlinked"); 692 693 Ok(Json(UnlinkAccountResponse { success: true })) 694} 695 696#[derive(Debug, Deserialize)] 697pub struct PendingRegistrationQuery { 698 pub token: String, 699} 700 701#[derive(Debug, Serialize)] 702pub struct PendingRegistrationResponse { 703 pub request_uri: String, 704 pub provider: SsoProviderType, 705 pub provider_user_id: String, 706 pub provider_username: Option<String>, 707 pub provider_email: Option<String>, 708 pub provider_email_verified: bool, 709} 710 711pub async fn get_pending_registration( 712 State(state): State<AppState>, 713 _rate_limit: RateLimited<SsoCallbackLimit>, 714 Query(query): Query<PendingRegistrationQuery>, 715) -> Result<Json<PendingRegistrationResponse>, ApiError> { 716 if query.token.len() > 100 { 717 return Err(ApiError::InvalidRequest("Invalid token".into())); 718 } 719 720 let pending = state 721 .sso_repo 722 .get_pending_registration(&query.token) 723 .await? 724 .ok_or(ApiError::SsoSessionExpired)?; 725 726 Ok(Json(PendingRegistrationResponse { 727 request_uri: pending.request_uri, 728 provider: pending.provider, 729 provider_user_id: pending.provider_user_id.into_inner(), 730 provider_username: pending.provider_username.map(|u| u.into_inner()), 731 provider_email: pending.provider_email.map(|e| e.into_inner()), 732 provider_email_verified: pending.provider_email_verified, 733 })) 734} 735 736#[derive(Debug, Deserialize)] 737pub struct CheckHandleQuery { 738 pub handle: String, 739 pub domain: Option<String>, 740} 741 742#[derive(Debug, Serialize)] 743pub struct CheckHandleResponse { 744 pub available: bool, 745 pub reason: Option<String>, 746} 747 748pub async fn check_handle_available( 749 State(state): State<AppState>, 750 Query(query): Query<CheckHandleQuery>, 751) -> Result<Json<CheckHandleResponse>, ApiError> { 752 if query.handle.len() > 100 { 753 return Ok(Json(CheckHandleResponse { 754 available: false, 755 reason: Some("Handle too long".into()), 756 })); 757 } 758 759 let validated = match tranquil_pds::api::validation::validate_short_handle(&query.handle) { 760 Ok(h) => h, 761 Err(e) => { 762 return Ok(Json(CheckHandleResponse { 763 available: false, 764 reason: Some(e.to_string()), 765 })); 766 } 767 }; 768 769 let available_domains = tranquil_config::get().server.available_user_domain_list(); 770 if let Some(ref d) = query.domain 771 && !available_domains.iter().any(|ad| ad == d) 772 { 773 return Err(ApiError::InvalidRequest("Unknown user domain".into())); 774 } 775 let domain = query.domain.as_deref().unwrap_or(&available_domains[0]); 776 let full_handle = format!("{}.{}", validated, domain); 777 let handle_typed: tranquil_pds::types::Handle = match full_handle.parse() { 778 Ok(h) => h, 779 Err(_) => return Err(ApiError::InvalidHandle(None)), 780 }; 781 782 let db_available = state 783 .user_repo 784 .check_handle_available_for_new_account(&handle_typed) 785 .await 786 .unwrap_or(false); 787 788 if !db_available { 789 return Ok(Json(CheckHandleResponse { 790 available: false, 791 reason: Some("Handle is already taken".into()), 792 })); 793 } 794 795 Ok(Json(CheckHandleResponse { 796 available: true, 797 reason: None, 798 })) 799} 800 801#[derive(Debug, Deserialize)] 802pub struct CompleteRegistrationInput { 803 pub token: String, 804 pub handle: String, 805 pub email: Option<String>, 806 pub invite_code: Option<String>, 807 pub verification_channel: Option<tranquil_db_traits::CommsChannel>, 808 pub discord_username: Option<String>, 809 pub telegram_username: Option<String>, 810 pub signal_username: Option<String>, 811 pub did_type: Option<String>, 812 pub did: Option<String>, 813} 814 815#[derive(Debug, Serialize)] 816#[serde(rename_all = "camelCase")] 817pub struct CompleteRegistrationResponse { 818 pub did: String, 819 pub handle: String, 820 pub redirect_url: String, 821 #[serde(skip_serializing_if = "Option::is_none")] 822 pub access_jwt: Option<String>, 823 #[serde(skip_serializing_if = "Option::is_none")] 824 pub refresh_jwt: Option<String>, 825 #[serde(skip_serializing_if = "Option::is_none")] 826 pub app_password: Option<String>, 827 #[serde(skip_serializing_if = "Option::is_none")] 828 pub app_password_name: Option<String>, 829} 830 831pub async fn complete_registration( 832 State(state): State<AppState>, 833 rate_limit: RateLimited<AccountCreationLimit>, 834 Json(input): Json<CompleteRegistrationInput>, 835) -> Result<Json<CompleteRegistrationResponse>, ApiError> { 836 let client_ip = rate_limit.client_ip(); 837 use jacquard_common::types::{integer::LimitedU32, string::Tid}; 838 use jacquard_repo::{mst::Mst, storage::BlockStore}; 839 use k256::ecdsa::SigningKey; 840 use rand::rngs::OsRng; 841 use serde_json::json; 842 use std::sync::Arc; 843 844 if input.token.len() > 100 { 845 return Err(ApiError::InvalidRequest("Invalid token".into())); 846 } 847 848 if input.handle.len() > 100 { 849 return Err(ApiError::InvalidHandle(None)); 850 } 851 852 let pending_preview = state 853 .sso_repo 854 .get_pending_registration(&input.token) 855 .await? 856 .ok_or(ApiError::SsoSessionExpired)?; 857 858 let cfg = tranquil_config::get(); 859 let hostname = &cfg.server.hostname; 860 let available_domains = cfg.server.available_user_domain_list(); 861 862 let matched_domain = available_domains 863 .iter() 864 .filter(|d| input.handle.ends_with(&format!(".{}", d))) 865 .max_by_key(|d| d.len()); 866 867 let handle = if !input.handle.contains('.') || matched_domain.is_some() { 868 let handle_to_validate = match matched_domain { 869 Some(domain) => input 870 .handle 871 .strip_suffix(&format!(".{}", domain)) 872 .unwrap_or(&input.handle), 873 None => &input.handle, 874 }; 875 match tranquil_pds::api::validation::validate_short_handle(handle_to_validate) { 876 Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])), 877 Err(_) => return Err(ApiError::InvalidHandle(None)), 878 } 879 } else { 880 match tranquil_pds::api::validation::validate_full_domain_handle(&input.handle) { 881 Ok(h) => h, 882 Err(_) => return Err(ApiError::InvalidHandle(None)), 883 } 884 }; 885 886 let verification_channel = input 887 .verification_channel 888 .unwrap_or(tranquil_db_traits::CommsChannel::Email); 889 let verification_recipient = match verification_channel { 890 tranquil_db_traits::CommsChannel::Email => { 891 let email = input 892 .email 893 .clone() 894 .or_else(|| { 895 pending_preview 896 .provider_email 897 .clone() 898 .map(|e| e.into_inner()) 899 }) 900 .map(|e| e.trim().to_string()) 901 .filter(|e| !e.is_empty()); 902 match email { 903 Some(e) if !e.is_empty() => e, 904 _ => return Err(ApiError::MissingEmail), 905 } 906 } 907 tranquil_db_traits::CommsChannel::Discord => match &input.discord_username { 908 Some(username) if !username.trim().is_empty() => { 909 let clean = username.trim().to_lowercase(); 910 if !tranquil_pds::api::validation::is_valid_discord_username(&clean) { 911 return Err(ApiError::InvalidRequest( 912 "Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(), 913 )); 914 } 915 clean 916 } 917 _ => return Err(ApiError::MissingDiscordId), 918 }, 919 tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username { 920 Some(username) if !username.trim().is_empty() => { 921 let clean = username.trim().trim_start_matches('@'); 922 if !tranquil_pds::api::validation::is_valid_telegram_username(clean) { 923 return Err(ApiError::InvalidRequest( 924 "Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(), 925 )); 926 } 927 clean.to_string() 928 } 929 _ => return Err(ApiError::MissingTelegramUsername), 930 }, 931 tranquil_db_traits::CommsChannel::Signal => match &input.signal_username { 932 Some(username) if !username.trim().is_empty() => { 933 username.trim().trim_start_matches('@').to_lowercase() 934 } 935 _ => return Err(ApiError::MissingSignalNumber), 936 }, 937 }; 938 939 let email = input 940 .email 941 .clone() 942 .or_else(|| { 943 pending_preview 944 .provider_email 945 .clone() 946 .map(|e| e.into_inner()) 947 }) 948 .map(|e| e.trim().to_string()) 949 .filter(|e| !e.is_empty()); 950 951 let email = match &email { 952 Some(e) => { 953 if e.len() > 254 { 954 return Err(ApiError::InvalidEmail); 955 } 956 if !tranquil_pds::api::validation::is_valid_email(e) { 957 return Err(ApiError::InvalidEmail); 958 } 959 Some(e.clone()) 960 } 961 None => None, 962 }; 963 964 let _validated_invite_code = if let Some(ref code) = input.invite_code { 965 match state.infra_repo.validate_invite_code(code).await { 966 Ok(validated) => Some(validated), 967 Err(_) => return Err(ApiError::InvalidInviteCode), 968 } 969 } else { 970 let invite_required = tranquil_config::get().server.invite_code_required; 971 if invite_required { 972 return Err(ApiError::InviteCodeRequired); 973 } 974 None 975 }; 976 977 let handle_typed: tranquil_pds::types::Handle = 978 handle.parse().map_err(|_| ApiError::InvalidHandle(None))?; 979 let reserved = state 980 .user_repo 981 .reserve_handle(&handle_typed, client_ip) 982 .await 983 .unwrap_or(false); 984 985 if !reserved { 986 return Err(ApiError::HandleNotAvailable(None)); 987 } 988 989 let secret_key = k256::SecretKey::random(&mut OsRng); 990 let secret_key_bytes = secret_key.to_bytes().to_vec(); 991 let signing_key = match SigningKey::from_slice(&secret_key_bytes) { 992 Ok(k) => k, 993 Err(e) => { 994 tracing::error!("Error creating signing key: {:?}", e); 995 return Err(ApiError::InternalError(None)); 996 } 997 }; 998 999 let pds_endpoint = format!("https://{}", hostname); 1000 let did_type = input.did_type.as_deref().unwrap_or("plc"); 1001 1002 let did = match did_type { 1003 "web" => { 1004 if !tranquil_pds::util::is_self_hosted_did_web_enabled() { 1005 return Err(ApiError::SelfHostedDidWebDisabled); 1006 } 1007 let encoded_handle = handle.replace(':', "%3A"); 1008 let self_hosted_did = format!("did:web:{}", encoded_handle); 1009 tracing::info!(did = %self_hosted_did, "Creating self-hosted did:web SSO account"); 1010 self_hosted_did 1011 } 1012 "web-external" => { 1013 let d = match &input.did { 1014 Some(d) if !d.trim().is_empty() => d.trim(), 1015 _ => { 1016 return Err(ApiError::InvalidRequest( 1017 "External did:web requires the 'did' field to be provided".into(), 1018 )); 1019 } 1020 }; 1021 if !d.starts_with("did:web:") { 1022 return Err(ApiError::InvalidDid( 1023 "External DID must be a did:web".into(), 1024 )); 1025 } 1026 tracing::info!(did = %d, "Creating external did:web SSO account"); 1027 d.to_string() 1028 } 1029 _ => { 1030 let rotation_key = tranquil_config::get() 1031 .secrets 1032 .plc_rotation_key 1033 .clone() 1034 .unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(&signing_key)); 1035 1036 let genesis_result = match tranquil_pds::plc::create_genesis_operation( 1037 &signing_key, 1038 &rotation_key, 1039 &handle, 1040 &pds_endpoint, 1041 ) { 1042 Ok(r) => r, 1043 Err(e) => { 1044 tracing::error!("Error creating PLC genesis operation: {:?}", e); 1045 return Err(ApiError::InternalError(Some( 1046 "Failed to create PLC operation".into(), 1047 ))); 1048 } 1049 }; 1050 1051 let plc_client = 1052 tranquil_pds::plc::PlcClient::with_cache(None, Some(state.cache.clone())); 1053 if let Err(e) = plc_client 1054 .send_operation(&genesis_result.did, &genesis_result.signed_operation) 1055 .await 1056 { 1057 tracing::error!("Failed to submit PLC genesis operation: {:?}", e); 1058 return Err(ApiError::UpstreamErrorMsg(format!( 1059 "Failed to register DID with PLC directory: {}", 1060 e 1061 ))); 1062 } 1063 genesis_result.did 1064 } 1065 }; 1066 tracing::info!(did = %did, handle = %handle, provider = %pending_preview.provider.as_str(), "Created DID for SSO account"); 1067 1068 let encrypted_key_bytes = match tranquil_pds::config::encrypt_key(&secret_key_bytes) { 1069 Ok(bytes) => bytes, 1070 Err(e) => { 1071 tracing::error!("Error encrypting signing key: {:?}", e); 1072 return Err(ApiError::InternalError(None)); 1073 } 1074 }; 1075 1076 let mst = Mst::new(Arc::new(state.block_store.clone())); 1077 let mst_root = match mst.persist().await { 1078 Ok(c) => c, 1079 Err(e) => { 1080 tracing::error!("Error persisting MST: {:?}", e); 1081 return Err(ApiError::InternalError(None)); 1082 } 1083 }; 1084 1085 let rev = Tid::now(LimitedU32::MIN); 1086 let did_typed: tranquil_pds::types::Did = did 1087 .parse() 1088 .map_err(|_| ApiError::InternalError(Some("Invalid DID".into())))?; 1089 let (commit_bytes, _sig) = match tranquil_pds::repo_ops::create_signed_commit( 1090 &did_typed, 1091 mst_root, 1092 rev.as_ref(), 1093 None, 1094 &signing_key, 1095 ) { 1096 Ok(result) => result, 1097 Err(e) => { 1098 tracing::error!("Error creating genesis commit: {:?}", e); 1099 return Err(ApiError::InternalError(None)); 1100 } 1101 }; 1102 1103 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 1104 Ok(c) => c, 1105 Err(e) => { 1106 tracing::error!("Error saving genesis commit: {:?}", e); 1107 return Err(ApiError::InternalError(None)); 1108 } 1109 }; 1110 1111 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; 1112 1113 let birthdate_pref = if tranquil_config::get().server.age_assurance_override { 1114 Some(json!({ 1115 "$type": "app.bsky.actor.defs#personalDetailsPref", 1116 "birthDate": "1998-05-06T00:00:00.000Z" 1117 })) 1118 } else { 1119 None 1120 }; 1121 1122 let create_input = tranquil_db_traits::CreateSsoAccountInput { 1123 handle: handle_typed.clone(), 1124 email: email.clone(), 1125 did: did_typed.clone(), 1126 preferred_comms_channel: verification_channel, 1127 discord_username: input 1128 .discord_username 1129 .clone() 1130 .map(|s| s.trim().to_lowercase()) 1131 .filter(|s| !s.is_empty()), 1132 telegram_username: input 1133 .telegram_username 1134 .clone() 1135 .map(|s| s.trim().trim_start_matches('@').to_string()) 1136 .filter(|s| !s.is_empty()), 1137 signal_username: input 1138 .signal_username 1139 .clone() 1140 .map(|s| s.trim().trim_start_matches('@').to_lowercase()) 1141 .filter(|s| !s.is_empty()), 1142 encrypted_key_bytes: encrypted_key_bytes.clone(), 1143 encryption_version: tranquil_pds::config::ENCRYPTION_VERSION, 1144 commit_cid: commit_cid.to_string(), 1145 repo_rev: rev.as_ref().to_string(), 1146 genesis_block_cids, 1147 invite_code: input.invite_code.clone(), 1148 birthdate_pref, 1149 sso_provider: pending_preview.provider, 1150 sso_provider_user_id: pending_preview.provider_user_id.clone().into_inner(), 1151 sso_provider_username: pending_preview 1152 .provider_username 1153 .clone() 1154 .map(|u| u.into_inner()), 1155 sso_provider_email: pending_preview 1156 .provider_email 1157 .clone() 1158 .map(|e| e.into_inner()), 1159 sso_provider_email_verified: pending_preview.provider_email_verified, 1160 pending_registration_token: input.token.clone(), 1161 }; 1162 1163 let create_result = match state.user_repo.create_sso_account(&create_input).await { 1164 Ok(r) => r, 1165 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { 1166 return Err(ApiError::HandleNotAvailable(None)); 1167 } 1168 Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { 1169 return Err(ApiError::EmailTaken); 1170 } 1171 Err(tranquil_db_traits::CreateAccountError::InvalidToken) => { 1172 return Err(ApiError::SsoSessionExpired); 1173 } 1174 Err(e) => { 1175 tracing::error!("Error creating SSO account: {:?}", e); 1176 return Err(ApiError::InternalError(None)); 1177 } 1178 }; 1179 1180 let _ = state 1181 .user_repo 1182 .release_handle_reservation(&handle_typed) 1183 .await; 1184 1185 if let Err(e) = 1186 tranquil_pds::repo_ops::sequence_identity_event(&state, &did_typed, Some(&handle_typed)) 1187 .await 1188 { 1189 tracing::warn!("Failed to sequence identity event for {}: {}", did, e); 1190 } 1191 if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( 1192 &state, 1193 &did_typed, 1194 tranquil_db_traits::AccountStatus::Active, 1195 ) 1196 .await 1197 { 1198 tracing::warn!("Failed to sequence account event for {}: {}", did, e); 1199 } 1200 1201 let profile_record = json!({ 1202 "$type": "app.bsky.actor.profile", 1203 "displayName": handle_typed.as_str() 1204 }); 1205 if let Err(e) = tranquil_pds::repo_ops::create_record_internal( 1206 &state, 1207 &did_typed, 1208 &tranquil_pds::types::PROFILE_COLLECTION, 1209 &tranquil_pds::types::PROFILE_RKEY, 1210 &profile_record, 1211 ) 1212 .await 1213 { 1214 tracing::warn!("Failed to create default profile for {}: {}", did, e); 1215 } 1216 1217 let app_password = generate_app_password(); 1218 let app_password_name = "bsky.app".to_string(); 1219 let app_password_hash = match bcrypt::hash(&app_password, bcrypt::DEFAULT_COST) { 1220 Ok(h) => h, 1221 Err(e) => { 1222 tracing::error!("Failed to hash app password: {:?}", e); 1223 return Err(ApiError::InternalError(None)); 1224 } 1225 }; 1226 1227 let app_password_data = tranquil_db_traits::AppPasswordCreate { 1228 user_id: create_result.user_id, 1229 name: app_password_name.clone(), 1230 password_hash: app_password_hash, 1231 privilege: tranquil_db_traits::AppPasswordPrivilege::Standard, 1232 scopes: None, 1233 created_by_controller_did: None, 1234 }; 1235 if let Err(e) = state 1236 .session_repo 1237 .create_app_password(&app_password_data) 1238 .await 1239 { 1240 tracing::warn!("Failed to create initial app password: {:?}", e); 1241 } 1242 1243 let is_standalone = pending_preview.request_uri == "standalone"; 1244 1245 if !is_standalone { 1246 let request_id = RequestId::new(pending_preview.request_uri.clone()); 1247 if let Err(e) = state 1248 .oauth_repo 1249 .set_authorization_did(&request_id, &did_typed, None) 1250 .await 1251 { 1252 tracing::error!("Failed to set authorization DID: {:?}", e); 1253 return Err(ApiError::InternalError(None)); 1254 } 1255 } 1256 1257 tracing::info!( 1258 did = %did, 1259 handle = %handle, 1260 provider = %pending_preview.provider.as_str(), 1261 provider_user_id = %pending_preview.provider_user_id, 1262 standalone = %is_standalone, 1263 "SSO registration completed successfully" 1264 ); 1265 1266 let user_id = state 1267 .user_repo 1268 .get_id_by_did(&did_typed) 1269 .await 1270 .unwrap_or(None); 1271 1272 let channel_auto_verified = verification_channel == tranquil_db_traits::CommsChannel::Email 1273 && pending_preview.provider_email_verified 1274 && pending_preview.provider_email.as_ref().map(|e| e.as_str()) == email.as_deref(); 1275 1276 if channel_auto_verified { 1277 let _ = state 1278 .user_repo 1279 .set_channel_verified(&did_typed, tranquil_db_traits::CommsChannel::Email) 1280 .await; 1281 tracing::info!(did = %did, "Auto-verified email from SSO provider"); 1282 1283 if is_standalone { 1284 let key_bytes = match tranquil_pds::config::decrypt_key( 1285 &encrypted_key_bytes, 1286 Some(tranquil_pds::config::ENCRYPTION_VERSION), 1287 ) { 1288 Ok(k) => k, 1289 Err(e) => { 1290 tracing::error!("Failed to decrypt user key: {:?}", e); 1291 return Err(ApiError::InternalError(None)); 1292 } 1293 }; 1294 1295 let access_meta = 1296 match tranquil_pds::auth::create_access_token_with_metadata(&did, &key_bytes) { 1297 Ok(m) => m, 1298 Err(e) => { 1299 tracing::error!("Failed to create access token: {:?}", e); 1300 return Err(ApiError::InternalError(None)); 1301 } 1302 }; 1303 let refresh_meta = 1304 match tranquil_pds::auth::create_refresh_token_with_metadata(&did, &key_bytes) { 1305 Ok(m) => m, 1306 Err(e) => { 1307 tracing::error!("Failed to create refresh token: {:?}", e); 1308 return Err(ApiError::InternalError(None)); 1309 } 1310 }; 1311 1312 let session_data = tranquil_db_traits::SessionTokenCreate { 1313 did: did_typed.clone(), 1314 access_jti: access_meta.jti.clone(), 1315 refresh_jti: refresh_meta.jti.clone(), 1316 access_expires_at: access_meta.expires_at, 1317 refresh_expires_at: refresh_meta.expires_at, 1318 login_type: tranquil_db_traits::LoginType::Modern, 1319 mfa_verified: false, 1320 scope: Some("transition:generic".to_string()), 1321 controller_did: None, 1322 app_password_name: None, 1323 }; 1324 if let Err(e) = state.session_repo.create_session(&session_data).await { 1325 tracing::error!("Failed to insert session: {:?}", e); 1326 return Err(ApiError::InternalError(None)); 1327 } 1328 1329 let hostname = &tranquil_config::get().server.hostname; 1330 if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_welcome( 1331 state.user_repo.as_ref(), 1332 state.infra_repo.as_ref(), 1333 user_id.unwrap_or(uuid::Uuid::nil()), 1334 hostname, 1335 ) 1336 .await 1337 { 1338 tracing::warn!("Failed to enqueue welcome notification: {:?}", e); 1339 } 1340 1341 return Ok(Json(CompleteRegistrationResponse { 1342 did, 1343 handle, 1344 redirect_url: "/app/dashboard".to_string(), 1345 access_jwt: Some(access_meta.token), 1346 refresh_jwt: Some(refresh_meta.token), 1347 app_password: Some(app_password), 1348 app_password_name: Some(app_password_name), 1349 })); 1350 } 1351 1352 return Ok(Json(CompleteRegistrationResponse { 1353 did, 1354 handle, 1355 redirect_url: format!( 1356 "/app/oauth/consent?request_uri={}", 1357 urlencoding::encode(&pending_preview.request_uri) 1358 ), 1359 access_jwt: None, 1360 refresh_jwt: None, 1361 app_password: Some(app_password), 1362 app_password_name: Some(app_password_name), 1363 })); 1364 } 1365 1366 if let Some(uid) = user_id { 1367 let verification_token = tranquil_pds::auth::verification_token::generate_signup_token( 1368 &did_typed, 1369 verification_channel, 1370 &verification_recipient, 1371 ); 1372 let formatted_token = 1373 tranquil_pds::auth::verification_token::format_token_for_display(&verification_token); 1374 if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification( 1375 state.user_repo.as_ref(), 1376 state.infra_repo.as_ref(), 1377 uid, 1378 verification_channel, 1379 &verification_recipient, 1380 &formatted_token, 1381 hostname, 1382 ) 1383 .await 1384 { 1385 tracing::warn!("Failed to enqueue signup verification: {:?}", e); 1386 } 1387 } 1388 1389 let redirect_url = if is_standalone { 1390 format!("/app/verify?did={}", urlencoding::encode(&did)) 1391 } else { 1392 format!( 1393 "/app/verify?did={}&request_uri={}", 1394 urlencoding::encode(&did), 1395 urlencoding::encode(&pending_preview.request_uri) 1396 ) 1397 }; 1398 1399 Ok(Json(CompleteRegistrationResponse { 1400 did, 1401 handle, 1402 redirect_url, 1403 access_jwt: None, 1404 refresh_jwt: None, 1405 app_password: Some(app_password), 1406 app_password_name: Some(app_password_name), 1407 })) 1408}