Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

refactor(api): rework session login flow to use common credential verification #83

merged opened by oyster.cafe targeting main from refactor/api
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mhi3qdcw2422
+256 -289
Diff #0
+256 -289
crates/tranquil-api/src/server/session.rs
··· 10 10 use tracing::{error, info, warn}; 11 11 use tranquil_db_traits::{SessionId, TokenFamilyId}; 12 12 use tranquil_pds::api::error::{ApiError, DbResultExt}; 13 - use tranquil_pds::api::{EmptyResponse, SuccessResponse}; 13 + use tranquil_pds::api::{EmptyResponse, PreferredLocaleOutput, SuccessResponse}; 14 14 use tranquil_pds::auth::{ 15 15 Active, Auth, NormalizedLoginIdentifier, Permissive, require_legacy_session_mfa, 16 16 require_reauth_window, ··· 20 20 use tranquil_pds::types::{AccountState, Did, Handle, PlainPassword}; 21 21 use tranquil_types::TokenId; 22 22 23 - fn full_handle(stored_handle: &str, _pds_hostname: &str) -> String { 24 - stored_handle.to_string() 25 - } 26 - 27 23 #[derive(Deserialize)] 28 24 #[serde(rename_all = "camelCase")] 29 25 pub struct CreateSessionInput { ··· 59 55 State(state): State<AppState>, 60 56 rate_limit: RateLimited<LoginLimit>, 61 57 Json(input): Json<CreateSessionInput>, 62 - ) -> Response { 58 + ) -> Result<Response, ApiError> { 63 59 let client_ip = rate_limit.client_ip(); 64 60 info!( 65 61 "create_session called with identifier: {}", 66 62 input.identifier 67 63 ); 68 - let pds_host = &tranquil_config::get().server.hostname; 69 64 let hostname_for_handles = tranquil_config::get().server.hostname_without_port(); 70 65 let normalized_identifier = 71 66 NormalizedLoginIdentifier::normalize(&input.identifier, hostname_for_handles); ··· 85 80 "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK", 86 81 ); 87 82 warn!("User not found for login attempt"); 88 - return ApiError::AuthenticationFailed(Some("Invalid identifier or password".into())) 89 - .into_response(); 83 + return Err(ApiError::AuthenticationFailed(Some( 84 + "Invalid identifier or password".into(), 85 + ))); 90 86 } 91 87 Err(e) => { 92 88 error!("Database error fetching user: {:?}", e); 93 - return ApiError::InternalError(None).into_response(); 89 + return Err(ApiError::InternalError(None)); 94 90 } 95 91 }; 96 92 let key_bytes = match tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version) ··· 98 94 Ok(k) => k, 99 95 Err(e) => { 100 96 error!("Failed to decrypt user key: {:?}", e); 101 - return ApiError::InternalError(None).into_response(); 97 + return Err(ApiError::InternalError(None)); 102 98 } 103 99 }; 104 - let (password_valid, app_password_name, app_password_scopes, app_password_controller) = if row 105 - .password_hash 106 - .as_ref() 107 - .map(|h| verify(&input.password, h).unwrap_or(false)) 108 - .unwrap_or(false) 109 - { 110 - (true, None, None, None) 111 - } else { 112 - let app_passwords = state 113 - .session_repo 114 - .get_app_passwords_for_login(row.id) 115 - .await 116 - .unwrap_or_default(); 117 - let matched = app_passwords 118 - .iter() 119 - .find(|app| verify(&input.password, &app.password_hash).unwrap_or(false)); 120 - match matched { 121 - Some(app) => ( 122 - true, 123 - Some(app.name.clone()), 124 - app.scopes.clone(), 125 - app.created_by_controller_did.clone(), 126 - ), 127 - None => (false, None, None, None), 100 + let credential = crate::common::verify_credential( 101 + state.session_repo.as_ref(), 102 + row.id, 103 + &input.password, 104 + row.password_hash.as_deref(), 105 + ) 106 + .await; 107 + let (app_password_name, app_password_scopes, app_password_controller) = match credential { 108 + Some(crate::common::CredentialMatch::MainPassword) => (None, None, None), 109 + Some(crate::common::CredentialMatch::AppPassword { 110 + name, 111 + scopes, 112 + controller_did, 113 + }) => (Some(name), scopes, controller_did), 114 + None => { 115 + warn!("Password verification failed for login attempt"); 116 + return Err(ApiError::AuthenticationFailed(Some( 117 + "Invalid identifier or password".into(), 118 + ))); 128 119 } 129 120 }; 130 - if !password_valid { 131 - warn!("Password verification failed for login attempt"); 132 - return ApiError::AuthenticationFailed(Some("Invalid identifier or password".into())) 133 - .into_response(); 134 - } 135 121 let account_state = AccountState::from_db_fields( 136 122 row.deactivated_at, 137 123 row.takedown_ref.clone(), ··· 140 126 ); 141 127 if account_state.is_takendown() && !input.allow_takendown { 142 128 warn!("Login attempt for takendown account: {}", row.did); 143 - return ApiError::AccountTakedown.into_response(); 129 + return Err(ApiError::AccountTakedown); 144 130 } 145 131 let is_verified = row.channel_verification.has_any_verified(); 146 132 let is_delegated = state ··· 159 145 .as_ref() 160 146 .map(|r| r.channel.as_str()) 161 147 .unwrap_or(row.preferred_comms_channel.as_str()); 162 - return ( 148 + return Ok(( 163 149 StatusCode::FORBIDDEN, 164 150 Json(json!({ 165 151 "error": "account_not_verified", ··· 169 155 "channel": channel 170 156 })), 171 157 ) 172 - .into_response(); 158 + .into_response()); 173 159 } 174 160 let has_totp = row.totp_enabled; 175 161 let email_2fa_enabled = row.email_2fa_enabled; ··· 190 176 Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::NotRequired) => {} 191 177 Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::Blocked) => { 192 178 warn!("Legacy login blocked for TOTP-enabled account: {}", row.did); 193 - return ApiError::LegacyLoginBlocked.into_response(); 179 + return Err(ApiError::LegacyLoginBlocked); 194 180 } 195 181 Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::ChallengeSent(code)) => { 196 182 let hostname = &tranquil_config::get().server.hostname; ··· 206 192 error!("Failed to send 2FA code: {:?}", e); 207 193 tranquil_pds::auth::legacy_2fa::clear_challenge(state.cache.as_ref(), &row.did) 208 194 .await; 209 - return ApiError::InternalError(Some( 195 + return Err(ApiError::InternalError(Some( 210 196 "Failed to send verification code. Please try again.".into(), 211 - )) 212 - .into_response(); 197 + ))); 213 198 } 214 - return ApiError::AuthFactorTokenRequired.into_response(); 199 + return Err(ApiError::AuthFactorTokenRequired); 215 200 } 216 201 Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::Verified) => {} 217 202 Err(tranquil_pds::auth::legacy_2fa::Legacy2faFlowError::Challenge(e)) => { ··· 219 204 return match e { 220 205 ChallengeError::CacheUnavailable => { 221 206 error!("Cache unavailable for 2FA, blocking legacy login"); 222 - ApiError::ServiceUnavailable(Some( 207 + Err(ApiError::ServiceUnavailable(Some( 223 208 "2FA service temporarily unavailable. Please try again later or use an OAuth client.".into(), 224 - )) 225 - .into_response() 209 + ))) 226 210 } 227 - ChallengeError::RateLimited => ApiError::RateLimitExceeded(Some( 211 + ChallengeError::RateLimited => Err(ApiError::RateLimitExceeded(Some( 228 212 "Please wait before requesting a new verification code.".into(), 229 - )) 230 - .into_response(), 213 + ))), 231 214 ChallengeError::CacheError => { 232 215 error!("Cache error during 2FA challenge creation"); 233 - ApiError::InternalError(None).into_response() 216 + Err(ApiError::InternalError(None)) 234 217 } 235 218 }; 236 219 } ··· 247 230 | ValidationError::InvalidCode 248 231 | ValidationError::CacheError => "Invalid verification code", 249 232 }; 250 - return ApiError::InvalidCode(Some(msg.into())).into_response(); 233 + return Err(ApiError::InvalidCode(Some(msg.into()))); 251 234 } 252 235 } 253 236 let access_meta = match tranquil_pds::auth::create_access_token_with_delegation( ··· 260 243 Ok(m) => m, 261 244 Err(e) => { 262 245 error!("Failed to create access token: {:?}", e); 263 - return ApiError::InternalError(None).into_response(); 246 + return Err(ApiError::InternalError(None)); 264 247 } 265 248 }; 266 249 let refresh_meta = ··· 268 251 Ok(m) => m, 269 252 Err(e) => { 270 253 error!("Failed to create refresh token: {:?}", e); 271 - return ApiError::InternalError(None).into_response(); 254 + return Err(ApiError::InternalError(None)); 272 255 } 273 256 }; 274 257 let did_for_doc = row.did.clone(); ··· 291 274 ); 292 275 if let Err(e) = insert_result { 293 276 error!("Failed to insert session: {:?}", e); 294 - return ApiError::InternalError(None).into_response(); 277 + return Err(ApiError::InternalError(None)); 295 278 } 296 279 if is_legacy_login { 297 280 warn!( ··· 313 296 error!("Failed to queue legacy login notification: {:?}", e); 314 297 } 315 298 } 316 - let handle = full_handle(&row.handle, pds_host); 299 + let handle = row.handle.clone(); 317 300 let is_active = account_state.is_active(); 318 301 let status = account_state.status_for_session().map(String::from); 319 302 let email_auth_factor_out = if email_2fa_enabled || has_totp { ··· 321 304 } else { 322 305 None 323 306 }; 324 - Json(CreateSessionOutput { 325 - access_jwt: access_meta.token, 326 - refresh_jwt: refresh_meta.token, 327 - handle: handle.into(), 328 - did: row.did, 329 - did_doc, 330 - email: row.email, 331 - email_confirmed: Some(row.channel_verification.email), 332 - email_auth_factor: email_auth_factor_out, 333 - active: Some(is_active), 334 - status, 335 - }) 336 - .into_response() 307 + Ok(( 308 + StatusCode::OK, 309 + Json(CreateSessionOutput { 310 + access_jwt: access_meta.token, 311 + refresh_jwt: refresh_meta.token, 312 + handle, 313 + did: row.did, 314 + did_doc, 315 + email: row.email, 316 + email_confirmed: Some(row.channel_verification.email), 317 + email_auth_factor: email_auth_factor_out, 318 + active: Some(is_active), 319 + status, 320 + }), 321 + ) 322 + .into_response()) 323 + } 324 + 325 + #[derive(Serialize)] 326 + #[serde(rename_all = "camelCase")] 327 + pub struct GetSessionOutput { 328 + pub handle: Handle, 329 + pub did: Did, 330 + pub active: bool, 331 + pub preferred_channel: String, 332 + pub preferred_channel_verified: bool, 333 + #[serde(skip_serializing_if = "Option::is_none")] 334 + pub preferred_locale: Option<String>, 335 + pub is_admin: bool, 336 + #[serde(skip_serializing_if = "Option::is_none")] 337 + pub email: Option<String>, 338 + #[serde(skip_serializing_if = "Option::is_none")] 339 + pub email_confirmed: Option<bool>, 340 + #[serde(skip_serializing_if = "Option::is_none")] 341 + pub email_auth_factor: Option<bool>, 342 + #[serde(skip_serializing_if = "Option::is_none")] 343 + pub status: Option<String>, 344 + #[serde(skip_serializing_if = "Option::is_none")] 345 + pub migrated_to_pds: Option<String>, 346 + #[serde(skip_serializing_if = "Option::is_none")] 347 + pub migrated_at: Option<chrono::DateTime<chrono::Utc>>, 348 + #[serde(skip_serializing_if = "Option::is_none")] 349 + pub did_doc: Option<serde_json::Value>, 337 350 } 338 351 339 352 pub async fn get_session( 340 353 State(state): State<AppState>, 341 354 auth: Auth<Permissive>, 342 - ) -> Result<Response, ApiError> { 355 + ) -> Result<Json<GetSessionOutput>, ApiError> { 343 356 let permissions = auth.permissions(); 344 357 let can_read_email = permissions.allows_email_read(); 345 358 ··· 354 367 let preferred_channel_verified = row 355 368 .channel_verification 356 369 .is_verified(row.preferred_comms_channel); 357 - let pds_hostname = &tranquil_config::get().server.hostname; 358 - let handle = full_handle(&row.handle, pds_hostname); 370 + let handle = row.handle.clone(); 359 371 let account_state = AccountState::from_db_fields( 360 372 row.deactivated_at, 361 373 row.takedown_ref.clone(), 362 374 row.migrated_to_pds.clone(), 363 375 row.migrated_at, 364 376 ); 365 - let email_value = if can_read_email { 366 - row.email.clone() 367 - } else { 368 - None 377 + let email = match can_read_email { 378 + true => row.email.clone(), 379 + false => None, 369 380 }; 370 - let email_confirmed_value = can_read_email && row.channel_verification.email; 371 - let mut response = json!({ 372 - "handle": handle, 373 - "did": &auth.did, 374 - "active": account_state.is_active(), 375 - "preferredChannel": row.preferred_comms_channel.as_str(), 376 - "preferredChannelVerified": preferred_channel_verified, 377 - "preferredLocale": row.preferred_locale, 378 - "isAdmin": row.is_admin 379 - }); 380 - if can_read_email { 381 - response["email"] = json!(email_value); 382 - response["emailConfirmed"] = json!(email_confirmed_value); 383 - } 384 - if row.email_2fa_enabled || row.totp_enabled { 385 - response["emailAuthFactor"] = json!(true); 386 - } 387 - if let Some(status) = account_state.status_for_session() { 388 - response["status"] = json!(status); 389 - } 390 - if let AccountState::Migrated { to_pds, at } = &account_state { 391 - response["migratedToPds"] = json!(to_pds); 392 - response["migratedAt"] = json!(at); 393 - } 394 - if let Some(doc) = did_doc { 395 - response["didDoc"] = doc; 396 - } 397 - Ok(Json(response).into_response()) 381 + let email_confirmed = match can_read_email { 382 + true => Some(row.channel_verification.email), 383 + false => None, 384 + }; 385 + let email_auth_factor = match row.email_2fa_enabled || row.totp_enabled { 386 + true => Some(true), 387 + false => None, 388 + }; 389 + let (migrated_to_pds, migrated_at) = match &account_state { 390 + AccountState::Migrated { to_pds, at } => (Some(to_pds.clone()), Some(*at)), 391 + _ => (None, None), 392 + }; 393 + Ok(Json(GetSessionOutput { 394 + handle, 395 + did: auth.did.clone(), 396 + active: account_state.is_active(), 397 + preferred_channel: row.preferred_comms_channel.as_str().to_string(), 398 + preferred_channel_verified, 399 + preferred_locale: row.preferred_locale, 400 + is_admin: row.is_admin, 401 + email, 402 + email_confirmed, 403 + email_auth_factor, 404 + status: account_state.status_for_session().map(String::from), 405 + migrated_to_pds, 406 + migrated_at, 407 + did_doc, 408 + })) 398 409 } 399 410 Ok(None) => Err(ApiError::AuthenticationFailed(None)), 400 411 Err(e) => { ··· 407 418 pub async fn delete_session( 408 419 State(state): State<AppState>, 409 420 headers: axum::http::HeaderMap, 410 - _auth: Auth<Active>, 411 - ) -> Result<Response, ApiError> { 412 - let extracted = tranquil_pds::auth::extract_auth_token_from_header( 413 - tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION), 414 - ) 415 - .ok_or(ApiError::AuthenticationRequired)?; 416 - let jti = tranquil_pds::auth::get_jti_from_token(&extracted.token) 417 - .map_err(|_| ApiError::AuthenticationFailed(None))?; 418 - let did = tranquil_pds::auth::get_did_from_token(&extracted.token).ok(); 421 + auth: Auth<Active>, 422 + ) -> Result<Json<EmptyResponse>, ApiError> { 423 + let jti = tranquil_pds::auth::extract_jti_from_headers(&headers) 424 + .ok_or(ApiError::AuthenticationRequired)?; 419 425 match state.session_repo.delete_session_by_access_jti(&jti).await { 420 426 Ok(rows) if rows > 0 => { 421 - if let Some(did) = did { 422 - let session_cache_key = tranquil_pds::cache_keys::session_key(&did, &jti); 423 - let _ = state.cache.delete(&session_cache_key).await; 424 - } 425 - Ok(EmptyResponse::ok().into_response()) 427 + let session_cache_key = tranquil_pds::cache_keys::session_key(&auth.did, &jti); 428 + let _ = state.cache.delete(&session_cache_key).await; 429 + Ok(Json(EmptyResponse {})) 426 430 } 427 431 Ok(_) => Err(ApiError::AuthenticationFailed(None)), 428 432 Err(_) => Err(ApiError::AuthenticationFailed(None)), 429 433 } 430 434 } 431 435 436 + #[derive(Serialize)] 437 + #[serde(rename_all = "camelCase")] 438 + pub struct RefreshSessionOutput { 439 + pub access_jwt: String, 440 + pub refresh_jwt: String, 441 + pub handle: Handle, 442 + pub did: Did, 443 + #[serde(skip_serializing_if = "Option::is_none")] 444 + pub email: Option<String>, 445 + pub email_confirmed: bool, 446 + pub preferred_channel: String, 447 + pub preferred_channel_verified: bool, 448 + #[serde(skip_serializing_if = "Option::is_none")] 449 + pub preferred_locale: Option<String>, 450 + pub is_admin: bool, 451 + pub active: bool, 452 + #[serde(skip_serializing_if = "Option::is_none")] 453 + pub did_doc: Option<serde_json::Value>, 454 + #[serde(skip_serializing_if = "Option::is_none")] 455 + pub status: Option<String>, 456 + } 457 + 432 458 pub async fn refresh_session( 433 459 State(state): State<AppState>, 434 460 _rate_limit: RateLimited<RefreshSessionLimit>, 435 461 headers: axum::http::HeaderMap, 436 - ) -> Response { 462 + ) -> Result<Json<RefreshSessionOutput>, ApiError> { 437 463 let extracted = match tranquil_pds::auth::extract_auth_token_from_header( 438 464 tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION), 439 465 ) { 440 466 Some(t) => t, 441 - None => return ApiError::AuthenticationRequired.into_response(), 467 + None => return Err(ApiError::AuthenticationRequired), 442 468 }; 443 469 let refresh_token = extracted.token; 444 470 let refresh_jti = match tranquil_pds::auth::get_jti_from_token(&refresh_token) { 445 471 Ok(jti) => jti, 446 472 Err(_) => { 447 - return ApiError::AuthenticationFailed(Some("Invalid token format".into())) 448 - .into_response(); 473 + return Err(ApiError::AuthenticationFailed(Some( 474 + "Invalid token format".into(), 475 + ))); 449 476 } 450 477 }; 451 478 if let Ok(Some(_)) = state ··· 454 481 .await 455 482 { 456 483 warn!("Refresh token reuse detected for jti: {}", refresh_jti); 457 - return ApiError::AuthenticationFailed(Some( 484 + return Err(ApiError::AuthenticationFailed(Some( 458 485 "Refresh token has been revoked due to suspected compromise".into(), 459 - )) 460 - .into_response(); 486 + ))); 461 487 } 462 488 let session_row = match state 463 489 .session_repo ··· 466 492 { 467 493 Ok(Some(row)) => row, 468 494 Ok(None) => { 469 - return ApiError::AuthenticationFailed(Some("Invalid refresh token".into())) 470 - .into_response(); 495 + return Err(ApiError::AuthenticationFailed(Some( 496 + "Invalid refresh token".into(), 497 + ))); 471 498 } 472 499 Err(e) => { 473 500 error!("Database error fetching session: {:?}", e); 474 - return ApiError::InternalError(None).into_response(); 501 + return Err(ApiError::InternalError(None)); 475 502 } 476 503 }; 477 504 let key_bytes = match tranquil_pds::config::decrypt_key( ··· 481 508 Ok(k) => k, 482 509 Err(e) => { 483 510 error!("Failed to decrypt user key: {:?}", e); 484 - return ApiError::InternalError(None).into_response(); 511 + return Err(ApiError::InternalError(None)); 485 512 } 486 513 }; 487 514 if tranquil_pds::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() { 488 - return ApiError::AuthenticationFailed(Some("Invalid refresh token".into())) 489 - .into_response(); 515 + return Err(ApiError::AuthenticationFailed(Some( 516 + "Invalid refresh token".into(), 517 + ))); 490 518 } 491 519 let new_access_meta = match tranquil_pds::auth::create_access_token_with_delegation( 492 520 &session_row.did, ··· 498 526 Ok(m) => m, 499 527 Err(e) => { 500 528 error!("Failed to create access token: {:?}", e); 501 - return ApiError::InternalError(None).into_response(); 529 + return Err(ApiError::InternalError(None)); 502 530 } 503 531 }; 504 532 let new_refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata( ··· 508 536 Ok(m) => m, 509 537 Err(e) => { 510 538 error!("Failed to create refresh token: {:?}", e); 511 - return ApiError::InternalError(None).into_response(); 539 + return Err(ApiError::InternalError(None)); 512 540 } 513 541 }; 514 542 let refresh_data = tranquil_db_traits::SessionRefreshData { ··· 527 555 Ok(tranquil_db_traits::RefreshSessionResult::Success) => {} 528 556 Ok(tranquil_db_traits::RefreshSessionResult::TokenAlreadyUsed) => { 529 557 warn!("Refresh token reuse detected during atomic operation"); 530 - return ApiError::AuthenticationFailed(Some( 558 + return Err(ApiError::AuthenticationFailed(Some( 531 559 "Refresh token has been revoked due to suspected compromise".into(), 532 - )) 533 - .into_response(); 560 + ))); 534 561 } 535 562 Ok(tranquil_db_traits::RefreshSessionResult::ConcurrentRefresh) => { 536 563 warn!( 537 564 "Concurrent refresh detected for session_id: {}", 538 565 session_row.id 539 566 ); 540 - return ApiError::AuthenticationFailed(Some( 567 + return Err(ApiError::AuthenticationFailed(Some( 541 568 "Refresh token has been revoked due to suspected compromise".into(), 542 - )) 543 - .into_response(); 569 + ))); 544 570 } 545 571 Err(e) => { 546 572 error!("Database error during session refresh: {:?}", e); 547 - return ApiError::InternalError(None).into_response(); 573 + return Err(ApiError::InternalError(None)); 548 574 } 549 575 } 550 576 let did_for_doc = session_row.did.clone(); ··· 558 584 let preferred_channel_verified = u 559 585 .channel_verification 560 586 .is_verified(u.preferred_comms_channel); 561 - let pds_hostname = &tranquil_config::get().server.hostname; 562 - let handle = full_handle(&u.handle, pds_hostname); 587 + let handle = u.handle.clone(); 563 588 let account_state = 564 589 AccountState::from_db_fields(u.deactivated_at, u.takedown_ref.clone(), None, None); 565 - let mut response = json!({ 566 - "accessJwt": new_access_meta.token, 567 - "refreshJwt": new_refresh_meta.token, 568 - "handle": handle, 569 - "did": session_row.did, 570 - "email": u.email, 571 - "emailConfirmed": u.channel_verification.email, 572 - "preferredChannel": u.preferred_comms_channel.as_str(), 573 - "preferredChannelVerified": preferred_channel_verified, 574 - "preferredLocale": u.preferred_locale, 575 - "isAdmin": u.is_admin, 576 - "active": account_state.is_active() 577 - }); 578 - if let Some(doc) = did_doc { 579 - response["didDoc"] = doc; 580 - } 581 - if let Some(status) = account_state.status_for_session() { 582 - response["status"] = json!(status); 583 - } 584 - Json(response).into_response() 590 + Ok(Json(RefreshSessionOutput { 591 + access_jwt: new_access_meta.token, 592 + refresh_jwt: new_refresh_meta.token, 593 + handle, 594 + did: session_row.did, 595 + email: u.email, 596 + email_confirmed: u.channel_verification.email, 597 + preferred_channel: u.preferred_comms_channel.as_str().to_string(), 598 + preferred_channel_verified, 599 + preferred_locale: u.preferred_locale, 600 + is_admin: u.is_admin, 601 + active: account_state.is_active(), 602 + did_doc, 603 + status: account_state.status_for_session().map(String::from), 604 + })) 585 605 } 586 606 Ok(None) => { 587 607 error!("User not found for existing session: {}", session_row.did); 588 - ApiError::InternalError(None).into_response() 608 + Err(ApiError::InternalError(None)) 589 609 } 590 610 Err(e) => { 591 611 error!("Database error fetching user: {:?}", e); 592 - ApiError::InternalError(None).into_response() 612 + Err(ApiError::InternalError(None)) 593 613 } 594 614 } 595 615 } ··· 617 637 pub async fn confirm_signup( 618 638 State(state): State<AppState>, 619 639 Json(input): Json<ConfirmSignupInput>, 620 - ) -> Response { 640 + ) -> Result<Json<ConfirmSignupOutput>, ApiError> { 621 641 info!("confirm_signup called for DID: {}", input.did); 622 642 let row = match state.user_repo.get_confirm_signup_by_did(&input.did).await { 623 643 Ok(Some(row)) => row, 624 644 Ok(None) => { 625 645 warn!("User not found for confirm_signup: {}", input.did); 626 - return ApiError::InvalidRequest("Invalid DID or verification code".into()) 627 - .into_response(); 646 + return Err(ApiError::InvalidRequest( 647 + "Invalid DID or verification code".into(), 648 + )); 628 649 } 629 650 Err(e) => { 630 651 error!("Database error in confirm_signup: {:?}", e); 631 - return ApiError::InternalError(None).into_response(); 652 + return Err(ApiError::InternalError(None)); 632 653 } 633 654 }; 634 655 ··· 656 677 "Token DID mismatch for confirm_signup: expected {}, got {}", 657 678 input.did, token_data.did 658 679 ); 659 - return ApiError::InvalidRequest("Invalid verification code".into()) 660 - .into_response(); 680 + return Err(ApiError::InvalidRequest("Invalid verification code".into())); 661 681 } 662 682 } 663 683 Err(tranquil_pds::auth::verification_token::VerifyError::Expired) => { 664 684 warn!("Verification code expired for user: {}", input.did); 665 - return ApiError::ExpiredToken(Some("Verification code has expired".into())) 666 - .into_response(); 685 + return Err(ApiError::ExpiredToken(Some( 686 + "Verification code has expired".into(), 687 + ))); 667 688 } 668 689 Err(e) => { 669 690 warn!("Invalid verification code for user {}: {:?}", input.did, e); 670 - return ApiError::InvalidRequest("Invalid verification code".into()).into_response(); 691 + return Err(ApiError::InvalidRequest("Invalid verification code".into())); 671 692 } 672 693 } 673 694 ··· 676 697 Ok(k) => k, 677 698 Err(e) => { 678 699 error!("Failed to decrypt user key: {:?}", e); 679 - return ApiError::InternalError(None).into_response(); 700 + return Err(ApiError::InternalError(None)); 680 701 } 681 702 }; 682 703 683 - let access_meta = 684 - match tranquil_pds::auth::create_access_token_with_metadata(&row.did, &key_bytes) { 685 - Ok(m) => m, 686 - Err(e) => { 687 - error!("Failed to create access token: {:?}", e); 688 - return ApiError::InternalError(None).into_response(); 689 - } 690 - }; 691 - let refresh_meta = 692 - match tranquil_pds::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { 693 - Ok(m) => m, 694 - Err(e) => { 695 - error!("Failed to create refresh token: {:?}", e); 696 - return ApiError::InternalError(None).into_response(); 697 - } 698 - }; 699 - 700 704 if let Err(e) = state 701 705 .user_repo 702 706 .set_channel_verified(&input.did, row.channel) 703 707 .await 704 708 { 705 709 error!("Failed to update verification status: {:?}", e); 706 - return ApiError::InternalError(None).into_response(); 710 + return Err(ApiError::InternalError(None)); 707 711 } 708 712 709 - let session_data = tranquil_db_traits::SessionTokenCreate { 710 - did: row.did.clone(), 711 - access_jti: access_meta.jti.clone(), 712 - refresh_jti: refresh_meta.jti.clone(), 713 - access_expires_at: access_meta.expires_at, 714 - refresh_expires_at: refresh_meta.expires_at, 715 - login_type: tranquil_db_traits::LoginType::Modern, 716 - mfa_verified: false, 717 - scope: Some("transition:generic transition:chat.bsky".to_string()), 718 - controller_did: None, 719 - app_password_name: None, 713 + let session = match crate::identity::provision::create_and_store_session( 714 + &state, 715 + &row.did, 716 + &row.did, 717 + &key_bytes, 718 + "transition:generic transition:chat.bsky", 719 + None, 720 + ) 721 + .await 722 + { 723 + Ok(s) => s, 724 + Err(_) => return Err(ApiError::InternalError(None)), 720 725 }; 721 - if let Err(e) = state.session_repo.create_session(&session_data).await { 722 - error!("Failed to insert session: {:?}", e); 723 - return ApiError::InternalError(None).into_response(); 724 - } 725 726 726 727 let hostname = &tranquil_config::get().server.hostname; 727 728 if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_welcome( ··· 734 735 { 735 736 warn!("Failed to enqueue welcome notification: {:?}", e); 736 737 } 737 - Json(ConfirmSignupOutput { 738 - access_jwt: access_meta.token, 739 - refresh_jwt: refresh_meta.token, 738 + Ok(Json(ConfirmSignupOutput { 739 + access_jwt: session.access_jwt, 740 + refresh_jwt: session.refresh_jwt, 740 741 handle: row.handle, 741 742 did: row.did, 742 743 email: row.email, 743 744 email_verified: matches!(row.channel, tranquil_db_traits::CommsChannel::Email), 744 745 preferred_channel: row.channel, 745 746 preferred_channel_verified: true, 746 - }) 747 - .into_response() 747 + })) 748 748 } 749 749 750 750 const AUTO_VERIFY_DEBOUNCE: std::time::Duration = std::time::Duration::from_secs(120); ··· 794 794 ); 795 795 return Some(result); 796 796 } 797 - let verification_token = 798 - tranquil_pds::auth::verification_token::generate_signup_token(did, row.channel, &recipient); 799 - let formatted_token = 800 - tranquil_pds::auth::verification_token::format_token_for_display(&verification_token); 801 - let hostname = &tranquil_config::get().server.hostname; 802 - if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification( 803 - state.user_repo.as_ref(), 804 - state.infra_repo.as_ref(), 797 + crate::identity::provision::enqueue_signup_verification( 798 + state, 805 799 row.id, 800 + did, 806 801 row.channel, 807 802 &recipient, 808 - &formatted_token, 809 - hostname, 810 803 ) 811 - .await 812 - { 813 - warn!("Failed to auto-resend verification for {}: {:?}", did, e); 814 - return Some(result); 815 - } 804 + .await; 816 805 let _ = state 817 806 .cache 818 807 .set(&debounce_key, "1", AUTO_VERIFY_DEBOUNCE) ··· 829 818 pub async fn resend_verification( 830 819 State(state): State<AppState>, 831 820 Json(input): Json<ResendVerificationInput>, 832 - ) -> Response { 821 + ) -> Result<Json<SuccessResponse>, ApiError> { 833 822 info!("resend_verification called for DID: {}", input.did); 834 823 let row = match state 835 824 .user_repo ··· 838 827 { 839 828 Ok(Some(row)) => row, 840 829 Ok(None) => { 841 - return ApiError::InvalidRequest("User not found".into()).into_response(); 830 + return Err(ApiError::InvalidRequest("User not found".into())); 842 831 } 843 832 Err(e) => { 844 833 error!("Database error in resend_verification: {:?}", e); 845 - return ApiError::InternalError(None).into_response(); 834 + return Err(ApiError::InternalError(None)); 846 835 } 847 836 }; 848 837 let is_verified = row.channel_verification.has_any_verified(); 849 838 if is_verified { 850 - return ApiError::InvalidRequest("Account is already verified".into()).into_response(); 839 + return Err(ApiError::InvalidRequest( 840 + "Account is already verified".into(), 841 + )); 851 842 } 852 843 853 844 let recipient = match row.channel { ··· 861 852 tranquil_db_traits::CommsChannel::Signal => row.signal_username.clone().unwrap_or_default(), 862 853 }; 863 854 864 - let verification_token = tranquil_pds::auth::verification_token::generate_signup_token( 865 - &input.did, 866 - row.channel, 867 - &recipient, 868 - ); 869 - let formatted_token = 870 - tranquil_pds::auth::verification_token::format_token_for_display(&verification_token); 871 - 872 - let hostname = &tranquil_config::get().server.hostname; 873 - if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification( 874 - state.user_repo.as_ref(), 875 - state.infra_repo.as_ref(), 855 + crate::identity::provision::enqueue_signup_verification( 856 + &state, 876 857 row.id, 858 + &input.did, 877 859 row.channel, 878 860 &recipient, 879 - &formatted_token, 880 - hostname, 881 861 ) 882 - .await 883 - { 884 - warn!("Failed to enqueue verification notification: {:?}", e); 885 - } 886 - SuccessResponse::ok().into_response() 862 + .await; 863 + Ok(Json(SuccessResponse { success: true })) 887 864 } 888 865 889 866 #[derive(Serialize)] ··· 914 891 State(state): State<AppState>, 915 892 headers: HeaderMap, 916 893 auth: Auth<Active>, 917 - ) -> Result<Response, ApiError> { 918 - let current_jti = headers 919 - .get("authorization") 920 - .and_then(|v| v.to_str().ok()) 921 - .and_then(|v| v.strip_prefix("Bearer ")) 922 - .and_then(|token| tranquil_pds::auth::get_jti_from_token(token).ok()); 894 + ) -> Result<Json<ListSessionsOutput>, ApiError> { 895 + let current_jti = tranquil_pds::auth::extract_jti_from_headers(&headers); 923 896 924 897 let jwt_rows = state 925 898 .session_repo ··· 959 932 let mut sessions: Vec<SessionInfo> = jwt_sessions.chain(oauth_sessions).collect(); 960 933 sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); 961 934 962 - Ok((StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response()) 935 + Ok(Json(ListSessionsOutput { sessions })) 963 936 } 964 937 965 938 fn extract_client_name(client_id: &str) -> String { ··· 982 955 State(state): State<AppState>, 983 956 auth: Auth<Active>, 984 957 Json(input): Json<RevokeSessionInput>, 985 - ) -> Result<Response, ApiError> { 958 + ) -> Result<Json<EmptyResponse>, ApiError> { 986 959 if let Some(jwt_id) = input.session_id.strip_prefix("jwt:") { 987 960 let session_id = jwt_id 988 961 .parse::<i32>() ··· 1021 994 } else { 1022 995 return Err(ApiError::InvalidRequest("Invalid session ID format".into())); 1023 996 } 1024 - Ok(EmptyResponse::ok().into_response()) 997 + Ok(Json(EmptyResponse {})) 1025 998 } 1026 999 1027 1000 pub async fn revoke_all_sessions( 1028 1001 State(state): State<AppState>, 1029 1002 headers: HeaderMap, 1030 1003 auth: Auth<Active>, 1031 - ) -> Result<Response, ApiError> { 1032 - let jti = tranquil_pds::auth::extract_auth_token_from_header( 1033 - headers.get("authorization").and_then(|v| v.to_str().ok()), 1034 - ) 1035 - .and_then(|extracted| tranquil_pds::auth::get_jti_from_token(&extracted.token).ok()) 1036 - .ok_or(ApiError::InvalidToken(None))?; 1004 + ) -> Result<Json<SuccessResponse>, ApiError> { 1005 + let jti = tranquil_pds::auth::extract_jti_from_headers(&headers) 1006 + .ok_or(ApiError::InvalidToken(None))?; 1037 1007 1038 1008 if auth.is_oauth() { 1039 1009 state ··· 1061 1031 } 1062 1032 1063 1033 info!(did = %&auth.did, "All other sessions revoked"); 1064 - Ok(SuccessResponse::ok().into_response()) 1034 + Ok(Json(SuccessResponse { success: true })) 1065 1035 } 1066 1036 1067 1037 #[derive(Serialize)] ··· 1074 1044 pub async fn get_legacy_login_preference( 1075 1045 State(state): State<AppState>, 1076 1046 auth: Auth<Active>, 1077 - ) -> Result<Response, ApiError> { 1047 + ) -> Result<Json<LegacyLoginPreferenceOutput>, ApiError> { 1078 1048 let pref = state 1079 1049 .user_repo 1080 1050 .get_legacy_login_pref(&auth.did) ··· 1084 1054 Ok(Json(LegacyLoginPreferenceOutput { 1085 1055 allow_legacy_login: pref.allow_legacy_login, 1086 1056 has_mfa: pref.has_mfa, 1087 - }) 1088 - .into_response()) 1057 + })) 1089 1058 } 1090 1059 1091 1060 #[derive(Deserialize)] ··· 1094 1063 pub allow_legacy_login: bool, 1095 1064 } 1096 1065 1066 + #[derive(Serialize)] 1067 + #[serde(rename_all = "camelCase")] 1068 + pub struct UpdateLegacyLoginOutput { 1069 + pub allow_legacy_login: bool, 1070 + } 1071 + 1097 1072 pub async fn update_legacy_login_preference( 1098 1073 State(state): State<AppState>, 1099 1074 auth: Auth<Active>, 1100 1075 Json(input): Json<UpdateLegacyLoginInput>, 1101 - ) -> Result<Response, ApiError> { 1102 - let session_mfa = match require_legacy_session_mfa(&state, &auth).await { 1103 - Ok(proof) => proof, 1104 - Err(response) => return Ok(response), 1105 - }; 1076 + ) -> Result<Json<UpdateLegacyLoginOutput>, ApiError> { 1077 + let session_mfa = require_legacy_session_mfa(&state, &auth).await?; 1106 1078 1107 - let reauth_mfa = match require_reauth_window(&state, &auth).await { 1108 - Ok(proof) => proof, 1109 - Err(response) => return Ok(response), 1110 - }; 1079 + let reauth_mfa = require_reauth_window(&state, &auth).await?; 1111 1080 1112 1081 let updated = state 1113 1082 .user_repo ··· 1122 1091 allow_legacy_login = input.allow_legacy_login, 1123 1092 "Legacy login preference updated" 1124 1093 ); 1125 - Ok(Json(json!({ 1126 - "allowLegacyLogin": input.allow_legacy_login 1094 + Ok(Json(UpdateLegacyLoginOutput { 1095 + allow_legacy_login: input.allow_legacy_login, 1127 1096 })) 1128 - .into_response()) 1129 1097 } 1130 1098 1131 1099 use tranquil_pds::comms::VALID_LOCALES; ··· 1140 1108 State(state): State<AppState>, 1141 1109 auth: Auth<Active>, 1142 1110 Json(input): Json<UpdateLocaleInput>, 1143 - ) -> Result<Response, ApiError> { 1111 + ) -> Result<Json<PreferredLocaleOutput>, ApiError> { 1144 1112 if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) { 1145 1113 return Err(ApiError::InvalidRequest(format!( 1146 1114 "Invalid locale. Valid options: {}", ··· 1161 1129 locale = %input.preferred_locale, 1162 1130 "User locale preference updated" 1163 1131 ); 1164 - Ok(Json(json!({ 1165 - "preferredLocale": input.preferred_locale 1132 + Ok(Json(PreferredLocaleOutput { 1133 + preferred_locale: Some(input.preferred_locale), 1166 1134 })) 1167 - .into_response()) 1168 1135 }

History

1 round 0 comments
sign up or login to add to the discussion
oyster.cafe submitted #0
1 commit
expand
refactor(api): rework session login flow to use common credential verification
expand 0 comments
pull request successfully merged