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

DRAFT: Better code quality via type safety #5

merged opened by lewis.moe targeting main from fix/code-quality-in-general

Ensuring at compile-time that we're definitely handling possible early failures in functions

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mdbo7zq5ae22
+2048 -2117
Diff #0
+46 -45
crates/tranquil-db/src/postgres/oauth.rs
··· 7 ScopePreference, TrustedDeviceRow, TwoFactorChallenge, 8 }; 9 use tranquil_oauth::{ 10 - AuthorizationRequestParameters, AuthorizedClientData, ClientAuth, DeviceData, RequestData, 11 - TokenData, 12 }; 13 use tranquil_types::{ 14 AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, ··· 59 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) 60 RETURNING id 61 "#, 62 - data.did, 63 - data.token_id, 64 data.created_at, 65 data.updated_at, 66 data.expires_at, 67 data.client_id, 68 client_auth_json, 69 - data.device_id, 70 parameters_json, 71 data.details, 72 - data.code, 73 - data.current_refresh_token, 74 data.scope, 75 - data.controller_did, 76 ) 77 .fetch_one(&self.pool) 78 .await ··· 95 .map_err(map_sqlx_error)?; 96 match row { 97 Some(r) => Ok(Some(TokenData { 98 - did: r.did, 99 - token_id: r.token_id, 100 created_at: r.created_at, 101 updated_at: r.updated_at, 102 expires_at: r.expires_at, 103 client_id: r.client_id, 104 client_auth: from_json(r.client_auth)?, 105 - device_id: r.device_id, 106 parameters: from_json(r.parameters)?, 107 details: r.details, 108 - code: r.code, 109 - current_refresh_token: r.current_refresh_token, 110 scope: r.scope, 111 - controller_did: r.controller_did, 112 })), 113 None => Ok(None), 114 } ··· 134 Some(r) => Ok(Some(( 135 r.id, 136 TokenData { 137 - did: r.did, 138 - token_id: r.token_id, 139 created_at: r.created_at, 140 updated_at: r.updated_at, 141 expires_at: r.expires_at, 142 client_id: r.client_id, 143 client_auth: from_json(r.client_auth)?, 144 - device_id: r.device_id, 145 parameters: from_json(r.parameters)?, 146 details: r.details, 147 - code: r.code, 148 - current_refresh_token: r.current_refresh_token, 149 scope: r.scope, 150 - controller_did: r.controller_did, 151 }, 152 ))), 153 None => Ok(None), ··· 176 Some(r) => Ok(Some(( 177 r.id, 178 TokenData { 179 - did: r.did, 180 - token_id: r.token_id, 181 created_at: r.created_at, 182 updated_at: r.updated_at, 183 expires_at: r.expires_at, 184 client_id: r.client_id, 185 client_auth: from_json(r.client_auth)?, 186 - device_id: r.device_id, 187 parameters: from_json(r.parameters)?, 188 details: r.details, 189 - code: r.code, 190 - current_refresh_token: r.current_refresh_token, 191 scope: r.scope, 192 - controller_did: r.controller_did, 193 }, 194 ))), 195 None => Ok(None), ··· 302 rows.into_iter() 303 .map(|r| { 304 Ok(TokenData { 305 - did: r.did, 306 - token_id: r.token_id, 307 created_at: r.created_at, 308 updated_at: r.updated_at, 309 expires_at: r.expires_at, 310 client_id: r.client_id, 311 client_auth: from_json(r.client_auth)?, 312 - device_id: r.device_id, 313 parameters: from_json(r.parameters)?, 314 details: r.details, 315 - code: r.code, 316 - current_refresh_token: r.current_refresh_token, 317 scope: r.scope, 318 - controller_did: r.controller_did, 319 }) 320 }) 321 .collect() ··· 407 VALUES ($1, $2, $3, $4, $5, $6, $7, $8) 408 "#, 409 request_id.as_str(), 410 - data.did, 411 - data.device_id, 412 data.client_id, 413 client_auth_json, 414 parameters_json, 415 data.expires_at, 416 - data.code, 417 ) 418 .execute(&self.pool) 419 .await ··· 448 client_auth, 449 parameters, 450 expires_at: r.expires_at, 451 - did: r.did, 452 - device_id: r.device_id, 453 - code: r.code, 454 - controller_did: r.controller_did, 455 })) 456 } 457 None => Ok(None), ··· 534 client_auth, 535 parameters, 536 expires_at: r.expires_at, 537 - did: r.did, 538 - device_id: r.device_id, 539 - code: r.code, 540 - controller_did: r.controller_did, 541 })) 542 } 543 None => Ok(None), ··· 655 VALUES ($1, $2, $3, $4, $5) 656 "#, 657 device_id.as_str(), 658 - data.session_id, 659 data.user_agent, 660 data.ip_address, 661 data.last_seen_at, ··· 679 .await 680 .map_err(map_sqlx_error)?; 681 Ok(row.map(|r| DeviceData { 682 - session_id: r.session_id, 683 user_agent: r.user_agent, 684 ip_address: r.ip_address, 685 last_seen_at: r.last_seen_at,
··· 7 ScopePreference, TrustedDeviceRow, TwoFactorChallenge, 8 }; 9 use tranquil_oauth::{ 10 + AuthorizationRequestParameters, AuthorizedClientData, ClientAuth, Code as OAuthCode, 11 + DeviceData, DeviceId as OAuthDeviceId, RequestData, SessionId as OAuthSessionId, TokenData, 12 + TokenId as OAuthTokenId, RefreshToken as OAuthRefreshToken, 13 }; 14 use tranquil_types::{ 15 AuthorizationCode, ClientId, DPoPProofId, DeviceId, Did, Handle, RefreshToken, RequestId, ··· 60 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) 61 RETURNING id 62 "#, 63 + data.did.as_str(), 64 + &data.token_id.0, 65 data.created_at, 66 data.updated_at, 67 data.expires_at, 68 data.client_id, 69 client_auth_json, 70 + data.device_id.as_ref().map(|d| d.0.as_str()), 71 parameters_json, 72 data.details, 73 + data.code.as_ref().map(|c| c.0.as_str()), 74 + data.current_refresh_token.as_ref().map(|r| r.0.as_str()), 75 data.scope, 76 + data.controller_did.as_ref().map(|d| d.as_str()), 77 ) 78 .fetch_one(&self.pool) 79 .await ··· 96 .map_err(map_sqlx_error)?; 97 match row { 98 Some(r) => Ok(Some(TokenData { 99 + did: r.did.parse().map_err(|_| DbError::Other("Invalid DID in token".into()))?, 100 + token_id: OAuthTokenId(r.token_id), 101 created_at: r.created_at, 102 updated_at: r.updated_at, 103 expires_at: r.expires_at, 104 client_id: r.client_id, 105 client_auth: from_json(r.client_auth)?, 106 + device_id: r.device_id.map(OAuthDeviceId), 107 parameters: from_json(r.parameters)?, 108 details: r.details, 109 + code: r.code.map(OAuthCode), 110 + current_refresh_token: r.current_refresh_token.map(OAuthRefreshToken), 111 scope: r.scope, 112 + controller_did: r.controller_did.map(|s| s.parse()).transpose().map_err(|_| DbError::Other("Invalid controller DID".into()))?, 113 })), 114 None => Ok(None), 115 } ··· 135 Some(r) => Ok(Some(( 136 r.id, 137 TokenData { 138 + did: r.did.parse().map_err(|_| DbError::Other("Invalid DID in token".into()))?, 139 + token_id: OAuthTokenId(r.token_id), 140 created_at: r.created_at, 141 updated_at: r.updated_at, 142 expires_at: r.expires_at, 143 client_id: r.client_id, 144 client_auth: from_json(r.client_auth)?, 145 + device_id: r.device_id.map(OAuthDeviceId), 146 parameters: from_json(r.parameters)?, 147 details: r.details, 148 + code: r.code.map(OAuthCode), 149 + current_refresh_token: r.current_refresh_token.map(OAuthRefreshToken), 150 scope: r.scope, 151 + controller_did: r.controller_did.map(|s| s.parse()).transpose().map_err(|_| DbError::Other("Invalid controller DID".into()))?, 152 }, 153 ))), 154 None => Ok(None), ··· 177 Some(r) => Ok(Some(( 178 r.id, 179 TokenData { 180 + did: r.did.parse().map_err(|_| DbError::Other("Invalid DID in token".into()))?, 181 + token_id: OAuthTokenId(r.token_id), 182 created_at: r.created_at, 183 updated_at: r.updated_at, 184 expires_at: r.expires_at, 185 client_id: r.client_id, 186 client_auth: from_json(r.client_auth)?, 187 + device_id: r.device_id.map(OAuthDeviceId), 188 parameters: from_json(r.parameters)?, 189 details: r.details, 190 + code: r.code.map(OAuthCode), 191 + current_refresh_token: r.current_refresh_token.map(OAuthRefreshToken), 192 scope: r.scope, 193 + controller_did: r.controller_did.map(|s| s.parse()).transpose().map_err(|_| DbError::Other("Invalid controller DID".into()))?, 194 }, 195 ))), 196 None => Ok(None), ··· 303 rows.into_iter() 304 .map(|r| { 305 Ok(TokenData { 306 + did: r.did.parse().map_err(|_| DbError::Other("Invalid DID in token".into()))?, 307 + token_id: OAuthTokenId(r.token_id), 308 created_at: r.created_at, 309 updated_at: r.updated_at, 310 expires_at: r.expires_at, 311 client_id: r.client_id, 312 client_auth: from_json(r.client_auth)?, 313 + device_id: r.device_id.map(OAuthDeviceId), 314 parameters: from_json(r.parameters)?, 315 details: r.details, 316 + code: r.code.map(OAuthCode), 317 + current_refresh_token: r.current_refresh_token.map(OAuthRefreshToken), 318 scope: r.scope, 319 + controller_did: r.controller_did.map(|s| s.parse()).transpose().map_err(|_| DbError::Other("Invalid controller DID".into()))?, 320 }) 321 }) 322 .collect() ··· 408 VALUES ($1, $2, $3, $4, $5, $6, $7, $8) 409 "#, 410 request_id.as_str(), 411 + data.did.as_ref().map(|d| d.as_str()), 412 + data.device_id.as_ref().map(|d| d.0.as_str()), 413 data.client_id, 414 client_auth_json, 415 parameters_json, 416 data.expires_at, 417 + data.code.as_ref().map(|c| c.0.as_str()), 418 ) 419 .execute(&self.pool) 420 .await ··· 449 client_auth, 450 parameters, 451 expires_at: r.expires_at, 452 + did: r.did.map(|s| s.parse()).transpose().map_err(|_| DbError::Other("Invalid DID in DB".into()))?, 453 + device_id: r.device_id.map(OAuthDeviceId), 454 + code: r.code.map(OAuthCode), 455 + controller_did: r.controller_did.map(|s| s.parse()).transpose().map_err(|_| DbError::Other("Invalid controller DID in DB".into()))?, 456 })) 457 } 458 None => Ok(None), ··· 535 client_auth, 536 parameters, 537 expires_at: r.expires_at, 538 + did: r.did.map(|s| s.parse()).transpose().map_err(|_| DbError::Other("Invalid DID in DB".into()))?, 539 + device_id: r.device_id.map(OAuthDeviceId), 540 + code: r.code.map(OAuthCode), 541 + controller_did: r.controller_did.map(|s| s.parse()).transpose().map_err(|_| DbError::Other("Invalid controller DID in DB".into()))?, 542 })) 543 } 544 None => Ok(None), ··· 656 VALUES ($1, $2, $3, $4, $5) 657 "#, 658 device_id.as_str(), 659 + &data.session_id.0, 660 data.user_agent, 661 data.ip_address, 662 data.last_seen_at, ··· 680 .await 681 .map_err(map_sqlx_error)?; 682 Ok(row.map(|r| DeviceData { 683 + session_id: OAuthSessionId(r.session_id), 684 user_agent: r.user_agent, 685 ip_address: r.ip_address, 686 last_seen_at: r.last_seen_at,
+6 -4
crates/tranquil-oauth/src/lib.rs
··· 10 }; 11 pub use error::OAuthError; 12 pub use types::{ 13 - AuthFlowState, AuthorizationRequestParameters, AuthorizationServerMetadata, 14 - AuthorizedClientData, ClientAuth, Code, DPoPClaims, DeviceData, DeviceId, JwkPublicKey, Jwks, 15 - OAuthClientMetadata, ParResponse, ProtectedResourceMetadata, RefreshToken, RefreshTokenState, 16 - RequestData, RequestId, SessionId, TokenData, TokenId, TokenRequest, TokenResponse, 17 };
··· 10 }; 11 pub use error::OAuthError; 12 pub use types::{ 13 + AuthFlow, AuthFlowWithUser, AuthorizationRequestParameters, AuthorizationServerMetadata, 14 + AuthorizedClientData, ClientAuth, Code, CodeChallengeMethod, DPoPClaims, DeviceData, DeviceId, 15 + FlowAuthenticated, FlowAuthorized, FlowExpired, FlowNotAuthenticated, FlowNotAuthorized, 16 + FlowPending, JwkPublicKey, Jwks, OAuthClientMetadata, ParResponse, Prompt, 17 + ProtectedResourceMetadata, RefreshToken, RefreshTokenState, RequestData, RequestId, 18 + ResponseMode, ResponseType, SessionId, TokenData, TokenId, TokenRequest, TokenResponse, 19 };
+250 -141
crates/tranquil-oauth/src/types.rs
··· 1 use chrono::{DateTime, Utc}; 2 use serde::{Deserialize, Serialize}; 3 use serde_json::Value as JsonValue; 4 5 - #[derive(Debug, Clone, Serialize, Deserialize)] 6 pub struct RequestId(pub String); 7 8 - #[derive(Debug, Clone, Serialize, Deserialize)] 9 pub struct TokenId(pub String); 10 11 - #[derive(Debug, Clone, Serialize, Deserialize)] 12 pub struct DeviceId(pub String); 13 14 - #[derive(Debug, Clone, Serialize, Deserialize)] 15 pub struct SessionId(pub String); 16 17 - #[derive(Debug, Clone, Serialize, Deserialize)] 18 pub struct Code(pub String); 19 20 - #[derive(Debug, Clone, Serialize, Deserialize)] 21 pub struct RefreshToken(pub String); 22 23 impl RequestId { ··· 82 PrivateKeyJwt { client_assertion: String }, 83 } 84 85 #[derive(Debug, Clone, Serialize, Deserialize)] 86 pub struct AuthorizationRequestParameters { 87 - pub response_type: String, 88 pub client_id: String, 89 pub redirect_uri: String, 90 pub scope: Option<String>, 91 pub state: Option<String>, 92 pub code_challenge: String, 93 - pub code_challenge_method: String, 94 - pub response_mode: Option<String>, 95 pub login_hint: Option<String>, 96 pub dpop_jkt: Option<String>, 97 - pub prompt: Option<String>, 98 #[serde(flatten)] 99 pub extra: Option<JsonValue>, 100 } ··· 105 pub client_auth: Option<ClientAuth>, 106 pub parameters: AuthorizationRequestParameters, 107 pub expires_at: DateTime<Utc>, 108 - pub did: Option<String>, 109 - pub device_id: Option<String>, 110 - pub code: Option<String>, 111 - pub controller_did: Option<String>, 112 } 113 114 #[derive(Debug, Clone)] 115 pub struct DeviceData { 116 - pub session_id: String, 117 pub user_agent: Option<String>, 118 pub ip_address: String, 119 pub last_seen_at: DateTime<Utc>, ··· 121 122 #[derive(Debug, Clone)] 123 pub struct TokenData { 124 - pub did: String, 125 - pub token_id: String, 126 pub created_at: DateTime<Utc>, 127 pub updated_at: DateTime<Utc>, 128 pub expires_at: DateTime<Utc>, 129 pub client_id: String, 130 pub client_auth: ClientAuth, 131 - pub device_id: Option<String>, 132 pub parameters: AuthorizationRequestParameters, 133 pub details: Option<JsonValue>, 134 - pub code: Option<String>, 135 - pub current_refresh_token: Option<String>, 136 pub scope: Option<String>, 137 - pub controller_did: Option<String>, 138 } 139 140 #[derive(Debug, Clone, Serialize, Deserialize)] ··· 247 pub keys: Vec<JwkPublicKey>, 248 } 249 250 - #[derive(Debug, Clone, PartialEq, Eq)] 251 - pub enum AuthFlowState { 252 - Pending, 253 - Authenticated { 254 - did: String, 255 - device_id: Option<String>, 256 - }, 257 - Authorized { 258 - did: String, 259 - device_id: Option<String>, 260 - code: String, 261 - }, 262 - Expired, 263 } 264 265 - impl AuthFlowState { 266 - pub fn from_request_data(data: &RequestData) -> Self { 267 - if data.expires_at < chrono::Utc::now() { 268 - return AuthFlowState::Expired; 269 - } 270 - match (&data.did, &data.code) { 271 - (Some(did), Some(code)) => AuthFlowState::Authorized { 272 - did: did.clone(), 273 - device_id: data.device_id.clone(), 274 - code: code.clone(), 275 - }, 276 - (Some(did), None) => AuthFlowState::Authenticated { 277 - did: did.clone(), 278 - device_id: data.device_id.clone(), 279 - }, 280 - (None, _) => AuthFlowState::Pending, 281 - } 282 - } 283 284 - pub fn is_pending(&self) -> bool { 285 - matches!(self, AuthFlowState::Pending) 286 - } 287 288 - pub fn is_authenticated(&self) -> bool { 289 - matches!(self, AuthFlowState::Authenticated { .. }) 290 - } 291 292 - pub fn is_authorized(&self) -> bool { 293 - matches!(self, AuthFlowState::Authorized { .. }) 294 } 295 296 - pub fn is_expired(&self) -> bool { 297 - matches!(self, AuthFlowState::Expired) 298 } 299 300 - pub fn can_authenticate(&self) -> bool { 301 - matches!(self, AuthFlowState::Pending) 302 } 303 304 - pub fn can_authorize(&self) -> bool { 305 - matches!(self, AuthFlowState::Authenticated { .. }) 306 } 307 308 - pub fn can_exchange(&self) -> bool { 309 - matches!(self, AuthFlowState::Authorized { .. }) 310 } 311 312 - pub fn did(&self) -> Option<&str> { 313 match self { 314 - AuthFlowState::Authenticated { did, .. } | AuthFlowState::Authorized { did, .. } => { 315 - Some(did) 316 - } 317 - _ => None, 318 } 319 } 320 321 - pub fn code(&self) -> Option<&str> { 322 match self { 323 - AuthFlowState::Authorized { code, .. } => Some(code), 324 - _ => None, 325 } 326 } 327 - } 328 329 - impl std::fmt::Display for AuthFlowState { 330 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 331 match self { 332 - AuthFlowState::Pending => write!(f, "pending"), 333 - AuthFlowState::Authenticated { did, .. } => write!(f, "authenticated ({})", did), 334 - AuthFlowState::Authorized { did, code, .. } => { 335 - write!( 336 - f, 337 - "authorized ({}, code={}...)", 338 - did, 339 - &code[..8.min(code.len())] 340 - ) 341 - } 342 - AuthFlowState::Expired => write!(f, "expired"), 343 } 344 } 345 } 346 347 #[derive(Debug, Clone, PartialEq, Eq)] 348 pub enum RefreshTokenState { 349 Valid, ··· 406 use chrono::{Duration, Utc}; 407 408 fn make_request_data( 409 - did: Option<String>, 410 - code: Option<String>, 411 expires_in: Duration, 412 ) -> RequestData { 413 RequestData { 414 client_id: "test-client".into(), 415 client_auth: None, 416 parameters: AuthorizationRequestParameters { 417 - response_type: "code".into(), 418 client_id: "test-client".into(), 419 redirect_uri: "https://example.com/callback".into(), 420 scope: Some("atproto".into()), 421 state: None, 422 code_challenge: "test".into(), 423 - code_challenge_method: "S256".into(), 424 response_mode: None, 425 login_hint: None, 426 dpop_jkt: None, ··· 435 } 436 } 437 438 #[test] 439 - fn test_auth_flow_state_pending() { 440 let data = make_request_data(None, None, Duration::minutes(5)); 441 - let state = AuthFlowState::from_request_data(&data); 442 - assert!(state.is_pending()); 443 - assert!(!state.is_authenticated()); 444 - assert!(!state.is_authorized()); 445 - assert!(!state.is_expired()); 446 - assert!(state.can_authenticate()); 447 - assert!(!state.can_authorize()); 448 - assert!(!state.can_exchange()); 449 - assert!(state.did().is_none()); 450 - assert!(state.code().is_none()); 451 } 452 453 #[test] 454 - fn test_auth_flow_state_authenticated() { 455 - let data = make_request_data(Some("did:plc:test".into()), None, Duration::minutes(5)); 456 - let state = AuthFlowState::from_request_data(&data); 457 - assert!(!state.is_pending()); 458 - assert!(state.is_authenticated()); 459 - assert!(!state.is_authorized()); 460 - assert!(!state.is_expired()); 461 - assert!(!state.can_authenticate()); 462 - assert!(state.can_authorize()); 463 - assert!(!state.can_exchange()); 464 - assert_eq!(state.did(), Some("did:plc:test")); 465 - assert!(state.code().is_none()); 466 } 467 468 #[test] 469 - fn test_auth_flow_state_authorized() { 470 let data = make_request_data( 471 - Some("did:plc:test".into()), 472 - Some("auth-code-123".into()), 473 Duration::minutes(5), 474 ); 475 - let state = AuthFlowState::from_request_data(&data); 476 - assert!(!state.is_pending()); 477 - assert!(!state.is_authenticated()); 478 - assert!(state.is_authorized()); 479 - assert!(!state.is_expired()); 480 - assert!(!state.can_authenticate()); 481 - assert!(!state.can_authorize()); 482 - assert!(state.can_exchange()); 483 - assert_eq!(state.did(), Some("did:plc:test")); 484 - assert_eq!(state.code(), Some("auth-code-123")); 485 } 486 487 #[test] 488 - fn test_auth_flow_state_expired() { 489 - let data = make_request_data( 490 - Some("did:plc:test".into()), 491 - Some("code".into()), 492 - Duration::minutes(-1), 493 - ); 494 - let state = AuthFlowState::from_request_data(&data); 495 - assert!(state.is_expired()); 496 - assert!(!state.can_authenticate()); 497 - assert!(!state.can_authorize()); 498 - assert!(!state.can_exchange()); 499 } 500 501 #[test]
··· 1 use chrono::{DateTime, Utc}; 2 use serde::{Deserialize, Serialize}; 3 use serde_json::Value as JsonValue; 4 + use tranquil_types::Did; 5 6 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 7 + #[serde(transparent)] 8 + #[sqlx(transparent)] 9 pub struct RequestId(pub String); 10 11 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 12 + #[serde(transparent)] 13 + #[sqlx(transparent)] 14 pub struct TokenId(pub String); 15 16 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 17 + #[serde(transparent)] 18 + #[sqlx(transparent)] 19 pub struct DeviceId(pub String); 20 21 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 22 + #[serde(transparent)] 23 + #[sqlx(transparent)] 24 pub struct SessionId(pub String); 25 26 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 27 + #[serde(transparent)] 28 + #[sqlx(transparent)] 29 pub struct Code(pub String); 30 31 + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] 32 + #[serde(transparent)] 33 + #[sqlx(transparent)] 34 pub struct RefreshToken(pub String); 35 36 impl RequestId { ··· 95 PrivateKeyJwt { client_assertion: String }, 96 } 97 98 + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] 99 + #[serde(rename_all = "snake_case")] 100 + pub enum ResponseType { 101 + #[default] 102 + Code, 103 + } 104 + 105 + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] 106 + pub enum CodeChallengeMethod { 107 + #[default] 108 + #[serde(rename = "S256")] 109 + S256, 110 + #[serde(rename = "plain")] 111 + Plain, 112 + } 113 + 114 + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] 115 + #[serde(rename_all = "snake_case")] 116 + pub enum ResponseMode { 117 + #[default] 118 + Query, 119 + Fragment, 120 + FormPost, 121 + } 122 + 123 + impl ResponseMode { 124 + pub fn as_str(&self) -> &'static str { 125 + match self { 126 + Self::Query => "query", 127 + Self::Fragment => "fragment", 128 + Self::FormPost => "form_post", 129 + } 130 + } 131 + } 132 + 133 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 134 + #[serde(rename_all = "snake_case")] 135 + pub enum Prompt { 136 + None, 137 + Login, 138 + Consent, 139 + SelectAccount, 140 + Create, 141 + } 142 + 143 + impl Prompt { 144 + pub fn as_str(&self) -> &'static str { 145 + match self { 146 + Self::None => "none", 147 + Self::Login => "login", 148 + Self::Consent => "consent", 149 + Self::SelectAccount => "select_account", 150 + Self::Create => "create", 151 + } 152 + } 153 + } 154 + 155 #[derive(Debug, Clone, Serialize, Deserialize)] 156 pub struct AuthorizationRequestParameters { 157 + pub response_type: ResponseType, 158 pub client_id: String, 159 pub redirect_uri: String, 160 pub scope: Option<String>, 161 pub state: Option<String>, 162 pub code_challenge: String, 163 + pub code_challenge_method: CodeChallengeMethod, 164 + pub response_mode: Option<ResponseMode>, 165 pub login_hint: Option<String>, 166 pub dpop_jkt: Option<String>, 167 + pub prompt: Option<Prompt>, 168 #[serde(flatten)] 169 pub extra: Option<JsonValue>, 170 } ··· 175 pub client_auth: Option<ClientAuth>, 176 pub parameters: AuthorizationRequestParameters, 177 pub expires_at: DateTime<Utc>, 178 + pub did: Option<Did>, 179 + pub device_id: Option<DeviceId>, 180 + pub code: Option<Code>, 181 + pub controller_did: Option<Did>, 182 } 183 184 #[derive(Debug, Clone)] 185 pub struct DeviceData { 186 + pub session_id: SessionId, 187 pub user_agent: Option<String>, 188 pub ip_address: String, 189 pub last_seen_at: DateTime<Utc>, ··· 191 192 #[derive(Debug, Clone)] 193 pub struct TokenData { 194 + pub did: Did, 195 + pub token_id: TokenId, 196 pub created_at: DateTime<Utc>, 197 pub updated_at: DateTime<Utc>, 198 pub expires_at: DateTime<Utc>, 199 pub client_id: String, 200 pub client_auth: ClientAuth, 201 + pub device_id: Option<DeviceId>, 202 pub parameters: AuthorizationRequestParameters, 203 pub details: Option<JsonValue>, 204 + pub code: Option<Code>, 205 + pub current_refresh_token: Option<RefreshToken>, 206 pub scope: Option<String>, 207 + pub controller_did: Option<Did>, 208 } 209 210 #[derive(Debug, Clone, Serialize, Deserialize)] ··· 317 pub keys: Vec<JwkPublicKey>, 318 } 319 320 + 321 + #[derive(Debug, Clone)] 322 + pub struct FlowPending { 323 + pub parameters: AuthorizationRequestParameters, 324 + pub client_id: String, 325 + pub client_auth: Option<ClientAuth>, 326 + pub expires_at: DateTime<Utc>, 327 + pub controller_did: Option<Did>, 328 } 329 330 + #[derive(Debug, Clone)] 331 + pub struct FlowAuthenticated { 332 + pub parameters: AuthorizationRequestParameters, 333 + pub client_id: String, 334 + pub client_auth: Option<ClientAuth>, 335 + pub expires_at: DateTime<Utc>, 336 + pub did: Did, 337 + pub device_id: Option<DeviceId>, 338 + pub controller_did: Option<Did>, 339 + } 340 341 + #[derive(Debug, Clone)] 342 + pub struct FlowAuthorized { 343 + pub parameters: AuthorizationRequestParameters, 344 + pub client_id: String, 345 + pub client_auth: Option<ClientAuth>, 346 + pub expires_at: DateTime<Utc>, 347 + pub did: Did, 348 + pub device_id: Option<DeviceId>, 349 + pub code: Code, 350 + pub controller_did: Option<Did>, 351 + } 352 353 + #[derive(Debug)] 354 + pub struct FlowExpired; 355 + 356 + #[derive(Debug)] 357 + pub struct FlowNotAuthenticated; 358 + 359 + #[derive(Debug)] 360 + pub struct FlowNotAuthorized; 361 362 + #[derive(Debug, Clone)] 363 + pub enum AuthFlow { 364 + Pending(FlowPending), 365 + Authenticated(FlowAuthenticated), 366 + Authorized(FlowAuthorized), 367 + } 368 + 369 + #[derive(Debug, Clone)] 370 + pub enum AuthFlowWithUser { 371 + Authenticated(FlowAuthenticated), 372 + Authorized(FlowAuthorized), 373 + } 374 + 375 + impl AuthFlow { 376 + pub fn from_request_data(data: RequestData) -> Result<Self, FlowExpired> { 377 + if data.expires_at < chrono::Utc::now() { 378 + return Err(FlowExpired); 379 + } 380 + match (data.did, data.code) { 381 + (None, _) => Ok(AuthFlow::Pending(FlowPending { 382 + parameters: data.parameters, 383 + client_id: data.client_id, 384 + client_auth: data.client_auth, 385 + expires_at: data.expires_at, 386 + controller_did: data.controller_did, 387 + })), 388 + (Some(did), None) => Ok(AuthFlow::Authenticated(FlowAuthenticated { 389 + parameters: data.parameters, 390 + client_id: data.client_id, 391 + client_auth: data.client_auth, 392 + expires_at: data.expires_at, 393 + did, 394 + device_id: data.device_id, 395 + controller_did: data.controller_did, 396 + })), 397 + (Some(did), Some(code)) => Ok(AuthFlow::Authorized(FlowAuthorized { 398 + parameters: data.parameters, 399 + client_id: data.client_id, 400 + client_auth: data.client_auth, 401 + expires_at: data.expires_at, 402 + did, 403 + device_id: data.device_id, 404 + code, 405 + controller_did: data.controller_did, 406 + })), 407 + } 408 } 409 410 + pub fn require_user(self) -> Result<AuthFlowWithUser, FlowNotAuthenticated> { 411 + match self { 412 + AuthFlow::Pending(_) => Err(FlowNotAuthenticated), 413 + AuthFlow::Authenticated(a) => Ok(AuthFlowWithUser::Authenticated(a)), 414 + AuthFlow::Authorized(a) => Ok(AuthFlowWithUser::Authorized(a)), 415 + } 416 } 417 418 + pub fn require_authorized(self) -> Result<FlowAuthorized, FlowNotAuthorized> { 419 + match self { 420 + AuthFlow::Authorized(a) => Ok(a), 421 + _ => Err(FlowNotAuthorized), 422 + } 423 } 424 + } 425 426 + impl AuthFlowWithUser { 427 + pub fn did(&self) -> &Did { 428 + match self { 429 + AuthFlowWithUser::Authenticated(a) => &a.did, 430 + AuthFlowWithUser::Authorized(a) => &a.did, 431 + } 432 } 433 434 + pub fn device_id(&self) -> Option<&DeviceId> { 435 + match self { 436 + AuthFlowWithUser::Authenticated(a) => a.device_id.as_ref(), 437 + AuthFlowWithUser::Authorized(a) => a.device_id.as_ref(), 438 + } 439 } 440 441 + pub fn parameters(&self) -> &AuthorizationRequestParameters { 442 match self { 443 + AuthFlowWithUser::Authenticated(a) => &a.parameters, 444 + AuthFlowWithUser::Authorized(a) => &a.parameters, 445 } 446 } 447 448 + pub fn client_id(&self) -> &str { 449 match self { 450 + AuthFlowWithUser::Authenticated(a) => &a.client_id, 451 + AuthFlowWithUser::Authorized(a) => &a.client_id, 452 } 453 } 454 455 + pub fn controller_did(&self) -> Option<&Did> { 456 match self { 457 + AuthFlowWithUser::Authenticated(a) => a.controller_did.as_ref(), 458 + AuthFlowWithUser::Authorized(a) => a.controller_did.as_ref(), 459 } 460 } 461 } 462 463 + 464 #[derive(Debug, Clone, PartialEq, Eq)] 465 pub enum RefreshTokenState { 466 Valid, ··· 523 use chrono::{Duration, Utc}; 524 525 fn make_request_data( 526 + did: Option<Did>, 527 + code: Option<Code>, 528 expires_in: Duration, 529 ) -> RequestData { 530 RequestData { 531 client_id: "test-client".into(), 532 client_auth: None, 533 parameters: AuthorizationRequestParameters { 534 + response_type: ResponseType::Code, 535 client_id: "test-client".into(), 536 redirect_uri: "https://example.com/callback".into(), 537 scope: Some("atproto".into()), 538 state: None, 539 code_challenge: "test".into(), 540 + code_challenge_method: CodeChallengeMethod::S256, 541 response_mode: None, 542 login_hint: None, 543 dpop_jkt: None, ··· 552 } 553 } 554 555 + fn test_did(s: &str) -> Did { 556 + s.parse().expect("valid test DID") 557 + } 558 + 559 + fn test_code(s: &str) -> Code { 560 + Code(s.to_string()) 561 + } 562 + 563 #[test] 564 + fn test_auth_flow_pending() { 565 let data = make_request_data(None, None, Duration::minutes(5)); 566 + let flow = AuthFlow::from_request_data(data).expect("should not be expired"); 567 + assert!(matches!(flow, AuthFlow::Pending(_))); 568 + assert!(flow.clone().require_user().is_err()); 569 + assert!(flow.require_authorized().is_err()); 570 } 571 572 #[test] 573 + fn test_auth_flow_authenticated() { 574 + let did = test_did("did:plc:test"); 575 + let data = make_request_data(Some(did.clone()), None, Duration::minutes(5)); 576 + let flow = AuthFlow::from_request_data(data).expect("should not be expired"); 577 + assert!(matches!(flow, AuthFlow::Authenticated(_))); 578 + let with_user = flow.clone().require_user().expect("should have user"); 579 + assert_eq!(with_user.did(), &did); 580 + assert!(flow.require_authorized().is_err()); 581 } 582 583 #[test] 584 + fn test_auth_flow_authorized() { 585 + let did = test_did("did:plc:test"); 586 + let code = test_code("auth-code-123"); 587 let data = make_request_data( 588 + Some(did.clone()), 589 + Some(code.clone()), 590 Duration::minutes(5), 591 ); 592 + let flow = AuthFlow::from_request_data(data).expect("should not be expired"); 593 + assert!(matches!(flow, AuthFlow::Authorized(_))); 594 + let with_user = flow.clone().require_user().expect("should have user"); 595 + assert_eq!(with_user.did(), &did); 596 + let authorized = flow.require_authorized().expect("should be authorized"); 597 + assert_eq!(authorized.did, did); 598 + assert_eq!(authorized.code, code); 599 } 600 601 #[test] 602 + fn test_auth_flow_expired() { 603 + let did = test_did("did:plc:test"); 604 + let code = test_code("code"); 605 + let data = make_request_data(Some(did), Some(code), Duration::minutes(-1)); 606 + let result = AuthFlow::from_request_data(data); 607 + assert!(result.is_err()); 608 } 609 610 #[test]
+4 -10
crates/tranquil-pds/src/api/admin/account/delete.rs
··· 1 use crate::api::EmptyResponse; 2 - use crate::api::error::ApiError; 3 use crate::auth::{Admin, Auth}; 4 use crate::state::AppState; 5 use crate::types::Did; ··· 9 response::{IntoResponse, Response}, 10 }; 11 use serde::Deserialize; 12 - use tracing::{error, warn}; 13 14 #[derive(Deserialize)] 15 pub struct DeleteAccountInput { ··· 26 .user_repo 27 .get_id_and_handle_by_did(did) 28 .await 29 - .map_err(|e| { 30 - error!("DB error in delete_account: {:?}", e); 31 - ApiError::InternalError(None) 32 - })? 33 .ok_or(ApiError::AccountNotFound) 34 .map(|row| (row.id, row.handle))?; 35 ··· 37 .user_repo 38 .admin_delete_account_complete(user_id, did) 39 .await 40 - .map_err(|e| { 41 - error!("Failed to delete account {}: {:?}", did, e); 42 - ApiError::InternalError(Some("Failed to delete account".into())) 43 - })?; 44 45 if let Err(e) = 46 crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await
··· 1 use crate::api::EmptyResponse; 2 + use crate::api::error::{ApiError, DbResultExt}; 3 use crate::auth::{Admin, Auth}; 4 use crate::state::AppState; 5 use crate::types::Did; ··· 9 response::{IntoResponse, Response}, 10 }; 11 use serde::Deserialize; 12 + use tracing::warn; 13 14 #[derive(Deserialize)] 15 pub struct DeleteAccountInput { ··· 26 .user_repo 27 .get_id_and_handle_by_did(did) 28 .await 29 + .log_db_err("in delete_account")? 30 .ok_or(ApiError::AccountNotFound) 31 .map(|row| (row.id, row.handle))?; 32 ··· 34 .user_repo 35 .admin_delete_account_complete(user_id, did) 36 .await 37 + .log_db_err("deleting account")?; 38 39 if let Err(e) = 40 crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await
+5 -7
crates/tranquil-pds/src/api/admin/account/email.rs
··· 1 - use crate::api::error::{ApiError, AtpJson}; 2 use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use crate::types::Did; 5 use axum::{ 6 Json, ··· 9 response::{IntoResponse, Response}, 10 }; 11 use serde::{Deserialize, Serialize}; 12 - use tracing::{error, warn}; 13 14 #[derive(Deserialize)] 15 #[serde(rename_all = "camelCase")] ··· 39 .user_repo 40 .get_by_did(&input.recipient_did) 41 .await 42 - .map_err(|e| { 43 - error!("DB error in send_email: {:?}", e); 44 - ApiError::InternalError(None) 45 - })? 46 .ok_or(ApiError::AccountNotFound)?; 47 48 let email = user.email.ok_or(ApiError::NoEmail)?; 49 let (user_id, handle) = (user.id, user.handle); 50 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 51 let subject = input 52 .subject 53 .clone()
··· 1 + use crate::api::error::{ApiError, AtpJson, DbResultExt}; 2 use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 + use crate::util::pds_hostname; 5 use crate::types::Did; 6 use axum::{ 7 Json, ··· 10 response::{IntoResponse, Response}, 11 }; 12 use serde::{Deserialize, Serialize}; 13 + use tracing::warn; 14 15 #[derive(Deserialize)] 16 #[serde(rename_all = "camelCase")] ··· 40 .user_repo 41 .get_by_did(&input.recipient_did) 42 .await 43 + .log_db_err("in send_email")? 44 .ok_or(ApiError::AccountNotFound)?; 45 46 let email = user.email.ok_or(ApiError::NoEmail)?; 47 let (user_id, handle) = (user.id, user.handle); 48 + let hostname = pds_hostname(); 49 let subject = input 50 .subject 51 .clone()
+3 -10
crates/tranquil-pds/src/api/admin/account/info.rs
··· 1 - use crate::api::error::ApiError; 2 use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; ··· 10 }; 11 use serde::{Deserialize, Serialize}; 12 use std::collections::HashMap; 13 - use tracing::error; 14 15 #[derive(Deserialize)] 16 pub struct GetAccountInfoParams { ··· 74 .infra_repo 75 .get_admin_account_info_by_did(&params.did) 76 .await 77 - .map_err(|e| { 78 - error!("DB error in get_account_info: {:?}", e); 79 - ApiError::InternalError(None) 80 - })? 81 .ok_or(ApiError::AccountNotFound)?; 82 83 let invited_by = get_invited_by(&state, account.id).await; ··· 214 .infra_repo 215 .get_admin_account_infos_by_dids(&dids_typed) 216 .await 217 - .map_err(|e| { 218 - error!("Failed to fetch account infos: {:?}", e); 219 - ApiError::InternalError(None) 220 - })?; 221 222 let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect(); 223
··· 1 + use crate::api::error::{ApiError, DbResultExt}; 2 use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; ··· 10 }; 11 use serde::{Deserialize, Serialize}; 12 use std::collections::HashMap; 13 14 #[derive(Deserialize)] 15 pub struct GetAccountInfoParams { ··· 73 .infra_repo 74 .get_admin_account_info_by_did(&params.did) 75 .await 76 + .log_db_err("in get_account_info")? 77 .ok_or(ApiError::AccountNotFound)?; 78 79 let invited_by = get_invited_by(&state, account.id).await; ··· 210 .infra_repo 211 .get_admin_account_infos_by_dids(&dids_typed) 212 .await 213 + .log_db_err("fetching account infos")?; 214 215 let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect(); 216
+2 -6
crates/tranquil-pds/src/api/admin/account/search.rs
··· 1 - use crate::api::error::ApiError; 2 use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; ··· 9 response::{IntoResponse, Response}, 10 }; 11 use serde::{Deserialize, Serialize}; 12 - use tracing::error; 13 14 #[derive(Deserialize)] 15 pub struct SearchAccountsParams { ··· 66 limit + 1, 67 ) 68 .await 69 - .map_err(|e| { 70 - error!("DB error in search_accounts: {:?}", e); 71 - ApiError::InternalError(None) 72 - })?; 73 74 let has_more = rows.len() > limit as usize; 75 let accounts: Vec<AccountView> = rows
··· 1 + use crate::api::error::{ApiError, DbResultExt}; 2 use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; ··· 9 response::{IntoResponse, Response}, 10 }; 11 use serde::{Deserialize, Serialize}; 12 13 #[derive(Deserialize)] 14 pub struct SearchAccountsParams { ··· 65 limit + 1, 66 ) 67 .await 68 + .log_db_err("in search_accounts")?; 69 70 let has_more = rows.len() > limit as usize; 71 let accounts: Vec<AccountView> = rows
+2 -2
crates/tranquil-pds/src/api/admin/account/update.rs
··· 2 use crate::api::error::ApiError; 3 use crate::auth::{Admin, Auth}; 4 use crate::state::AppState; 5 use crate::types::{Did, Handle, PlainPassword}; 6 use axum::{ 7 Json, ··· 69 { 70 return Err(ApiError::InvalidHandle(None)); 71 } 72 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 73 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 74 let handle = if !input_handle.contains('.') { 75 format!("{}.{}", input_handle, hostname_for_handles) 76 } else {
··· 2 use crate::api::error::ApiError; 3 use crate::auth::{Admin, Auth}; 4 use crate::state::AppState; 5 + use crate::util::pds_hostname_without_port; 6 use crate::types::{Did, Handle, PlainPassword}; 7 use axum::{ 8 Json, ··· 70 { 71 return Err(ApiError::InvalidHandle(None)); 72 } 73 + let hostname_for_handles = pds_hostname_without_port(); 74 let handle = if !input_handle.contains('.') { 75 format!("{}.{}", input_handle, hostname_for_handles) 76 } else {
+13 -49
crates/tranquil-pds/src/api/admin/config.rs
··· 1 - use crate::api::error::ApiError; 2 use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use axum::{Json, extract::State}; ··· 56 .infra_repo 57 .get_server_configs(keys) 58 .await 59 - .map_err(|e| { 60 - error!("DB error fetching server config: {:?}", e); 61 - ApiError::InternalError(None) 62 - })?; 63 64 let config_map: std::collections::HashMap<String, String> = rows.into_iter().collect(); 65 ··· 92 .infra_repo 93 .upsert_server_config("server_name", trimmed) 94 .await 95 - .map_err(|e| { 96 - error!("DB error upserting server_name: {:?}", e); 97 - ApiError::InternalError(None) 98 - })?; 99 } 100 101 if let Some(ref color) = req.primary_color { ··· 104 .infra_repo 105 .delete_server_config("primary_color") 106 .await 107 - .map_err(|e| { 108 - error!("DB error deleting primary_color: {:?}", e); 109 - ApiError::InternalError(None) 110 - })?; 111 } else if is_valid_hex_color(color) { 112 state 113 .infra_repo 114 .upsert_server_config("primary_color", color) 115 .await 116 - .map_err(|e| { 117 - error!("DB error upserting primary_color: {:?}", e); 118 - ApiError::InternalError(None) 119 - })?; 120 } else { 121 return Err(ApiError::InvalidRequest( 122 "Invalid primary color format (expected #RRGGBB)".into(), ··· 130 .infra_repo 131 .delete_server_config("primary_color_dark") 132 .await 133 - .map_err(|e| { 134 - error!("DB error deleting primary_color_dark: {:?}", e); 135 - ApiError::InternalError(None) 136 - })?; 137 } else if is_valid_hex_color(color) { 138 state 139 .infra_repo 140 .upsert_server_config("primary_color_dark", color) 141 .await 142 - .map_err(|e| { 143 - error!("DB error upserting primary_color_dark: {:?}", e); 144 - ApiError::InternalError(None) 145 - })?; 146 } else { 147 return Err(ApiError::InvalidRequest( 148 "Invalid primary dark color format (expected #RRGGBB)".into(), ··· 156 .infra_repo 157 .delete_server_config("secondary_color") 158 .await 159 - .map_err(|e| { 160 - error!("DB error deleting secondary_color: {:?}", e); 161 - ApiError::InternalError(None) 162 - })?; 163 } else if is_valid_hex_color(color) { 164 state 165 .infra_repo 166 .upsert_server_config("secondary_color", color) 167 .await 168 - .map_err(|e| { 169 - error!("DB error upserting secondary_color: {:?}", e); 170 - ApiError::InternalError(None) 171 - })?; 172 } else { 173 return Err(ApiError::InvalidRequest( 174 "Invalid secondary color format (expected #RRGGBB)".into(), ··· 182 .infra_repo 183 .delete_server_config("secondary_color_dark") 184 .await 185 - .map_err(|e| { 186 - error!("DB error deleting secondary_color_dark: {:?}", e); 187 - ApiError::InternalError(None) 188 - })?; 189 } else if is_valid_hex_color(color) { 190 state 191 .infra_repo 192 .upsert_server_config("secondary_color_dark", color) 193 .await 194 - .map_err(|e| { 195 - error!("DB error upserting secondary_color_dark: {:?}", e); 196 - ApiError::InternalError(None) 197 - })?; 198 } else { 199 return Err(ApiError::InvalidRequest( 200 "Invalid secondary dark color format (expected #RRGGBB)".into(), ··· 235 .infra_repo 236 .delete_server_config("logo_cid") 237 .await 238 - .map_err(|e| { 239 - error!("DB error deleting logo_cid: {:?}", e); 240 - ApiError::InternalError(None) 241 - })?; 242 } else { 243 state 244 .infra_repo 245 .upsert_server_config("logo_cid", logo_cid) 246 .await 247 - .map_err(|e| { 248 - error!("DB error upserting logo_cid: {:?}", e); 249 - ApiError::InternalError(None) 250 - })?; 251 } 252 } 253
··· 1 + use crate::api::error::{ApiError, DbResultExt}; 2 use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use axum::{Json, extract::State}; ··· 56 .infra_repo 57 .get_server_configs(keys) 58 .await 59 + .log_db_err("fetching server config")?; 60 61 let config_map: std::collections::HashMap<String, String> = rows.into_iter().collect(); 62 ··· 89 .infra_repo 90 .upsert_server_config("server_name", trimmed) 91 .await 92 + .log_db_err("upserting server_name")?; 93 } 94 95 if let Some(ref color) = req.primary_color { ··· 98 .infra_repo 99 .delete_server_config("primary_color") 100 .await 101 + .log_db_err("deleting primary_color")?; 102 } else if is_valid_hex_color(color) { 103 state 104 .infra_repo 105 .upsert_server_config("primary_color", color) 106 .await 107 + .log_db_err("upserting primary_color")?; 108 } else { 109 return Err(ApiError::InvalidRequest( 110 "Invalid primary color format (expected #RRGGBB)".into(), ··· 118 .infra_repo 119 .delete_server_config("primary_color_dark") 120 .await 121 + .log_db_err("deleting primary_color_dark")?; 122 } else if is_valid_hex_color(color) { 123 state 124 .infra_repo 125 .upsert_server_config("primary_color_dark", color) 126 .await 127 + .log_db_err("upserting primary_color_dark")?; 128 } else { 129 return Err(ApiError::InvalidRequest( 130 "Invalid primary dark color format (expected #RRGGBB)".into(), ··· 138 .infra_repo 139 .delete_server_config("secondary_color") 140 .await 141 + .log_db_err("deleting secondary_color")?; 142 } else if is_valid_hex_color(color) { 143 state 144 .infra_repo 145 .upsert_server_config("secondary_color", color) 146 .await 147 + .log_db_err("upserting secondary_color")?; 148 } else { 149 return Err(ApiError::InvalidRequest( 150 "Invalid secondary color format (expected #RRGGBB)".into(), ··· 158 .infra_repo 159 .delete_server_config("secondary_color_dark") 160 .await 161 + .log_db_err("deleting secondary_color_dark")?; 162 } else if is_valid_hex_color(color) { 163 state 164 .infra_repo 165 .upsert_server_config("secondary_color_dark", color) 166 .await 167 + .log_db_err("upserting secondary_color_dark")?; 168 } else { 169 return Err(ApiError::InvalidRequest( 170 "Invalid secondary dark color format (expected #RRGGBB)".into(), ··· 205 .infra_repo 206 .delete_server_config("logo_cid") 207 .await 208 + .log_db_err("deleting logo_cid")?; 209 } else { 210 state 211 .infra_repo 212 .upsert_server_config("logo_cid", logo_cid) 213 .await 214 + .log_db_err("upserting logo_cid")?; 215 } 216 } 217
+2 -5
crates/tranquil-pds/src/api/admin/invite.rs
··· 1 use crate::api::EmptyResponse; 2 - use crate::api::error::ApiError; 3 use crate::auth::{Admin, Auth}; 4 use crate::state::AppState; 5 use axum::{ ··· 91 .infra_repo 92 .list_invite_codes(params.cursor.as_deref(), limit, sort_order) 93 .await 94 - .map_err(|e| { 95 - error!("DB error fetching invite codes: {:?}", e); 96 - ApiError::InternalError(None) 97 - })?; 98 99 let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect(); 100 let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect();
··· 1 use crate::api::EmptyResponse; 2 + use crate::api::error::{ApiError, DbResultExt}; 3 use crate::auth::{Admin, Auth}; 4 use crate::state::AppState; 5 use axum::{ ··· 91 .infra_repo 92 .list_invite_codes(params.cursor.as_deref(), limit, sort_order) 93 .await 94 + .log_db_err("fetching invite codes")?; 95 96 let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect(); 97 let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect();
+2 -2
crates/tranquil-pds/src/api/age_assurance.rs
··· 33 } 34 35 async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option<String> { 36 - let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 37 tracing::debug!(?auth_header, "age assurance: extracting token"); 38 39 let extracted = extract_auth_token_from_header(auth_header)?; 40 tracing::debug!("age assurance: got token, validating"); 41 42 - let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok()); 43 let http_uri = "/"; 44 45 let auth_user = match validate_token_with_dpop(
··· 33 } 34 35 async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option<String> { 36 + let auth_header = crate::util::get_header_str(headers, "Authorization"); 37 tracing::debug!(?auth_header, "age assurance: extracting token"); 38 39 let extracted = extract_auth_token_from_header(auth_header)?; 40 tracing::debug!("age assurance: got token, validating"); 41 42 + let dpop_proof = crate::util::get_header_str(headers, "DPoP"); 43 let http_uri = "/"; 44 45 let auth_user = match validate_token_with_dpop(
+21 -70
crates/tranquil-pds/src/api/delegation.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::create_signed_commit; 3 use crate::auth::{Active, Auth}; 4 - use crate::delegation::{DelegationActionType, SCOPE_PRESETS, scopes}; 5 - use crate::state::{AppState, RateLimitKind}; 6 use crate::types::{Did, Handle, Nsid, Rkey}; 7 - use crate::util::extract_client_ip; 8 use axum::{ 9 Json, 10 extract::{Query, State}, 11 - http::{HeaderMap, StatusCode}, 12 response::{IntoResponse, Response}, 13 }; 14 use jacquard_common::types::{integer::LimitedU32, string::Tid}; ··· 93 return Ok(ApiError::ControllerNotFound.into_response()); 94 } 95 96 - match state.delegation_repo.controls_any_accounts(&auth.did).await { 97 - Ok(true) => { 98 - return Ok(ApiError::InvalidDelegation( 99 - "Cannot add controllers to an account that controls other accounts".into(), 100 - ) 101 - .into_response()); 102 - } 103 - Err(e) => { 104 - tracing::error!("Failed to check delegation status: {:?}", e); 105 - return Ok( 106 - ApiError::InternalError(Some("Failed to verify delegation status".into())) 107 - .into_response(), 108 - ); 109 - } 110 - Ok(false) => {} 111 - } 112 113 - match state 114 - .delegation_repo 115 - .has_any_controllers(&input.controller_did) 116 - .await 117 - { 118 - Ok(true) => { 119 - return Ok(ApiError::InvalidDelegation( 120 - "Cannot add a controlled account as a controller".into(), 121 - ) 122 - .into_response()); 123 - } 124 - Err(e) => { 125 - tracing::error!("Failed to check controller status: {:?}", e); 126 - return Ok( 127 - ApiError::InternalError(Some("Failed to verify controller status".into())) 128 - .into_response(), 129 - ); 130 - } 131 - Ok(false) => {} 132 } 133 134 match state ··· 456 457 pub async fn create_delegated_account( 458 State(state): State<AppState>, 459 - headers: HeaderMap, 460 auth: Auth<Active>, 461 Json(input): Json<CreateDelegatedAccountInput>, 462 ) -> Result<Response, ApiError> { 463 - let client_ip = extract_client_ip(&headers); 464 - if !state 465 - .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 466 - .await 467 - { 468 - warn!(ip = %client_ip, "Delegated account creation rate limit exceeded"); 469 - return Ok(ApiError::RateLimitExceeded(Some( 470 - "Too many account creation attempts. Please try again later.".into(), 471 - )) 472 - .into_response()); 473 - } 474 - 475 if let Err(e) = scopes::validate_delegation_scopes(&input.controller_scopes) { 476 return Ok(ApiError::InvalidScopes(e).into_response()); 477 } 478 479 - match state.delegation_repo.has_any_controllers(&auth.did).await { 480 - Ok(true) => { 481 - return Ok(ApiError::InvalidDelegation( 482 - "Cannot create delegated accounts from a controlled account".into(), 483 - ) 484 - .into_response()); 485 - } 486 - Err(e) => { 487 - tracing::error!("Failed to check controller status: {:?}", e); 488 - return Ok( 489 - ApiError::InternalError(Some("Failed to verify controller status".into())) 490 - .into_response(), 491 - ); 492 - } 493 - Ok(false) => {} 494 - } 495 496 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 497 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 498 let pds_suffix = format!(".{}", hostname_for_handles); 499 500 let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) {
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::create_signed_commit; 3 use crate::auth::{Active, Auth}; 4 + use crate::delegation::{ 5 + DelegationActionType, SCOPE_PRESETS, scopes, verify_can_add_controllers, 6 + verify_can_be_controller, verify_can_control_accounts, 7 + }; 8 + use crate::rate_limit::{AccountCreationLimit, RateLimited}; 9 + use crate::state::AppState; 10 use crate::types::{Did, Handle, Nsid, Rkey}; 11 + use crate::util::{pds_hostname, pds_hostname_without_port}; 12 use axum::{ 13 Json, 14 extract::{Query, State}, 15 + http::StatusCode, 16 response::{IntoResponse, Response}, 17 }; 18 use jacquard_common::types::{integer::LimitedU32, string::Tid}; ··· 97 return Ok(ApiError::ControllerNotFound.into_response()); 98 } 99 100 + let _can_add = match verify_can_add_controllers(&state, &auth).await { 101 + Ok(proof) => proof, 102 + Err(response) => return Ok(response), 103 + }; 104 105 + if let Err(response) = verify_can_be_controller(&state, &input.controller_did).await { 106 + return Ok(response); 107 } 108 109 match state ··· 431 432 pub async fn create_delegated_account( 433 State(state): State<AppState>, 434 + _rate_limit: RateLimited<AccountCreationLimit>, 435 auth: Auth<Active>, 436 Json(input): Json<CreateDelegatedAccountInput>, 437 ) -> Result<Response, ApiError> { 438 if let Err(e) = scopes::validate_delegation_scopes(&input.controller_scopes) { 439 return Ok(ApiError::InvalidScopes(e).into_response()); 440 } 441 442 + let _can_control = match verify_can_control_accounts(&state, &auth).await { 443 + Ok(proof) => proof, 444 + Err(response) => return Ok(response), 445 + }; 446 447 + let hostname = pds_hostname(); 448 + let hostname_for_handles = pds_hostname_without_port(); 449 let pds_suffix = format!(".{}", hostname_for_handles); 450 451 let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) {
+19
crates/tranquil-pds/src/api/error.rs
··· 694 } 695 } 696 697 #[allow(clippy::result_large_err)] 698 pub fn parse_did(s: &str) -> Result<tranquil_types::Did, Response> { 699 s.parse() ··· 756 _ => "Invalid request body".to_string(), 757 } 758 }
··· 694 } 695 } 696 697 + impl From<crate::rate_limit::UserRateLimitError> for ApiError { 698 + fn from(e: crate::rate_limit::UserRateLimitError) -> Self { 699 + Self::RateLimitExceeded(e.message) 700 + } 701 + } 702 + 703 #[allow(clippy::result_large_err)] 704 pub fn parse_did(s: &str) -> Result<tranquil_types::Did, Response> { 705 s.parse() ··· 762 _ => "Invalid request body".to_string(), 763 } 764 } 765 + 766 + pub trait DbResultExt<T> { 767 + fn log_db_err(self, ctx: &str) -> Result<T, ApiError>; 768 + } 769 + 770 + impl<T, E: std::fmt::Debug> DbResultExt<T> for Result<T, E> { 771 + fn log_db_err(self, ctx: &str) -> Result<T, ApiError> { 772 + self.map_err(|e| { 773 + tracing::error!("DB error {}: {:?}", ctx, e); 774 + ApiError::DatabaseError 775 + }) 776 + } 777 + }
+16 -41
crates/tranquil-pds/src/api/identity/account.rs
··· 3 use crate::api::repo::record::utils::create_signed_commit; 4 use crate::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token}; 5 use crate::plc::{PlcClient, create_genesis_operation, signing_key_to_did_key}; 6 - use crate::state::{AppState, RateLimitKind}; 7 use crate::types::{Did, Handle, Nsid, PlainPassword, Rkey}; 8 use crate::validation::validate_password; 9 use axum::{ 10 Json, ··· 22 use std::sync::Arc; 23 use tracing::{debug, error, info, warn}; 24 25 - fn extract_client_ip(headers: &HeaderMap) -> String { 26 - if let Some(forwarded) = headers.get("x-forwarded-for") 27 - && let Ok(value) = forwarded.to_str() 28 - && let Some(first_ip) = value.split(',').next() 29 - { 30 - return first_ip.trim().to_string(); 31 - } 32 - if let Some(real_ip) = headers.get("x-real-ip") 33 - && let Ok(value) = real_ip.to_str() 34 - { 35 - return value.trim().to_string(); 36 - } 37 - "unknown".to_string() 38 - } 39 - 40 #[derive(Deserialize)] 41 #[serde(rename_all = "camelCase")] 42 pub struct CreateAccountInput { ··· 68 69 pub async fn create_account( 70 State(state): State<AppState>, 71 headers: HeaderMap, 72 Json(input): Json<CreateAccountInput>, 73 ) -> Response { ··· 84 } else { 85 info!("create_account called"); 86 } 87 - let client_ip = extract_client_ip(&headers); 88 - if !state 89 - .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 90 - .await 91 - { 92 - warn!(ip = %client_ip, "Account creation rate limit exceeded"); 93 - return ApiError::RateLimitExceeded(Some( 94 - "Too many account creation attempts. Please try again later.".into(), 95 - )) 96 - .into_response(); 97 - } 98 99 let migration_auth = if let Some(extracted) = 100 - extract_auth_token_from_header(headers.get("Authorization").and_then(|h| h.to_str().ok())) 101 { 102 let token = extracted.token; 103 if is_service_token(&token) { ··· 143 if (is_migration || is_did_web_byod) 144 && let (Some(provided_did), Some(auth_did)) = (input.did.as_ref(), migration_auth.as_ref()) 145 { 146 - if provided_did != auth_did { 147 info!( 148 "[MIGRATION] createAccount: Service token mismatch - token_did={} provided_did={}", 149 auth_did, provided_did ··· 164 } 165 } 166 167 - let hostname_for_validation = 168 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 169 let pds_suffix = format!(".{}", hostname_for_validation); 170 171 let validated_short_handle = if !input.handle.contains('.') ··· 242 _ => return ApiError::InvalidVerificationChannel.into_response(), 243 }) 244 }; 245 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 246 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 247 let pds_endpoint = format!("https://{}", hostname); 248 let suffix = format!(".{}", hostname_for_handles); 249 let handle = if input.handle.ends_with(&suffix) { ··· 308 } 309 if !is_did_web_byod 310 && let Err(e) = 311 - verify_did_web(d, &hostname, &input.handle, input.signing_key.as_deref()).await 312 { 313 return ApiError::InvalidDid(e).into_response(); 314 } ··· 324 if !is_did_web_byod 325 && let Err(e) = verify_did_web( 326 d, 327 - &hostname, 328 &input.handle, 329 input.signing_key.as_deref(), 330 ) ··· 478 error!("Error creating session: {:?}", e); 479 return ApiError::InternalError(None).into_response(); 480 } 481 - let hostname = 482 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 483 let verification_required = if let Some(ref user_email) = email { 484 let token = 485 crate::auth::verification_token::generate_migration_token(&did, user_email); ··· 491 reactivated.user_id, 492 user_email, 493 &formatted_token, 494 - &hostname, 495 ) 496 .await 497 { ··· 756 warn!("Failed to create default profile for {}: {}", did, e); 757 } 758 } 759 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 760 if !is_migration { 761 if let Some(ref recipient) = verification_recipient { 762 let verification_token = crate::auth::verification_token::generate_signup_token( ··· 772 verification_channel, 773 recipient, 774 &formatted_token, 775 - &hostname, 776 ) 777 .await 778 { ··· 791 user_id, 792 user_email, 793 &formatted_token, 794 - &hostname, 795 ) 796 .await 797 {
··· 3 use crate::api::repo::record::utils::create_signed_commit; 4 use crate::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token}; 5 use crate::plc::{PlcClient, create_genesis_operation, signing_key_to_did_key}; 6 + use crate::rate_limit::{AccountCreationLimit, RateLimited}; 7 + use crate::state::AppState; 8 use crate::types::{Did, Handle, Nsid, PlainPassword, Rkey}; 9 + use crate::util::{pds_hostname, pds_hostname_without_port}; 10 use crate::validation::validate_password; 11 use axum::{ 12 Json, ··· 24 use std::sync::Arc; 25 use tracing::{debug, error, info, warn}; 26 27 #[derive(Deserialize)] 28 #[serde(rename_all = "camelCase")] 29 pub struct CreateAccountInput { ··· 55 56 pub async fn create_account( 57 State(state): State<AppState>, 58 + _rate_limit: RateLimited<AccountCreationLimit>, 59 headers: HeaderMap, 60 Json(input): Json<CreateAccountInput>, 61 ) -> Response { ··· 72 } else { 73 info!("create_account called"); 74 } 75 76 let migration_auth = if let Some(extracted) = 77 + extract_auth_token_from_header(crate::util::get_header_str(&headers, "Authorization")) 78 { 79 let token = extracted.token; 80 if is_service_token(&token) { ··· 120 if (is_migration || is_did_web_byod) 121 && let (Some(provided_did), Some(auth_did)) = (input.did.as_ref(), migration_auth.as_ref()) 122 { 123 + if provided_did != auth_did.as_str() { 124 info!( 125 "[MIGRATION] createAccount: Service token mismatch - token_did={} provided_did={}", 126 auth_did, provided_did ··· 141 } 142 } 143 144 + let hostname_for_validation = pds_hostname_without_port(); 145 let pds_suffix = format!(".{}", hostname_for_validation); 146 147 let validated_short_handle = if !input.handle.contains('.') ··· 218 _ => return ApiError::InvalidVerificationChannel.into_response(), 219 }) 220 }; 221 + let hostname = pds_hostname(); 222 + let hostname_for_handles = pds_hostname_without_port(); 223 let pds_endpoint = format!("https://{}", hostname); 224 let suffix = format!(".{}", hostname_for_handles); 225 let handle = if input.handle.ends_with(&suffix) { ··· 284 } 285 if !is_did_web_byod 286 && let Err(e) = 287 + verify_did_web(d, hostname, &input.handle, input.signing_key.as_deref()).await 288 { 289 return ApiError::InvalidDid(e).into_response(); 290 } ··· 300 if !is_did_web_byod 301 && let Err(e) = verify_did_web( 302 d, 303 + hostname, 304 &input.handle, 305 input.signing_key.as_deref(), 306 ) ··· 454 error!("Error creating session: {:?}", e); 455 return ApiError::InternalError(None).into_response(); 456 } 457 + let hostname = pds_hostname(); 458 let verification_required = if let Some(ref user_email) = email { 459 let token = 460 crate::auth::verification_token::generate_migration_token(&did, user_email); ··· 466 reactivated.user_id, 467 user_email, 468 &formatted_token, 469 + hostname, 470 ) 471 .await 472 { ··· 731 warn!("Failed to create default profile for {}: {}", did, e); 732 } 733 } 734 + let hostname = pds_hostname(); 735 if !is_migration { 736 if let Some(ref recipient) = verification_recipient { 737 let verification_token = crate::auth::verification_token::generate_signup_token( ··· 747 verification_channel, 748 recipient, 749 &formatted_token, 750 + hostname, 751 ) 752 .await 753 { ··· 766 user_id, 767 user_email, 768 &formatted_token, 769 + hostname, 770 ) 771 .await 772 {
+23 -29
crates/tranquil-pds/src/api/identity/did.rs
··· 1 use crate::api::{ApiError, DidResponse, EmptyResponse}; 2 use crate::auth::{Auth, NotTakendown}; 3 use crate::plc::signing_key_to_did_key; 4 use crate::state::AppState; 5 use crate::types::Handle; 6 use axum::{ 7 Json, 8 extract::{Path, Query, State}, ··· 101 } 102 103 pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -> Response { 104 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 105 - let host_header = headers 106 - .get("host") 107 - .and_then(|h| h.to_str().ok()) 108 - .unwrap_or(&hostname); 109 let host_without_port = host_header.split(':').next().unwrap_or(host_header); 110 - let hostname_without_port = hostname.split(':').next().unwrap_or(&hostname); 111 if host_without_port != hostname_without_port 112 && host_without_port.ends_with(&format!(".{}", hostname_without_port)) 113 { 114 let handle = host_without_port 115 .strip_suffix(&format!(".{}", hostname_without_port)) 116 .unwrap_or(host_without_port); 117 - return serve_subdomain_did_doc(&state, handle, &hostname).await; 118 } 119 let did = if hostname.contains(':') { 120 format!("did:web:{}", hostname.replace(':', "%3A")) ··· 257 } 258 259 pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response { 260 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 261 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 262 let current_handle = format!("{}.{}", handle, hostname_for_handles); 263 let current_handle_typed: Handle = match current_handle.parse() { 264 Ok(h) => h, ··· 531 ApiError::AuthenticationFailed(Some("OAuth tokens cannot get DID credentials".into())) 532 })?; 533 534 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 535 let pds_endpoint = format!("https://{}", hostname); 536 let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes) 537 .map_err(|_| ApiError::InternalError(None))?; ··· 585 return Ok(e); 586 } 587 let did = auth.did.clone(); 588 - if !state 589 - .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 590 - .await 591 - { 592 - return Err(ApiError::RateLimitExceeded(Some( 593 - "Too many handle updates. Try again later.".into(), 594 - ))); 595 - } 596 - if !state 597 - .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 598 - .await 599 - { 600 - return Err(ApiError::RateLimitExceeded(Some( 601 - "Daily handle update limit exceeded.".into(), 602 - ))); 603 - } 604 let user_row = state 605 .user_repo 606 .get_id_and_handle_by_did(&did) ··· 639 "Inappropriate language in handle".into(), 640 ))); 641 } 642 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 643 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 644 let suffix = format!(".{}", hostname_for_handles); 645 let is_service_domain = 646 crate::handle::is_service_domain_handle(&new_handle, hostname_for_handles); ··· 772 } 773 774 pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response { 775 - let host = match headers.get("host").and_then(|h| h.to_str().ok()) { 776 Some(h) => h, 777 None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(), 778 };
··· 1 use crate::api::{ApiError, DidResponse, EmptyResponse}; 2 use crate::auth::{Auth, NotTakendown}; 3 use crate::plc::signing_key_to_did_key; 4 + use crate::rate_limit::{HandleUpdateDailyLimit, HandleUpdateLimit, check_user_rate_limit_with_message}; 5 use crate::state::AppState; 6 use crate::types::Handle; 7 + use crate::util::{get_header_str, pds_hostname, pds_hostname_without_port}; 8 use axum::{ 9 Json, 10 extract::{Path, Query, State}, ··· 103 } 104 105 pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -> Response { 106 + let hostname = pds_hostname(); 107 + let hostname_without_port = pds_hostname_without_port(); 108 + let host_header = get_header_str(&headers, "host").unwrap_or(hostname); 109 let host_without_port = host_header.split(':').next().unwrap_or(host_header); 110 if host_without_port != hostname_without_port 111 && host_without_port.ends_with(&format!(".{}", hostname_without_port)) 112 { 113 let handle = host_without_port 114 .strip_suffix(&format!(".{}", hostname_without_port)) 115 .unwrap_or(host_without_port); 116 + return serve_subdomain_did_doc(&state, handle, hostname).await; 117 } 118 let did = if hostname.contains(':') { 119 format!("did:web:{}", hostname.replace(':', "%3A")) ··· 256 } 257 258 pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<String>) -> Response { 259 + let hostname = pds_hostname(); 260 + let hostname_for_handles = pds_hostname_without_port(); 261 let current_handle = format!("{}.{}", handle, hostname_for_handles); 262 let current_handle_typed: Handle = match current_handle.parse() { 263 Ok(h) => h, ··· 530 ApiError::AuthenticationFailed(Some("OAuth tokens cannot get DID credentials".into())) 531 })?; 532 533 + let hostname = pds_hostname(); 534 let pds_endpoint = format!("https://{}", hostname); 535 let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes) 536 .map_err(|_| ApiError::InternalError(None))?; ··· 584 return Ok(e); 585 } 586 let did = auth.did.clone(); 587 + let _rate_limit = check_user_rate_limit_with_message::<HandleUpdateLimit>( 588 + &state, 589 + &did, 590 + "Too many handle updates. Try again later.", 591 + ) 592 + .await?; 593 + let _daily_rate_limit = check_user_rate_limit_with_message::<HandleUpdateDailyLimit>( 594 + &state, 595 + &did, 596 + "Daily handle update limit exceeded.", 597 + ) 598 + .await?; 599 let user_row = state 600 .user_repo 601 .get_id_and_handle_by_did(&did) ··· 634 "Inappropriate language in handle".into(), 635 ))); 636 } 637 + let hostname_for_handles = pds_hostname_without_port(); 638 let suffix = format!(".{}", hostname_for_handles); 639 let is_service_domain = 640 crate::handle::is_service_domain_handle(&new_handle, hostname_for_handles); ··· 766 } 767 768 pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response { 769 + let host = match crate::util::get_header_str(&headers, "host") { 770 Some(h) => h, 771 None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(), 772 };
+7 -12
crates/tranquil-pds/src/api/identity/plc/request.rs
··· 1 use crate::api::EmptyResponse; 2 - use crate::api::error::ApiError; 3 use crate::auth::{Auth, Permissive}; 4 use crate::state::AppState; 5 use axum::{ 6 extract::State, 7 response::{IntoResponse, Response}, 8 }; 9 use chrono::{Duration, Utc}; 10 - use tracing::{error, info, warn}; 11 12 fn generate_plc_token() -> String { 13 crate::util::generate_token_code() ··· 28 .user_repo 29 .get_id_by_did(&auth.did) 30 .await 31 - .map_err(|e| { 32 - error!("DB error: {:?}", e); 33 - ApiError::InternalError(None) 34 - })? 35 .ok_or(ApiError::AccountNotFound)?; 36 37 let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await; ··· 41 .infra_repo 42 .insert_plc_token(user_id, &plc_token, expires_at) 43 .await 44 - .map_err(|e| { 45 - error!("Failed to create PLC token: {:?}", e); 46 - ApiError::InternalError(None) 47 - })?; 48 49 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 50 if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation( 51 state.user_repo.as_ref(), 52 state.infra_repo.as_ref(), 53 user_id, 54 &plc_token, 55 - &hostname, 56 ) 57 .await 58 {
··· 1 use crate::api::EmptyResponse; 2 + use crate::api::error::{ApiError, DbResultExt}; 3 use crate::auth::{Auth, Permissive}; 4 use crate::state::AppState; 5 + use crate::util::pds_hostname; 6 use axum::{ 7 extract::State, 8 response::{IntoResponse, Response}, 9 }; 10 use chrono::{Duration, Utc}; 11 + use tracing::{info, warn}; 12 13 fn generate_plc_token() -> String { 14 crate::util::generate_token_code() ··· 29 .user_repo 30 .get_id_by_did(&auth.did) 31 .await 32 + .log_db_err("fetching user id")? 33 .ok_or(ApiError::AccountNotFound)?; 34 35 let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await; ··· 39 .infra_repo 40 .insert_plc_token(user_id, &plc_token, expires_at) 41 .await 42 + .log_db_err("creating PLC token")?; 43 44 + let hostname = pds_hostname(); 45 if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation( 46 state.user_repo.as_ref(), 47 state.infra_repo.as_ref(), 48 user_id, 49 &plc_token, 50 + hostname, 51 ) 52 .await 53 {
+4 -12
crates/tranquil-pds/src/api/identity/plc/sign.rs
··· 1 use crate::api::ApiError; 2 use crate::auth::{Auth, Permissive}; 3 use crate::circuit_breaker::with_circuit_breaker; ··· 64 .user_repo 65 .get_id_by_did(did) 66 .await 67 - .map_err(|e| { 68 - error!("DB error: {:?}", e); 69 - ApiError::InternalError(None) 70 - })? 71 .ok_or(ApiError::AccountNotFound)?; 72 73 let token_expiry = state 74 .infra_repo 75 .get_plc_token_expiry(user_id, token) 76 .await 77 - .map_err(|e| { 78 - error!("DB error: {:?}", e); 79 - ApiError::InternalError(None) 80 - })? 81 .ok_or_else(|| ApiError::InvalidToken(Some("Invalid or expired token".into())))?; 82 83 if Utc::now() > token_expiry { ··· 88 .user_repo 89 .get_user_key_by_id(user_id) 90 .await 91 - .map_err(|e| { 92 - error!("DB error: {:?}", e); 93 - ApiError::InternalError(None) 94 - })? 95 .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; 96 97 let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
··· 1 + use crate::api::error::DbResultExt; 2 use crate::api::ApiError; 3 use crate::auth::{Auth, Permissive}; 4 use crate::circuit_breaker::with_circuit_breaker; ··· 65 .user_repo 66 .get_id_by_did(did) 67 .await 68 + .log_db_err("fetching user id")? 69 .ok_or(ApiError::AccountNotFound)?; 70 71 let token_expiry = state 72 .infra_repo 73 .get_plc_token_expiry(user_id, token) 74 .await 75 + .log_db_err("fetching PLC token expiry")? 76 .ok_or_else(|| ApiError::InvalidToken(Some("Invalid or expired token".into())))?; 77 78 if Utc::now() > token_expiry { ··· 83 .user_repo 84 .get_user_key_by_id(user_id) 85 .await 86 + .log_db_err("fetching user key")? 87 .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; 88 89 let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
+5 -9
crates/tranquil-pds/src/api/identity/plc/submit.rs
··· 1 use crate::api::{ApiError, EmptyResponse}; 2 use crate::auth::{Auth, Permissive}; 3 use crate::circuit_breaker::with_circuit_breaker; 4 use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; 5 use crate::state::AppState; 6 use axum::{ 7 Json, 8 extract::State, ··· 40 .map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?; 41 42 let op = &input.operation; 43 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 44 let public_url = format!("https://{}", hostname); 45 let user = state 46 .user_repo 47 .get_id_and_handle_by_did(did) 48 .await 49 - .map_err(|e| { 50 - error!("DB error: {:?}", e); 51 - ApiError::InternalError(None) 52 - })? 53 .ok_or(ApiError::AccountNotFound)?; 54 55 let key_row = state 56 .user_repo 57 .get_user_key_by_id(user.id) 58 .await 59 - .map_err(|e| { 60 - error!("DB error: {:?}", e); 61 - ApiError::InternalError(None) 62 - })? 63 .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; 64 65 let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
··· 1 + use crate::api::error::DbResultExt; 2 use crate::api::{ApiError, EmptyResponse}; 3 use crate::auth::{Auth, Permissive}; 4 use crate::circuit_breaker::with_circuit_breaker; 5 use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; 6 use crate::state::AppState; 7 + use crate::util::pds_hostname; 8 use axum::{ 9 Json, 10 extract::State, ··· 42 .map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?; 43 44 let op = &input.operation; 45 + let hostname = pds_hostname(); 46 let public_url = format!("https://{}", hostname); 47 let user = state 48 .user_repo 49 .get_id_and_handle_by_did(did) 50 .await 51 + .log_db_err("fetching user")? 52 .ok_or(ApiError::AccountNotFound)?; 53 54 let key_row = state 55 .user_repo 56 .get_user_key_by_id(user.id) 57 .await 58 + .log_db_err("fetching user key")? 59 .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; 60 61 let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
+3 -2
crates/tranquil-pds/src/api/notification_prefs.rs
··· 1 use crate::api::error::ApiError; 2 use crate::auth::{Active, Auth}; 3 use crate::state::AppState; 4 use axum::{ 5 Json, 6 extract::State, ··· 145 let formatted_token = crate::auth::verification_token::format_token_for_display(&token); 146 147 if channel == "email" { 148 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 149 let handle_str = handle.unwrap_or("user"); 150 crate::comms::comms_repo::enqueue_email_update( 151 state.infra_repo.as_ref(), ··· 153 identifier, 154 handle_str, 155 &formatted_token, 156 - &hostname, 157 ) 158 .await 159 .map_err(|e| format!("Failed to enqueue email notification: {}", e))?;
··· 1 use crate::api::error::ApiError; 2 use crate::auth::{Active, Auth}; 3 use crate::state::AppState; 4 + use crate::util::pds_hostname; 5 use axum::{ 6 Json, 7 extract::State, ··· 146 let formatted_token = crate::auth::verification_token::format_token_for_display(&token); 147 148 if channel == "email" { 149 + let hostname = pds_hostname(); 150 let handle_str = handle.unwrap_or("user"); 151 crate::comms::comms_repo::enqueue_email_update( 152 state.infra_repo.as_ref(), ··· 154 identifier, 155 handle_str, 156 &formatted_token, 157 + hostname, 158 ) 159 .await 160 .map_err(|e| format!("Failed to enqueue email notification: {}", e))?;
+4 -7
crates/tranquil-pds/src/api/proxy.rs
··· 3 use crate::api::error::ApiError; 4 use crate::api::proxy_client::proxy_client; 5 use crate::state::AppState; 6 use axum::{ 7 body::Bytes, 8 extract::{RawQuery, Request, State}, ··· 191 .into_response(); 192 } 193 194 - let Some(proxy_header) = headers 195 - .get("atproto-proxy") 196 - .and_then(|h| h.to_str().ok()) 197 - .map(String::from) 198 - else { 199 return ApiError::InvalidRequest("Missing required atproto-proxy header".into()) 200 .into_response(); 201 }; ··· 217 218 let mut auth_header_val = headers.get("Authorization").cloned(); 219 if let Some(extracted) = crate::auth::extract_auth_token_from_header( 220 - headers.get("Authorization").and_then(|h| h.to_str().ok()), 221 ) { 222 let token = extracted.token; 223 - let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok()); 224 let http_uri = crate::util::build_full_url(&uri.to_string()); 225 226 match crate::auth::validate_token_with_dpop(
··· 3 use crate::api::error::ApiError; 4 use crate::api::proxy_client::proxy_client; 5 use crate::state::AppState; 6 + use crate::util::get_header_str; 7 use axum::{ 8 body::Bytes, 9 extract::{RawQuery, Request, State}, ··· 192 .into_response(); 193 } 194 195 + let Some(proxy_header) = get_header_str(&headers, "atproto-proxy").map(String::from) else { 196 return ApiError::InvalidRequest("Missing required atproto-proxy header".into()) 197 .into_response(); 198 }; ··· 214 215 let mut auth_header_val = headers.get("Authorization").cloned(); 216 if let Some(extracted) = crate::auth::extract_auth_token_from_header( 217 + crate::util::get_header_str(&headers, "Authorization"), 218 ) { 219 let token = extracted.token; 220 + let dpop_proof = crate::util::get_header_str(&headers, "DPoP"); 221 let http_uri = crate::util::build_full_url(&uri.to_string()); 222 223 match crate::auth::validate_token_with_dpop(
+13 -26
crates/tranquil-pds/src/api/repo/blob.rs
··· 1 - use crate::api::error::ApiError; 2 - use crate::auth::{Auth, AuthAny, NotTakendown, Permissive}; 3 use crate::delegation::DelegationActionType; 4 use crate::state::AppState; 5 use crate::types::{CidLink, Did}; 6 - use crate::util::get_max_blob_size; 7 use axum::body::Body; 8 use axum::{ 9 Json, ··· 56 if user.status.is_takendown() { 57 return Err(ApiError::AccountTakedown); 58 } 59 - let mime_type_for_check = headers 60 - .get("content-type") 61 - .and_then(|h| h.to_str().ok()) 62 - .unwrap_or("application/octet-stream"); 63 - if let Err(e) = crate::auth::scope_check::check_blob_scope( 64 - user.is_oauth(), 65 - user.scope.as_deref(), 66 - mime_type_for_check, 67 - ) { 68 - return Ok(e); 69 - } 70 (user.did.clone(), user.controller_did.clone()) 71 } 72 }; ··· 80 return Err(ApiError::Forbidden); 81 } 82 83 - let client_mime_hint = headers 84 - .get("content-type") 85 - .and_then(|h| h.to_str().ok()) 86 - .unwrap_or("application/octet-stream"); 87 88 let user_id = state 89 .user_repo ··· 232 .user_repo 233 .get_by_did(did) 234 .await 235 - .map_err(|e| { 236 - error!("DB error fetching user: {:?}", e); 237 - ApiError::InternalError(None) 238 - })? 239 .ok_or(ApiError::InternalError(None))?; 240 241 let limit = params.limit.unwrap_or(500).clamp(1, 1000); ··· 244 .blob_repo 245 .list_missing_blobs(user.id, cursor, limit + 1) 246 .await 247 - .map_err(|e| { 248 - error!("DB error fetching missing blobs: {:?}", e); 249 - ApiError::InternalError(None) 250 - })?; 251 252 let has_more = missing.len() > limit as usize; 253 let blobs: Vec<RecordBlob> = missing
··· 1 + use crate::api::error::{ApiError, DbResultExt}; 2 + use crate::auth::{Auth, AuthAny, NotTakendown, Permissive, VerifyScope}; 3 use crate::delegation::DelegationActionType; 4 use crate::state::AppState; 5 use crate::types::{CidLink, Did}; 6 + use crate::util::{get_header_str, get_max_blob_size}; 7 use axum::body::Body; 8 use axum::{ 9 Json, ··· 56 if user.status.is_takendown() { 57 return Err(ApiError::AccountTakedown); 58 } 59 + let mime_type_for_check = 60 + get_header_str(&headers, "content-type").unwrap_or("application/octet-stream"); 61 + let _scope_proof = match user.verify_blob_upload(mime_type_for_check) { 62 + Ok(proof) => proof, 63 + Err(e) => return Ok(e.into_response()), 64 + }; 65 (user.did.clone(), user.controller_did.clone()) 66 } 67 }; ··· 75 return Err(ApiError::Forbidden); 76 } 77 78 + let client_mime_hint = 79 + get_header_str(&headers, "content-type").unwrap_or("application/octet-stream"); 80 81 let user_id = state 82 .user_repo ··· 225 .user_repo 226 .get_by_did(did) 227 .await 228 + .log_db_err("fetching user")? 229 .ok_or(ApiError::InternalError(None))?; 230 231 let limit = params.limit.unwrap_or(500).clamp(1, 1000); ··· 234 .blob_repo 235 .list_missing_blobs(user.id, cursor, limit + 1) 236 .await 237 + .log_db_err("fetching missing blobs")?; 238 239 let has_more = missing.len() > limit as usize; 240 let blobs: Vec<RecordBlob> = missing
+2 -5
crates/tranquil-pds/src/api/repo/import.rs
··· 1 use crate::api::EmptyResponse; 2 - use crate::api::error::ApiError; 3 use crate::api::repo::record::create_signed_commit; 4 use crate::auth::{Auth, NotTakendown}; 5 use crate::state::AppState; ··· 49 .user_repo 50 .get_by_did(did) 51 .await 52 - .map_err(|e| { 53 - error!("DB error fetching user: {:?}", e); 54 - ApiError::InternalError(None) 55 - })? 56 .ok_or(ApiError::AccountNotFound)?; 57 if user.takedown_ref.is_some() { 58 return Err(ApiError::AccountTakedown);
··· 1 use crate::api::EmptyResponse; 2 + use crate::api::error::{ApiError, DbResultExt}; 3 use crate::api::repo::record::create_signed_commit; 4 use crate::auth::{Auth, NotTakendown}; 5 use crate::state::AppState; ··· 49 .user_repo 50 .get_by_did(did) 51 .await 52 + .log_db_err("fetching user")? 53 .ok_or(ApiError::AccountNotFound)?; 54 if user.takedown_ref.is_some() { 55 return Err(ApiError::AccountTakedown);
+2 -2
crates/tranquil-pds/src/api/repo/meta.rs
··· 1 use crate::api::error::ApiError; 2 use crate::state::AppState; 3 use crate::types::AtIdentifier; 4 use axum::{ 5 Json, 6 extract::{Query, State}, ··· 18 State(state): State<AppState>, 19 Query(input): Query<DescribeRepoInput>, 20 ) -> Response { 21 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 22 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 23 let user_row = if input.repo.is_did() { 24 let did: crate::types::Did = match input.repo.as_str().parse() { 25 Ok(d) => d,
··· 1 use crate::api::error::ApiError; 2 use crate::state::AppState; 3 use crate::types::AtIdentifier; 4 + use crate::util::pds_hostname_without_port; 5 use axum::{ 6 Json, 7 extract::{Query, State}, ··· 19 State(state): State<AppState>, 20 Query(input): Query<DescribeRepoInput>, 21 ) -> Response { 22 + let hostname_for_handles = pds_hostname_without_port(); 23 let user_row = if input.repo.is_did() { 24 let did: crate::types::Did = match input.repo.as_str().parse() { 25 Ok(d) => d,
+16 -35
crates/tranquil-pds/src/api/repo/record/batch.rs
··· 1 use super::validation::validate_record_with_status; 2 use crate::api::error::ApiError; 3 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; 4 - use crate::auth::{Active, Auth}; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 271 input.writes.len() 272 ); 273 let did = auth.did.clone(); 274 - let is_oauth = auth.is_oauth(); 275 - let scope = auth.scope.clone(); 276 let controller_did = auth.controller_did.clone(); 277 if input.repo.as_str() != did { 278 return Err(ApiError::InvalidRepo( ··· 310 ))); 311 } 312 313 - let has_custom_scope = scope 314 - .as_ref() 315 - .map(|s| s != "com.atproto.access") 316 - .unwrap_or(false); 317 - if is_oauth || has_custom_scope { 318 use std::collections::HashSet; 319 let create_collections: HashSet<&Nsid> = input 320 .writes ··· 350 }) 351 .collect(); 352 353 - let scope_checks = create_collections 354 - .iter() 355 - .map(|c| (crate::oauth::RepoAction::Create, c)) 356 - .chain( 357 - update_collections 358 - .iter() 359 - .map(|c| (crate::oauth::RepoAction::Update, c)), 360 - ) 361 - .chain( 362 - delete_collections 363 - .iter() 364 - .map(|c| (crate::oauth::RepoAction::Delete, c)), 365 - ); 366 - 367 - if let Some(err) = scope_checks 368 - .filter_map(|(action, collection)| { 369 - crate::auth::scope_check::check_repo_scope( 370 - is_oauth, 371 - scope.as_deref(), 372 - action, 373 - collection, 374 - ) 375 - .err() 376 - }) 377 - .next() 378 - { 379 - return Ok(err); 380 } 381 } 382
··· 1 use super::validation::validate_record_with_status; 2 use crate::api::error::ApiError; 3 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; 4 + use crate::auth::{Active, Auth, VerifyScope}; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 271 input.writes.len() 272 ); 273 let did = auth.did.clone(); 274 let controller_did = auth.controller_did.clone(); 275 if input.repo.as_str() != did { 276 return Err(ApiError::InvalidRepo( ··· 308 ))); 309 } 310 311 + { 312 use std::collections::HashSet; 313 let create_collections: HashSet<&Nsid> = input 314 .writes ··· 344 }) 345 .collect(); 346 347 + for collection in &create_collections { 348 + if let Err(e) = auth.verify_repo_create(collection) { 349 + return Ok(e.into_response()); 350 + } 351 + } 352 + for collection in &update_collections { 353 + if let Err(e) = auth.verify_repo_update(collection) { 354 + return Ok(e.into_response()); 355 + } 356 + } 357 + for collection in &delete_collections { 358 + if let Err(e) = auth.verify_repo_delete(collection) { 359 + return Ok(e.into_response()); 360 + } 361 } 362 } 363
+6 -10
crates/tranquil-pds/src/api/repo/record/delete.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log}; 3 use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; 4 - use crate::auth::{Active, Auth}; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 43 auth: Auth<Active>, 44 Json(input): Json<DeleteRecordInput>, 45 ) -> Result<Response, crate::api::error::ApiError> { 46 let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 47 Ok(res) => res, 48 Err(err_res) => return Ok(err_res), 49 }; 50 51 - if let Err(e) = crate::auth::scope_check::check_repo_scope( 52 - repo_auth.is_oauth, 53 - repo_auth.scope.as_deref(), 54 - crate::oauth::RepoAction::Delete, 55 - &input.collection, 56 - ) { 57 - return Ok(e); 58 - } 59 - 60 let did = repo_auth.did; 61 let user_id = repo_auth.user_id; 62 let current_root_cid = repo_auth.current_root_cid;
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log}; 3 use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; 4 + use crate::auth::{Active, Auth, VerifyScope}; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 43 auth: Auth<Active>, 44 Json(input): Json<DeleteRecordInput>, 45 ) -> Result<Response, crate::api::error::ApiError> { 46 + let _scope_proof = match auth.verify_repo_delete(&input.collection) { 47 + Ok(proof) => proof, 48 + Err(e) => return Ok(e.into_response()), 49 + }; 50 + 51 let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 52 Ok(res) => res, 53 Err(err_res) => return Ok(err_res), 54 }; 55 56 let did = repo_auth.did; 57 let user_id = repo_auth.user_id; 58 let current_root_cid = repo_auth.current_root_cid;
+3 -4
crates/tranquil-pds/src/api/repo/record/read.rs
··· 1 use crate::api::error::ApiError; 2 use crate::state::AppState; 3 use crate::types::{AtIdentifier, Nsid, Rkey}; 4 use axum::{ 5 Json, 6 extract::{Query, State}, ··· 58 _headers: HeaderMap, 59 Query(input): Query<GetRecordInput>, 60 ) -> Response { 61 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 62 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 63 let user_id_opt = if input.repo.is_did() { 64 let did: crate::types::Did = match input.repo.as_str().parse() { 65 Ok(d) => d, ··· 157 State(state): State<AppState>, 158 Query(input): Query<ListRecordsInput>, 159 ) -> Response { 160 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 161 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 162 let user_id_opt = if input.repo.is_did() { 163 let did: crate::types::Did = match input.repo.as_str().parse() { 164 Ok(d) => d,
··· 1 use crate::api::error::ApiError; 2 use crate::state::AppState; 3 use crate::types::{AtIdentifier, Nsid, Rkey}; 4 + use crate::util::pds_hostname_without_port; 5 use axum::{ 6 Json, 7 extract::{Query, State}, ··· 59 _headers: HeaderMap, 60 Query(input): Query<GetRecordInput>, 61 ) -> Response { 62 + let hostname_for_handles = pds_hostname_without_port(); 63 let user_id_opt = if input.repo.is_did() { 64 let did: crate::types::Did = match input.repo.as_str().parse() { 65 Ok(d) => d, ··· 157 State(state): State<AppState>, 158 Query(input): Query<ListRecordsInput>, 159 ) -> Response { 160 + let hostname_for_handles = pds_hostname_without_port(); 161 let user_id_opt = if input.repo.is_did() { 162 let did: crate::types::Did = match input.repo.as_str().parse() { 163 Ok(d) => d,
+15 -27
crates/tranquil-pds/src/api/repo/record/write.rs
··· 3 use crate::api::repo::record::utils::{ 4 CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, 5 }; 6 - use crate::auth::{Active, Auth}; 7 use crate::delegation::DelegationActionType; 8 use crate::repo::tracking::TrackingBlockStore; 9 use crate::state::AppState; ··· 127 auth: Auth<Active>, 128 Json(input): Json<CreateRecordInput>, 129 ) -> Result<Response, crate::api::error::ApiError> { 130 let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 131 Ok(res) => res, 132 Err(err_res) => return Ok(err_res), 133 }; 134 135 - if let Err(e) = crate::auth::scope_check::check_repo_scope( 136 - repo_auth.is_oauth, 137 - repo_auth.scope.as_deref(), 138 - crate::oauth::RepoAction::Create, 139 - &input.collection, 140 - ) { 141 - return Ok(e); 142 - } 143 - 144 let did = repo_auth.did; 145 let user_id = repo_auth.user_id; 146 let current_root_cid = repo_auth.current_root_cid; ··· 434 auth: Auth<Active>, 435 Json(input): Json<PutRecordInput>, 436 ) -> Result<Response, crate::api::error::ApiError> { 437 let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 438 Ok(res) => res, 439 Err(err_res) => return Ok(err_res), 440 }; 441 442 - if let Err(e) = crate::auth::scope_check::check_repo_scope( 443 - repo_auth.is_oauth, 444 - repo_auth.scope.as_deref(), 445 - crate::oauth::RepoAction::Create, 446 - &input.collection, 447 - ) { 448 - return Ok(e); 449 - } 450 - if let Err(e) = crate::auth::scope_check::check_repo_scope( 451 - repo_auth.is_oauth, 452 - repo_auth.scope.as_deref(), 453 - crate::oauth::RepoAction::Update, 454 - &input.collection, 455 - ) { 456 - return Ok(e); 457 - } 458 - 459 let did = repo_auth.did; 460 let user_id = repo_auth.user_id; 461 let current_root_cid = repo_auth.current_root_cid;
··· 3 use crate::api::repo::record::utils::{ 4 CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, 5 }; 6 + use crate::auth::{Active, Auth, VerifyScope}; 7 use crate::delegation::DelegationActionType; 8 use crate::repo::tracking::TrackingBlockStore; 9 use crate::state::AppState; ··· 127 auth: Auth<Active>, 128 Json(input): Json<CreateRecordInput>, 129 ) -> Result<Response, crate::api::error::ApiError> { 130 + let _scope_proof = match auth.verify_repo_create(&input.collection) { 131 + Ok(proof) => proof, 132 + Err(e) => return Ok(e.into_response()), 133 + }; 134 + 135 let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 136 Ok(res) => res, 137 Err(err_res) => return Ok(err_res), 138 }; 139 140 let did = repo_auth.did; 141 let user_id = repo_auth.user_id; 142 let current_root_cid = repo_auth.current_root_cid; ··· 430 auth: Auth<Active>, 431 Json(input): Json<PutRecordInput>, 432 ) -> Result<Response, crate::api::error::ApiError> { 433 + let _create_proof = match auth.verify_repo_create(&input.collection) { 434 + Ok(proof) => proof, 435 + Err(e) => return Ok(e.into_response()), 436 + }; 437 + let _update_proof = match auth.verify_repo_update(&input.collection) { 438 + Ok(proof) => proof, 439 + Err(e) => return Ok(e.into_response()), 440 + }; 441 + 442 let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 443 Ok(res) => res, 444 Err(err_res) => return Ok(err_res), 445 }; 446 447 let did = repo_auth.did; 448 let user_id = repo_auth.user_id; 449 let current_root_cid = repo_auth.current_root_cid;
+18 -26
crates/tranquil-pds/src/api/server/account_status.rs
··· 1 use crate::api::EmptyResponse; 2 - use crate::api::error::ApiError; 3 - use crate::auth::{Auth, NotTakendown, Permissive}; 4 use crate::cache::Cache; 5 use crate::plc::PlcClient; 6 use crate::state::AppState; 7 use crate::types::PlainPassword; 8 use axum::{ 9 Json, 10 extract::State, ··· 130 did: &crate::types::Did, 131 with_retry: bool, 132 ) -> Result<(), ApiError> { 133 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 134 let expected_endpoint = format!("https://{}", hostname); 135 136 if did.as_str().starts_with("did:plc:") { ··· 219 .and_then(|v| v.get("atproto")) 220 .and_then(|k| k.as_str()); 221 222 - let user_key = user_repo.get_user_key_by_did(did).await.map_err(|e| { 223 - error!("Failed to fetch user key: {:?}", e); 224 - ApiError::InternalError(None) 225 - })?; 226 227 if let Some(key_info) = user_key { 228 let key_bytes = ··· 523 State(state): State<AppState>, 524 auth: Auth<NotTakendown>, 525 ) -> Result<Response, ApiError> { 526 - let did = &auth.did; 527 - 528 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await { 529 - return Ok(crate::api::server::reauth::legacy_mfa_required_response( 530 - &*state.user_repo, 531 - &*state.session_repo, 532 - did, 533 - ) 534 - .await); 535 - } 536 537 let user_id = state 538 .user_repo 539 - .get_id_by_did(did) 540 .await 541 .ok() 542 .flatten() ··· 545 let expires_at = Utc::now() + Duration::minutes(15); 546 state 547 .infra_repo 548 - .create_deletion_request(&confirmation_token, did, expires_at) 549 .await 550 - .map_err(|e| { 551 - error!("DB error creating deletion token: {:?}", e); 552 - ApiError::InternalError(None) 553 - })?; 554 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 555 if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion( 556 state.user_repo.as_ref(), 557 state.infra_repo.as_ref(), 558 user_id, 559 &confirmation_token, 560 - &hostname, 561 ) 562 .await 563 { 564 warn!("Failed to enqueue account deletion notification: {:?}", e); 565 } 566 - info!("Account deletion requested for user {}", did); 567 Ok(EmptyResponse::ok().into_response()) 568 } 569
··· 1 use crate::api::EmptyResponse; 2 + use crate::api::error::{ApiError, DbResultExt}; 3 + use crate::auth::{Auth, NotTakendown, Permissive, require_legacy_session_mfa}; 4 use crate::cache::Cache; 5 use crate::plc::PlcClient; 6 use crate::state::AppState; 7 use crate::types::PlainPassword; 8 + use crate::util::pds_hostname; 9 use axum::{ 10 Json, 11 extract::State, ··· 131 did: &crate::types::Did, 132 with_retry: bool, 133 ) -> Result<(), ApiError> { 134 + let hostname = pds_hostname(); 135 let expected_endpoint = format!("https://{}", hostname); 136 137 if did.as_str().starts_with("did:plc:") { ··· 220 .and_then(|v| v.get("atproto")) 221 .and_then(|k| k.as_str()); 222 223 + let user_key = user_repo 224 + .get_user_key_by_did(did) 225 + .await 226 + .log_db_err("fetching user key")?; 227 228 if let Some(key_info) = user_key { 229 let key_bytes = ··· 524 State(state): State<AppState>, 525 auth: Auth<NotTakendown>, 526 ) -> Result<Response, ApiError> { 527 + let session_mfa = match require_legacy_session_mfa(&state, &auth).await { 528 + Ok(proof) => proof, 529 + Err(response) => return Ok(response), 530 + }; 531 532 let user_id = state 533 .user_repo 534 + .get_id_by_did(session_mfa.did()) 535 .await 536 .ok() 537 .flatten() ··· 540 let expires_at = Utc::now() + Duration::minutes(15); 541 state 542 .infra_repo 543 + .create_deletion_request(&confirmation_token, session_mfa.did(), expires_at) 544 .await 545 + .log_db_err("creating deletion token")?; 546 + let hostname = pds_hostname(); 547 if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion( 548 state.user_repo.as_ref(), 549 state.infra_repo.as_ref(), 550 user_id, 551 &confirmation_token, 552 + hostname, 553 ) 554 .await 555 { 556 warn!("Failed to enqueue account deletion notification: {:?}", e); 557 } 558 + info!("Account deletion requested for user {}", session_mfa.did()); 559 Ok(EmptyResponse::ok().into_response()) 560 } 561
+13 -46
crates/tranquil-pds/src/api/server/app_password.rs
··· 1 use crate::api::EmptyResponse; 2 - use crate::api::error::ApiError; 3 use crate::auth::{Auth, NotTakendown, Permissive, generate_app_password}; 4 use crate::delegation::{DelegationActionType, intersect_scopes}; 5 - use crate::state::{AppState, RateLimitKind}; 6 use axum::{ 7 Json, 8 extract::State, 9 - http::HeaderMap, 10 response::{IntoResponse, Response}, 11 }; 12 use serde::{Deserialize, Serialize}; 13 use serde_json::json; 14 - use tracing::{error, warn}; 15 use tranquil_db_traits::AppPasswordCreate; 16 17 #[derive(Serialize)] ··· 39 .user_repo 40 .get_by_did(&auth.did) 41 .await 42 - .map_err(|e| { 43 - error!("DB error getting user: {:?}", e); 44 - ApiError::InternalError(None) 45 - })? 46 .ok_or(ApiError::AccountNotFound)?; 47 48 let rows = state 49 .session_repo 50 .list_app_passwords(user.id) 51 .await 52 - .map_err(|e| { 53 - error!("DB error listing app passwords: {:?}", e); 54 - ApiError::InternalError(None) 55 - })?; 56 let passwords: Vec<AppPassword> = rows 57 .iter() 58 .map(|row| AppPassword { ··· 89 90 pub async fn create_app_password( 91 State(state): State<AppState>, 92 - headers: HeaderMap, 93 auth: Auth<NotTakendown>, 94 Json(input): Json<CreateAppPasswordInput>, 95 ) -> Result<Response, ApiError> { 96 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 97 - if !state 98 - .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 99 - .await 100 - { 101 - warn!(ip = %client_ip, "App password creation rate limit exceeded"); 102 - return Err(ApiError::RateLimitExceeded(None)); 103 - } 104 - 105 let user = state 106 .user_repo 107 .get_by_did(&auth.did) 108 .await 109 - .map_err(|e| { 110 - error!("DB error getting user: {:?}", e); 111 - ApiError::InternalError(None) 112 - })? 113 .ok_or(ApiError::AccountNotFound)?; 114 115 let name = input.name.trim(); ··· 121 .session_repo 122 .get_app_password_by_name(user.id, name) 123 .await 124 - .map_err(|e| { 125 - error!("DB error checking app password: {:?}", e); 126 - ApiError::InternalError(None) 127 - })? 128 .is_some() 129 { 130 return Err(ApiError::DuplicateAppPassword); ··· 187 .session_repo 188 .create_app_password(&create_data) 189 .await 190 - .map_err(|e| { 191 - error!("DB error creating app password: {:?}", e); 192 - ApiError::InternalError(None) 193 - })?; 194 195 if let Some(ref controller) = controller_did { 196 let _ = state ··· 234 .user_repo 235 .get_by_did(&auth.did) 236 .await 237 - .map_err(|e| { 238 - error!("DB error getting user: {:?}", e); 239 - ApiError::InternalError(None) 240 - })? 241 .ok_or(ApiError::AccountNotFound)?; 242 243 let name = input.name.trim(); ··· 255 .session_repo 256 .delete_sessions_by_app_password(&auth.did, name) 257 .await 258 - .map_err(|e| { 259 - error!("DB error revoking sessions for app password: {:?}", e); 260 - ApiError::InternalError(None) 261 - })?; 262 263 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 264 let cache_key = format!("auth:session:{}:{}", &auth.did, jti); ··· 273 .session_repo 274 .delete_app_password(user.id, name) 275 .await 276 - .map_err(|e| { 277 - error!("DB error revoking app password: {:?}", e); 278 - ApiError::InternalError(None) 279 - })?; 280 281 Ok(EmptyResponse::ok().into_response()) 282 }
··· 1 use crate::api::EmptyResponse; 2 + use crate::api::error::{ApiError, DbResultExt}; 3 use crate::auth::{Auth, NotTakendown, Permissive, generate_app_password}; 4 use crate::delegation::{DelegationActionType, intersect_scopes}; 5 + use crate::rate_limit::{AppPasswordLimit, RateLimited}; 6 + use crate::state::AppState; 7 use axum::{ 8 Json, 9 extract::State, 10 response::{IntoResponse, Response}, 11 }; 12 use serde::{Deserialize, Serialize}; 13 use serde_json::json; 14 + use tracing::error; 15 use tranquil_db_traits::AppPasswordCreate; 16 17 #[derive(Serialize)] ··· 39 .user_repo 40 .get_by_did(&auth.did) 41 .await 42 + .log_db_err("getting user")? 43 .ok_or(ApiError::AccountNotFound)?; 44 45 let rows = state 46 .session_repo 47 .list_app_passwords(user.id) 48 .await 49 + .log_db_err("listing app passwords")?; 50 let passwords: Vec<AppPassword> = rows 51 .iter() 52 .map(|row| AppPassword { ··· 83 84 pub async fn create_app_password( 85 State(state): State<AppState>, 86 + _rate_limit: RateLimited<AppPasswordLimit>, 87 auth: Auth<NotTakendown>, 88 Json(input): Json<CreateAppPasswordInput>, 89 ) -> Result<Response, ApiError> { 90 let user = state 91 .user_repo 92 .get_by_did(&auth.did) 93 .await 94 + .log_db_err("getting user")? 95 .ok_or(ApiError::AccountNotFound)?; 96 97 let name = input.name.trim(); ··· 103 .session_repo 104 .get_app_password_by_name(user.id, name) 105 .await 106 + .log_db_err("checking app password")? 107 .is_some() 108 { 109 return Err(ApiError::DuplicateAppPassword); ··· 166 .session_repo 167 .create_app_password(&create_data) 168 .await 169 + .log_db_err("creating app password")?; 170 171 if let Some(ref controller) = controller_did { 172 let _ = state ··· 210 .user_repo 211 .get_by_did(&auth.did) 212 .await 213 + .log_db_err("getting user")? 214 .ok_or(ApiError::AccountNotFound)?; 215 216 let name = input.name.trim(); ··· 228 .session_repo 229 .delete_sessions_by_app_password(&auth.did, name) 230 .await 231 + .log_db_err("revoking sessions for app password")?; 232 233 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 234 let cache_key = format!("auth:session:{}:{}", &auth.did, jti); ··· 243 .session_repo 244 .delete_app_password(user.id, name) 245 .await 246 + .log_db_err("revoking app password")?; 247 248 Ok(EmptyResponse::ok().into_response()) 249 }
+21 -92
crates/tranquil-pds/src/api/server/email.rs
··· 1 - use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 3 use crate::auth::{Auth, NotTakendown}; 4 - use crate::state::{AppState, RateLimitKind}; 5 use axum::{ 6 Json, 7 extract::State, ··· 44 45 pub async fn request_email_update( 46 State(state): State<AppState>, 47 - headers: axum::http::HeaderMap, 48 auth: Auth<NotTakendown>, 49 input: Option<Json<RequestEmailUpdateInput>>, 50 ) -> Result<Response, ApiError> { 51 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 52 - if !state 53 - .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 54 - .await 55 - { 56 - warn!(ip = %client_ip, "Email update rate limit exceeded"); 57 - return Err(ApiError::RateLimitExceeded(None)); 58 - } 59 - 60 if let Err(e) = crate::auth::scope_check::check_account_scope( 61 auth.is_oauth(), 62 auth.scope.as_deref(), ··· 70 .user_repo 71 .get_email_info_by_did(&auth.did) 72 .await 73 - .map_err(|e| { 74 - error!("DB error: {:?}", e); 75 - ApiError::InternalError(None) 76 - })? 77 .ok_or(ApiError::AccountNotFound)?; 78 79 let Some(current_email) = user.email else { ··· 111 } 112 } 113 114 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 115 if let Err(e) = crate::comms::comms_repo::enqueue_email_update_token( 116 state.user_repo.as_ref(), 117 state.infra_repo.as_ref(), 118 user.id, 119 &code, 120 &formatted_code, 121 - &hostname, 122 ) 123 .await 124 { ··· 139 140 pub async fn confirm_email( 141 State(state): State<AppState>, 142 - headers: axum::http::HeaderMap, 143 auth: Auth<NotTakendown>, 144 Json(input): Json<ConfirmEmailInput>, 145 ) -> Result<Response, ApiError> { 146 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 147 - if !state 148 - .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 149 - .await 150 - { 151 - warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 152 - return Err(ApiError::RateLimitExceeded(None)); 153 - } 154 - 155 if let Err(e) = crate::auth::scope_check::check_account_scope( 156 auth.is_oauth(), 157 auth.scope.as_deref(), ··· 166 .user_repo 167 .get_email_info_by_did(did) 168 .await 169 - .map_err(|e| { 170 - error!("DB error: {:?}", e); 171 - ApiError::InternalError(None) 172 - })? 173 .ok_or(ApiError::AccountNotFound)?; 174 175 let Some(ref email) = user.email else { ··· 213 .user_repo 214 .set_email_verified(user.id, true) 215 .await 216 - .map_err(|e| { 217 - error!("DB error confirming email: {:?}", e); 218 - ApiError::InternalError(None) 219 - })?; 220 221 info!("Email confirmed for user {}", user.id); 222 Ok(EmptyResponse::ok().into_response()) ··· 250 .user_repo 251 .get_email_info_by_did(did) 252 .await 253 - .map_err(|e| { 254 - error!("DB error: {:?}", e); 255 - ApiError::InternalError(None) 256 - })? 257 .ok_or(ApiError::AccountNotFound)?; 258 259 let user_id = user.id; ··· 325 .user_repo 326 .update_email(user_id, &new_email) 327 .await 328 - .map_err(|e| { 329 - error!("DB error updating email: {:?}", e); 330 - ApiError::InternalError(None) 331 - })?; 332 333 let verification_token = 334 crate::auth::verification_token::generate_signup_token(did, "email", &new_email); 335 let formatted_token = 336 crate::auth::verification_token::format_token_for_display(&verification_token); 337 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 338 if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification( 339 state.infra_repo.as_ref(), 340 user_id, 341 "email", 342 &new_email, 343 &formatted_token, 344 - &hostname, 345 ) 346 .await 347 { ··· 371 372 pub async fn check_email_verified( 373 State(state): State<AppState>, 374 - headers: axum::http::HeaderMap, 375 Json(input): Json<CheckEmailVerifiedInput>, 376 ) -> Response { 377 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 378 - if !state 379 - .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 380 - .await 381 - { 382 - return ApiError::RateLimitExceeded(None).into_response(); 383 - } 384 - 385 match state 386 .user_repo 387 .check_email_verified_by_identifier(&input.identifier) ··· 403 404 pub async fn authorize_email_update( 405 State(state): State<AppState>, 406 - headers: axum::http::HeaderMap, 407 axum::extract::Query(query): axum::extract::Query<AuthorizeEmailUpdateQuery>, 408 ) -> Response { 409 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 410 - if !state 411 - .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 412 - .await 413 - { 414 - return ApiError::RateLimitExceeded(None).into_response(); 415 - } 416 - 417 let verified = crate::auth::verification_token::verify_token_signature(&query.token); 418 419 let token_data = match verified { ··· 488 489 info!(did = %did, "Email update authorized via link click"); 490 491 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 492 let redirect_url = format!( 493 "https://{}/app/verify?type=email-authorize-success", 494 hostname ··· 499 500 pub async fn check_email_update_status( 501 State(state): State<AppState>, 502 - headers: axum::http::HeaderMap, 503 auth: Auth<NotTakendown>, 504 ) -> Result<Response, ApiError> { 505 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 506 - if !state 507 - .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 508 - .await 509 - { 510 - return Err(ApiError::RateLimitExceeded(None)); 511 - } 512 - 513 if let Err(e) = crate::auth::scope_check::check_account_scope( 514 auth.is_oauth(), 515 auth.scope.as_deref(), ··· 549 550 pub async fn check_email_in_use( 551 State(state): State<AppState>, 552 - headers: axum::http::HeaderMap, 553 Json(input): Json<CheckEmailInUseInput>, 554 ) -> Response { 555 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 556 - if !state 557 - .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 558 - .await 559 - { 560 - return ApiError::RateLimitExceeded(None).into_response(); 561 - } 562 - 563 let email = input.email.trim().to_lowercase(); 564 if email.is_empty() { 565 return ApiError::InvalidRequest("email is required".into()).into_response(); ··· 587 588 pub async fn check_comms_channel_in_use( 589 State(state): State<AppState>, 590 - headers: axum::http::HeaderMap, 591 Json(input): Json<CheckCommsChannelInUseInput>, 592 ) -> Response { 593 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 594 - if !state 595 - .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 596 - .await 597 - { 598 - return ApiError::RateLimitExceeded(None).into_response(); 599 - } 600 - 601 let channel = match input.channel.to_lowercase().as_str() { 602 "email" => CommsChannel::Email, 603 "discord" => CommsChannel::Discord,
··· 1 + use crate::api::error::{ApiError, DbResultExt}; 2 use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 3 use crate::auth::{Auth, NotTakendown}; 4 + use crate::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit}; 5 + use crate::state::AppState; 6 + use crate::util::pds_hostname; 7 use axum::{ 8 Json, 9 extract::State, ··· 46 47 pub async fn request_email_update( 48 State(state): State<AppState>, 49 + _rate_limit: RateLimited<EmailUpdateLimit>, 50 auth: Auth<NotTakendown>, 51 input: Option<Json<RequestEmailUpdateInput>>, 52 ) -> Result<Response, ApiError> { 53 if let Err(e) = crate::auth::scope_check::check_account_scope( 54 auth.is_oauth(), 55 auth.scope.as_deref(), ··· 63 .user_repo 64 .get_email_info_by_did(&auth.did) 65 .await 66 + .log_db_err("getting email info")? 67 .ok_or(ApiError::AccountNotFound)?; 68 69 let Some(current_email) = user.email else { ··· 101 } 102 } 103 104 + let hostname = pds_hostname(); 105 if let Err(e) = crate::comms::comms_repo::enqueue_email_update_token( 106 state.user_repo.as_ref(), 107 state.infra_repo.as_ref(), 108 user.id, 109 &code, 110 &formatted_code, 111 + hostname, 112 ) 113 .await 114 { ··· 129 130 pub async fn confirm_email( 131 State(state): State<AppState>, 132 + _rate_limit: RateLimited<EmailUpdateLimit>, 133 auth: Auth<NotTakendown>, 134 Json(input): Json<ConfirmEmailInput>, 135 ) -> Result<Response, ApiError> { 136 if let Err(e) = crate::auth::scope_check::check_account_scope( 137 auth.is_oauth(), 138 auth.scope.as_deref(), ··· 147 .user_repo 148 .get_email_info_by_did(did) 149 .await 150 + .log_db_err("getting email info")? 151 .ok_or(ApiError::AccountNotFound)?; 152 153 let Some(ref email) = user.email else { ··· 191 .user_repo 192 .set_email_verified(user.id, true) 193 .await 194 + .log_db_err("confirming email")?; 195 196 info!("Email confirmed for user {}", user.id); 197 Ok(EmptyResponse::ok().into_response()) ··· 225 .user_repo 226 .get_email_info_by_did(did) 227 .await 228 + .log_db_err("getting email info")? 229 .ok_or(ApiError::AccountNotFound)?; 230 231 let user_id = user.id; ··· 297 .user_repo 298 .update_email(user_id, &new_email) 299 .await 300 + .log_db_err("updating email")?; 301 302 let verification_token = 303 crate::auth::verification_token::generate_signup_token(did, "email", &new_email); 304 let formatted_token = 305 crate::auth::verification_token::format_token_for_display(&verification_token); 306 + let hostname = pds_hostname(); 307 if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification( 308 state.infra_repo.as_ref(), 309 user_id, 310 "email", 311 &new_email, 312 &formatted_token, 313 + hostname, 314 ) 315 .await 316 { ··· 340 341 pub async fn check_email_verified( 342 State(state): State<AppState>, 343 + _rate_limit: RateLimited<VerificationCheckLimit>, 344 Json(input): Json<CheckEmailVerifiedInput>, 345 ) -> Response { 346 match state 347 .user_repo 348 .check_email_verified_by_identifier(&input.identifier) ··· 364 365 pub async fn authorize_email_update( 366 State(state): State<AppState>, 367 + _rate_limit: RateLimited<VerificationCheckLimit>, 368 axum::extract::Query(query): axum::extract::Query<AuthorizeEmailUpdateQuery>, 369 ) -> Response { 370 let verified = crate::auth::verification_token::verify_token_signature(&query.token); 371 372 let token_data = match verified { ··· 441 442 info!(did = %did, "Email update authorized via link click"); 443 444 + let hostname = pds_hostname(); 445 let redirect_url = format!( 446 "https://{}/app/verify?type=email-authorize-success", 447 hostname ··· 452 453 pub async fn check_email_update_status( 454 State(state): State<AppState>, 455 + _rate_limit: RateLimited<VerificationCheckLimit>, 456 auth: Auth<NotTakendown>, 457 ) -> Result<Response, ApiError> { 458 if let Err(e) = crate::auth::scope_check::check_account_scope( 459 auth.is_oauth(), 460 auth.scope.as_deref(), ··· 494 495 pub async fn check_email_in_use( 496 State(state): State<AppState>, 497 + _rate_limit: RateLimited<VerificationCheckLimit>, 498 Json(input): Json<CheckEmailInUseInput>, 499 ) -> Response { 500 let email = input.email.trim().to_lowercase(); 501 if email.is_empty() { 502 return ApiError::InvalidRequest("email is required".into()).into_response(); ··· 524 525 pub async fn check_comms_channel_in_use( 526 State(state): State<AppState>, 527 + _rate_limit: RateLimited<VerificationCheckLimit>, 528 Json(input): Json<CheckCommsChannelInUseInput>, 529 ) -> Response { 530 let channel = match input.channel.to_lowercase().as_str() { 531 "email" => CommsChannel::Email, 532 "discord" => CommsChannel::Discord,
+5 -9
crates/tranquil-pds/src/api/server/invite.rs
··· 1 use crate::api::ApiError; 2 use crate::auth::{Admin, Auth, NotTakendown}; 3 use crate::state::AppState; 4 use crate::types::Did; 5 use axum::{ 6 Json, 7 extract::State, ··· 24 } 25 26 fn gen_invite_code() -> String { 27 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 28 let hostname_prefix = hostname.replace('.', "-"); 29 format!("{}-{}", hostname_prefix, gen_random_token()) 30 } ··· 121 .user_repo 122 .get_any_admin_user_id() 123 .await 124 - .map_err(|e| { 125 - error!("DB error looking up admin user: {:?}", e); 126 - ApiError::InternalError(None) 127 - })? 128 .ok_or_else(|| { 129 error!("No admin user found to create invite codes"); 130 ApiError::InternalError(None) ··· 202 .infra_repo 203 .get_invite_codes_for_account(&auth.did) 204 .await 205 - .map_err(|e| { 206 - error!("DB error fetching invite codes: {:?}", e); 207 - ApiError::InternalError(None) 208 - })?; 209 210 let filtered_codes: Vec<_> = codes_info 211 .into_iter()
··· 1 + use crate::api::error::DbResultExt; 2 use crate::api::ApiError; 3 use crate::auth::{Admin, Auth, NotTakendown}; 4 use crate::state::AppState; 5 use crate::types::Did; 6 + use crate::util::pds_hostname; 7 use axum::{ 8 Json, 9 extract::State, ··· 26 } 27 28 fn gen_invite_code() -> String { 29 + let hostname = pds_hostname(); 30 let hostname_prefix = hostname.replace('.', "-"); 31 format!("{}-{}", hostname_prefix, gen_random_token()) 32 } ··· 123 .user_repo 124 .get_any_admin_user_id() 125 .await 126 + .log_db_err("looking up admin user")? 127 .ok_or_else(|| { 128 error!("No admin user found to create invite codes"); 129 ApiError::InternalError(None) ··· 201 .infra_repo 202 .get_invite_codes_for_account(&auth.did) 203 .await 204 + .log_db_err("fetching invite codes")?; 205 206 let filtered_codes: Vec<_> = codes_info 207 .into_iter()
+3 -2
crates/tranquil-pds/src/api/server/meta.rs
··· 1 use crate::state::AppState; 2 use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; 3 use serde_json::json; 4 ··· 30 } 31 32 pub async fn describe_server() -> impl IntoResponse { 33 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 34 let domains_str = 35 - std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| pds_hostname.clone()); 36 let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect(); 37 let invite_code_required = std::env::var("INVITE_CODE_REQUIRED") 38 .map(|v| v == "true" || v == "1")
··· 1 use crate::state::AppState; 2 + use crate::util::pds_hostname; 3 use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; 4 use serde_json::json; 5 ··· 31 } 32 33 pub async fn describe_server() -> impl IntoResponse { 34 + let pds_hostname = pds_hostname(); 35 let domains_str = 36 + std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| pds_hostname.to_string()); 37 let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect(); 38 let invite_code_required = std::env::var("INVITE_CODE_REQUIRED") 39 .map(|v| v == "true" || v == "1")
+6 -13
crates/tranquil-pds/src/api/server/migration.rs
··· 1 use crate::api::ApiError; 2 use crate::auth::{Active, Auth}; 3 use crate::state::AppState; 4 use axum::{ 5 Json, 6 extract::State, ··· 49 .user_repo 50 .get_user_for_did_doc(&auth.did) 51 .await 52 - .map_err(|e| { 53 - tracing::error!("DB error getting user: {:?}", e); 54 - ApiError::InternalError(None) 55 - })? 56 .ok_or(ApiError::AccountNotFound)?; 57 58 if let Some(ref methods) = input.verification_methods { ··· 107 .user_repo 108 .upsert_did_web_overrides(user.id, verification_methods_json, also_known_as) 109 .await 110 - .map_err(|e| { 111 - tracing::error!("DB error upserting did_web_overrides: {:?}", e); 112 - ApiError::InternalError(None) 113 - })?; 114 115 if let Some(ref endpoint) = input.service_endpoint { 116 let endpoint_clean = endpoint.trim().trim_end_matches('/'); ··· 118 .user_repo 119 .update_migrated_to_pds(&auth.did, endpoint_clean) 120 .await 121 - .map_err(|e| { 122 - tracing::error!("DB error updating service endpoint: {:?}", e); 123 - ApiError::InternalError(None) 124 - })?; 125 } 126 127 let did_doc = build_did_document(&state, &auth.did).await; ··· 154 } 155 156 async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value { 157 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 158 159 let user = match state.user_repo.get_user_for_did_doc_build(did).await { 160 Ok(Some(row)) => row,
··· 1 + use crate::api::error::DbResultExt; 2 use crate::api::ApiError; 3 use crate::auth::{Active, Auth}; 4 use crate::state::AppState; 5 + use crate::util::pds_hostname; 6 use axum::{ 7 Json, 8 extract::State, ··· 51 .user_repo 52 .get_user_for_did_doc(&auth.did) 53 .await 54 + .log_db_err("getting user")? 55 .ok_or(ApiError::AccountNotFound)?; 56 57 if let Some(ref methods) = input.verification_methods { ··· 106 .user_repo 107 .upsert_did_web_overrides(user.id, verification_methods_json, also_known_as) 108 .await 109 + .log_db_err("upserting did_web_overrides")?; 110 111 if let Some(ref endpoint) = input.service_endpoint { 112 let endpoint_clean = endpoint.trim().trim_end_matches('/'); ··· 114 .user_repo 115 .update_migrated_to_pds(&auth.did, endpoint_clean) 116 .await 117 + .log_db_err("updating service endpoint")?; 118 } 119 120 let did_doc = build_did_document(&state, &auth.did).await; ··· 147 } 148 149 async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value { 150 + let hostname = pds_hostname(); 151 152 let user = match state.user_repo.get_user_for_did_doc_build(did).await { 153 Ok(Some(row)) => row,
+17 -64
crates/tranquil-pds/src/api/server/passkey_account.rs
··· 19 20 use crate::api::repo::record::utils::create_signed_commit; 21 use crate::auth::{ServiceTokenVerifier, generate_app_password, is_service_token}; 22 - use crate::state::{AppState, RateLimitKind}; 23 use crate::types::{Did, Handle, Nsid, PlainPassword, Rkey}; 24 use crate::validation::validate_password; 25 26 - fn extract_client_ip(headers: &HeaderMap) -> String { 27 - if let Some(forwarded) = headers.get("x-forwarded-for") 28 - && let Ok(value) = forwarded.to_str() 29 - && let Some(first_ip) = value.split(',').next() 30 - { 31 - return first_ip.trim().to_string(); 32 - } 33 - if let Some(real_ip) = headers.get("x-real-ip") 34 - && let Ok(value) = real_ip.to_str() 35 - { 36 - return value.trim().to_string(); 37 - } 38 - "unknown".to_string() 39 - } 40 - 41 fn generate_setup_token() -> String { 42 let mut rng = rand::thread_rng(); 43 (0..32) ··· 80 81 pub async fn create_passkey_account( 82 State(state): State<AppState>, 83 headers: HeaderMap, 84 Json(input): Json<CreatePasskeyAccountInput>, 85 ) -> Response { 86 - let client_ip = extract_client_ip(&headers); 87 - if !state 88 - .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 89 - .await 90 - { 91 - warn!(ip = %client_ip, "Account creation rate limit exceeded"); 92 - return ApiError::RateLimitExceeded(Some( 93 - "Too many account creation attempts. Please try again later.".into(), 94 - )) 95 - .into_response(); 96 - } 97 - 98 let byod_auth = if let Some(extracted) = crate::auth::extract_auth_token_from_header( 99 - headers.get("Authorization").and_then(|h| h.to_str().ok()), 100 ) { 101 let token = extracted.token; 102 if is_service_token(&token) { ··· 135 .map(|d| d.starts_with("did:web:")) 136 .unwrap_or(false); 137 138 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 139 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 140 let pds_suffix = format!(".{}", hostname_for_handles); 141 142 let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) { ··· 268 } 269 if is_byod_did_web { 270 if let Some(ref auth_did) = byod_auth 271 - && d != auth_did 272 { 273 return ApiError::AuthorizationError(format!( 274 "Service token issuer {} does not match DID {}", ··· 280 } else { 281 if let Err(e) = crate::api::identity::did::verify_did_web( 282 d, 283 - &hostname, 284 &input.handle, 285 input.signing_key.as_deref(), 286 ) ··· 296 if let Some(ref auth_did) = byod_auth { 297 if let Some(ref provided_did) = input.did { 298 if provided_did.starts_with("did:plc:") { 299 - if provided_did != auth_did { 300 return ApiError::AuthorizationError(format!( 301 "Service token issuer {} does not match DID {}", 302 auth_did, provided_did ··· 521 verification_channel, 522 &verification_recipient, 523 &formatted_token, 524 - &hostname, 525 ) 526 .await 527 { ··· 626 return ApiError::InvalidToken(None).into_response(); 627 } 628 629 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 630 - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 631 - Ok(w) => w, 632 - Err(e) => { 633 - error!("Failed to create WebAuthn config: {:?}", e); 634 - return ApiError::InternalError(None).into_response(); 635 - } 636 - }; 637 638 let reg_state = match state 639 .user_repo ··· 768 return ApiError::InvalidToken(None).into_response(); 769 } 770 771 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 772 - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 773 - Ok(w) => w, 774 - Err(e) => { 775 - error!("Failed to create WebAuthn config: {:?}", e); 776 - return ApiError::InternalError(None).into_response(); 777 - } 778 - }; 779 780 let existing_passkeys = state 781 .user_repo ··· 840 841 pub async fn request_passkey_recovery( 842 State(state): State<AppState>, 843 - headers: HeaderMap, 844 Json(input): Json<RequestPasskeyRecoveryInput>, 845 ) -> Response { 846 - let client_ip = extract_client_ip(&headers); 847 - if !state 848 - .check_rate_limit(RateLimitKind::PasswordReset, &client_ip) 849 - .await 850 - { 851 - return ApiError::RateLimitExceeded(None).into_response(); 852 - } 853 - 854 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 855 - let hostname_for_handles = pds_hostname.split(':').next().unwrap_or(&pds_hostname); 856 let identifier = input.email.trim().to_lowercase(); 857 let identifier = identifier.strip_prefix('@').unwrap_or(&identifier); 858 let normalized_handle = if identifier.contains('@') || identifier.contains('.') { ··· 890 return ApiError::InternalError(None).into_response(); 891 } 892 893 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 894 let recovery_url = format!( 895 "https://{}/app/recover-passkey?did={}&token={}", 896 hostname, ··· 903 state.infra_repo.as_ref(), 904 user.id, 905 &recovery_url, 906 - &hostname, 907 ) 908 .await; 909
··· 19 20 use crate::api::repo::record::utils::create_signed_commit; 21 use crate::auth::{ServiceTokenVerifier, generate_app_password, is_service_token}; 22 + use crate::rate_limit::{AccountCreationLimit, PasswordResetLimit, RateLimited}; 23 + use crate::state::AppState; 24 use crate::types::{Did, Handle, Nsid, PlainPassword, Rkey}; 25 + use crate::util::{pds_hostname, pds_hostname_without_port}; 26 use crate::validation::validate_password; 27 28 fn generate_setup_token() -> String { 29 let mut rng = rand::thread_rng(); 30 (0..32) ··· 67 68 pub async fn create_passkey_account( 69 State(state): State<AppState>, 70 + _rate_limit: RateLimited<AccountCreationLimit>, 71 headers: HeaderMap, 72 Json(input): Json<CreatePasskeyAccountInput>, 73 ) -> Response { 74 let byod_auth = if let Some(extracted) = crate::auth::extract_auth_token_from_header( 75 + crate::util::get_header_str(&headers, "Authorization"), 76 ) { 77 let token = extracted.token; 78 if is_service_token(&token) { ··· 111 .map(|d| d.starts_with("did:web:")) 112 .unwrap_or(false); 113 114 + let hostname = pds_hostname(); 115 + let hostname_for_handles = pds_hostname_without_port(); 116 let pds_suffix = format!(".{}", hostname_for_handles); 117 118 let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) { ··· 244 } 245 if is_byod_did_web { 246 if let Some(ref auth_did) = byod_auth 247 + && d != auth_did.as_str() 248 { 249 return ApiError::AuthorizationError(format!( 250 "Service token issuer {} does not match DID {}", ··· 256 } else { 257 if let Err(e) = crate::api::identity::did::verify_did_web( 258 d, 259 + hostname, 260 &input.handle, 261 input.signing_key.as_deref(), 262 ) ··· 272 if let Some(ref auth_did) = byod_auth { 273 if let Some(ref provided_did) = input.did { 274 if provided_did.starts_with("did:plc:") { 275 + if provided_did != auth_did.as_str() { 276 return ApiError::AuthorizationError(format!( 277 "Service token issuer {} does not match DID {}", 278 auth_did, provided_did ··· 497 verification_channel, 498 &verification_recipient, 499 &formatted_token, 500 + hostname, 501 ) 502 .await 503 { ··· 602 return ApiError::InvalidToken(None).into_response(); 603 } 604 605 + let webauthn = &state.webauthn_config; 606 607 let reg_state = match state 608 .user_repo ··· 737 return ApiError::InvalidToken(None).into_response(); 738 } 739 740 + let webauthn = &state.webauthn_config; 741 742 let existing_passkeys = state 743 .user_repo ··· 802 803 pub async fn request_passkey_recovery( 804 State(state): State<AppState>, 805 + _rate_limit: RateLimited<PasswordResetLimit>, 806 Json(input): Json<RequestPasskeyRecoveryInput>, 807 ) -> Response { 808 + let hostname_for_handles = pds_hostname_without_port(); 809 let identifier = input.email.trim().to_lowercase(); 810 let identifier = identifier.strip_prefix('@').unwrap_or(&identifier); 811 let normalized_handle = if identifier.contains('@') || identifier.contains('.') { ··· 843 return ApiError::InternalError(None).into_response(); 844 } 845 846 + let hostname = pds_hostname(); 847 let recovery_url = format!( 848 "https://{}/app/recover-passkey?did={}&token={}", 849 hostname, ··· 856 state.infra_repo.as_ref(), 857 user.id, 858 &recovery_url, 859 + hostname, 860 ) 861 .await; 862
+20 -56
crates/tranquil-pds/src/api/server/passkeys.rs
··· 1 use crate::api::EmptyResponse; 2 - use crate::api::error::ApiError; 3 - use crate::auth::webauthn::WebAuthnConfig; 4 - use crate::auth::{Active, Auth}; 5 use crate::state::AppState; 6 use axum::{ 7 Json, ··· 12 use tracing::{error, info, warn}; 13 use webauthn_rs::prelude::*; 14 15 - fn get_webauthn() -> Result<WebAuthnConfig, ApiError> { 16 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 17 - WebAuthnConfig::new(&hostname).map_err(|e| { 18 - error!("Failed to create WebAuthn config: {}", e); 19 - ApiError::InternalError(Some("WebAuthn configuration failed".into())) 20 - }) 21 - } 22 - 23 #[derive(Deserialize)] 24 #[serde(rename_all = "camelCase")] 25 pub struct StartRegistrationInput { ··· 37 auth: Auth<Active>, 38 Json(input): Json<StartRegistrationInput>, 39 ) -> Result<Response, ApiError> { 40 - let webauthn = get_webauthn()?; 41 42 let handle = state 43 .user_repo 44 .get_handle_by_did(&auth.did) 45 .await 46 - .map_err(|e| { 47 - error!("DB error fetching user: {:?}", e); 48 - ApiError::InternalError(None) 49 - })? 50 .ok_or(ApiError::AccountNotFound)?; 51 52 let existing_passkeys = state 53 .user_repo 54 .get_passkeys_for_user(&auth.did) 55 .await 56 - .map_err(|e| { 57 - error!("DB error fetching existing passkeys: {:?}", e); 58 - ApiError::InternalError(None) 59 - })?; 60 61 let exclude_credentials: Vec<CredentialID> = existing_passkeys 62 .iter() ··· 81 .user_repo 82 .save_webauthn_challenge(&auth.did, "registration", &state_json) 83 .await 84 - .map_err(|e| { 85 - error!("Failed to save registration state: {:?}", e); 86 - ApiError::InternalError(None) 87 - })?; 88 89 let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 90 ··· 112 auth: Auth<Active>, 113 Json(input): Json<FinishRegistrationInput>, 114 ) -> Result<Response, ApiError> { 115 - let webauthn = get_webauthn()?; 116 117 let reg_state_json = state 118 .user_repo 119 .load_webauthn_challenge(&auth.did, "registration") 120 .await 121 - .map_err(|e| { 122 - error!("DB error loading registration state: {:?}", e); 123 - ApiError::InternalError(None) 124 - })? 125 .ok_or(ApiError::NoRegistrationInProgress)?; 126 127 let reg_state: SecurityKeyRegistration = ··· 157 input.friendly_name.as_deref(), 158 ) 159 .await 160 - .map_err(|e| { 161 - error!("Failed to save passkey: {:?}", e); 162 - ApiError::InternalError(None) 163 - })?; 164 165 if let Err(e) = state 166 .user_repo ··· 208 .user_repo 209 .get_passkeys_for_user(&auth.did) 210 .await 211 - .map_err(|e| { 212 - error!("DB error fetching passkeys: {:?}", e); 213 - ApiError::InternalError(None) 214 - })?; 215 216 let passkey_infos: Vec<PasskeyInfo> = passkeys 217 .into_iter() ··· 241 auth: Auth<Active>, 242 Json(input): Json<DeletePasskeyInput>, 243 ) -> Result<Response, ApiError> { 244 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 245 - { 246 - return Ok(crate::api::server::reauth::legacy_mfa_required_response( 247 - &*state.user_repo, 248 - &*state.session_repo, 249 - &auth.did, 250 - ) 251 - .await); 252 - } 253 254 - if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.did).await { 255 - return Ok(crate::api::server::reauth::reauth_required_response( 256 - &*state.user_repo, 257 - &*state.session_repo, 258 - &auth.did, 259 - ) 260 - .await); 261 - } 262 263 let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 264 265 - match state.user_repo.delete_passkey(id, &auth.did).await { 266 Ok(true) => { 267 - info!(did = %auth.did, passkey_id = %id, "Passkey deleted"); 268 Ok(EmptyResponse::ok().into_response()) 269 } 270 Ok(false) => Err(ApiError::PasskeyNotFound),
··· 1 use crate::api::EmptyResponse; 2 + use crate::api::error::{ApiError, DbResultExt}; 3 + use crate::auth::{Active, Auth, require_legacy_session_mfa, require_reauth_window}; 4 use crate::state::AppState; 5 use axum::{ 6 Json, ··· 11 use tracing::{error, info, warn}; 12 use webauthn_rs::prelude::*; 13 14 #[derive(Deserialize)] 15 #[serde(rename_all = "camelCase")] 16 pub struct StartRegistrationInput { ··· 28 auth: Auth<Active>, 29 Json(input): Json<StartRegistrationInput>, 30 ) -> Result<Response, ApiError> { 31 + let webauthn = &state.webauthn_config; 32 33 let handle = state 34 .user_repo 35 .get_handle_by_did(&auth.did) 36 .await 37 + .log_db_err("fetching user")? 38 .ok_or(ApiError::AccountNotFound)?; 39 40 let existing_passkeys = state 41 .user_repo 42 .get_passkeys_for_user(&auth.did) 43 .await 44 + .log_db_err("fetching existing passkeys")?; 45 46 let exclude_credentials: Vec<CredentialID> = existing_passkeys 47 .iter() ··· 66 .user_repo 67 .save_webauthn_challenge(&auth.did, "registration", &state_json) 68 .await 69 + .log_db_err("saving registration state")?; 70 71 let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 72 ··· 94 auth: Auth<Active>, 95 Json(input): Json<FinishRegistrationInput>, 96 ) -> Result<Response, ApiError> { 97 + let webauthn = &state.webauthn_config; 98 99 let reg_state_json = state 100 .user_repo 101 .load_webauthn_challenge(&auth.did, "registration") 102 .await 103 + .log_db_err("loading registration state")? 104 .ok_or(ApiError::NoRegistrationInProgress)?; 105 106 let reg_state: SecurityKeyRegistration = ··· 136 input.friendly_name.as_deref(), 137 ) 138 .await 139 + .log_db_err("saving passkey")?; 140 141 if let Err(e) = state 142 .user_repo ··· 184 .user_repo 185 .get_passkeys_for_user(&auth.did) 186 .await 187 + .log_db_err("fetching passkeys")?; 188 189 let passkey_infos: Vec<PasskeyInfo> = passkeys 190 .into_iter() ··· 214 auth: Auth<Active>, 215 Json(input): Json<DeletePasskeyInput>, 216 ) -> Result<Response, ApiError> { 217 + let session_mfa = match require_legacy_session_mfa(&state, &auth).await { 218 + Ok(proof) => proof, 219 + Err(response) => return Ok(response), 220 + }; 221 222 + let reauth_mfa = match require_reauth_window(&state, &auth).await { 223 + Ok(proof) => proof, 224 + Err(response) => return Ok(response), 225 + }; 226 227 let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 228 229 + match state.user_repo.delete_passkey(id, reauth_mfa.did()).await { 230 Ok(true) => { 231 + info!(did = %session_mfa.did(), passkey_id = %id, "Passkey deleted"); 232 Ok(EmptyResponse::ok().into_response()) 233 } 234 Ok(false) => Err(ApiError::PasskeyNotFound),
+62 -164
crates/tranquil-pds/src/api/server/password.rs
··· 1 - use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; 3 - use crate::auth::{Active, Auth}; 4 - use crate::state::{AppState, RateLimitKind}; 5 use crate::types::PlainPassword; 6 use crate::validation::validate_password; 7 use axum::{ 8 Json, 9 extract::State, 10 - http::HeaderMap, 11 response::{IntoResponse, Response}, 12 }; 13 - use bcrypt::{DEFAULT_COST, hash, verify}; 14 use chrono::{Duration, Utc}; 15 use serde::Deserialize; 16 use tracing::{error, info, warn}; ··· 18 fn generate_reset_code() -> String { 19 crate::util::generate_token_code() 20 } 21 - fn extract_client_ip(headers: &HeaderMap) -> String { 22 - if let Some(forwarded) = headers.get("x-forwarded-for") 23 - && let Ok(value) = forwarded.to_str() 24 - && let Some(first_ip) = value.split(',').next() 25 - { 26 - return first_ip.trim().to_string(); 27 - } 28 - if let Some(real_ip) = headers.get("x-real-ip") 29 - && let Ok(value) = real_ip.to_str() 30 - { 31 - return value.trim().to_string(); 32 - } 33 - "unknown".to_string() 34 - } 35 36 #[derive(Deserialize)] 37 pub struct RequestPasswordResetInput { ··· 41 42 pub async fn request_password_reset( 43 State(state): State<AppState>, 44 - headers: HeaderMap, 45 Json(input): Json<RequestPasswordResetInput>, 46 ) -> Response { 47 - let client_ip = extract_client_ip(&headers); 48 - if !state 49 - .check_rate_limit(RateLimitKind::PasswordReset, &client_ip) 50 - .await 51 - { 52 - warn!(ip = %client_ip, "Password reset rate limit exceeded"); 53 - return ApiError::RateLimitExceeded(None).into_response(); 54 - } 55 let identifier = input.email.trim(); 56 if identifier.is_empty() { 57 return ApiError::InvalidRequest("email or handle is required".into()).into_response(); 58 } 59 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 60 - let hostname_for_handles = pds_hostname.split(':').next().unwrap_or(&pds_hostname); 61 let normalized = identifier.to_lowercase(); 62 let normalized = normalized.strip_prefix('@').unwrap_or(&normalized); 63 let is_email_lookup = normalized.contains('@'); ··· 101 error!("DB error setting reset code: {:?}", e); 102 return ApiError::InternalError(None).into_response(); 103 } 104 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 105 if let Err(e) = crate::comms::comms_repo::enqueue_password_reset( 106 state.user_repo.as_ref(), 107 state.infra_repo.as_ref(), 108 user_id, 109 &code, 110 - &hostname, 111 ) 112 .await 113 { ··· 135 136 pub async fn reset_password( 137 State(state): State<AppState>, 138 - headers: HeaderMap, 139 Json(input): Json<ResetPasswordInput>, 140 ) -> Response { 141 - let client_ip = extract_client_ip(&headers); 142 - if !state 143 - .check_rate_limit(RateLimitKind::ResetPassword, &client_ip) 144 - .await 145 - { 146 - warn!(ip = %client_ip, "Reset password rate limit exceeded"); 147 - return ApiError::RateLimitExceeded(None).into_response(); 148 - } 149 let token = input.token.trim(); 150 let password = &input.password; 151 if token.is_empty() { ··· 230 auth: Auth<Active>, 231 Json(input): Json<ChangePasswordInput>, 232 ) -> Result<Response, ApiError> { 233 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 234 - { 235 - return Ok(crate::api::server::reauth::legacy_mfa_required_response( 236 - &*state.user_repo, 237 - &*state.session_repo, 238 - &auth.did, 239 - ) 240 - .await); 241 - } 242 243 - let current_password = &input.current_password; 244 - let new_password = &input.new_password; 245 - if current_password.is_empty() { 246 return Err(ApiError::InvalidRequest( 247 "currentPassword is required".into(), 248 )); 249 } 250 - if new_password.is_empty() { 251 return Err(ApiError::InvalidRequest("newPassword is required".into())); 252 } 253 - if let Err(e) = validate_password(new_password) { 254 return Err(ApiError::InvalidRequest(e.to_string())); 255 } 256 let user = state 257 .user_repo 258 - .get_id_and_password_hash_by_did(&auth.did) 259 .await 260 - .map_err(|e| { 261 - error!("DB error in change_password: {:?}", e); 262 - ApiError::InternalError(None) 263 - })? 264 .ok_or(ApiError::AccountNotFound)?; 265 266 - let (user_id, password_hash) = (user.id, user.password_hash); 267 - let valid = verify(current_password, &password_hash).map_err(|e| { 268 - error!("Password verification error: {:?}", e); 269 - ApiError::InternalError(None) 270 - })?; 271 - if !valid { 272 - return Err(ApiError::InvalidPassword( 273 - "Current password is incorrect".into(), 274 - )); 275 - } 276 - let new_password_clone = new_password.to_string(); 277 let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)) 278 .await 279 .map_err(|e| { ··· 287 288 state 289 .user_repo 290 - .update_password_hash(user_id, &new_hash) 291 .await 292 - .map_err(|e| { 293 - error!("DB error updating password: {:?}", e); 294 - ApiError::InternalError(None) 295 - })?; 296 297 - info!(did = %&auth.did, "Password changed successfully"); 298 Ok(EmptyResponse::ok().into_response()) 299 } 300 ··· 302 State(state): State<AppState>, 303 auth: Auth<Active>, 304 ) -> Result<Response, ApiError> { 305 - match state.user_repo.has_password_by_did(&auth.did).await { 306 - Ok(Some(has)) => Ok(HasPasswordResponse::response(has).into_response()), 307 - Ok(None) => Err(ApiError::AccountNotFound), 308 - Err(e) => { 309 - error!("DB error: {:?}", e); 310 - Err(ApiError::InternalError(None)) 311 - } 312 - } 313 } 314 315 pub async fn remove_password( 316 State(state): State<AppState>, 317 auth: Auth<Active>, 318 ) -> Result<Response, ApiError> { 319 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 320 - { 321 - return Ok(crate::api::server::reauth::legacy_mfa_required_response( 322 - &*state.user_repo, 323 - &*state.session_repo, 324 - &auth.did, 325 - ) 326 - .await); 327 - } 328 329 - if crate::api::server::reauth::check_reauth_required_cached( 330 - &*state.session_repo, 331 - &state.cache, 332 - &auth.did, 333 - ) 334 - .await 335 - { 336 - return Ok(crate::api::server::reauth::reauth_required_response( 337 - &*state.user_repo, 338 - &*state.session_repo, 339 - &auth.did, 340 - ) 341 - .await); 342 - } 343 344 let has_passkeys = state 345 .user_repo 346 - .has_passkeys(&auth.did) 347 .await 348 .unwrap_or(false); 349 if !has_passkeys { ··· 354 355 let user = state 356 .user_repo 357 - .get_password_info_by_did(&auth.did) 358 .await 359 - .map_err(|e| { 360 - error!("DB error: {:?}", e); 361 - ApiError::InternalError(None) 362 - })? 363 .ok_or(ApiError::AccountNotFound)?; 364 365 if user.password_hash.is_none() { ··· 372 .user_repo 373 .remove_user_password(user.id) 374 .await 375 - .map_err(|e| { 376 - error!("DB error removing password: {:?}", e); 377 - ApiError::InternalError(None) 378 - })?; 379 380 - info!(did = %&auth.did, "Password removed - account is now passkey-only"); 381 Ok(SuccessResponse::ok().into_response()) 382 } 383 ··· 392 auth: Auth<Active>, 393 Json(input): Json<SetPasswordInput>, 394 ) -> Result<Response, ApiError> { 395 - let has_password = state 396 - .user_repo 397 - .has_password_by_did(&auth.did) 398 - .await 399 - .ok() 400 - .flatten() 401 - .unwrap_or(false); 402 - let has_passkeys = state 403 - .user_repo 404 - .has_passkeys(&auth.did) 405 - .await 406 - .unwrap_or(false); 407 - let has_totp = state 408 - .user_repo 409 - .has_totp_enabled(&auth.did) 410 - .await 411 - .unwrap_or(false); 412 - 413 - let has_any_reauth_method = has_password || has_passkeys || has_totp; 414 - 415 - if has_any_reauth_method 416 - && crate::api::server::reauth::check_reauth_required_cached( 417 - &*state.session_repo, 418 - &state.cache, 419 - &auth.did, 420 - ) 421 - .await 422 - { 423 - return Ok(crate::api::server::reauth::reauth_required_response( 424 - &*state.user_repo, 425 - &*state.session_repo, 426 - &auth.did, 427 - ) 428 - .await); 429 - } 430 431 let new_password = &input.new_password; 432 if new_password.is_empty() { ··· 436 return Err(ApiError::InvalidRequest(e.to_string())); 437 } 438 439 let user = state 440 .user_repo 441 - .get_password_info_by_did(&auth.did) 442 .await 443 - .map_err(|e| { 444 - error!("DB error: {:?}", e); 445 - ApiError::InternalError(None) 446 - })? 447 .ok_or(ApiError::AccountNotFound)?; 448 449 if user.password_hash.is_some() { ··· 468 .user_repo 469 .set_new_user_password(user.id, &new_hash) 470 .await 471 - .map_err(|e| { 472 - error!("DB error setting password: {:?}", e); 473 - ApiError::InternalError(None) 474 - })?; 475 476 - info!(did = %&auth.did, "Password set for passkey-only account"); 477 Ok(SuccessResponse::ok().into_response()) 478 }
··· 1 + use crate::api::error::{ApiError, DbResultExt}; 2 use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; 3 + use crate::auth::{ 4 + Active, Auth, require_legacy_session_mfa, require_reauth_window, 5 + require_reauth_window_if_available, 6 + }; 7 + use crate::rate_limit::{PasswordResetLimit, RateLimited, ResetPasswordLimit}; 8 + use crate::state::AppState; 9 use crate::types::PlainPassword; 10 + use crate::util::{pds_hostname, pds_hostname_without_port}; 11 use crate::validation::validate_password; 12 use axum::{ 13 Json, 14 extract::State, 15 response::{IntoResponse, Response}, 16 }; 17 + use bcrypt::{DEFAULT_COST, hash}; 18 use chrono::{Duration, Utc}; 19 use serde::Deserialize; 20 use tracing::{error, info, warn}; ··· 22 fn generate_reset_code() -> String { 23 crate::util::generate_token_code() 24 } 25 26 #[derive(Deserialize)] 27 pub struct RequestPasswordResetInput { ··· 31 32 pub async fn request_password_reset( 33 State(state): State<AppState>, 34 + _rate_limit: RateLimited<PasswordResetLimit>, 35 Json(input): Json<RequestPasswordResetInput>, 36 ) -> Response { 37 let identifier = input.email.trim(); 38 if identifier.is_empty() { 39 return ApiError::InvalidRequest("email or handle is required".into()).into_response(); 40 } 41 + let hostname_for_handles = pds_hostname_without_port(); 42 let normalized = identifier.to_lowercase(); 43 let normalized = normalized.strip_prefix('@').unwrap_or(&normalized); 44 let is_email_lookup = normalized.contains('@'); ··· 82 error!("DB error setting reset code: {:?}", e); 83 return ApiError::InternalError(None).into_response(); 84 } 85 + let hostname = pds_hostname(); 86 if let Err(e) = crate::comms::comms_repo::enqueue_password_reset( 87 state.user_repo.as_ref(), 88 state.infra_repo.as_ref(), 89 user_id, 90 &code, 91 + hostname, 92 ) 93 .await 94 { ··· 116 117 pub async fn reset_password( 118 State(state): State<AppState>, 119 + _rate_limit: RateLimited<ResetPasswordLimit>, 120 Json(input): Json<ResetPasswordInput>, 121 ) -> Response { 122 let token = input.token.trim(); 123 let password = &input.password; 124 if token.is_empty() { ··· 203 auth: Auth<Active>, 204 Json(input): Json<ChangePasswordInput>, 205 ) -> Result<Response, ApiError> { 206 + use crate::auth::verify_password_mfa; 207 208 + let session_mfa = match require_legacy_session_mfa(&state, &auth).await { 209 + Ok(proof) => proof, 210 + Err(response) => return Ok(response), 211 + }; 212 + 213 + if input.current_password.is_empty() { 214 return Err(ApiError::InvalidRequest( 215 "currentPassword is required".into(), 216 )); 217 } 218 + if input.new_password.is_empty() { 219 return Err(ApiError::InvalidRequest("newPassword is required".into())); 220 } 221 + if let Err(e) = validate_password(&input.new_password) { 222 return Err(ApiError::InvalidRequest(e.to_string())); 223 } 224 + 225 + let password_mfa = verify_password_mfa(&state, &auth, &input.current_password).await?; 226 + 227 let user = state 228 .user_repo 229 + .get_id_and_password_hash_by_did(password_mfa.did()) 230 .await 231 + .log_db_err("in change_password")? 232 .ok_or(ApiError::AccountNotFound)?; 233 234 + let new_password_clone = input.new_password.to_string(); 235 let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)) 236 .await 237 .map_err(|e| { ··· 245 246 state 247 .user_repo 248 + .update_password_hash(user.id, &new_hash) 249 .await 250 + .log_db_err("updating password")?; 251 252 + info!(did = %session_mfa.did(), "Password changed successfully"); 253 Ok(EmptyResponse::ok().into_response()) 254 } 255 ··· 257 State(state): State<AppState>, 258 auth: Auth<Active>, 259 ) -> Result<Response, ApiError> { 260 + let has = state 261 + .user_repo 262 + .has_password_by_did(&auth.did) 263 + .await 264 + .log_db_err("checking password status")? 265 + .ok_or(ApiError::AccountNotFound)?; 266 + Ok(HasPasswordResponse::response(has).into_response()) 267 } 268 269 pub async fn remove_password( 270 State(state): State<AppState>, 271 auth: Auth<Active>, 272 ) -> Result<Response, ApiError> { 273 + let session_mfa = match require_legacy_session_mfa(&state, &auth).await { 274 + Ok(proof) => proof, 275 + Err(response) => return Ok(response), 276 + }; 277 278 + let reauth_mfa = match require_reauth_window(&state, &auth).await { 279 + Ok(proof) => proof, 280 + Err(response) => return Ok(response), 281 + }; 282 283 let has_passkeys = state 284 .user_repo 285 + .has_passkeys(reauth_mfa.did()) 286 .await 287 .unwrap_or(false); 288 if !has_passkeys { ··· 293 294 let user = state 295 .user_repo 296 + .get_password_info_by_did(reauth_mfa.did()) 297 .await 298 + .log_db_err("getting password info")? 299 .ok_or(ApiError::AccountNotFound)?; 300 301 if user.password_hash.is_none() { ··· 308 .user_repo 309 .remove_user_password(user.id) 310 .await 311 + .log_db_err("removing password")?; 312 313 + info!(did = %session_mfa.did(), "Password removed - account is now passkey-only"); 314 Ok(SuccessResponse::ok().into_response()) 315 } 316 ··· 325 auth: Auth<Active>, 326 Json(input): Json<SetPasswordInput>, 327 ) -> Result<Response, ApiError> { 328 + let reauth_mfa = match require_reauth_window_if_available(&state, &auth).await { 329 + Ok(proof) => proof, 330 + Err(response) => return Ok(response), 331 + }; 332 333 let new_password = &input.new_password; 334 if new_password.is_empty() { ··· 338 return Err(ApiError::InvalidRequest(e.to_string())); 339 } 340 341 + let did = reauth_mfa.as_ref().map(|m| m.did()).unwrap_or(&auth.did); 342 + 343 let user = state 344 .user_repo 345 + .get_password_info_by_did(did) 346 .await 347 + .log_db_err("getting password info")? 348 .ok_or(ApiError::AccountNotFound)?; 349 350 if user.password_hash.is_some() { ··· 369 .user_repo 370 .set_new_user_password(user.id, &new_hash) 371 .await 372 + .log_db_err("setting password")?; 373 374 + info!(did = %did, "Password set for passkey-only account"); 375 Ok(SuccessResponse::ok().into_response()) 376 }
+21 -58
crates/tranquil-pds/src/api/server/reauth.rs
··· 1 - use crate::api::error::ApiError; 2 use axum::{ 3 Json, 4 extract::State, ··· 11 use tranquil_db_traits::{SessionRepository, UserRepository}; 12 13 use crate::auth::{Active, Auth}; 14 - use crate::state::{AppState, RateLimitKind}; 15 use crate::types::PlainPassword; 16 17 - const REAUTH_WINDOW_SECONDS: i64 = 300; 18 19 #[derive(Serialize)] 20 #[serde(rename_all = "camelCase")] ··· 32 .session_repo 33 .get_last_reauth_at(&auth.did) 34 .await 35 - .map_err(|e| { 36 - error!("DB error: {:?}", e); 37 - ApiError::InternalError(None) 38 - })?; 39 40 let reauth_required = is_reauth_required(last_reauth_at); 41 let available_methods = ··· 70 .user_repo 71 .get_password_hash_by_did(&auth.did) 72 .await 73 - .map_err(|e| { 74 - error!("DB error: {:?}", e); 75 - ApiError::InternalError(None) 76 - })? 77 .ok_or(ApiError::AccountNotFound)?; 78 79 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); ··· 97 98 let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 99 .await 100 - .map_err(|e| { 101 - error!("DB error updating reauth: {:?}", e); 102 - ApiError::InternalError(None) 103 - })?; 104 105 info!(did = %&auth.did, "Re-auth successful via password"); 106 Ok(Json(ReauthResponse { reauthed_at }).into_response()) ··· 117 auth: Auth<Active>, 118 Json(input): Json<TotpReauthInput>, 119 ) -> Result<Response, ApiError> { 120 - if !state 121 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 122 - .await 123 - { 124 - warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 125 - return Err(ApiError::RateLimitExceeded(Some( 126 - "Too many verification attempts. Please try again in a few minutes.".into(), 127 - ))); 128 - } 129 130 let valid = 131 crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code) ··· 140 141 let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 142 .await 143 - .map_err(|e| { 144 - error!("DB error updating reauth: {:?}", e); 145 - ApiError::InternalError(None) 146 - })?; 147 148 info!(did = %&auth.did, "Re-auth successful via TOTP"); 149 Ok(Json(ReauthResponse { reauthed_at }).into_response()) ··· 159 State(state): State<AppState>, 160 auth: Auth<Active>, 161 ) -> Result<Response, ApiError> { 162 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 163 - 164 let stored_passkeys = state 165 .user_repo 166 .get_passkeys_for_user(&auth.did) 167 .await 168 - .map_err(|e| { 169 - error!("Failed to get passkeys: {:?}", e); 170 - ApiError::InternalError(None) 171 - })?; 172 173 if stored_passkeys.is_empty() { 174 return Err(ApiError::NoPasskeys); ··· 185 ))); 186 } 187 188 - let webauthn = crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname).map_err(|e| { 189 - error!("Failed to create WebAuthn config: {:?}", e); 190 - ApiError::InternalError(None) 191 - })?; 192 193 let (rcr, auth_state) = webauthn.start_authentication(passkeys).map_err(|e| { 194 error!("Failed to start passkey authentication: {:?}", e); ··· 204 .user_repo 205 .save_webauthn_challenge(&auth.did, "authentication", &state_json) 206 .await 207 - .map_err(|e| { 208 - error!("Failed to save authentication state: {:?}", e); 209 - ApiError::InternalError(None) 210 - })?; 211 212 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 213 Ok(Json(PasskeyReauthStartResponse { options }).into_response()) ··· 224 auth: Auth<Active>, 225 Json(input): Json<PasskeyReauthFinishInput>, 226 ) -> Result<Response, ApiError> { 227 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 228 - 229 let auth_state_json = state 230 .user_repo 231 .load_webauthn_challenge(&auth.did, "authentication") 232 .await 233 - .map_err(|e| { 234 - error!("Failed to load authentication state: {:?}", e); 235 - ApiError::InternalError(None) 236 - })? 237 .ok_or(ApiError::NoChallengeInProgress)?; 238 239 let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = ··· 248 ApiError::InvalidCredential 249 })?; 250 251 - let webauthn = crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname).map_err(|e| { 252 - error!("Failed to create WebAuthn config: {:?}", e); 253 - ApiError::InternalError(None) 254 - })?; 255 - 256 - let auth_result = webauthn 257 .finish_authentication(&credential, &auth_state) 258 .map_err(|e| { 259 warn!(did = %&auth.did, "Passkey re-auth failed: {:?}", e); ··· 287 288 let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 289 .await 290 - .map_err(|e| { 291 - error!("DB error updating reauth: {:?}", e); 292 - ApiError::InternalError(None) 293 - })?; 294 295 info!(did = %&auth.did, "Re-auth successful via passkey"); 296 Ok(Json(ReauthResponse { reauthed_at }).into_response())
··· 1 + use crate::api::error::{ApiError, DbResultExt}; 2 use axum::{ 3 Json, 4 extract::State, ··· 11 use tranquil_db_traits::{SessionRepository, UserRepository}; 12 13 use crate::auth::{Active, Auth}; 14 + use crate::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message}; 15 + use crate::state::AppState; 16 use crate::types::PlainPassword; 17 18 + pub const REAUTH_WINDOW_SECONDS: i64 = 300; 19 20 #[derive(Serialize)] 21 #[serde(rename_all = "camelCase")] ··· 33 .session_repo 34 .get_last_reauth_at(&auth.did) 35 .await 36 + .log_db_err("getting last reauth")?; 37 38 let reauth_required = is_reauth_required(last_reauth_at); 39 let available_methods = ··· 68 .user_repo 69 .get_password_hash_by_did(&auth.did) 70 .await 71 + .log_db_err("fetching password hash")? 72 .ok_or(ApiError::AccountNotFound)?; 73 74 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); ··· 92 93 let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 94 .await 95 + .log_db_err("updating reauth")?; 96 97 info!(did = %&auth.did, "Re-auth successful via password"); 98 Ok(Json(ReauthResponse { reauthed_at }).into_response()) ··· 109 auth: Auth<Active>, 110 Json(input): Json<TotpReauthInput>, 111 ) -> Result<Response, ApiError> { 112 + let _rate_limit = check_user_rate_limit_with_message::<TotpVerifyLimit>( 113 + &state, 114 + &auth.did, 115 + "Too many verification attempts. Please try again in a few minutes.", 116 + ) 117 + .await?; 118 119 let valid = 120 crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code) ··· 129 130 let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 131 .await 132 + .log_db_err("updating reauth")?; 133 134 info!(did = %&auth.did, "Re-auth successful via TOTP"); 135 Ok(Json(ReauthResponse { reauthed_at }).into_response()) ··· 145 State(state): State<AppState>, 146 auth: Auth<Active>, 147 ) -> Result<Response, ApiError> { 148 let stored_passkeys = state 149 .user_repo 150 .get_passkeys_for_user(&auth.did) 151 .await 152 + .log_db_err("getting passkeys")?; 153 154 if stored_passkeys.is_empty() { 155 return Err(ApiError::NoPasskeys); ··· 166 ))); 167 } 168 169 + let webauthn = &state.webauthn_config; 170 171 let (rcr, auth_state) = webauthn.start_authentication(passkeys).map_err(|e| { 172 error!("Failed to start passkey authentication: {:?}", e); ··· 182 .user_repo 183 .save_webauthn_challenge(&auth.did, "authentication", &state_json) 184 .await 185 + .log_db_err("saving authentication state")?; 186 187 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 188 Ok(Json(PasskeyReauthStartResponse { options }).into_response()) ··· 199 auth: Auth<Active>, 200 Json(input): Json<PasskeyReauthFinishInput>, 201 ) -> Result<Response, ApiError> { 202 let auth_state_json = state 203 .user_repo 204 .load_webauthn_challenge(&auth.did, "authentication") 205 .await 206 + .log_db_err("loading authentication state")? 207 .ok_or(ApiError::NoChallengeInProgress)?; 208 209 let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = ··· 218 ApiError::InvalidCredential 219 })?; 220 221 + let auth_result = state 222 + .webauthn_config 223 .finish_authentication(&credential, &auth_state) 224 .map_err(|e| { 225 warn!(did = %&auth.did, "Passkey re-auth failed: {:?}", e); ··· 253 254 let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 255 .await 256 + .log_db_err("updating reauth")?; 257 258 info!(did = %&auth.did, "Re-auth successful via passkey"); 259 Ok(Json(ReauthResponse { reauthed_at }).into_response())
+2 -2
crates/tranquil-pds/src/api/server/service_auth.rs
··· 51 headers: axum::http::HeaderMap, 52 Query(params): Query<GetServiceAuthParams>, 53 ) -> Response { 54 - let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 55 - let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok()); 56 info!( 57 has_auth_header = auth_header.is_some(), 58 has_dpop_proof = dpop_proof.is_some(),
··· 51 headers: axum::http::HeaderMap, 52 Query(params): Query<GetServiceAuthParams>, 53 ) -> Response { 54 + let auth_header = crate::util::get_header_str(&headers, "Authorization"); 55 + let dpop_proof = crate::util::get_header_str(&headers, "DPoP"); 56 info!( 57 has_auth_header = auth_header.is_some(), 58 has_dpop_proof = dpop_proof.is_some(),
+46 -120
crates/tranquil-pds/src/api/server/session.rs
··· 1 - use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, SuccessResponse}; 3 - use crate::auth::{Active, Auth, Permissive}; 4 - use crate::state::{AppState, RateLimitKind}; 5 use crate::types::{AccountState, Did, Handle, PlainPassword}; 6 use axum::{ 7 Json, 8 extract::State, ··· 15 use tracing::{error, info, warn}; 16 use tranquil_types::TokenId; 17 18 - fn extract_client_ip(headers: &HeaderMap) -> String { 19 - if let Some(forwarded) = headers.get("x-forwarded-for") 20 - && let Ok(value) = forwarded.to_str() 21 - && let Some(first_ip) = value.split(',').next() 22 - { 23 - return first_ip.trim().to_string(); 24 - } 25 - if let Some(real_ip) = headers.get("x-real-ip") 26 - && let Ok(value) = real_ip.to_str() 27 - { 28 - return value.trim().to_string(); 29 - } 30 - "unknown".to_string() 31 - } 32 - 33 fn normalize_handle(identifier: &str, pds_hostname: &str) -> String { 34 let identifier = identifier.trim(); 35 if identifier.contains('@') || identifier.starts_with("did:") { ··· 75 76 pub async fn create_session( 77 State(state): State<AppState>, 78 - headers: HeaderMap, 79 Json(input): Json<CreateSessionInput>, 80 ) -> Response { 81 info!( 82 "create_session called with identifier: {}", 83 input.identifier 84 ); 85 - let client_ip = extract_client_ip(&headers); 86 - if !state 87 - .check_rate_limit(RateLimitKind::Login, &client_ip) 88 - .await 89 - { 90 - warn!(ip = %client_ip, "Login rate limit exceeded"); 91 - return ApiError::RateLimitExceeded(None).into_response(); 92 - } 93 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 94 - let hostname_for_handles = pds_hostname.split(':').next().unwrap_or(&pds_hostname); 95 let normalized_identifier = normalize_handle(&input.identifier, hostname_for_handles); 96 info!( 97 "Normalized identifier: {} -> {}", ··· 246 ip = %client_ip, 247 "Legacy login on TOTP-enabled account - sending notification" 248 ); 249 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 250 if let Err(e) = crate::comms::comms_repo::enqueue_legacy_login( 251 state.user_repo.as_ref(), 252 state.infra_repo.as_ref(), 253 row.id, 254 - &hostname, 255 - &client_ip, 256 row.preferred_comms_channel, 257 ) 258 .await ··· 260 error!("Failed to queue legacy login notification: {:?}", e); 261 } 262 } 263 - let handle = full_handle(&row.handle, &pds_hostname); 264 let is_active = account_state.is_active(); 265 let status = account_state.status_for_session().map(String::from); 266 Json(CreateSessionOutput { ··· 299 tranquil_db_traits::CommsChannel::Telegram => ("telegram", row.telegram_verified), 300 tranquil_db_traits::CommsChannel::Signal => ("signal", row.signal_verified), 301 }; 302 - let pds_hostname = 303 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 304 - let handle = full_handle(&row.handle, &pds_hostname); 305 let account_state = AccountState::from_db_fields( 306 row.deactivated_at, 307 row.takedown_ref.clone(), ··· 353 _auth: Auth<Active>, 354 ) -> Result<Response, ApiError> { 355 let extracted = crate::auth::extract_auth_token_from_header( 356 - headers.get("Authorization").and_then(|h| h.to_str().ok()), 357 ) 358 .ok_or(ApiError::AuthenticationRequired)?; 359 let jti = crate::auth::get_jti_from_token(&extracted.token) ··· 374 375 pub async fn refresh_session( 376 State(state): State<AppState>, 377 headers: axum::http::HeaderMap, 378 ) -> Response { 379 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 380 - if !state 381 - .check_rate_limit(RateLimitKind::RefreshSession, &client_ip) 382 - .await 383 - { 384 - tracing::warn!(ip = %client_ip, "Refresh session rate limit exceeded"); 385 - return ApiError::RateLimitExceeded(None).into_response(); 386 - } 387 let extracted = match crate::auth::extract_auth_token_from_header( 388 - headers.get("Authorization").and_then(|h| h.to_str().ok()), 389 ) { 390 Some(t) => t, 391 None => return ApiError::AuthenticationRequired.into_response(), ··· 509 tranquil_db_traits::CommsChannel::Telegram => ("telegram", u.telegram_verified), 510 tranquil_db_traits::CommsChannel::Signal => ("signal", u.signal_verified), 511 }; 512 - let pds_hostname = 513 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 514 - let handle = full_handle(&u.handle, &pds_hostname); 515 let account_state = 516 AccountState::from_db_fields(u.deactivated_at, u.takedown_ref.clone(), None, None); 517 let mut response = json!({ ··· 675 return ApiError::InternalError(None).into_response(); 676 } 677 678 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 679 if let Err(e) = crate::comms::comms_repo::enqueue_welcome( 680 state.user_repo.as_ref(), 681 state.infra_repo.as_ref(), 682 row.id, 683 - &hostname, 684 ) 685 .await 686 { ··· 756 let formatted_token = 757 crate::auth::verification_token::format_token_for_display(&verification_token); 758 759 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 760 if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification( 761 state.infra_repo.as_ref(), 762 row.id, 763 channel_str, 764 &recipient, 765 &formatted_token, 766 - &hostname, 767 ) 768 .await 769 { ··· 804 .session_repo 805 .list_sessions_by_did(&auth.did) 806 .await 807 - .map_err(|e| { 808 - error!("DB error fetching JWT sessions: {:?}", e); 809 - ApiError::InternalError(None) 810 - })?; 811 812 let oauth_rows = state 813 .oauth_repo 814 .list_sessions_by_did(&auth.did) 815 .await 816 - .map_err(|e| { 817 - error!("DB error fetching OAuth sessions: {:?}", e); 818 - ApiError::InternalError(None) 819 - })?; 820 821 let jwt_sessions = jwt_rows.into_iter().map(|row| SessionInfo { 822 id: format!("jwt:{}", row.id), ··· 876 .session_repo 877 .get_session_access_jti_by_id(session_id, &auth.did) 878 .await 879 - .map_err(|e| { 880 - error!("DB error in revoke_session: {:?}", e); 881 - ApiError::InternalError(None) 882 - })? 883 .ok_or(ApiError::SessionNotFound)?; 884 state 885 .session_repo 886 .delete_session_by_id(session_id) 887 .await 888 - .map_err(|e| { 889 - error!("DB error deleting session: {:?}", e); 890 - ApiError::InternalError(None) 891 - })?; 892 let cache_key = format!("auth:session:{}:{}", &auth.did, access_jti); 893 if let Err(e) = state.cache.delete(&cache_key).await { 894 warn!("Failed to invalidate session cache: {:?}", e); ··· 902 .oauth_repo 903 .delete_session_by_id(session_id, &auth.did) 904 .await 905 - .map_err(|e| { 906 - error!("DB error deleting OAuth session: {:?}", e); 907 - ApiError::InternalError(None) 908 - })?; 909 if deleted == 0 { 910 return Err(ApiError::SessionNotFound); 911 } ··· 932 .session_repo 933 .delete_sessions_by_did(&auth.did) 934 .await 935 - .map_err(|e| { 936 - error!("DB error revoking JWT sessions: {:?}", e); 937 - ApiError::InternalError(None) 938 - })?; 939 let jti_typed = TokenId::from(jti.clone()); 940 state 941 .oauth_repo 942 .delete_sessions_by_did_except(&auth.did, &jti_typed) 943 .await 944 - .map_err(|e| { 945 - error!("DB error revoking OAuth sessions: {:?}", e); 946 - ApiError::InternalError(None) 947 - })?; 948 } else { 949 state 950 .session_repo 951 .delete_sessions_by_did_except_jti(&auth.did, &jti) 952 .await 953 - .map_err(|e| { 954 - error!("DB error revoking JWT sessions: {:?}", e); 955 - ApiError::InternalError(None) 956 - })?; 957 state 958 .oauth_repo 959 .delete_sessions_by_did(&auth.did) 960 .await 961 - .map_err(|e| { 962 - error!("DB error revoking OAuth sessions: {:?}", e); 963 - ApiError::InternalError(None) 964 - })?; 965 } 966 967 info!(did = %&auth.did, "All other sessions revoked"); ··· 983 .user_repo 984 .get_legacy_login_pref(&auth.did) 985 .await 986 - .map_err(|e| { 987 - error!("DB error: {:?}", e); 988 - ApiError::InternalError(None) 989 - })? 990 .ok_or(ApiError::AccountNotFound)?; 991 Ok(Json(LegacyLoginPreferenceOutput { 992 allow_legacy_login: pref.allow_legacy_login, ··· 1006 auth: Auth<Active>, 1007 Json(input): Json<UpdateLegacyLoginInput>, 1008 ) -> Result<Response, ApiError> { 1009 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 1010 - { 1011 - return Ok(crate::api::server::reauth::legacy_mfa_required_response( 1012 - &*state.user_repo, 1013 - &*state.session_repo, 1014 - &auth.did, 1015 - ) 1016 - .await); 1017 - } 1018 1019 - if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.did).await { 1020 - return Ok(crate::api::server::reauth::reauth_required_response( 1021 - &*state.user_repo, 1022 - &*state.session_repo, 1023 - &auth.did, 1024 - ) 1025 - .await); 1026 - } 1027 1028 let updated = state 1029 .user_repo 1030 - .update_legacy_login(&auth.did, input.allow_legacy_login) 1031 .await 1032 - .map_err(|e| { 1033 - error!("DB error: {:?}", e); 1034 - ApiError::InternalError(None) 1035 - })?; 1036 if !updated { 1037 return Err(ApiError::AccountNotFound); 1038 } 1039 info!( 1040 - did = %&auth.did, 1041 allow_legacy_login = input.allow_legacy_login, 1042 "Legacy login preference updated" 1043 ); ··· 1071 .user_repo 1072 .update_locale(&auth.did, &input.preferred_locale) 1073 .await 1074 - .map_err(|e| { 1075 - error!("DB error updating locale: {:?}", e); 1076 - ApiError::InternalError(None) 1077 - })?; 1078 if !updated { 1079 return Err(ApiError::AccountNotFound); 1080 }
··· 1 + use crate::api::error::{ApiError, DbResultExt}; 2 use crate::api::{EmptyResponse, SuccessResponse}; 3 + use crate::auth::{Active, Auth, Permissive, require_legacy_session_mfa, require_reauth_window}; 4 + use crate::rate_limit::{LoginLimit, RateLimited, RefreshSessionLimit}; 5 + use crate::state::AppState; 6 use crate::types::{AccountState, Did, Handle, PlainPassword}; 7 + use crate::util::{pds_hostname, pds_hostname_without_port}; 8 use axum::{ 9 Json, 10 extract::State, ··· 17 use tracing::{error, info, warn}; 18 use tranquil_types::TokenId; 19 20 fn normalize_handle(identifier: &str, pds_hostname: &str) -> String { 21 let identifier = identifier.trim(); 22 if identifier.contains('@') || identifier.starts_with("did:") { ··· 62 63 pub async fn create_session( 64 State(state): State<AppState>, 65 + rate_limit: RateLimited<LoginLimit>, 66 Json(input): Json<CreateSessionInput>, 67 ) -> Response { 68 + let client_ip = rate_limit.client_ip(); 69 info!( 70 "create_session called with identifier: {}", 71 input.identifier 72 ); 73 + let pds_host = pds_hostname(); 74 + let hostname_for_handles = pds_hostname_without_port(); 75 let normalized_identifier = normalize_handle(&input.identifier, hostname_for_handles); 76 info!( 77 "Normalized identifier: {} -> {}", ··· 226 ip = %client_ip, 227 "Legacy login on TOTP-enabled account - sending notification" 228 ); 229 + let hostname = pds_hostname(); 230 if let Err(e) = crate::comms::comms_repo::enqueue_legacy_login( 231 state.user_repo.as_ref(), 232 state.infra_repo.as_ref(), 233 row.id, 234 + hostname, 235 + client_ip, 236 row.preferred_comms_channel, 237 ) 238 .await ··· 240 error!("Failed to queue legacy login notification: {:?}", e); 241 } 242 } 243 + let handle = full_handle(&row.handle, pds_host); 244 let is_active = account_state.is_active(); 245 let status = account_state.status_for_session().map(String::from); 246 Json(CreateSessionOutput { ··· 279 tranquil_db_traits::CommsChannel::Telegram => ("telegram", row.telegram_verified), 280 tranquil_db_traits::CommsChannel::Signal => ("signal", row.signal_verified), 281 }; 282 + let pds_hostname = pds_hostname(); 283 + let handle = full_handle(&row.handle, pds_hostname); 284 let account_state = AccountState::from_db_fields( 285 row.deactivated_at, 286 row.takedown_ref.clone(), ··· 332 _auth: Auth<Active>, 333 ) -> Result<Response, ApiError> { 334 let extracted = crate::auth::extract_auth_token_from_header( 335 + crate::util::get_header_str(&headers, "Authorization"), 336 ) 337 .ok_or(ApiError::AuthenticationRequired)?; 338 let jti = crate::auth::get_jti_from_token(&extracted.token) ··· 353 354 pub async fn refresh_session( 355 State(state): State<AppState>, 356 + _rate_limit: RateLimited<RefreshSessionLimit>, 357 headers: axum::http::HeaderMap, 358 ) -> Response { 359 let extracted = match crate::auth::extract_auth_token_from_header( 360 + crate::util::get_header_str(&headers, "Authorization"), 361 ) { 362 Some(t) => t, 363 None => return ApiError::AuthenticationRequired.into_response(), ··· 481 tranquil_db_traits::CommsChannel::Telegram => ("telegram", u.telegram_verified), 482 tranquil_db_traits::CommsChannel::Signal => ("signal", u.signal_verified), 483 }; 484 + let pds_hostname = pds_hostname(); 485 + let handle = full_handle(&u.handle, pds_hostname); 486 let account_state = 487 AccountState::from_db_fields(u.deactivated_at, u.takedown_ref.clone(), None, None); 488 let mut response = json!({ ··· 646 return ApiError::InternalError(None).into_response(); 647 } 648 649 + let hostname = pds_hostname(); 650 if let Err(e) = crate::comms::comms_repo::enqueue_welcome( 651 state.user_repo.as_ref(), 652 state.infra_repo.as_ref(), 653 row.id, 654 + hostname, 655 ) 656 .await 657 { ··· 727 let formatted_token = 728 crate::auth::verification_token::format_token_for_display(&verification_token); 729 730 + let hostname = pds_hostname(); 731 if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification( 732 state.infra_repo.as_ref(), 733 row.id, 734 channel_str, 735 &recipient, 736 &formatted_token, 737 + hostname, 738 ) 739 .await 740 { ··· 775 .session_repo 776 .list_sessions_by_did(&auth.did) 777 .await 778 + .log_db_err("fetching JWT sessions")?; 779 780 let oauth_rows = state 781 .oauth_repo 782 .list_sessions_by_did(&auth.did) 783 .await 784 + .log_db_err("fetching OAuth sessions")?; 785 786 let jwt_sessions = jwt_rows.into_iter().map(|row| SessionInfo { 787 id: format!("jwt:{}", row.id), ··· 841 .session_repo 842 .get_session_access_jti_by_id(session_id, &auth.did) 843 .await 844 + .log_db_err("in revoke_session")? 845 .ok_or(ApiError::SessionNotFound)?; 846 state 847 .session_repo 848 .delete_session_by_id(session_id) 849 .await 850 + .log_db_err("deleting session")?; 851 let cache_key = format!("auth:session:{}:{}", &auth.did, access_jti); 852 if let Err(e) = state.cache.delete(&cache_key).await { 853 warn!("Failed to invalidate session cache: {:?}", e); ··· 861 .oauth_repo 862 .delete_session_by_id(session_id, &auth.did) 863 .await 864 + .log_db_err("deleting OAuth session")?; 865 if deleted == 0 { 866 return Err(ApiError::SessionNotFound); 867 } ··· 888 .session_repo 889 .delete_sessions_by_did(&auth.did) 890 .await 891 + .log_db_err("revoking JWT sessions")?; 892 let jti_typed = TokenId::from(jti.clone()); 893 state 894 .oauth_repo 895 .delete_sessions_by_did_except(&auth.did, &jti_typed) 896 .await 897 + .log_db_err("revoking OAuth sessions")?; 898 } else { 899 state 900 .session_repo 901 .delete_sessions_by_did_except_jti(&auth.did, &jti) 902 .await 903 + .log_db_err("revoking JWT sessions")?; 904 state 905 .oauth_repo 906 .delete_sessions_by_did(&auth.did) 907 .await 908 + .log_db_err("revoking OAuth sessions")?; 909 } 910 911 info!(did = %&auth.did, "All other sessions revoked"); ··· 927 .user_repo 928 .get_legacy_login_pref(&auth.did) 929 .await 930 + .log_db_err("getting legacy login pref")? 931 .ok_or(ApiError::AccountNotFound)?; 932 Ok(Json(LegacyLoginPreferenceOutput { 933 allow_legacy_login: pref.allow_legacy_login, ··· 947 auth: Auth<Active>, 948 Json(input): Json<UpdateLegacyLoginInput>, 949 ) -> Result<Response, ApiError> { 950 + let session_mfa = match require_legacy_session_mfa(&state, &auth).await { 951 + Ok(proof) => proof, 952 + Err(response) => return Ok(response), 953 + }; 954 955 + let reauth_mfa = match require_reauth_window(&state, &auth).await { 956 + Ok(proof) => proof, 957 + Err(response) => return Ok(response), 958 + }; 959 960 let updated = state 961 .user_repo 962 + .update_legacy_login(reauth_mfa.did(), input.allow_legacy_login) 963 .await 964 + .log_db_err("updating legacy login")?; 965 if !updated { 966 return Err(ApiError::AccountNotFound); 967 } 968 info!( 969 + did = %session_mfa.did(), 970 allow_legacy_login = input.allow_legacy_login, 971 "Legacy login preference updated" 972 ); ··· 1000 .user_repo 1001 .update_locale(&auth.did, &input.preferred_locale) 1002 .await 1003 + .log_db_err("updating locale")?; 1004 if !updated { 1005 return Err(ApiError::AccountNotFound); 1006 }
+45 -148
crates/tranquil-pds/src/api/server/totp.rs
··· 1 use crate::api::EmptyResponse; 2 - use crate::api::error::ApiError; 3 - use crate::auth::{Active, Auth}; 4 use crate::auth::{ 5 - decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, 6 - generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, 7 - verify_backup_code, verify_totp_code, 8 }; 9 - use crate::state::{AppState, RateLimitKind}; 10 use crate::types::PlainPassword; 11 use axum::{ 12 Json, 13 extract::State, ··· 45 .user_repo 46 .get_handle_by_did(&auth.did) 47 .await 48 - .map_err(|e| { 49 - error!("DB error fetching handle: {:?}", e); 50 - ApiError::InternalError(None) 51 - })? 52 .ok_or(ApiError::AccountNotFound)?; 53 54 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 55 - let uri = generate_totp_uri(&secret, &handle, &hostname); 56 57 - let qr_code = generate_qr_png_base64(&secret, &handle, &hostname).map_err(|e| { 58 error!("Failed to generate QR code: {:?}", e); 59 ApiError::InternalError(Some("Failed to generate QR code".into())) 60 })?; ··· 68 .user_repo 69 .upsert_totp_secret(&auth.did, &encrypted_secret, ENCRYPTION_VERSION) 70 .await 71 - .map_err(|e| { 72 - error!("Failed to store TOTP secret: {:?}", e); 73 - ApiError::InternalError(None) 74 - })?; 75 76 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 77 ··· 101 auth: Auth<Active>, 102 Json(input): Json<EnableTotpInput>, 103 ) -> Result<Response, ApiError> { 104 - if !state 105 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 106 - .await 107 - { 108 - warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 109 - return Err(ApiError::RateLimitExceeded(None)); 110 - } 111 112 let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 113 Ok(Some(row)) => row, ··· 152 .user_repo 153 .enable_totp_with_backup_codes(&auth.did, &backup_hashes) 154 .await 155 - .map_err(|e| { 156 - error!("Failed to enable TOTP: {:?}", e); 157 - ApiError::InternalError(None) 158 - })?; 159 160 info!(did = %&auth.did, "TOTP enabled with {} backup codes", backup_codes.len()); 161 ··· 173 auth: Auth<Active>, 174 Json(input): Json<DisableTotpInput>, 175 ) -> Result<Response, ApiError> { 176 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 177 - { 178 - return Ok(crate::api::server::reauth::legacy_mfa_required_response( 179 - &*state.user_repo, 180 - &*state.session_repo, 181 - &auth.did, 182 - ) 183 - .await); 184 - } 185 - 186 - if !state 187 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 188 - .await 189 - { 190 - warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 191 - return Err(ApiError::RateLimitExceeded(None)); 192 - } 193 - 194 - let password_hash = state 195 - .user_repo 196 - .get_password_hash_by_did(&auth.did) 197 - .await 198 - .map_err(|e| { 199 - error!("DB error fetching user: {:?}", e); 200 - ApiError::InternalError(None) 201 - })? 202 - .ok_or(ApiError::AccountNotFound)?; 203 - 204 - let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 205 - if !password_valid { 206 - return Err(ApiError::InvalidPassword("Password is incorrect".into())); 207 - } 208 - 209 - let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 210 - Ok(Some(row)) if row.verified => row, 211 - Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 212 - Err(e) => { 213 - error!("DB error fetching TOTP: {:?}", e); 214 - return Err(ApiError::InternalError(None)); 215 - } 216 }; 217 218 - let code = input.code.trim(); 219 - let code_valid = if is_backup_code_format(code) { 220 - verify_backup_code_for_user(&state, &auth.did, code).await 221 - } else { 222 - let secret = decrypt_totp_secret( 223 - &totp_record.secret_encrypted, 224 - totp_record.encryption_version, 225 - ) 226 - .map_err(|e| { 227 - error!("Failed to decrypt TOTP secret: {:?}", e); 228 - ApiError::InternalError(None) 229 - })?; 230 - verify_totp_code(&secret, code) 231 - }; 232 233 - if !code_valid { 234 - return Err(ApiError::InvalidCode(Some( 235 - "Invalid verification code".into(), 236 - ))); 237 - } 238 239 state 240 .user_repo 241 - .delete_totp_and_backup_codes(&auth.did) 242 .await 243 - .map_err(|e| { 244 - error!("Failed to delete TOTP: {:?}", e); 245 - ApiError::InternalError(None) 246 - })?; 247 248 - info!(did = %&auth.did, "TOTP disabled"); 249 250 Ok(EmptyResponse::ok().into_response()) 251 } ··· 275 .user_repo 276 .count_unused_backup_codes(&auth.did) 277 .await 278 - .map_err(|e| { 279 - error!("DB error counting backup codes: {:?}", e); 280 - ApiError::InternalError(None) 281 - })?; 282 283 Ok(Json(GetTotpStatusResponse { 284 enabled, ··· 305 auth: Auth<Active>, 306 Json(input): Json<RegenerateBackupCodesInput>, 307 ) -> Result<Response, ApiError> { 308 - if !state 309 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 310 - .await 311 - { 312 - warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 313 - return Err(ApiError::RateLimitExceeded(None)); 314 - } 315 - 316 - let password_hash = state 317 - .user_repo 318 - .get_password_hash_by_did(&auth.did) 319 - .await 320 - .map_err(|e| { 321 - error!("DB error fetching user: {:?}", e); 322 - ApiError::InternalError(None) 323 - })? 324 - .ok_or(ApiError::AccountNotFound)?; 325 - 326 - let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 327 - if !password_valid { 328 - return Err(ApiError::InvalidPassword("Password is incorrect".into())); 329 - } 330 - 331 - let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 332 - Ok(Some(row)) if row.verified => row, 333 - Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 334 - Err(e) => { 335 - error!("DB error fetching TOTP: {:?}", e); 336 - return Err(ApiError::InternalError(None)); 337 - } 338 - }; 339 - 340 - let secret = decrypt_totp_secret( 341 - &totp_record.secret_encrypted, 342 - totp_record.encryption_version, 343 ) 344 - .map_err(|e| { 345 - error!("Failed to decrypt TOTP secret: {:?}", e); 346 - ApiError::InternalError(None) 347 - })?; 348 349 - let code = input.code.trim(); 350 - if !verify_totp_code(&secret, code) { 351 - return Err(ApiError::InvalidCode(Some( 352 - "Invalid verification code".into(), 353 - ))); 354 - } 355 356 let backup_codes = generate_backup_codes(); 357 let backup_hashes: Vec<_> = backup_codes ··· 365 366 state 367 .user_repo 368 - .replace_backup_codes(&auth.did, &backup_hashes) 369 .await 370 - .map_err(|e| { 371 - error!("Failed to regenerate backup codes: {:?}", e); 372 - ApiError::InternalError(None) 373 - })?; 374 375 - info!(did = %&auth.did, "Backup codes regenerated"); 376 377 Ok(Json(RegenerateBackupCodesResponse { backup_codes }).into_response()) 378 }
··· 1 use crate::api::EmptyResponse; 2 + use crate::api::error::{ApiError, DbResultExt}; 3 use crate::auth::{ 4 + Active, Auth, decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, 5 + generate_qr_png_base64, generate_totp_secret, generate_totp_uri, hash_backup_code, 6 + is_backup_code_format, require_legacy_session_mfa, verify_backup_code, verify_password_mfa, 7 + verify_totp_code, verify_totp_mfa, 8 }; 9 + use crate::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message}; 10 + use crate::state::AppState; 11 use crate::types::PlainPassword; 12 + use crate::util::pds_hostname; 13 use axum::{ 14 Json, 15 extract::State, ··· 47 .user_repo 48 .get_handle_by_did(&auth.did) 49 .await 50 + .log_db_err("fetching handle")? 51 .ok_or(ApiError::AccountNotFound)?; 52 53 + let hostname = pds_hostname(); 54 + let uri = generate_totp_uri(&secret, &handle, hostname); 55 56 + let qr_code = generate_qr_png_base64(&secret, &handle, hostname).map_err(|e| { 57 error!("Failed to generate QR code: {:?}", e); 58 ApiError::InternalError(Some("Failed to generate QR code".into())) 59 })?; ··· 67 .user_repo 68 .upsert_totp_secret(&auth.did, &encrypted_secret, ENCRYPTION_VERSION) 69 .await 70 + .log_db_err("storing TOTP secret")?; 71 72 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 73 ··· 97 auth: Auth<Active>, 98 Json(input): Json<EnableTotpInput>, 99 ) -> Result<Response, ApiError> { 100 + let _rate_limit = check_user_rate_limit_with_message::<TotpVerifyLimit>( 101 + &state, 102 + &auth.did, 103 + "Too many verification attempts. Please try again in a few minutes.", 104 + ) 105 + .await?; 106 107 let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 108 Ok(Some(row)) => row, ··· 147 .user_repo 148 .enable_totp_with_backup_codes(&auth.did, &backup_hashes) 149 .await 150 + .log_db_err("enabling TOTP")?; 151 152 info!(did = %&auth.did, "TOTP enabled with {} backup codes", backup_codes.len()); 153 ··· 165 auth: Auth<Active>, 166 Json(input): Json<DisableTotpInput>, 167 ) -> Result<Response, ApiError> { 168 + let _session_mfa = match require_legacy_session_mfa(&state, &auth).await { 169 + Ok(proof) => proof, 170 + Err(response) => return Ok(response), 171 }; 172 173 + let _rate_limit = check_user_rate_limit_with_message::<TotpVerifyLimit>( 174 + &state, 175 + &auth.did, 176 + "Too many verification attempts. Please try again in a few minutes.", 177 + ) 178 + .await?; 179 180 + let password_mfa = verify_password_mfa(&state, &auth, &input.password).await?; 181 + let totp_mfa = verify_totp_mfa(&state, &auth, &input.code).await?; 182 183 state 184 .user_repo 185 + .delete_totp_and_backup_codes(totp_mfa.did()) 186 .await 187 + .log_db_err("deleting TOTP")?; 188 189 + info!(did = %password_mfa.did(), "TOTP disabled (verified via {} and {})", password_mfa.method(), totp_mfa.method()); 190 191 Ok(EmptyResponse::ok().into_response()) 192 } ··· 216 .user_repo 217 .count_unused_backup_codes(&auth.did) 218 .await 219 + .log_db_err("counting backup codes")?; 220 221 Ok(Json(GetTotpStatusResponse { 222 enabled, ··· 243 auth: Auth<Active>, 244 Json(input): Json<RegenerateBackupCodesInput>, 245 ) -> Result<Response, ApiError> { 246 + let _rate_limit = check_user_rate_limit_with_message::<TotpVerifyLimit>( 247 + &state, 248 + &auth.did, 249 + "Too many verification attempts. Please try again in a few minutes.", 250 ) 251 + .await?; 252 253 + let password_mfa = verify_password_mfa(&state, &auth, &input.password).await?; 254 + let totp_mfa = verify_totp_mfa(&state, &auth, &input.code).await?; 255 256 let backup_codes = generate_backup_codes(); 257 let backup_hashes: Vec<_> = backup_codes ··· 265 266 state 267 .user_repo 268 + .replace_backup_codes(totp_mfa.did(), &backup_hashes) 269 .await 270 + .log_db_err("replacing backup codes")?; 271 272 + info!(did = %password_mfa.did(), "Backup codes regenerated (verified via {} and {})", password_mfa.method(), totp_mfa.method()); 273 274 Ok(Json(RegenerateBackupCodesResponse { backup_codes }).into_response()) 275 }
+4 -13
crates/tranquil-pds/src/api/server/trusted_devices.rs
··· 1 use crate::api::SuccessResponse; 2 - use crate::api::error::ApiError; 3 use axum::{ 4 Json, 5 extract::State, ··· 79 .oauth_repo 80 .list_trusted_devices(&auth.did) 81 .await 82 - .map_err(|e| { 83 - error!("DB error: {:?}", e); 84 - ApiError::InternalError(None) 85 - })?; 86 87 let devices = rows 88 .into_iter() ··· 134 .oauth_repo 135 .revoke_device_trust(&device_id) 136 .await 137 - .map_err(|e| { 138 - error!("DB error: {:?}", e); 139 - ApiError::InternalError(None) 140 - })?; 141 142 info!(did = %&auth.did, device_id = %input.device_id, "Trusted device revoked"); 143 Ok(SuccessResponse::ok().into_response()) ··· 175 .oauth_repo 176 .update_device_friendly_name(&device_id, input.friendly_name.as_deref()) 177 .await 178 - .map_err(|e| { 179 - error!("DB error: {:?}", e); 180 - ApiError::InternalError(None) 181 - })?; 182 183 info!(did = %auth.did, device_id = %input.device_id, "Trusted device updated"); 184 Ok(SuccessResponse::ok().into_response())
··· 1 use crate::api::SuccessResponse; 2 + use crate::api::error::{ApiError, DbResultExt}; 3 use axum::{ 4 Json, 5 extract::State, ··· 79 .oauth_repo 80 .list_trusted_devices(&auth.did) 81 .await 82 + .log_db_err("listing trusted devices")?; 83 84 let devices = rows 85 .into_iter() ··· 131 .oauth_repo 132 .revoke_device_trust(&device_id) 133 .await 134 + .log_db_err("revoking device trust")?; 135 136 info!(did = %&auth.did, device_id = %input.device_id, "Trusted device revoked"); 137 Ok(SuccessResponse::ok().into_response()) ··· 169 .oauth_repo 170 .update_device_friendly_name(&device_id, input.friendly_name.as_deref()) 171 .await 172 + .log_db_err("updating device friendly name")?; 173 174 info!(did = %auth.did, device_id = %input.device_id, "Trusted device updated"); 175 Ok(SuccessResponse::ok().into_response())
+3 -2
crates/tranquil-pds/src/api/server/verify_email.rs
··· 5 use tracing::{info, warn}; 6 7 use crate::state::AppState; 8 9 #[derive(Deserialize)] 10 #[serde(rename_all = "camelCase")] ··· 70 return Ok(Json(ResendMigrationVerificationOutput { sent: true })); 71 } 72 73 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 74 let token = crate::auth::verification_token::generate_migration_token(&user.did, &email); 75 let formatted_token = crate::auth::verification_token::format_token_for_display(&token); 76 ··· 80 user.id, 81 &email, 82 &formatted_token, 83 - &hostname, 84 ) 85 .await 86 {
··· 5 use tracing::{info, warn}; 6 7 use crate::state::AppState; 8 + use crate::util::pds_hostname; 9 10 #[derive(Deserialize)] 11 #[serde(rename_all = "camelCase")] ··· 71 return Ok(Json(ResendMigrationVerificationOutput { sent: true })); 72 } 73 74 + let hostname = pds_hostname(); 75 let token = crate::auth::verification_token::generate_migration_token(&user.did, &email); 76 let formatted_token = crate::auth::verification_token::format_token_for_display(&token); 77 ··· 81 user.id, 82 &email, 83 &formatted_token, 84 + hostname, 85 ) 86 .await 87 {
+14 -47
crates/tranquil-pds/src/api/server/verify_token.rs
··· 1 - use crate::api::error::ApiError; 2 use crate::types::Did; 3 use axum::{Json, extract::State}; 4 use serde::{Deserialize, Serialize}; 5 - use tracing::{error, info, warn}; 6 7 use crate::auth::verification_token::{ 8 VerificationPurpose, normalize_token_input, verify_token_signature, ··· 81 .user_repo 82 .get_verification_info(&did_typed) 83 .await 84 - .map_err(|e| { 85 - warn!(error = ?e, "Database error during migration verification"); 86 - ApiError::InternalError(None) 87 - })? 88 .ok_or(ApiError::AccountNotFound)?; 89 90 if user.email.as_ref().map(|e| e.to_lowercase()) != Some(identifier.to_string()) { ··· 96 .user_repo 97 .set_email_verified_flag(user.id) 98 .await 99 - .map_err(|e| { 100 - warn!(error = ?e, "Failed to update email_verified status"); 101 - ApiError::InternalError(None) 102 - })?; 103 } 104 105 info!(did = %did, "Migration email verified successfully"); ··· 125 .user_repo 126 .get_id_by_did(&did_typed) 127 .await 128 - .map_err(|_| ApiError::InternalError(None))? 129 .ok_or(ApiError::AccountNotFound)?; 130 131 match channel { ··· 134 .user_repo 135 .verify_email_channel(user_id, identifier) 136 .await 137 - .map_err(|e| { 138 - error!("Failed to update email channel: {:?}", e); 139 - ApiError::InternalError(None) 140 - })?; 141 if !success { 142 return Err(ApiError::EmailTaken); 143 } ··· 147 .user_repo 148 .verify_discord_channel(user_id, identifier) 149 .await 150 - .map_err(|e| { 151 - error!("Failed to update discord channel: {:?}", e); 152 - ApiError::InternalError(None) 153 - })?; 154 } 155 "telegram" => { 156 state 157 .user_repo 158 .verify_telegram_channel(user_id, identifier) 159 .await 160 - .map_err(|e| { 161 - error!("Failed to update telegram channel: {:?}", e); 162 - ApiError::InternalError(None) 163 - })?; 164 } 165 "signal" => { 166 state 167 .user_repo 168 .verify_signal_channel(user_id, identifier) 169 .await 170 - .map_err(|e| { 171 - error!("Failed to update signal channel: {:?}", e); 172 - ApiError::InternalError(None) 173 - })?; 174 } 175 _ => { 176 return Err(ApiError::InvalidChannel); ··· 200 .user_repo 201 .get_verification_info(&did_typed) 202 .await 203 - .map_err(|e| { 204 - warn!(error = ?e, "Database error during signup verification"); 205 - ApiError::InternalError(None) 206 - })? 207 .ok_or(ApiError::AccountNotFound)?; 208 209 let is_verified = user.email_verified ··· 226 .user_repo 227 .set_email_verified_flag(user.id) 228 .await 229 - .map_err(|e| { 230 - warn!(error = ?e, "Failed to update email verified status"); 231 - ApiError::InternalError(None) 232 - })?; 233 } 234 "discord" => { 235 state 236 .user_repo 237 .set_discord_verified_flag(user.id) 238 .await 239 - .map_err(|e| { 240 - warn!(error = ?e, "Failed to update discord verified status"); 241 - ApiError::InternalError(None) 242 - })?; 243 } 244 "telegram" => { 245 state 246 .user_repo 247 .set_telegram_verified_flag(user.id) 248 .await 249 - .map_err(|e| { 250 - warn!(error = ?e, "Failed to update telegram verified status"); 251 - ApiError::InternalError(None) 252 - })?; 253 } 254 "signal" => { 255 state 256 .user_repo 257 .set_signal_verified_flag(user.id) 258 .await 259 - .map_err(|e| { 260 - warn!(error = ?e, "Failed to update signal verified status"); 261 - ApiError::InternalError(None) 262 - })?; 263 } 264 _ => { 265 return Err(ApiError::InvalidChannel);
··· 1 + use crate::api::error::{ApiError, DbResultExt}; 2 use crate::types::Did; 3 use axum::{Json, extract::State}; 4 use serde::{Deserialize, Serialize}; 5 + use tracing::{info, warn}; 6 7 use crate::auth::verification_token::{ 8 VerificationPurpose, normalize_token_input, verify_token_signature, ··· 81 .user_repo 82 .get_verification_info(&did_typed) 83 .await 84 + .log_db_err("during migration verification")? 85 .ok_or(ApiError::AccountNotFound)?; 86 87 if user.email.as_ref().map(|e| e.to_lowercase()) != Some(identifier.to_string()) { ··· 93 .user_repo 94 .set_email_verified_flag(user.id) 95 .await 96 + .log_db_err("updating email_verified status")?; 97 } 98 99 info!(did = %did, "Migration email verified successfully"); ··· 119 .user_repo 120 .get_id_by_did(&did_typed) 121 .await 122 + .log_db_err("fetching user id")? 123 .ok_or(ApiError::AccountNotFound)?; 124 125 match channel { ··· 128 .user_repo 129 .verify_email_channel(user_id, identifier) 130 .await 131 + .log_db_err("updating email channel")?; 132 if !success { 133 return Err(ApiError::EmailTaken); 134 } ··· 138 .user_repo 139 .verify_discord_channel(user_id, identifier) 140 .await 141 + .log_db_err("updating discord channel")?; 142 } 143 "telegram" => { 144 state 145 .user_repo 146 .verify_telegram_channel(user_id, identifier) 147 .await 148 + .log_db_err("updating telegram channel")?; 149 } 150 "signal" => { 151 state 152 .user_repo 153 .verify_signal_channel(user_id, identifier) 154 .await 155 + .log_db_err("updating signal channel")?; 156 } 157 _ => { 158 return Err(ApiError::InvalidChannel); ··· 182 .user_repo 183 .get_verification_info(&did_typed) 184 .await 185 + .log_db_err("during signup verification")? 186 .ok_or(ApiError::AccountNotFound)?; 187 188 let is_verified = user.email_verified ··· 205 .user_repo 206 .set_email_verified_flag(user.id) 207 .await 208 + .log_db_err("updating email verified status")?; 209 } 210 "discord" => { 211 state 212 .user_repo 213 .set_discord_verified_flag(user.id) 214 .await 215 + .log_db_err("updating discord verified status")?; 216 } 217 "telegram" => { 218 state 219 .user_repo 220 .set_telegram_verified_flag(user.id) 221 .await 222 + .log_db_err("updating telegram verified status")?; 223 } 224 "signal" => { 225 state 226 .user_repo 227 .set_signal_verified_flag(user.id) 228 .await 229 + .log_db_err("updating signal verified status")?; 230 } 231 _ => { 232 return Err(ApiError::InvalidChannel);
+22 -18
crates/tranquil-pds/src/auth/extractor.rs
··· 9 10 use super::{ 11 AccountStatus, AuthSource, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, 12 - is_service_token, validate_bearer_token_for_service_auth, 13 }; 14 use crate::api::error::ApiError; 15 use crate::oauth::scopes::{RepoAction, ScopePermissions}; ··· 293 return Ok(ExtractedAuth::Service(claims)); 294 } 295 296 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 297 let method = parts.method.as_str(); 298 let uri = build_full_url(&parts.uri.to_string()); 299 ··· 358 } 359 } 360 361 impl<P: AuthPolicy> FromRequestParts<AppState> for Auth<P> { 362 type Rejection = AuthError; 363 ··· 418 ) -> Result<Self, Self::Rejection> { 419 match extract_auth_internal(parts, state).await? { 420 ExtractedAuth::Service(claims) => { 421 - let did: Did = claims 422 - .iss 423 - .parse() 424 - .map_err(|_| AuthError::AuthenticationFailed)?; 425 Ok(ServiceAuth { did, claims }) 426 } 427 ExtractedAuth::User(_) => Err(AuthError::AuthenticationFailed), ··· 438 ) -> Result<Option<Self>, Self::Rejection> { 439 match extract_auth_internal(parts, state).await { 440 Ok(ExtractedAuth::Service(claims)) => { 441 - let did: Did = claims 442 - .iss 443 - .parse() 444 - .map_err(|_| AuthError::AuthenticationFailed)?; 445 Ok(Some(ServiceAuth { did, claims })) 446 } 447 Ok(ExtractedAuth::User(_)) => Err(AuthError::AuthenticationFailed), ··· 503 Ok(AuthAny::User(Auth(user, PhantomData))) 504 } 505 ExtractedAuth::Service(claims) => { 506 - let did: Did = claims 507 - .iss 508 - .parse() 509 - .map_err(|_| AuthError::AuthenticationFailed)?; 510 Ok(AuthAny::Service(ServiceAuth { did, claims })) 511 } 512 } ··· 526 Ok(Some(AuthAny::User(Auth(user, PhantomData)))) 527 } 528 Ok(ExtractedAuth::Service(claims)) => { 529 - let did: Did = claims 530 - .iss 531 - .parse() 532 - .map_err(|_| AuthError::AuthenticationFailed)?; 533 Ok(Some(AuthAny::Service(ServiceAuth { did, claims }))) 534 } 535 Err(AuthError::MissingToken) => Ok(None),
··· 9 10 use super::{ 11 AccountStatus, AuthSource, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, 12 + is_service_token, scope_verified::VerifyScope, validate_bearer_token_for_service_auth, 13 }; 14 use crate::api::error::ApiError; 15 use crate::oauth::scopes::{RepoAction, ScopePermissions}; ··· 293 return Ok(ExtractedAuth::Service(claims)); 294 } 295 296 + let dpop_proof = crate::util::get_header_str(&parts.headers, "DPoP"); 297 let method = parts.method.as_str(); 298 let uri = build_full_url(&parts.uri.to_string()); 299 ··· 358 } 359 } 360 361 + impl<P: AuthPolicy> AsRef<AuthenticatedUser> for Auth<P> { 362 + fn as_ref(&self) -> &AuthenticatedUser { 363 + &self.0 364 + } 365 + } 366 + 367 + impl<P: AuthPolicy> VerifyScope for Auth<P> { 368 + fn needs_scope_check(&self) -> bool { 369 + self.0.is_oauth() 370 + } 371 + 372 + fn permissions(&self) -> ScopePermissions { 373 + self.0.permissions() 374 + } 375 + } 376 + 377 impl<P: AuthPolicy> FromRequestParts<AppState> for Auth<P> { 378 type Rejection = AuthError; 379 ··· 434 ) -> Result<Self, Self::Rejection> { 435 match extract_auth_internal(parts, state).await? { 436 ExtractedAuth::Service(claims) => { 437 + let did = claims.iss.clone(); 438 Ok(ServiceAuth { did, claims }) 439 } 440 ExtractedAuth::User(_) => Err(AuthError::AuthenticationFailed), ··· 451 ) -> Result<Option<Self>, Self::Rejection> { 452 match extract_auth_internal(parts, state).await { 453 Ok(ExtractedAuth::Service(claims)) => { 454 + let did = claims.iss.clone(); 455 Ok(Some(ServiceAuth { did, claims })) 456 } 457 Ok(ExtractedAuth::User(_)) => Err(AuthError::AuthenticationFailed), ··· 513 Ok(AuthAny::User(Auth(user, PhantomData))) 514 } 515 ExtractedAuth::Service(claims) => { 516 + let did = claims.iss.clone(); 517 Ok(AuthAny::Service(ServiceAuth { did, claims })) 518 } 519 } ··· 533 Ok(Some(AuthAny::User(Auth(user, PhantomData)))) 534 } 535 Ok(ExtractedAuth::Service(claims)) => { 536 + let did = claims.iss.clone(); 537 Ok(Some(AuthAny::Service(ServiceAuth { did, claims }))) 538 } 539 Err(AuthError::MissingToken) => Ok(None),
+223
crates/tranquil-pds/src/auth/mfa_verified.rs
···
··· 1 + use axum::response::Response; 2 + 3 + use super::AuthenticatedUser; 4 + use crate::state::AppState; 5 + use crate::types::Did; 6 + 7 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 8 + pub enum MfaMethod { 9 + Totp, 10 + Passkey, 11 + Password, 12 + RecoveryCode, 13 + SessionReauth, 14 + } 15 + 16 + impl MfaMethod { 17 + pub fn as_str(&self) -> &'static str { 18 + match self { 19 + Self::Totp => "totp", 20 + Self::Passkey => "passkey", 21 + Self::Password => "password", 22 + Self::RecoveryCode => "recovery_code", 23 + Self::SessionReauth => "session_reauth", 24 + } 25 + } 26 + } 27 + 28 + impl std::fmt::Display for MfaMethod { 29 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 + write!(f, "{}", self.as_str()) 31 + } 32 + } 33 + 34 + pub struct MfaVerified<'a> { 35 + user: &'a AuthenticatedUser, 36 + method: MfaMethod, 37 + } 38 + 39 + impl<'a> MfaVerified<'a> { 40 + fn new(user: &'a AuthenticatedUser, method: MfaMethod) -> Self { 41 + Self { user, method } 42 + } 43 + 44 + pub(crate) fn from_totp(user: &'a AuthenticatedUser) -> Self { 45 + Self::new(user, MfaMethod::Totp) 46 + } 47 + 48 + pub(crate) fn from_password(user: &'a AuthenticatedUser) -> Self { 49 + Self::new(user, MfaMethod::Password) 50 + } 51 + 52 + pub(crate) fn from_recovery_code(user: &'a AuthenticatedUser) -> Self { 53 + Self::new(user, MfaMethod::RecoveryCode) 54 + } 55 + 56 + pub(crate) fn from_session_reauth(user: &'a AuthenticatedUser) -> Self { 57 + Self::new(user, MfaMethod::SessionReauth) 58 + } 59 + 60 + pub fn user(&self) -> &AuthenticatedUser { 61 + self.user 62 + } 63 + 64 + pub fn did(&self) -> &Did { 65 + &self.user.did 66 + } 67 + 68 + pub fn method(&self) -> MfaMethod { 69 + self.method 70 + } 71 + } 72 + 73 + pub async fn require_legacy_session_mfa<'a>( 74 + state: &AppState, 75 + user: &'a AuthenticatedUser, 76 + ) -> Result<MfaVerified<'a>, Response> { 77 + use crate::api::server::reauth::{check_legacy_session_mfa, legacy_mfa_required_response}; 78 + 79 + if check_legacy_session_mfa(&*state.session_repo, &user.did).await { 80 + Ok(MfaVerified::from_session_reauth(user)) 81 + } else { 82 + Err(legacy_mfa_required_response(&*state.user_repo, &*state.session_repo, &user.did).await) 83 + } 84 + } 85 + 86 + pub async fn require_reauth_window<'a>( 87 + state: &AppState, 88 + user: &'a AuthenticatedUser, 89 + ) -> Result<MfaVerified<'a>, Response> { 90 + use chrono::Utc; 91 + use crate::api::server::reauth::{REAUTH_WINDOW_SECONDS, reauth_required_response}; 92 + 93 + let status = state.session_repo.get_session_mfa_status(&user.did).await.ok().flatten(); 94 + 95 + match status { 96 + Some(s) => { 97 + if let Some(last_reauth) = s.last_reauth_at { 98 + let elapsed = Utc::now().signed_duration_since(last_reauth); 99 + if elapsed.num_seconds() <= REAUTH_WINDOW_SECONDS { 100 + return Ok(MfaVerified::from_session_reauth(user)); 101 + } 102 + } 103 + Err(reauth_required_response(&*state.user_repo, &*state.session_repo, &user.did).await) 104 + } 105 + None => { 106 + Err(reauth_required_response(&*state.user_repo, &*state.session_repo, &user.did).await) 107 + } 108 + } 109 + } 110 + 111 + pub async fn require_reauth_window_if_available<'a>( 112 + state: &AppState, 113 + user: &'a AuthenticatedUser, 114 + ) -> Result<Option<MfaVerified<'a>>, Response> { 115 + use crate::api::server::reauth::{check_reauth_required_cached, reauth_required_response}; 116 + 117 + let has_password = state 118 + .user_repo 119 + .has_password_by_did(&user.did) 120 + .await 121 + .ok() 122 + .flatten() 123 + .unwrap_or(false); 124 + let has_passkeys = state 125 + .user_repo 126 + .has_passkeys(&user.did) 127 + .await 128 + .unwrap_or(false); 129 + let has_totp = state 130 + .user_repo 131 + .has_totp_enabled(&user.did) 132 + .await 133 + .unwrap_or(false); 134 + 135 + let has_any_reauth_method = has_password || has_passkeys || has_totp; 136 + 137 + if !has_any_reauth_method { 138 + return Ok(None); 139 + } 140 + 141 + if check_reauth_required_cached(&*state.session_repo, &state.cache, &user.did).await { 142 + Err(reauth_required_response(&*state.user_repo, &*state.session_repo, &user.did).await) 143 + } else { 144 + Ok(Some(MfaVerified::from_session_reauth(user))) 145 + } 146 + } 147 + 148 + pub async fn verify_password_mfa<'a>( 149 + state: &AppState, 150 + user: &'a AuthenticatedUser, 151 + password: &str, 152 + ) -> Result<MfaVerified<'a>, crate::api::error::ApiError> { 153 + let hash = state 154 + .user_repo 155 + .get_password_hash_by_did(&user.did) 156 + .await 157 + .ok() 158 + .flatten(); 159 + 160 + match hash { 161 + Some(h) => { 162 + if bcrypt::verify(password, &h).unwrap_or(false) { 163 + Ok(MfaVerified::from_password(user)) 164 + } else { 165 + Err(crate::api::error::ApiError::InvalidPassword( 166 + "Password is incorrect".into(), 167 + )) 168 + } 169 + } 170 + None => Err(crate::api::error::ApiError::AccountNotFound), 171 + } 172 + } 173 + 174 + pub async fn verify_totp_mfa<'a>( 175 + state: &AppState, 176 + user: &'a AuthenticatedUser, 177 + code: &str, 178 + ) -> Result<MfaVerified<'a>, crate::api::error::ApiError> { 179 + use crate::auth::{decrypt_totp_secret, is_backup_code_format, verify_totp_code}; 180 + 181 + let code = code.trim(); 182 + 183 + if is_backup_code_format(code) { 184 + let backup_codes = state.user_repo.get_unused_backup_codes(&user.did).await.ok().unwrap_or_default(); 185 + let code_upper = code.to_uppercase(); 186 + 187 + let matched = backup_codes 188 + .iter() 189 + .find(|row| crate::auth::verify_backup_code(&code_upper, &row.code_hash)); 190 + 191 + return match matched { 192 + Some(row) => { 193 + let _ = state.user_repo.mark_backup_code_used(row.id).await; 194 + Ok(MfaVerified::from_recovery_code(user)) 195 + } 196 + None => Err(crate::api::error::ApiError::InvalidCode(Some( 197 + "Invalid backup code".into(), 198 + ))), 199 + }; 200 + } 201 + 202 + let totp_record = match state.user_repo.get_totp_record(&user.did).await { 203 + Ok(Some(row)) if row.verified => row, 204 + _ => { 205 + return Err(crate::api::error::ApiError::TotpNotEnabled); 206 + } 207 + }; 208 + 209 + let secret = decrypt_totp_secret( 210 + &totp_record.secret_encrypted, 211 + totp_record.encryption_version, 212 + ) 213 + .map_err(|_| crate::api::error::ApiError::InternalError(None))?; 214 + 215 + if verify_totp_code(&secret, code) { 216 + let _ = state.user_repo.update_totp_last_used(&user.did).await; 217 + Ok(MfaVerified::from_totp(user)) 218 + } else { 219 + Err(crate::api::error::ApiError::InvalidCode(Some( 220 + "Invalid verification code".into(), 221 + ))) 222 + } 223 + }
+10
crates/tranquil-pds/src/auth/mod.rs
··· 11 use tranquil_db_traits::OAuthRepository; 12 13 pub mod extractor; 14 pub mod scope_check; 15 pub mod service; 16 pub mod verification_token; 17 pub mod webauthn; ··· 20 Active, Admin, AnyUser, Auth, AuthAny, AuthError, AuthPolicy, ExtractedToken, NotTakendown, 21 Permissive, ServiceAuth, extract_auth_token_from_header, extract_bearer_token_from_header, 22 }; 23 pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; 24 25 pub use tranquil_auth::{
··· 11 use tranquil_db_traits::OAuthRepository; 12 13 pub mod extractor; 14 + pub mod mfa_verified; 15 pub mod scope_check; 16 + pub mod scope_verified; 17 pub mod service; 18 pub mod verification_token; 19 pub mod webauthn; ··· 22 Active, Admin, AnyUser, Auth, AuthAny, AuthError, AuthPolicy, ExtractedToken, NotTakendown, 23 Permissive, ServiceAuth, extract_auth_token_from_header, extract_bearer_token_from_header, 24 }; 25 + pub use mfa_verified::{ 26 + MfaMethod, MfaVerified, require_legacy_session_mfa, require_reauth_window, 27 + require_reauth_window_if_available, verify_password_mfa, verify_totp_mfa, 28 + }; 29 + pub use scope_verified::{ 30 + AccountManage, AccountRead, BlobUpload, IdentityAccess, RepoCreate, RepoDelete, RepoUpdate, 31 + RpcCall, ScopeAction, ScopeVerificationError, ScopeVerified, VerifyScope, 32 + }; 33 pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; 34 35 pub use tranquil_auth::{
+277
crates/tranquil-pds/src/auth/scope_verified.rs
···
··· 1 + use std::marker::PhantomData; 2 + 3 + use axum::response::{IntoResponse, Response}; 4 + 5 + use crate::api::error::ApiError; 6 + use crate::oauth::scopes::{AccountAction, AccountAttr, IdentityAttr, RepoAction, ScopePermissions}; 7 + 8 + use super::AuthenticatedUser; 9 + 10 + #[derive(Debug)] 11 + pub struct ScopeVerificationError { 12 + message: String, 13 + } 14 + 15 + impl ScopeVerificationError { 16 + pub fn new(message: impl Into<String>) -> Self { 17 + Self { 18 + message: message.into(), 19 + } 20 + } 21 + 22 + pub fn message(&self) -> &str { 23 + &self.message 24 + } 25 + } 26 + 27 + impl std::fmt::Display for ScopeVerificationError { 28 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 + write!(f, "{}", self.message) 30 + } 31 + } 32 + 33 + impl std::error::Error for ScopeVerificationError {} 34 + 35 + impl IntoResponse for ScopeVerificationError { 36 + fn into_response(self) -> Response { 37 + ApiError::InsufficientScope(Some(self.message)).into_response() 38 + } 39 + } 40 + 41 + mod private { 42 + pub trait Sealed {} 43 + } 44 + 45 + pub trait ScopeAction: private::Sealed {} 46 + 47 + pub struct RepoCreate; 48 + pub struct RepoUpdate; 49 + pub struct RepoDelete; 50 + pub struct BlobUpload; 51 + pub struct RpcCall; 52 + pub struct AccountRead; 53 + pub struct AccountManage; 54 + pub struct IdentityAccess; 55 + 56 + impl private::Sealed for RepoCreate {} 57 + impl private::Sealed for RepoUpdate {} 58 + impl private::Sealed for RepoDelete {} 59 + impl private::Sealed for BlobUpload {} 60 + impl private::Sealed for RpcCall {} 61 + impl private::Sealed for AccountRead {} 62 + impl private::Sealed for AccountManage {} 63 + impl private::Sealed for IdentityAccess {} 64 + 65 + impl ScopeAction for RepoCreate {} 66 + impl ScopeAction for RepoUpdate {} 67 + impl ScopeAction for RepoDelete {} 68 + impl ScopeAction for BlobUpload {} 69 + impl ScopeAction for RpcCall {} 70 + impl ScopeAction for AccountRead {} 71 + impl ScopeAction for AccountManage {} 72 + impl ScopeAction for IdentityAccess {} 73 + 74 + pub struct ScopeVerified<'a, A: ScopeAction> { 75 + user: &'a AuthenticatedUser, 76 + _action: PhantomData<A>, 77 + } 78 + 79 + impl<'a, A: ScopeAction> ScopeVerified<'a, A> { 80 + pub fn user(&self) -> &AuthenticatedUser { 81 + self.user 82 + } 83 + 84 + pub fn did(&self) -> &crate::types::Did { 85 + &self.user.did 86 + } 87 + 88 + pub fn is_admin(&self) -> bool { 89 + self.user.is_admin 90 + } 91 + 92 + pub fn controller_did(&self) -> Option<&crate::types::Did> { 93 + self.user.controller_did.as_ref() 94 + } 95 + } 96 + 97 + pub trait VerifyScope { 98 + fn needs_scope_check(&self) -> bool; 99 + fn permissions(&self) -> ScopePermissions; 100 + 101 + fn verify_repo_create<'a>( 102 + &'a self, 103 + collection: &str, 104 + ) -> Result<ScopeVerified<'a, RepoCreate>, ScopeVerificationError> 105 + where 106 + Self: AsRef<AuthenticatedUser>, 107 + { 108 + if !self.needs_scope_check() { 109 + return Ok(ScopeVerified { 110 + user: self.as_ref(), 111 + _action: PhantomData, 112 + }); 113 + } 114 + self.permissions() 115 + .assert_repo(RepoAction::Create, collection) 116 + .map_err(|e| ScopeVerificationError::new(e.to_string()))?; 117 + Ok(ScopeVerified { 118 + user: self.as_ref(), 119 + _action: PhantomData, 120 + }) 121 + } 122 + 123 + fn verify_repo_update<'a>( 124 + &'a self, 125 + collection: &str, 126 + ) -> Result<ScopeVerified<'a, RepoUpdate>, ScopeVerificationError> 127 + where 128 + Self: AsRef<AuthenticatedUser>, 129 + { 130 + if !self.needs_scope_check() { 131 + return Ok(ScopeVerified { 132 + user: self.as_ref(), 133 + _action: PhantomData, 134 + }); 135 + } 136 + self.permissions() 137 + .assert_repo(RepoAction::Update, collection) 138 + .map_err(|e| ScopeVerificationError::new(e.to_string()))?; 139 + Ok(ScopeVerified { 140 + user: self.as_ref(), 141 + _action: PhantomData, 142 + }) 143 + } 144 + 145 + fn verify_repo_delete<'a>( 146 + &'a self, 147 + collection: &str, 148 + ) -> Result<ScopeVerified<'a, RepoDelete>, ScopeVerificationError> 149 + where 150 + Self: AsRef<AuthenticatedUser>, 151 + { 152 + if !self.needs_scope_check() { 153 + return Ok(ScopeVerified { 154 + user: self.as_ref(), 155 + _action: PhantomData, 156 + }); 157 + } 158 + self.permissions() 159 + .assert_repo(RepoAction::Delete, collection) 160 + .map_err(|e| ScopeVerificationError::new(e.to_string()))?; 161 + Ok(ScopeVerified { 162 + user: self.as_ref(), 163 + _action: PhantomData, 164 + }) 165 + } 166 + 167 + fn verify_blob_upload<'a>( 168 + &'a self, 169 + mime_type: &str, 170 + ) -> Result<ScopeVerified<'a, BlobUpload>, ScopeVerificationError> 171 + where 172 + Self: AsRef<AuthenticatedUser>, 173 + { 174 + if !self.needs_scope_check() { 175 + return Ok(ScopeVerified { 176 + user: self.as_ref(), 177 + _action: PhantomData, 178 + }); 179 + } 180 + self.permissions() 181 + .assert_blob(mime_type) 182 + .map_err(|e| ScopeVerificationError::new(e.to_string()))?; 183 + Ok(ScopeVerified { 184 + user: self.as_ref(), 185 + _action: PhantomData, 186 + }) 187 + } 188 + 189 + fn verify_rpc<'a>( 190 + &'a self, 191 + aud: &str, 192 + lxm: &str, 193 + ) -> Result<ScopeVerified<'a, RpcCall>, ScopeVerificationError> 194 + where 195 + Self: AsRef<AuthenticatedUser>, 196 + { 197 + if !self.needs_scope_check() { 198 + return Ok(ScopeVerified { 199 + user: self.as_ref(), 200 + _action: PhantomData, 201 + }); 202 + } 203 + self.permissions() 204 + .assert_rpc(aud, lxm) 205 + .map_err(|e| ScopeVerificationError::new(e.to_string()))?; 206 + Ok(ScopeVerified { 207 + user: self.as_ref(), 208 + _action: PhantomData, 209 + }) 210 + } 211 + 212 + fn verify_account_read<'a>( 213 + &'a self, 214 + attr: AccountAttr, 215 + ) -> Result<ScopeVerified<'a, AccountRead>, ScopeVerificationError> 216 + where 217 + Self: AsRef<AuthenticatedUser>, 218 + { 219 + if !self.needs_scope_check() { 220 + return Ok(ScopeVerified { 221 + user: self.as_ref(), 222 + _action: PhantomData, 223 + }); 224 + } 225 + self.permissions() 226 + .assert_account(attr, AccountAction::Read) 227 + .map_err(|e| ScopeVerificationError::new(e.to_string()))?; 228 + Ok(ScopeVerified { 229 + user: self.as_ref(), 230 + _action: PhantomData, 231 + }) 232 + } 233 + 234 + fn verify_account_manage<'a>( 235 + &'a self, 236 + attr: AccountAttr, 237 + ) -> Result<ScopeVerified<'a, AccountManage>, ScopeVerificationError> 238 + where 239 + Self: AsRef<AuthenticatedUser>, 240 + { 241 + if !self.needs_scope_check() { 242 + return Ok(ScopeVerified { 243 + user: self.as_ref(), 244 + _action: PhantomData, 245 + }); 246 + } 247 + self.permissions() 248 + .assert_account(attr, AccountAction::Manage) 249 + .map_err(|e| ScopeVerificationError::new(e.to_string()))?; 250 + Ok(ScopeVerified { 251 + user: self.as_ref(), 252 + _action: PhantomData, 253 + }) 254 + } 255 + 256 + fn verify_identity<'a>( 257 + &'a self, 258 + attr: IdentityAttr, 259 + ) -> Result<ScopeVerified<'a, IdentityAccess>, ScopeVerificationError> 260 + where 261 + Self: AsRef<AuthenticatedUser>, 262 + { 263 + if !self.needs_scope_check() { 264 + return Ok(ScopeVerified { 265 + user: self.as_ref(), 266 + _action: PhantomData, 267 + }); 268 + } 269 + self.permissions() 270 + .assert_identity(attr) 271 + .map_err(|e| ScopeVerificationError::new(e.to_string()))?; 272 + Ok(ScopeVerified { 273 + user: self.as_ref(), 274 + _action: PhantomData, 275 + }) 276 + } 277 + }
+10 -8
crates/tranquil-pds/src/auth/service.rs
··· 1 use anyhow::{Result, anyhow}; 2 use base64::Engine as _; 3 use base64::engine::general_purpose::URL_SAFE_NO_PAD; ··· 42 43 #[derive(Debug, Clone, Serialize, Deserialize)] 44 pub struct ServiceTokenClaims { 45 - pub iss: String, 46 #[serde(default)] 47 - pub sub: Option<String>, 48 - pub aud: String, 49 pub exp: usize, 50 #[serde(default)] 51 pub iat: Option<usize>, ··· 56 } 57 58 impl ServiceTokenClaims { 59 - pub fn subject(&self) -> &str { 60 - self.sub.as_deref().unwrap_or(&self.iss) 61 } 62 } 63 ··· 79 .unwrap_or_else(|_| "https://plc.directory".to_string()); 80 81 let pds_hostname = 82 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 83 let pds_did = format!("did:web:{}", pds_hostname); 84 85 let client = Client::builder() ··· 130 return Err(anyhow!("Token expired")); 131 } 132 133 - if claims.aud != self.pds_did { 134 return Err(anyhow!( 135 "Invalid audience: expected {}, got {}", 136 self.pds_did, ··· 154 } 155 } 156 157 - let did = &claims.iss; 158 let public_key = self.resolve_signing_key(did).await?; 159 160 let signature_bytes = URL_SAFE_NO_PAD
··· 1 + use crate::types::Did; 2 + use crate::util::pds_hostname; 3 use anyhow::{Result, anyhow}; 4 use base64::Engine as _; 5 use base64::engine::general_purpose::URL_SAFE_NO_PAD; ··· 44 45 #[derive(Debug, Clone, Serialize, Deserialize)] 46 pub struct ServiceTokenClaims { 47 + pub iss: Did, 48 #[serde(default)] 49 + pub sub: Option<Did>, 50 + pub aud: Did, 51 pub exp: usize, 52 #[serde(default)] 53 pub iat: Option<usize>, ··· 58 } 59 60 impl ServiceTokenClaims { 61 + pub fn subject(&self) -> &Did { 62 + self.sub.as_ref().unwrap_or(&self.iss) 63 } 64 } 65 ··· 81 .unwrap_or_else(|_| "https://plc.directory".to_string()); 82 83 let pds_hostname = 84 + pds_hostname(); 85 let pds_did = format!("did:web:{}", pds_hostname); 86 87 let client = Client::builder() ··· 132 return Err(anyhow!("Token expired")); 133 } 134 135 + if claims.aud.as_str() != self.pds_did { 136 return Err(anyhow!( 137 "Invalid audience: expected {}, got {}", 138 self.pds_did, ··· 156 } 157 } 158 159 + let did = claims.iss.as_str(); 160 let public_key = self.resolve_signing_key(did).await?; 161 162 let signature_bytes = URL_SAFE_NO_PAD
+1 -1
crates/tranquil-pds/src/auth/webauthn.rs
··· 7 8 impl WebAuthnConfig { 9 pub fn new(hostname: &str) -> Result<Self, String> { 10 - let rp_id = hostname.to_string(); 11 let rp_origin = Url::parse(&format!("https://{}", hostname)) 12 .map_err(|e| format!("Invalid origin URL: {}", e))?; 13
··· 7 8 impl WebAuthnConfig { 9 pub fn new(hostname: &str) -> Result<Self, String> { 10 + let rp_id = hostname.split(':').next().unwrap_or(hostname).to_string(); 11 let rp_origin = Url::parse(&format!("https://{}", hostname)) 12 .map_err(|e| format!("Invalid origin URL: {}", e))?; 13
+6 -2
crates/tranquil-pds/src/crawlers.rs
··· 1 use crate::circuit_breaker::CircuitBreaker; 2 use crate::sync::firehose::SequencedEvent; 3 use reqwest::Client; 4 use std::sync::Arc; 5 use std::sync::atomic::{AtomicU64, Ordering}; ··· 40 } 41 42 pub fn from_env() -> Option<Self> { 43 - let hostname = std::env::var("PDS_HOSTNAME").ok()?; 44 45 let crawler_urls: Vec<String> = std::env::var("CRAWLERS") 46 .unwrap_or_default() ··· 53 return None; 54 } 55 56 - Some(Self::new(hostname, crawler_urls)) 57 } 58 59 fn should_notify(&self) -> bool {
··· 1 use crate::circuit_breaker::CircuitBreaker; 2 use crate::sync::firehose::SequencedEvent; 3 + use crate::util::pds_hostname; 4 use reqwest::Client; 5 use std::sync::Arc; 6 use std::sync::atomic::{AtomicU64, Ordering}; ··· 41 } 42 43 pub fn from_env() -> Option<Self> { 44 + let hostname = pds_hostname(); 45 + if hostname == "localhost" { 46 + return None; 47 + } 48 49 let crawler_urls: Vec<String> = std::env::var("CRAWLERS") 50 .unwrap_or_default() ··· 57 return None; 58 } 59 60 + Some(Self::new(hostname.to_string(), crawler_urls)) 61 } 62 63 fn should_notify(&self) -> bool {
+5
crates/tranquil-pds/src/delegation/mod.rs
··· 1 pub mod scopes; 2 3 pub use scopes::{SCOPE_PRESETS, ScopePreset, intersect_scopes}; 4 pub use tranquil_db_traits::DelegationActionType;
··· 1 + pub mod roles; 2 pub mod scopes; 3 4 + pub use roles::{ 5 + CanAddControllers, CanControlAccounts, verify_can_add_controllers, verify_can_be_controller, 6 + verify_can_control_accounts, 7 + }; 8 pub use scopes::{SCOPE_PRESETS, ScopePreset, intersect_scopes}; 9 pub use tranquil_db_traits::DelegationActionType;
+88
crates/tranquil-pds/src/delegation/roles.rs
···
··· 1 + use axum::response::{IntoResponse, Response}; 2 + 3 + use crate::api::error::ApiError; 4 + use crate::auth::AuthenticatedUser; 5 + use crate::state::AppState; 6 + use crate::types::Did; 7 + 8 + pub struct CanAddControllers<'a> { 9 + user: &'a AuthenticatedUser, 10 + } 11 + 12 + pub struct CanControlAccounts<'a> { 13 + user: &'a AuthenticatedUser, 14 + } 15 + 16 + impl<'a> CanAddControllers<'a> { 17 + pub fn did(&self) -> &Did { 18 + &self.user.did 19 + } 20 + 21 + pub fn user(&self) -> &AuthenticatedUser { 22 + self.user 23 + } 24 + } 25 + 26 + impl<'a> CanControlAccounts<'a> { 27 + pub fn did(&self) -> &Did { 28 + &self.user.did 29 + } 30 + 31 + pub fn user(&self) -> &AuthenticatedUser { 32 + self.user 33 + } 34 + } 35 + 36 + pub async fn verify_can_add_controllers<'a>( 37 + state: &AppState, 38 + user: &'a AuthenticatedUser, 39 + ) -> Result<CanAddControllers<'a>, Response> { 40 + match state.delegation_repo.controls_any_accounts(&user.did).await { 41 + Ok(true) => Err(ApiError::InvalidDelegation( 42 + "Cannot add controllers to an account that controls other accounts".into(), 43 + ) 44 + .into_response()), 45 + Ok(false) => Ok(CanAddControllers { user }), 46 + Err(e) => { 47 + tracing::error!("Failed to check delegation status: {:?}", e); 48 + Err(ApiError::InternalError(Some("Failed to verify delegation status".into())) 49 + .into_response()) 50 + } 51 + } 52 + } 53 + 54 + pub async fn verify_can_control_accounts<'a>( 55 + state: &AppState, 56 + user: &'a AuthenticatedUser, 57 + ) -> Result<CanControlAccounts<'a>, Response> { 58 + match state.delegation_repo.has_any_controllers(&user.did).await { 59 + Ok(true) => Err(ApiError::InvalidDelegation( 60 + "Cannot create delegated accounts from a controlled account".into(), 61 + ) 62 + .into_response()), 63 + Ok(false) => Ok(CanControlAccounts { user }), 64 + Err(e) => { 65 + tracing::error!("Failed to check controller status: {:?}", e); 66 + Err(ApiError::InternalError(Some("Failed to verify controller status".into())) 67 + .into_response()) 68 + } 69 + } 70 + } 71 + 72 + pub async fn verify_can_be_controller( 73 + state: &AppState, 74 + controller_did: &Did, 75 + ) -> Result<(), Response> { 76 + match state.delegation_repo.has_any_controllers(controller_did).await { 77 + Ok(true) => Err(ApiError::InvalidDelegation( 78 + "Cannot add a controlled account as a controller".into(), 79 + ) 80 + .into_response()), 81 + Ok(false) => Ok(()), 82 + Err(e) => { 83 + tracing::error!("Failed to check controller status: {:?}", e); 84 + Err(ApiError::InternalError(Some("Failed to verify controller status".into())) 85 + .into_response()) 86 + } 87 + } 88 + }
+94 -255
crates/tranquil-pds/src/oauth/endpoints/authorize.rs
··· 1 use crate::comms::{channel_display_name, comms_repo::enqueue_2fa_code}; 2 use crate::oauth::{ 3 - AuthFlowState, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, SessionId, 4 db::should_show_consent, scopes::expand_include_scopes, 5 }; 6 - use crate::state::{AppState, RateLimitKind}; 7 use crate::types::{Did, Handle, PlainPassword}; 8 use axum::{ 9 Json, 10 extract::{Query, State}, ··· 79 || s.starts_with("include:") 80 } 81 82 - fn validate_auth_flow_state( 83 - flow_state: &AuthFlowState, 84 - require_authenticated: bool, 85 - ) -> Option<Response> { 86 - if flow_state.is_expired() { 87 - return Some(json_error( 88 - StatusCode::BAD_REQUEST, 89 - "invalid_request", 90 - "Authorization request has expired", 91 - )); 92 - } 93 - if require_authenticated && flow_state.is_pending() { 94 - return Some(json_error( 95 - StatusCode::FORBIDDEN, 96 - "access_denied", 97 - "Not authenticated", 98 - )); 99 - } 100 - None 101 - } 102 - 103 fn extract_device_cookie(headers: &HeaderMap) -> Option<String> { 104 headers 105 .get("cookie") ··· 113 }) 114 } 115 116 - fn extract_client_ip(headers: &HeaderMap) -> String { 117 - if let Some(forwarded) = headers.get("x-forwarded-for") 118 - && let Ok(value) = forwarded.to_str() 119 - && let Some(first_ip) = value.split(',').next() 120 - { 121 - return first_ip.trim().to_string(); 122 - } 123 - if let Some(real_ip) = headers.get("x-real-ip") 124 - && let Ok(value) = real_ip.to_str() 125 - { 126 - return value.trim().to_string(); 127 - } 128 - "0.0.0.0".to_string() 129 - } 130 - 131 fn extract_user_agent(headers: &HeaderMap) -> Option<String> { 132 headers 133 .get("user-agent") ··· 282 283 if let Some(ref login_hint) = request_data.parameters.login_hint { 284 tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation"); 285 - let pds_hostname = 286 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 287 - let hostname_for_handles = pds_hostname.split(':').next().unwrap_or(&pds_hostname); 288 let normalized = if login_hint.contains('@') || login_hint.starts_with("did:") { 289 login_hint.clone() 290 } else if !login_hint.contains('.') { ··· 340 tracing::info!("No login_hint in request"); 341 } 342 343 - if request_data.parameters.prompt.as_deref() == Some("create") { 344 return redirect_see_other(&format!( 345 "/app/oauth/register?request_uri={}", 346 url_encode(&request_uri) ··· 485 486 pub async fn authorize_post( 487 State(state): State<AppState>, 488 headers: HeaderMap, 489 Json(form): Json<AuthorizeSubmit>, 490 ) -> Response { 491 let json_response = wants_json(&headers); 492 - let client_ip = extract_client_ip(&headers); 493 - if !state 494 - .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 495 - .await 496 - { 497 - tracing::warn!(ip = %client_ip, "OAuth authorize rate limit exceeded"); 498 - if json_response { 499 - return ( 500 - axum::http::StatusCode::TOO_MANY_REQUESTS, 501 - Json(serde_json::json!({ 502 - "error": "RateLimitExceeded", 503 - "error_description": "Too many login attempts. Please try again later." 504 - })), 505 - ) 506 - .into_response(); 507 - } 508 - return redirect_to_frontend_error( 509 - "RateLimitExceeded", 510 - "Too many login attempts. Please try again later.", 511 - ); 512 - } 513 let form_request_id = RequestId::from(form.request_uri.clone()); 514 let request_data = match state 515 .oauth_repo ··· 584 url_encode(error_msg) 585 )) 586 }; 587 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 588 - let hostname_for_handles = pds_hostname.split(':').next().unwrap_or(&pds_hostname); 589 let normalized_username = form.username.trim(); 590 let normalized_username = normalized_username 591 .strip_prefix('@') ··· 600 tracing::debug!( 601 original_username = %form.username, 602 normalized_username = %normalized_username, 603 - pds_hostname = %pds_hostname, 604 "Normalized username for lookup" 605 ); 606 let user = match state ··· 748 .await 749 { 750 Ok(challenge) => { 751 - let hostname = 752 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 753 if let Err(e) = enqueue_2fa_code( 754 state.user_repo.as_ref(), 755 state.infra_repo.as_ref(), 756 user.id, 757 &challenge.code, 758 - &hostname, 759 ) 760 .await 761 { ··· 792 } else { 793 let new_id = DeviceId::generate(); 794 let device_data = DeviceData { 795 - session_id: SessionId::generate().0, 796 user_agent: extract_user_agent(&headers), 797 - ip_address: extract_client_ip(&headers), 798 last_seen_at: Utc::now(), 799 }; 800 let new_device_id_typed = DeviceIdType::from(new_id.0.clone()); ··· 888 &request_data.parameters.redirect_uri, 889 &code.0, 890 request_data.parameters.state.as_deref(), 891 - request_data.parameters.response_mode.as_deref(), 892 ); 893 if let Some(cookie) = new_cookie { 894 ( ··· 905 &request_data.parameters.redirect_uri, 906 &code.0, 907 request_data.parameters.state.as_deref(), 908 - request_data.parameters.response_mode.as_deref(), 909 ); 910 if let Some(cookie) = new_cookie { 911 ( ··· 1068 .await 1069 { 1070 Ok(challenge) => { 1071 - let hostname = 1072 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1073 if let Err(e) = enqueue_2fa_code( 1074 state.user_repo.as_ref(), 1075 state.infra_repo.as_ref(), 1076 user.id, 1077 &challenge.code, 1078 - &hostname, 1079 ) 1080 .await 1081 { ··· 1169 &request_data.parameters.redirect_uri, 1170 &code.0, 1171 request_data.parameters.state.as_deref(), 1172 - request_data.parameters.response_mode.as_deref(), 1173 ); 1174 Json(serde_json::json!({ 1175 "redirect_uri": redirect_url ··· 1193 '?' 1194 }; 1195 redirect_url.push(separator); 1196 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1197 redirect_url.push_str(&format!( 1198 "iss={}", 1199 - url_encode(&format!("https://{}", pds_hostname)) 1200 )); 1201 if let Some(req_state) = state { 1202 redirect_url.push_str(&format!("&state={}", url_encode(req_state))); ··· 1211 state: Option<&str>, 1212 response_mode: Option<&str>, 1213 ) -> String { 1214 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1215 let mut url = format!( 1216 "https://{}/oauth/authorize/redirect?redirect_uri={}&code={}", 1217 - pds_hostname, 1218 url_encode(redirect_uri), 1219 url_encode(code) 1220 ); ··· 1459 ); 1460 } 1461 }; 1462 - let flow_state = AuthFlowState::from_request_data(&request_data); 1463 - 1464 - if let Some(err_response) = validate_auth_flow_state(&flow_state, true) { 1465 - if flow_state.is_expired() { 1466 let _ = state 1467 .oauth_repo 1468 .delete_authorization_request(&consent_request_id) 1469 .await; 1470 - } 1471 - return err_response; 1472 - } 1473 - 1474 - let did_str = flow_state.did().unwrap().to_string(); 1475 - let did: Did = match did_str.parse() { 1476 - Ok(d) => d, 1477 - Err(_) => { 1478 return json_error( 1479 StatusCode::BAD_REQUEST, 1480 "invalid_request", 1481 - "Invalid DID format in request.", 1482 ); 1483 } 1484 }; 1485 let client_cache = ClientMetadataCache::new(3600); 1486 let client_metadata = client_cache 1487 .get(&request_data.parameters.client_id) ··· 1635 logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()), 1636 scopes, 1637 show_consent, 1638 - did: did_str, 1639 handle: account_handle, 1640 is_delegation, 1641 controller_did: controller_did_resp, ··· 1676 ); 1677 } 1678 }; 1679 - let flow_state = AuthFlowState::from_request_data(&request_data); 1680 - 1681 - if flow_state.is_expired() { 1682 - let _ = state 1683 - .oauth_repo 1684 - .delete_authorization_request(&consent_post_request_id) 1685 - .await; 1686 - return json_error( 1687 - StatusCode::BAD_REQUEST, 1688 - "invalid_request", 1689 - "Authorization request has expired", 1690 - ); 1691 - } 1692 - if flow_state.is_pending() { 1693 - return json_error(StatusCode::FORBIDDEN, "access_denied", "Not authenticated"); 1694 - } 1695 - 1696 - let did_str = flow_state.did().unwrap().to_string(); 1697 - let did: Did = match did_str.parse() { 1698 - Ok(d) => d, 1699 Err(_) => { 1700 return json_error( 1701 StatusCode::BAD_REQUEST, 1702 "invalid_request", 1703 - "Invalid DID format", 1704 ); 1705 } 1706 }; 1707 let original_scope_str = request_data 1708 .parameters 1709 .scope ··· 1799 let consent_post_device_id = request_data 1800 .device_id 1801 .as_ref() 1802 - .map(|d| DeviceIdType::from(d.clone())); 1803 let consent_post_code = AuthorizationCode::from(code.0.clone()); 1804 if state 1805 .oauth_repo ··· 1823 redirect_uri, 1824 &code.0, 1825 request_data.parameters.state.as_deref(), 1826 - request_data.parameters.response_mode.as_deref(), 1827 ); 1828 tracing::info!( 1829 intermediate_url = %intermediate_url, ··· 1835 1836 pub async fn authorize_2fa_post( 1837 State(state): State<AppState>, 1838 headers: HeaderMap, 1839 Json(form): Json<Authorize2faSubmit>, 1840 ) -> Response { ··· 1848 ) 1849 .into_response() 1850 }; 1851 - let client_ip = extract_client_ip(&headers); 1852 - if !state 1853 - .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 1854 - .await 1855 - { 1856 - tracing::warn!(ip = %client_ip, "OAuth 2FA rate limit exceeded"); 1857 - return json_error( 1858 - StatusCode::TOO_MANY_REQUESTS, 1859 - "RateLimitExceeded", 1860 - "Too many attempts. Please try again later.", 1861 - ); 1862 - } 1863 let twofa_post_request_id = RequestId::from(form.request_uri.clone()); 1864 let request_data = match state 1865 .oauth_repo ··· 1956 &request_data.parameters.redirect_uri, 1957 &code.0, 1958 request_data.parameters.state.as_deref(), 1959 - request_data.parameters.response_mode.as_deref(), 1960 ); 1961 return Json(serde_json::json!({ 1962 "redirect_uri": redirect_url ··· 1990 "No 2FA challenge found. Please start over.", 1991 ); 1992 } 1993 - if !state 1994 - .check_rate_limit(RateLimitKind::TotpVerify, &did) 1995 - .await 1996 - { 1997 - tracing::warn!(did = %did, "TOTP verification rate limit exceeded"); 1998 - return json_error( 1999 - StatusCode::TOO_MANY_REQUESTS, 2000 - "RateLimitExceeded", 2001 - "Too many verification attempts. Please try again in a few minutes.", 2002 - ); 2003 - } 2004 let totp_valid = 2005 crate::api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await; 2006 if !totp_valid { ··· 2065 &request_data.parameters.redirect_uri, 2066 &code.0, 2067 request_data.parameters.state.as_deref(), 2068 - request_data.parameters.response_mode.as_deref(), 2069 ); 2070 Json(serde_json::json!({ 2071 "redirect_uri": redirect_url ··· 2089 State(state): State<AppState>, 2090 Query(query): Query<CheckPasskeysQuery>, 2091 ) -> Response { 2092 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2093 - let hostname_for_handles = pds_hostname.split(':').next().unwrap_or(&pds_hostname); 2094 let normalized_identifier = query.identifier.trim(); 2095 let normalized_identifier = normalized_identifier 2096 .strip_prefix('@') ··· 2131 State(state): State<AppState>, 2132 Query(query): Query<CheckPasskeysQuery>, 2133 ) -> Response { 2134 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2135 - let hostname_for_handles = pds_hostname.split(':').next().unwrap_or(&pds_hostname); 2136 let identifier = query.identifier.trim(); 2137 let identifier = identifier.strip_prefix('@').unwrap_or(identifier); 2138 let normalized_identifier = if identifier.contains('@') || identifier.starts_with("did:") { ··· 2200 2201 pub async fn passkey_start( 2202 State(state): State<AppState>, 2203 - headers: HeaderMap, 2204 Json(form): Json<PasskeyStartInput>, 2205 ) -> Response { 2206 - let client_ip = extract_client_ip(&headers); 2207 - 2208 - if !state 2209 - .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 2210 - .await 2211 - { 2212 - tracing::warn!(ip = %client_ip, "OAuth passkey rate limit exceeded"); 2213 - return ( 2214 - StatusCode::TOO_MANY_REQUESTS, 2215 - Json(serde_json::json!({ 2216 - "error": "RateLimitExceeded", 2217 - "error_description": "Too many login attempts. Please try again later." 2218 - })), 2219 - ) 2220 - .into_response(); 2221 - } 2222 - 2223 let passkey_start_request_id = RequestId::from(form.request_uri.clone()); 2224 let request_data = match state 2225 .oauth_repo ··· 2264 .into_response(); 2265 } 2266 2267 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2268 - let hostname_for_handles = pds_hostname.split(':').next().unwrap_or(&pds_hostname); 2269 let normalized_username = form.identifier.trim(); 2270 let normalized_username = normalized_username 2271 .strip_prefix('@') ··· 2386 .into_response(); 2387 } 2388 2389 - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 2390 - Ok(w) => w, 2391 - Err(e) => { 2392 - tracing::error!(error = %e, "Failed to create WebAuthn config"); 2393 - return ( 2394 - StatusCode::INTERNAL_SERVER_ERROR, 2395 - Json(serde_json::json!({ 2396 - "error": "server_error", 2397 - "error_description": "WebAuthn configuration failed." 2398 - })), 2399 - ) 2400 - .into_response(); 2401 - } 2402 - }; 2403 - 2404 - let (rcr, auth_state) = match webauthn.start_authentication(passkeys) { 2405 Ok(result) => result, 2406 Err(e) => { 2407 tracing::error!(error = %e, "Failed to start passkey authentication"); ··· 2680 } 2681 }; 2682 2683 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2684 - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 2685 - Ok(w) => w, 2686 - Err(e) => { 2687 - tracing::error!(error = %e, "Failed to create WebAuthn config"); 2688 - return ( 2689 - StatusCode::INTERNAL_SERVER_ERROR, 2690 - Json(serde_json::json!({ 2691 - "error": "server_error", 2692 - "error_description": "WebAuthn configuration failed." 2693 - })), 2694 - ) 2695 - .into_response(); 2696 - } 2697 - }; 2698 - 2699 - let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { 2700 Ok(r) => r, 2701 Err(e) => { 2702 tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication"); ··· 2769 .await 2770 { 2771 Ok(challenge) => { 2772 - let hostname = 2773 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2774 if let Err(e) = enqueue_2fa_code( 2775 state.user_repo.as_ref(), 2776 state.infra_repo.as_ref(), 2777 user.id, 2778 &challenge.code, 2779 - &hostname, 2780 ) 2781 .await 2782 { ··· 2859 &request_data.parameters.redirect_uri, 2860 &code.0, 2861 request_data.parameters.state.as_deref(), 2862 - request_data.parameters.response_mode.as_deref(), 2863 ); 2864 2865 Json(serde_json::json!({ ··· 2884 State(state): State<AppState>, 2885 Query(query): Query<AuthorizePasskeyQuery>, 2886 ) -> Response { 2887 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2888 - 2889 let auth_passkey_start_request_id = RequestId::from(query.request_uri.clone()); 2890 let request_data = match state 2891 .oauth_repo ··· 2994 .into_response(); 2995 } 2996 2997 - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 2998 - Ok(w) => w, 2999 - Err(e) => { 3000 - tracing::error!("Failed to create WebAuthn config: {:?}", e); 3001 - return ( 3002 - StatusCode::INTERNAL_SERVER_ERROR, 3003 - Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 3004 - ) 3005 - .into_response(); 3006 - } 3007 - }; 3008 - 3009 - let (rcr, auth_state) = match webauthn.start_authentication(passkeys) { 3010 Ok(result) => result, 3011 Err(e) => { 3012 tracing::error!("Failed to start passkey authentication: {:?}", e); ··· 3063 headers: HeaderMap, 3064 Json(form): Json<AuthorizePasskeySubmit>, 3065 ) -> Response { 3066 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 3067 let passkey_finish_request_id = RequestId::from(form.request_uri.clone()); 3068 3069 let request_data = match state ··· 3193 } 3194 }; 3195 3196 - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 3197 - Ok(w) => w, 3198 - Err(e) => { 3199 - tracing::error!("Failed to create WebAuthn config: {:?}", e); 3200 - return ( 3201 - StatusCode::INTERNAL_SERVER_ERROR, 3202 - Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 3203 - ) 3204 - .into_response(); 3205 - } 3206 - }; 3207 - 3208 - let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { 3209 Ok(r) => r, 3210 Err(e) => { 3211 tracing::warn!("Passkey authentication failed: {:?}", e); ··· 3292 state.infra_repo.as_ref(), 3293 user.id, 3294 &challenge.code, 3295 - &pds_hostname, 3296 ) 3297 .await 3298 { ··· 3347 3348 pub async fn register_complete( 3349 State(state): State<AppState>, 3350 - headers: HeaderMap, 3351 Json(form): Json<RegisterCompleteInput>, 3352 ) -> Response { 3353 - let client_ip = extract_client_ip(&headers); 3354 - 3355 - if !state 3356 - .check_rate_limit(RateLimitKind::OAuthRegisterComplete, &client_ip) 3357 - .await 3358 - { 3359 - return ( 3360 - StatusCode::TOO_MANY_REQUESTS, 3361 - Json(serde_json::json!({ 3362 - "error": "RateLimitExceeded", 3363 - "error_description": "Too many attempts. Please try again later." 3364 - })), 3365 - ) 3366 - .into_response(); 3367 - } 3368 - 3369 let did = Did::from(form.did.clone()); 3370 3371 let request_id = RequestId::from(form.request_uri.clone()); ··· 3417 .into_response(); 3418 } 3419 3420 - if request_data.parameters.prompt.as_deref() != Some("create") { 3421 tracing::warn!( 3422 request_uri = %form.request_uri, 3423 prompt = ?request_data.parameters.prompt, ··· 3636 &request_data.parameters.redirect_uri, 3637 &code.0, 3638 request_data.parameters.state.as_deref(), 3639 - request_data.parameters.response_mode.as_deref(), 3640 ); 3641 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 3642 } ··· 3662 None => { 3663 let new_id = DeviceId::generate(); 3664 let device_data = DeviceData { 3665 - session_id: SessionId::generate().0, 3666 user_agent: extract_user_agent(&headers), 3667 - ip_address: extract_client_ip(&headers), 3668 last_seen_at: Utc::now(), 3669 }; 3670 let device_typed = DeviceIdType::from(new_id.0.clone());
··· 1 use crate::comms::{channel_display_name, comms_repo::enqueue_2fa_code}; 2 use crate::oauth::{ 3 + AuthFlow, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, Prompt, SessionId, 4 db::should_show_consent, scopes::expand_include_scopes, 5 }; 6 + use crate::rate_limit::{ 7 + OAuthAuthorizeLimit, OAuthRateLimited, OAuthRegisterCompleteLimit, TotpVerifyLimit, 8 + check_user_rate_limit, 9 + }; 10 + use crate::state::AppState; 11 use crate::types::{Did, Handle, PlainPassword}; 12 + use crate::util::{extract_client_ip, pds_hostname, pds_hostname_without_port}; 13 use axum::{ 14 Json, 15 extract::{Query, State}, ··· 84 || s.starts_with("include:") 85 } 86 87 fn extract_device_cookie(headers: &HeaderMap) -> Option<String> { 88 headers 89 .get("cookie") ··· 97 }) 98 } 99 100 fn extract_user_agent(headers: &HeaderMap) -> Option<String> { 101 headers 102 .get("user-agent") ··· 251 252 if let Some(ref login_hint) = request_data.parameters.login_hint { 253 tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation"); 254 + let hostname_for_handles = pds_hostname_without_port(); 255 let normalized = if login_hint.contains('@') || login_hint.starts_with("did:") { 256 login_hint.clone() 257 } else if !login_hint.contains('.') { ··· 307 tracing::info!("No login_hint in request"); 308 } 309 310 + if request_data.parameters.prompt == Some(Prompt::Create) { 311 return redirect_see_other(&format!( 312 "/app/oauth/register?request_uri={}", 313 url_encode(&request_uri) ··· 452 453 pub async fn authorize_post( 454 State(state): State<AppState>, 455 + _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 456 headers: HeaderMap, 457 Json(form): Json<AuthorizeSubmit>, 458 ) -> Response { 459 let json_response = wants_json(&headers); 460 let form_request_id = RequestId::from(form.request_uri.clone()); 461 let request_data = match state 462 .oauth_repo ··· 531 url_encode(error_msg) 532 )) 533 }; 534 + let hostname_for_handles = pds_hostname_without_port(); 535 let normalized_username = form.username.trim(); 536 let normalized_username = normalized_username 537 .strip_prefix('@') ··· 546 tracing::debug!( 547 original_username = %form.username, 548 normalized_username = %normalized_username, 549 + pds_hostname = %pds_hostname(), 550 "Normalized username for lookup" 551 ); 552 let user = match state ··· 694 .await 695 { 696 Ok(challenge) => { 697 + let hostname = pds_hostname(); 698 if let Err(e) = enqueue_2fa_code( 699 state.user_repo.as_ref(), 700 state.infra_repo.as_ref(), 701 user.id, 702 &challenge.code, 703 + hostname, 704 ) 705 .await 706 { ··· 737 } else { 738 let new_id = DeviceId::generate(); 739 let device_data = DeviceData { 740 + session_id: SessionId::generate(), 741 user_agent: extract_user_agent(&headers), 742 + ip_address: extract_client_ip(&headers, None), 743 last_seen_at: Utc::now(), 744 }; 745 let new_device_id_typed = DeviceIdType::from(new_id.0.clone()); ··· 833 &request_data.parameters.redirect_uri, 834 &code.0, 835 request_data.parameters.state.as_deref(), 836 + request_data.parameters.response_mode.map(|m| m.as_str()), 837 ); 838 if let Some(cookie) = new_cookie { 839 ( ··· 850 &request_data.parameters.redirect_uri, 851 &code.0, 852 request_data.parameters.state.as_deref(), 853 + request_data.parameters.response_mode.map(|m| m.as_str()), 854 ); 855 if let Some(cookie) = new_cookie { 856 ( ··· 1013 .await 1014 { 1015 Ok(challenge) => { 1016 + let hostname = pds_hostname(); 1017 if let Err(e) = enqueue_2fa_code( 1018 state.user_repo.as_ref(), 1019 state.infra_repo.as_ref(), 1020 user.id, 1021 &challenge.code, 1022 + hostname, 1023 ) 1024 .await 1025 { ··· 1113 &request_data.parameters.redirect_uri, 1114 &code.0, 1115 request_data.parameters.state.as_deref(), 1116 + request_data.parameters.response_mode.map(|m| m.as_str()), 1117 ); 1118 Json(serde_json::json!({ 1119 "redirect_uri": redirect_url ··· 1137 '?' 1138 }; 1139 redirect_url.push(separator); 1140 + let pds_host = pds_hostname(); 1141 redirect_url.push_str(&format!( 1142 "iss={}", 1143 + url_encode(&format!("https://{}", pds_host)) 1144 )); 1145 if let Some(req_state) = state { 1146 redirect_url.push_str(&format!("&state={}", url_encode(req_state))); ··· 1155 state: Option<&str>, 1156 response_mode: Option<&str>, 1157 ) -> String { 1158 + let pds_host = pds_hostname(); 1159 let mut url = format!( 1160 "https://{}/oauth/authorize/redirect?redirect_uri={}&code={}", 1161 + pds_host, 1162 url_encode(redirect_uri), 1163 url_encode(code) 1164 ); ··· 1403 ); 1404 } 1405 }; 1406 + let flow_with_user = match AuthFlow::from_request_data(request_data.clone()) { 1407 + Ok(flow) => match flow.require_user() { 1408 + Ok(u) => u, 1409 + Err(_) => { 1410 + return json_error( 1411 + StatusCode::FORBIDDEN, 1412 + "access_denied", 1413 + "Not authenticated", 1414 + ); 1415 + } 1416 + }, 1417 + Err(_) => { 1418 let _ = state 1419 .oauth_repo 1420 .delete_authorization_request(&consent_request_id) 1421 .await; 1422 return json_error( 1423 StatusCode::BAD_REQUEST, 1424 "invalid_request", 1425 + "Authorization request has expired", 1426 ); 1427 } 1428 }; 1429 + 1430 + let did = flow_with_user.did().clone(); 1431 let client_cache = ClientMetadataCache::new(3600); 1432 let client_metadata = client_cache 1433 .get(&request_data.parameters.client_id) ··· 1581 logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()), 1582 scopes, 1583 show_consent, 1584 + did: did.to_string(), 1585 handle: account_handle, 1586 is_delegation, 1587 controller_did: controller_did_resp, ··· 1622 ); 1623 } 1624 }; 1625 + let flow_with_user = match AuthFlow::from_request_data(request_data.clone()) { 1626 + Ok(flow) => match flow.require_user() { 1627 + Ok(u) => u, 1628 + Err(_) => { 1629 + return json_error(StatusCode::FORBIDDEN, "access_denied", "Not authenticated"); 1630 + } 1631 + }, 1632 Err(_) => { 1633 + let _ = state 1634 + .oauth_repo 1635 + .delete_authorization_request(&consent_post_request_id) 1636 + .await; 1637 return json_error( 1638 StatusCode::BAD_REQUEST, 1639 "invalid_request", 1640 + "Authorization request has expired", 1641 ); 1642 } 1643 }; 1644 + 1645 + let did = flow_with_user.did().clone(); 1646 let original_scope_str = request_data 1647 .parameters 1648 .scope ··· 1738 let consent_post_device_id = request_data 1739 .device_id 1740 .as_ref() 1741 + .map(|d| DeviceIdType::from(d.0.clone())); 1742 let consent_post_code = AuthorizationCode::from(code.0.clone()); 1743 if state 1744 .oauth_repo ··· 1762 redirect_uri, 1763 &code.0, 1764 request_data.parameters.state.as_deref(), 1765 + request_data.parameters.response_mode.map(|m| m.as_str()), 1766 ); 1767 tracing::info!( 1768 intermediate_url = %intermediate_url, ··· 1774 1775 pub async fn authorize_2fa_post( 1776 State(state): State<AppState>, 1777 + _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 1778 headers: HeaderMap, 1779 Json(form): Json<Authorize2faSubmit>, 1780 ) -> Response { ··· 1788 ) 1789 .into_response() 1790 }; 1791 let twofa_post_request_id = RequestId::from(form.request_uri.clone()); 1792 let request_data = match state 1793 .oauth_repo ··· 1884 &request_data.parameters.redirect_uri, 1885 &code.0, 1886 request_data.parameters.state.as_deref(), 1887 + request_data.parameters.response_mode.map(|m| m.as_str()), 1888 ); 1889 return Json(serde_json::json!({ 1890 "redirect_uri": redirect_url ··· 1918 "No 2FA challenge found. Please start over.", 1919 ); 1920 } 1921 + let _rate_proof = match check_user_rate_limit::<TotpVerifyLimit>(&state, &did).await { 1922 + Ok(proof) => proof, 1923 + Err(_) => { 1924 + return json_error( 1925 + StatusCode::TOO_MANY_REQUESTS, 1926 + "RateLimitExceeded", 1927 + "Too many verification attempts. Please try again in a few minutes.", 1928 + ) 1929 + } 1930 + }; 1931 let totp_valid = 1932 crate::api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await; 1933 if !totp_valid { ··· 1992 &request_data.parameters.redirect_uri, 1993 &code.0, 1994 request_data.parameters.state.as_deref(), 1995 + request_data.parameters.response_mode.map(|m| m.as_str()), 1996 ); 1997 Json(serde_json::json!({ 1998 "redirect_uri": redirect_url ··· 2016 State(state): State<AppState>, 2017 Query(query): Query<CheckPasskeysQuery>, 2018 ) -> Response { 2019 + let hostname_for_handles = pds_hostname_without_port(); 2020 let normalized_identifier = query.identifier.trim(); 2021 let normalized_identifier = normalized_identifier 2022 .strip_prefix('@') ··· 2057 State(state): State<AppState>, 2058 Query(query): Query<CheckPasskeysQuery>, 2059 ) -> Response { 2060 + let hostname_for_handles = pds_hostname_without_port(); 2061 let identifier = query.identifier.trim(); 2062 let identifier = identifier.strip_prefix('@').unwrap_or(identifier); 2063 let normalized_identifier = if identifier.contains('@') || identifier.starts_with("did:") { ··· 2125 2126 pub async fn passkey_start( 2127 State(state): State<AppState>, 2128 + _rate_limit: OAuthRateLimited<OAuthAuthorizeLimit>, 2129 Json(form): Json<PasskeyStartInput>, 2130 ) -> Response { 2131 let passkey_start_request_id = RequestId::from(form.request_uri.clone()); 2132 let request_data = match state 2133 .oauth_repo ··· 2172 .into_response(); 2173 } 2174 2175 + let hostname_for_handles = pds_hostname_without_port(); 2176 let normalized_username = form.identifier.trim(); 2177 let normalized_username = normalized_username 2178 .strip_prefix('@') ··· 2293 .into_response(); 2294 } 2295 2296 + let (rcr, auth_state) = match state.webauthn_config.start_authentication(passkeys) { 2297 Ok(result) => result, 2298 Err(e) => { 2299 tracing::error!(error = %e, "Failed to start passkey authentication"); ··· 2572 } 2573 }; 2574 2575 + let auth_result = match state 2576 + .webauthn_config 2577 + .finish_authentication(&credential, &auth_state) 2578 + { 2579 Ok(r) => r, 2580 Err(e) => { 2581 tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication"); ··· 2648 .await 2649 { 2650 Ok(challenge) => { 2651 + let hostname = pds_hostname(); 2652 if let Err(e) = enqueue_2fa_code( 2653 state.user_repo.as_ref(), 2654 state.infra_repo.as_ref(), 2655 user.id, 2656 &challenge.code, 2657 + hostname, 2658 ) 2659 .await 2660 { ··· 2737 &request_data.parameters.redirect_uri, 2738 &code.0, 2739 request_data.parameters.state.as_deref(), 2740 + request_data.parameters.response_mode.map(|m| m.as_str()), 2741 ); 2742 2743 Json(serde_json::json!({ ··· 2762 State(state): State<AppState>, 2763 Query(query): Query<AuthorizePasskeyQuery>, 2764 ) -> Response { 2765 let auth_passkey_start_request_id = RequestId::from(query.request_uri.clone()); 2766 let request_data = match state 2767 .oauth_repo ··· 2870 .into_response(); 2871 } 2872 2873 + let (rcr, auth_state) = match state.webauthn_config.start_authentication(passkeys) { 2874 Ok(result) => result, 2875 Err(e) => { 2876 tracing::error!("Failed to start passkey authentication: {:?}", e); ··· 2927 headers: HeaderMap, 2928 Json(form): Json<AuthorizePasskeySubmit>, 2929 ) -> Response { 2930 + let pds_hostname = pds_hostname(); 2931 let passkey_finish_request_id = RequestId::from(form.request_uri.clone()); 2932 2933 let request_data = match state ··· 3057 } 3058 }; 3059 3060 + let auth_result = match state 3061 + .webauthn_config 3062 + .finish_authentication(&credential, &auth_state) 3063 + { 3064 Ok(r) => r, 3065 Err(e) => { 3066 tracing::warn!("Passkey authentication failed: {:?}", e); ··· 3147 state.infra_repo.as_ref(), 3148 user.id, 3149 &challenge.code, 3150 + pds_hostname, 3151 ) 3152 .await 3153 { ··· 3202 3203 pub async fn register_complete( 3204 State(state): State<AppState>, 3205 + _rate_limit: OAuthRateLimited<OAuthRegisterCompleteLimit>, 3206 Json(form): Json<RegisterCompleteInput>, 3207 ) -> Response { 3208 let did = Did::from(form.did.clone()); 3209 3210 let request_id = RequestId::from(form.request_uri.clone()); ··· 3256 .into_response(); 3257 } 3258 3259 + if request_data.parameters.prompt != Some(Prompt::Create) { 3260 tracing::warn!( 3261 request_uri = %form.request_uri, 3262 prompt = ?request_data.parameters.prompt, ··· 3475 &request_data.parameters.redirect_uri, 3476 &code.0, 3477 request_data.parameters.state.as_deref(), 3478 + request_data.parameters.response_mode.map(|m| m.as_str()), 3479 ); 3480 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 3481 } ··· 3501 None => { 3502 let new_id = DeviceId::generate(); 3503 let device_data = DeviceData { 3504 + session_id: SessionId::generate(), 3505 user_agent: extract_user_agent(&headers), 3506 + ip_address: extract_client_ip(&headers, None), 3507 last_seen_at: Utc::now(), 3508 }; 3509 let device_typed = DeviceIdType::from(new_id.0.clone());
+32 -64
crates/tranquil-pds/src/oauth/endpoints/delegation.rs
··· 1 use crate::auth::{Active, Auth}; 2 use crate::delegation::DelegationActionType; 3 - use crate::state::{AppState, RateLimitKind}; 4 use crate::types::PlainPassword; 5 use crate::util::extract_client_ip; 6 use axum::{ 7 Json, 8 extract::State, 9 - http::{HeaderMap, StatusCode}, 10 response::{IntoResponse, Response}, 11 }; 12 use serde::{Deserialize, Serialize}; ··· 35 36 pub async fn delegation_auth( 37 State(state): State<AppState>, 38 headers: HeaderMap, 39 Json(form): Json<DelegationAuthSubmit>, 40 ) -> Response { 41 - let client_ip = extract_client_ip(&headers); 42 - if !state 43 - .check_rate_limit(RateLimitKind::Login, &client_ip) 44 - .await 45 - { 46 - return ( 47 - StatusCode::TOO_MANY_REQUESTS, 48 - Json(DelegationAuthResponse { 49 - success: false, 50 - needs_totp: None, 51 - redirect_uri: None, 52 - error: Some("Too many login attempts. Please try again later.".to_string()), 53 - }), 54 - ) 55 - .into_response(); 56 - } 57 - 58 let request_id = RequestId::from(form.request_uri.clone()); 59 let request = match state 60 .oauth_repo ··· 82 } 83 }; 84 85 - let delegated_did_str = match form.delegated_did.as_ref().or(request.did.as_ref()) { 86 - Some(did) => did.clone(), 87 - None => { 88 - return Json(DelegationAuthResponse { 89 - success: false, 90 - needs_totp: None, 91 - redirect_uri: None, 92 - error: Some("No delegated account selected".to_string()), 93 - }) 94 - .into_response(); 95 - } 96 - }; 97 - 98 - let delegated_did: Did = match delegated_did_str.parse() { 99 - Ok(d) => d, 100 - Err(_) => { 101 - return Json(DelegationAuthResponse { 102 - success: false, 103 - needs_totp: None, 104 - redirect_uri: None, 105 - error: Some("Invalid delegated DID".to_string()), 106 - }) 107 - .into_response(); 108 } 109 }; 110 111 let controller_did: Did = match form.controller_did.parse() { ··· 249 .into_response(); 250 } 251 252 - let ip = extract_client_ip(&headers); 253 let user_agent = headers 254 .get("user-agent") 255 .and_then(|v| v.to_str().ok()) ··· 266 "client_id": request.client_id, 267 "granted_scopes": grant.granted_scopes 268 })), 269 - Some(&ip), 270 user_agent.as_deref(), 271 ) 272 .await; ··· 291 292 pub async fn delegation_totp_verify( 293 State(state): State<AppState>, 294 headers: HeaderMap, 295 Json(form): Json<DelegationTotpSubmit>, 296 ) -> Response { 297 - let client_ip = extract_client_ip(&headers); 298 - if !state 299 - .check_rate_limit(RateLimitKind::TotpVerify, &client_ip) 300 - .await 301 - { 302 - return ( 303 - StatusCode::TOO_MANY_REQUESTS, 304 - Json(DelegationAuthResponse { 305 - success: false, 306 - needs_totp: None, 307 - redirect_uri: None, 308 - error: Some("Too many verification attempts. Please try again later.".to_string()), 309 - }), 310 - ) 311 - .into_response(); 312 - } 313 - 314 let totp_request_id = RequestId::from(form.request_uri.clone()); 315 let request = match state 316 .oauth_repo ··· 420 .into_response(); 421 } 422 423 - let ip = extract_client_ip(&headers); 424 let user_agent = headers 425 .get("user-agent") 426 .and_then(|v| v.to_str().ok()) ··· 437 "client_id": request.client_id, 438 "granted_scopes": grant.granted_scopes 439 })), 440 - Some(&ip), 441 user_agent.as_deref(), 442 ) 443 .await; ··· 564 .into_response(); 565 } 566 567 - let ip = extract_client_ip(&headers); 568 let user_agent = headers 569 .get("user-agent") 570 .and_then(|v| v.to_str().ok())
··· 1 use crate::auth::{Active, Auth}; 2 use crate::delegation::DelegationActionType; 3 + use crate::rate_limit::{LoginLimit, OAuthRateLimited, TotpVerifyLimit}; 4 + use crate::state::AppState; 5 use crate::types::PlainPassword; 6 use crate::util::extract_client_ip; 7 use axum::{ 8 Json, 9 extract::State, 10 + http::HeaderMap, 11 response::{IntoResponse, Response}, 12 }; 13 use serde::{Deserialize, Serialize}; ··· 36 37 pub async fn delegation_auth( 38 State(state): State<AppState>, 39 + rate_limit: OAuthRateLimited<LoginLimit>, 40 headers: HeaderMap, 41 Json(form): Json<DelegationAuthSubmit>, 42 ) -> Response { 43 + let client_ip = rate_limit.client_ip(); 44 let request_id = RequestId::from(form.request_uri.clone()); 45 let request = match state 46 .oauth_repo ··· 68 } 69 }; 70 71 + let delegated_did: Did = if let Some(did_str) = form.delegated_did.as_ref() { 72 + match did_str.parse() { 73 + Ok(d) => d, 74 + Err(_) => { 75 + return Json(DelegationAuthResponse { 76 + success: false, 77 + needs_totp: None, 78 + redirect_uri: None, 79 + error: Some("Invalid delegated DID".to_string()), 80 + }) 81 + .into_response(); 82 + } 83 } 84 + } else if let Some(did) = request.did.as_ref() { 85 + did.clone() 86 + } else { 87 + return Json(DelegationAuthResponse { 88 + success: false, 89 + needs_totp: None, 90 + redirect_uri: None, 91 + error: Some("No delegated account selected".to_string()), 92 + }) 93 + .into_response(); 94 }; 95 96 let controller_did: Did = match form.controller_did.parse() { ··· 234 .into_response(); 235 } 236 237 let user_agent = headers 238 .get("user-agent") 239 .and_then(|v| v.to_str().ok()) ··· 250 "client_id": request.client_id, 251 "granted_scopes": grant.granted_scopes 252 })), 253 + Some(client_ip), 254 user_agent.as_deref(), 255 ) 256 .await; ··· 275 276 pub async fn delegation_totp_verify( 277 State(state): State<AppState>, 278 + rate_limit: OAuthRateLimited<TotpVerifyLimit>, 279 headers: HeaderMap, 280 Json(form): Json<DelegationTotpSubmit>, 281 ) -> Response { 282 + let client_ip = rate_limit.client_ip(); 283 let totp_request_id = RequestId::from(form.request_uri.clone()); 284 let request = match state 285 .oauth_repo ··· 389 .into_response(); 390 } 391 392 let user_agent = headers 393 .get("user-agent") 394 .and_then(|v| v.to_str().ok()) ··· 405 "client_id": request.client_id, 406 "granted_scopes": grant.granted_scopes 407 })), 408 + Some(client_ip), 409 user_agent.as_deref(), 410 ) 411 .await; ··· 532 .into_response(); 533 } 534 535 + let ip = extract_client_ip(&headers, None); 536 let user_agent = headers 537 .get("user-agent") 538 .and_then(|v| v.to_str().ok())
+3 -2
crates/tranquil-pds/src/oauth/endpoints/metadata.rs
··· 1 use crate::oauth::jwks::{JwkSet, create_jwk_set}; 2 use crate::state::AppState; 3 use axum::{Json, extract::State}; 4 use serde::{Deserialize, Serialize}; 5 ··· 57 pub async fn oauth_protected_resource( 58 State(_state): State<AppState>, 59 ) -> Json<ProtectedResourceMetadata> { 60 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 61 let public_url = format!("https://{}", pds_hostname); 62 Json(ProtectedResourceMetadata { 63 resource: public_url.clone(), ··· 71 pub async fn oauth_authorization_server( 72 State(_state): State<AppState>, 73 ) -> Json<AuthorizationServerMetadata> { 74 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 75 let issuer = format!("https://{}", pds_hostname); 76 Json(AuthorizationServerMetadata { 77 issuer: issuer.clone(),
··· 1 use crate::oauth::jwks::{JwkSet, create_jwk_set}; 2 use crate::state::AppState; 3 + use crate::util::pds_hostname; 4 use axum::{Json, extract::State}; 5 use serde::{Deserialize, Serialize}; 6 ··· 58 pub async fn oauth_protected_resource( 59 State(_state): State<AppState>, 60 ) -> Json<ProtectedResourceMetadata> { 61 + let pds_hostname = pds_hostname(); 62 let public_url = format!("https://{}", pds_hostname); 63 Json(ProtectedResourceMetadata { 64 resource: public_url.clone(), ··· 72 pub async fn oauth_authorization_server( 73 State(_state): State<AppState>, 74 ) -> Json<AuthorizationServerMetadata> { 75 + let pds_hostname = pds_hostname(); 76 let issuer = format!("https://{}", pds_hostname); 77 Json(AuthorizationServerMetadata { 78 issuer: issuer.clone(),
+57 -50
crates/tranquil-pds/src/oauth/endpoints/par.rs
··· 1 use crate::oauth::{ 2 - AuthorizationRequestParameters, ClientAuth, ClientMetadataCache, OAuthError, RequestData, 3 - RequestId, 4 scopes::{ParsedScope, parse_scope}, 5 }; 6 - use crate::state::{AppState, RateLimitKind}; 7 use axum::body::Bytes; 8 use axum::{Json, extract::State, http::HeaderMap}; 9 use chrono::{Duration, Utc}; ··· 49 50 pub async fn pushed_authorization_request( 51 State(state): State<AppState>, 52 headers: HeaderMap, 53 body: Bytes, 54 ) -> Result<(axum::http::StatusCode, Json<ParResponse>), OAuthError> { ··· 70 .to_string(), 71 )); 72 }; 73 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 74 - if !state 75 - .check_rate_limit(RateLimitKind::OAuthPar, &client_ip) 76 - .await 77 - { 78 - tracing::warn!(ip = %client_ip, "OAuth PAR rate limit exceeded"); 79 - return Err(OAuthError::RateLimited); 80 - } 81 - if request.response_type != "code" { 82 - return Err(OAuthError::InvalidRequest( 83 - "response_type must be 'code'".to_string(), 84 - )); 85 - } 86 let code_challenge = request 87 .code_challenge 88 .as_ref() 89 .filter(|s| !s.is_empty()) 90 .ok_or_else(|| OAuthError::InvalidRequest("code_challenge is required".to_string()))?; 91 - let code_challenge_method = request.code_challenge_method.as_deref().unwrap_or(""); 92 - if code_challenge_method != "S256" { 93 - return Err(OAuthError::InvalidRequest( 94 - "code_challenge_method must be 'S256'".to_string(), 95 - )); 96 - } 97 let client_cache = ClientMetadataCache::new(3600); 98 let client_metadata = client_cache.get(&request.client_id).await?; 99 client_cache.validate_redirect_uri(&client_metadata, &request.redirect_uri)?; ··· 101 let validated_scope = validate_scope(&request.scope, &client_metadata)?; 102 let request_id = RequestId::generate(); 103 let expires_at = Utc::now() + Duration::seconds(PAR_EXPIRY_SECONDS); 104 - let response_mode = match request.response_mode.as_deref() { 105 - Some("fragment") => Some("fragment".to_string()), 106 - Some("query") | None => None, 107 - Some(mode) => { 108 - return Err(OAuthError::InvalidRequest(format!( 109 - "Unsupported response_mode: {}", 110 - mode 111 - ))); 112 - } 113 - }; 114 - let prompt = validate_prompt(&request.prompt)?; 115 let parameters = AuthorizationRequestParameters { 116 - response_type: request.response_type, 117 client_id: request.client_id.clone(), 118 redirect_uri: request.redirect_uri, 119 scope: validated_scope, 120 state: request.state, 121 code_challenge: code_challenge.clone(), 122 - code_challenge_method: code_challenge_method.to_string(), 123 response_mode, 124 login_hint: request.login_hint, 125 dpop_jkt: request.dpop_jkt, ··· 266 false 267 } 268 269 - fn validate_prompt(prompt: &Option<String>) -> Result<Option<String>, OAuthError> { 270 - const VALID_PROMPTS: &[&str] = &["none", "login", "consent", "select_account", "create"]; 271 272 - match prompt { 273 - None => Ok(None), 274 - Some(p) if p.is_empty() => Ok(None), 275 - Some(p) => { 276 - if VALID_PROMPTS.contains(&p.as_str()) { 277 - Ok(Some(p.clone())) 278 - } else { 279 - Err(OAuthError::InvalidRequest(format!( 280 - "Unsupported prompt value: {}", 281 - p 282 - ))) 283 - } 284 - } 285 } 286 }
··· 1 use crate::oauth::{ 2 + AuthorizationRequestParameters, ClientAuth, ClientMetadataCache, CodeChallengeMethod, 3 + OAuthError, Prompt, RequestData, RequestId, ResponseMode, ResponseType, 4 scopes::{ParsedScope, parse_scope}, 5 }; 6 + use crate::rate_limit::{OAuthParLimit, OAuthRateLimited}; 7 + use crate::state::AppState; 8 use axum::body::Bytes; 9 use axum::{Json, extract::State, http::HeaderMap}; 10 use chrono::{Duration, Utc}; ··· 50 51 pub async fn pushed_authorization_request( 52 State(state): State<AppState>, 53 + _rate_limit: OAuthRateLimited<OAuthParLimit>, 54 headers: HeaderMap, 55 body: Bytes, 56 ) -> Result<(axum::http::StatusCode, Json<ParResponse>), OAuthError> { ··· 72 .to_string(), 73 )); 74 }; 75 + let response_type = parse_response_type(&request.response_type)?; 76 let code_challenge = request 77 .code_challenge 78 .as_ref() 79 .filter(|s| !s.is_empty()) 80 .ok_or_else(|| OAuthError::InvalidRequest("code_challenge is required".to_string()))?; 81 + let code_challenge_method = parse_code_challenge_method(request.code_challenge_method.as_deref())?; 82 let client_cache = ClientMetadataCache::new(3600); 83 let client_metadata = client_cache.get(&request.client_id).await?; 84 client_cache.validate_redirect_uri(&client_metadata, &request.redirect_uri)?; ··· 86 let validated_scope = validate_scope(&request.scope, &client_metadata)?; 87 let request_id = RequestId::generate(); 88 let expires_at = Utc::now() + Duration::seconds(PAR_EXPIRY_SECONDS); 89 + let response_mode = parse_response_mode(request.response_mode.as_deref())?; 90 + let prompt = parse_prompt(request.prompt.as_deref())?; 91 let parameters = AuthorizationRequestParameters { 92 + response_type, 93 client_id: request.client_id.clone(), 94 redirect_uri: request.redirect_uri, 95 scope: validated_scope, 96 state: request.state, 97 code_challenge: code_challenge.clone(), 98 + code_challenge_method, 99 response_mode, 100 login_hint: request.login_hint, 101 dpop_jkt: request.dpop_jkt, ··· 242 false 243 } 244 245 + fn parse_response_type(value: &str) -> Result<ResponseType, OAuthError> { 246 + match value { 247 + "code" => Ok(ResponseType::Code), 248 + other => Err(OAuthError::InvalidRequest(format!( 249 + "response_type must be 'code', got '{}'", 250 + other 251 + ))), 252 + } 253 + } 254 255 + fn parse_code_challenge_method(value: Option<&str>) -> Result<CodeChallengeMethod, OAuthError> { 256 + match value { 257 + Some("S256") | None => Ok(CodeChallengeMethod::S256), 258 + Some("plain") => Err(OAuthError::InvalidRequest( 259 + "code_challenge_method 'plain' is not allowed, use 'S256'".to_string(), 260 + )), 261 + Some(other) => Err(OAuthError::InvalidRequest(format!( 262 + "Unsupported code_challenge_method: {}", 263 + other 264 + ))), 265 + } 266 + } 267 + 268 + fn parse_response_mode(value: Option<&str>) -> Result<Option<ResponseMode>, OAuthError> { 269 + match value { 270 + None | Some("query") => Ok(None), 271 + Some("fragment") => Ok(Some(ResponseMode::Fragment)), 272 + Some("form_post") => Ok(Some(ResponseMode::FormPost)), 273 + Some(other) => Err(OAuthError::InvalidRequest(format!( 274 + "Unsupported response_mode: {}", 275 + other 276 + ))), 277 + } 278 + } 279 + 280 + fn parse_prompt(value: Option<&str>) -> Result<Option<Prompt>, OAuthError> { 281 + match value { 282 + None | Some("") => Ok(None), 283 + Some("none") => Ok(Some(Prompt::None)), 284 + Some("login") => Ok(Some(Prompt::Login)), 285 + Some("consent") => Ok(Some(Prompt::Consent)), 286 + Some("select_account") => Ok(Some(Prompt::SelectAccount)), 287 + Some("create") => Ok(Some(Prompt::Create)), 288 + Some(other) => Err(OAuthError::InvalidRequest(format!( 289 + "Unsupported prompt value: {}", 290 + other 291 + ))), 292 } 293 }
+42 -42
crates/tranquil-pds/src/oauth/endpoints/token/grants.rs
··· 3 use crate::config::AuthConfig; 4 use crate::delegation::intersect_scopes; 5 use crate::oauth::{ 6 - AuthFlowState, ClientAuth, ClientMetadataCache, DPoPVerifier, OAuthError, RefreshToken, 7 - TokenData, TokenId, 8 db::{enforce_token_limit_for_user, lookup_refresh_token}, 9 scopes::expand_include_scopes, 10 verify_client_auth, 11 }; 12 use crate::state::AppState; 13 use axum::Json; 14 use axum::http::HeaderMap; 15 use chrono::{Duration, Utc}; ··· 51 .map_err(crate::oauth::db_err_to_oauth)? 52 .ok_or_else(|| OAuthError::InvalidGrant("Invalid or expired code".to_string()))?; 53 54 - let flow_state = AuthFlowState::from_request_data(&auth_request); 55 - if flow_state.is_expired() { 56 - return Err(OAuthError::InvalidGrant( 57 - "Authorization code has expired".to_string(), 58 - )); 59 - } 60 - if !flow_state.can_exchange() { 61 - return Err(OAuthError::InvalidGrant( 62 - "Authorization not completed".to_string(), 63 - )); 64 - } 65 66 if let Some(request_client_id) = &request.client_auth.client_id 67 - && request_client_id != &auth_request.client_id 68 { 69 return Err(OAuthError::InvalidGrant("client_id mismatch".to_string())); 70 } 71 - let did = flow_state.did().unwrap().to_string(); 72 let client_metadata_cache = ClientMetadataCache::new(3600); 73 - let client_metadata = client_metadata_cache.get(&auth_request.client_id).await?; 74 let client_auth = if let (Some(assertion), Some(assertion_type)) = ( 75 &request.client_auth.client_assertion, 76 &request.client_auth.client_assertion_type, ··· 91 ClientAuth::None 92 }; 93 verify_client_auth(&client_metadata_cache, &client_metadata, &client_auth).await?; 94 - verify_pkce(&auth_request.parameters.code_challenge, &code_verifier)?; 95 if let Some(req_redirect_uri) = &redirect_uri 96 - && req_redirect_uri != &auth_request.parameters.redirect_uri 97 { 98 return Err(OAuthError::InvalidGrant( 99 "redirect_uri mismatch".to_string(), ··· 103 let config = AuthConfig::get(); 104 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes()); 105 let pds_hostname = 106 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 107 let token_endpoint = format!("https://{}/oauth/token", pds_hostname); 108 let result = verifier.verify_proof(proof, "POST", &token_endpoint, None)?; 109 if !state ··· 116 "DPoP proof has already been used".to_string(), 117 )); 118 } 119 - if let Some(expected_jkt) = &auth_request.parameters.dpop_jkt 120 && result.jkt.as_str() != expected_jkt 121 { 122 return Err(OAuthError::InvalidDpopProof( ··· 124 )); 125 } 126 Some(result.jkt.as_str().to_string()) 127 - } else if auth_request.parameters.dpop_jkt.is_some() || client_metadata.requires_dpop() { 128 return Err(OAuthError::UseDpopNonce( 129 DPoPVerifier::new(AuthConfig::get().dpop_secret().as_bytes()).generate_nonce(), 130 )); ··· 135 let refresh_token = RefreshToken::generate(); 136 let now = Utc::now(); 137 138 - let (raw_scope, controller_did) = if let Some(ref controller) = auth_request.controller_did { 139 let did_parsed: Did = did 140 .parse() 141 .map_err(|_| OAuthError::InvalidRequest("Invalid DID format".to_string()))?; ··· 149 .ok() 150 .flatten(); 151 let granted_scopes = grant.map(|g| g.granted_scopes).unwrap_or_default(); 152 - let requested = auth_request 153 .parameters 154 .scope 155 .as_deref() ··· 157 let intersected = intersect_scopes(requested, &granted_scopes); 158 (Some(intersected), Some(controller.clone())) 159 } else { 160 - (auth_request.parameters.scope.clone(), None) 161 }; 162 163 let final_scope = if let Some(ref scope) = raw_scope { ··· 177 final_scope.as_deref(), 178 controller_did.as_deref(), 179 )?; 180 - let stored_client_auth = auth_request.client_auth.unwrap_or(ClientAuth::None); 181 let refresh_expiry_days = if matches!(stored_client_auth, ClientAuth::None) { 182 REFRESH_TOKEN_EXPIRY_DAYS_PUBLIC 183 } else { 184 REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL 185 }; 186 - let mut stored_parameters = auth_request.parameters.clone(); 187 stored_parameters.dpop_jkt = dpop_jkt.clone(); 188 let token_data = TokenData { 189 - did: did.clone(), 190 - token_id: token_id.0.clone(), 191 created_at: now, 192 updated_at: now, 193 expires_at: now + Duration::days(refresh_expiry_days), 194 - client_id: auth_request.client_id.clone(), 195 client_auth: stored_client_auth, 196 - device_id: auth_request.device_id, 197 parameters: stored_parameters, 198 details: None, 199 code: None, 200 - current_refresh_token: Some(refresh_token.0.clone()), 201 scope: final_scope.clone(), 202 controller_did: controller_did.clone(), 203 }; ··· 209 tracing::info!( 210 did = %did, 211 token_id = %token_id.0, 212 - client_id = %auth_request.client_id, 213 "Authorization code grant completed, token created" 214 ); 215 tokio::spawn({ ··· 280 ); 281 let dpop_jkt = token_data.parameters.dpop_jkt.as_deref(); 282 let access_token = create_access_token_with_delegation( 283 - &token_data.token_id, 284 - &token_data.did, 285 dpop_jkt, 286 token_data.scope.as_deref(), 287 - token_data.controller_did.as_deref(), 288 )?; 289 let mut response_headers = HeaderMap::new(); 290 let config = AuthConfig::get(); ··· 296 access_token, 297 token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(), 298 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64, 299 - refresh_token: token_data.current_refresh_token, 300 scope: token_data.scope, 301 - sub: Some(token_data.did), 302 }), 303 )); 304 } ··· 338 let config = AuthConfig::get(); 339 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes()); 340 let pds_hostname = 341 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 342 let token_endpoint = format!("https://{}/oauth/token", pds_hostname); 343 let result = verifier.verify_proof(proof, "POST", &token_endpoint, None)?; 344 if !state ··· 385 "Refresh token rotated successfully" 386 ); 387 let access_token = create_access_token_with_delegation( 388 - &token_data.token_id, 389 - &token_data.did, 390 dpop_jkt.as_deref(), 391 token_data.scope.as_deref(), 392 - token_data.controller_did.as_deref(), 393 )?; 394 let mut response_headers = HeaderMap::new(); 395 let config = AuthConfig::get(); ··· 403 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64, 404 refresh_token: Some(new_refresh_token.0), 405 scope: token_data.scope, 406 - sub: Some(token_data.did), 407 }), 408 )) 409 }
··· 3 use crate::config::AuthConfig; 4 use crate::delegation::intersect_scopes; 5 use crate::oauth::{ 6 + AuthFlow, ClientAuth, ClientMetadataCache, DPoPVerifier, OAuthError, RefreshToken, TokenData, 7 + TokenId, 8 db::{enforce_token_limit_for_user, lookup_refresh_token}, 9 scopes::expand_include_scopes, 10 verify_client_auth, 11 }; 12 use crate::state::AppState; 13 + use crate::util::pds_hostname; 14 use axum::Json; 15 use axum::http::HeaderMap; 16 use chrono::{Duration, Utc}; ··· 52 .map_err(crate::oauth::db_err_to_oauth)? 53 .ok_or_else(|| OAuthError::InvalidGrant("Invalid or expired code".to_string()))?; 54 55 + let flow = AuthFlow::from_request_data(auth_request).map_err(|_| { 56 + OAuthError::InvalidGrant("Authorization code has expired".to_string()) 57 + })?; 58 + 59 + let authorized = flow.require_authorized().map_err(|_| { 60 + OAuthError::InvalidGrant("Authorization not completed".to_string()) 61 + })?; 62 63 if let Some(request_client_id) = &request.client_auth.client_id 64 + && request_client_id != &authorized.client_id 65 { 66 return Err(OAuthError::InvalidGrant("client_id mismatch".to_string())); 67 } 68 + let did = authorized.did.to_string(); 69 let client_metadata_cache = ClientMetadataCache::new(3600); 70 + let client_metadata = client_metadata_cache.get(&authorized.client_id).await?; 71 let client_auth = if let (Some(assertion), Some(assertion_type)) = ( 72 &request.client_auth.client_assertion, 73 &request.client_auth.client_assertion_type, ··· 88 ClientAuth::None 89 }; 90 verify_client_auth(&client_metadata_cache, &client_metadata, &client_auth).await?; 91 + verify_pkce(&authorized.parameters.code_challenge, &code_verifier)?; 92 if let Some(req_redirect_uri) = &redirect_uri 93 + && req_redirect_uri != &authorized.parameters.redirect_uri 94 { 95 return Err(OAuthError::InvalidGrant( 96 "redirect_uri mismatch".to_string(), ··· 100 let config = AuthConfig::get(); 101 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes()); 102 let pds_hostname = 103 + pds_hostname(); 104 let token_endpoint = format!("https://{}/oauth/token", pds_hostname); 105 let result = verifier.verify_proof(proof, "POST", &token_endpoint, None)?; 106 if !state ··· 113 "DPoP proof has already been used".to_string(), 114 )); 115 } 116 + if let Some(expected_jkt) = &authorized.parameters.dpop_jkt 117 && result.jkt.as_str() != expected_jkt 118 { 119 return Err(OAuthError::InvalidDpopProof( ··· 121 )); 122 } 123 Some(result.jkt.as_str().to_string()) 124 + } else if authorized.parameters.dpop_jkt.is_some() || client_metadata.requires_dpop() { 125 return Err(OAuthError::UseDpopNonce( 126 DPoPVerifier::new(AuthConfig::get().dpop_secret().as_bytes()).generate_nonce(), 127 )); ··· 132 let refresh_token = RefreshToken::generate(); 133 let now = Utc::now(); 134 135 + let (raw_scope, controller_did) = if let Some(ref controller) = authorized.controller_did { 136 let did_parsed: Did = did 137 .parse() 138 .map_err(|_| OAuthError::InvalidRequest("Invalid DID format".to_string()))?; ··· 146 .ok() 147 .flatten(); 148 let granted_scopes = grant.map(|g| g.granted_scopes).unwrap_or_default(); 149 + let requested = authorized 150 .parameters 151 .scope 152 .as_deref() ··· 154 let intersected = intersect_scopes(requested, &granted_scopes); 155 (Some(intersected), Some(controller.clone())) 156 } else { 157 + (authorized.parameters.scope.clone(), None) 158 }; 159 160 let final_scope = if let Some(ref scope) = raw_scope { ··· 174 final_scope.as_deref(), 175 controller_did.as_deref(), 176 )?; 177 + let stored_client_auth = authorized.client_auth.unwrap_or(ClientAuth::None); 178 let refresh_expiry_days = if matches!(stored_client_auth, ClientAuth::None) { 179 REFRESH_TOKEN_EXPIRY_DAYS_PUBLIC 180 } else { 181 REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL 182 }; 183 + let mut stored_parameters = authorized.parameters.clone(); 184 stored_parameters.dpop_jkt = dpop_jkt.clone(); 185 + let did_typed: Did = did 186 + .parse() 187 + .map_err(|_| OAuthError::InvalidRequest("Invalid DID format".to_string()))?; 188 let token_data = TokenData { 189 + did: did_typed, 190 + token_id: token_id.clone(), 191 created_at: now, 192 updated_at: now, 193 expires_at: now + Duration::days(refresh_expiry_days), 194 + client_id: authorized.client_id.clone(), 195 client_auth: stored_client_auth, 196 + device_id: authorized.device_id.clone(), 197 parameters: stored_parameters, 198 details: None, 199 code: None, 200 + current_refresh_token: Some(refresh_token.clone()), 201 scope: final_scope.clone(), 202 controller_did: controller_did.clone(), 203 }; ··· 209 tracing::info!( 210 did = %did, 211 token_id = %token_id.0, 212 + client_id = %authorized.client_id, 213 "Authorization code grant completed, token created" 214 ); 215 tokio::spawn({ ··· 280 ); 281 let dpop_jkt = token_data.parameters.dpop_jkt.as_deref(); 282 let access_token = create_access_token_with_delegation( 283 + &token_data.token_id.0, 284 + token_data.did.as_str(), 285 dpop_jkt, 286 token_data.scope.as_deref(), 287 + token_data.controller_did.as_ref().map(|d| d.as_str()), 288 )?; 289 let mut response_headers = HeaderMap::new(); 290 let config = AuthConfig::get(); ··· 296 access_token, 297 token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(), 298 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64, 299 + refresh_token: token_data.current_refresh_token.map(|r| r.0), 300 scope: token_data.scope, 301 + sub: Some(token_data.did.to_string()), 302 }), 303 )); 304 } ··· 338 let config = AuthConfig::get(); 339 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes()); 340 let pds_hostname = 341 + pds_hostname(); 342 let token_endpoint = format!("https://{}/oauth/token", pds_hostname); 343 let result = verifier.verify_proof(proof, "POST", &token_endpoint, None)?; 344 if !state ··· 385 "Refresh token rotated successfully" 386 ); 387 let access_token = create_access_token_with_delegation( 388 + &token_data.token_id.0, 389 + token_data.did.as_str(), 390 dpop_jkt.as_deref(), 391 token_data.scope.as_deref(), 392 + token_data.controller_did.as_ref().map(|d| d.as_str()), 393 )?; 394 let mut response_headers = HeaderMap::new(); 395 let config = AuthConfig::get(); ··· 403 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64, 404 refresh_token: Some(new_refresh_token.0), 405 scope: token_data.scope, 406 + sub: Some(token_data.did.to_string()), 407 }), 408 )) 409 }
+2 -1
crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs
··· 1 use crate::config::AuthConfig; 2 use crate::oauth::OAuthError; 3 use base64::Engine; 4 use base64::engine::general_purpose::URL_SAFE_NO_PAD; 5 use chrono::Utc; ··· 51 ) -> Result<String, OAuthError> { 52 use serde_json::json; 53 let jti = uuid::Uuid::new_v4().to_string(); 54 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 55 let issuer = format!("https://{}", pds_hostname); 56 let now = Utc::now().timestamp(); 57 let exp = now + ACCESS_TOKEN_EXPIRY_SECONDS;
··· 1 use crate::config::AuthConfig; 2 use crate::oauth::OAuthError; 3 + use crate::util::pds_hostname; 4 use base64::Engine; 5 use base64::engine::general_purpose::URL_SAFE_NO_PAD; 6 use chrono::Utc; ··· 52 ) -> Result<String, OAuthError> { 53 use serde_json::json; 54 let jti = uuid::Uuid::new_v4().to_string(); 55 + let pds_hostname = pds_hostname(); 56 let issuer = format!("https://{}", pds_hostname); 57 let now = Utc::now().timestamp(); 58 let exp = now + ACCESS_TOKEN_EXPIRY_SECONDS;
+8 -22
crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs
··· 1 use super::helpers::extract_token_claims; 2 use crate::oauth::OAuthError; 3 - use crate::state::{AppState, RateLimitKind}; 4 use axum::extract::State; 5 - use axum::http::{HeaderMap, StatusCode}; 6 use axum::{Form, Json}; 7 use chrono::Utc; 8 use serde::{Deserialize, Serialize}; ··· 17 18 pub async fn revoke_token( 19 State(state): State<AppState>, 20 - headers: HeaderMap, 21 Form(request): Form<RevokeRequest>, 22 ) -> Result<StatusCode, OAuthError> { 23 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 24 - if !state 25 - .check_rate_limit(RateLimitKind::OAuthIntrospect, &client_ip) 26 - .await 27 - { 28 - tracing::warn!(ip = %client_ip, "OAuth revoke rate limit exceeded"); 29 - return Err(OAuthError::RateLimited); 30 - } 31 if let Some(token) = &request.token { 32 let refresh_token = RefreshToken::from(token.clone()); 33 if let Some((db_id, _)) = state ··· 89 90 pub async fn introspect_token( 91 State(state): State<AppState>, 92 - headers: HeaderMap, 93 Form(request): Form<IntrospectRequest>, 94 ) -> Result<Json<IntrospectResponse>, OAuthError> { 95 - let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 96 - if !state 97 - .check_rate_limit(RateLimitKind::OAuthIntrospect, &client_ip) 98 - .await 99 - { 100 - tracing::warn!(ip = %client_ip, "OAuth introspect rate limit exceeded"); 101 - return Err(OAuthError::RateLimited); 102 - } 103 let inactive_response = IntrospectResponse { 104 active: false, 105 scope: None, ··· 126 if token_data.expires_at < Utc::now() { 127 return Ok(Json(inactive_response)); 128 } 129 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 130 let issuer = format!("https://{}", pds_hostname); 131 Ok(Json(IntrospectResponse { 132 active: true, ··· 141 exp: Some(token_info.exp), 142 iat: Some(token_info.iat), 143 nbf: Some(token_info.iat), 144 - sub: Some(token_data.did), 145 aud: Some(issuer.clone()), 146 iss: Some(issuer), 147 jti: Some(token_info.jti),
··· 1 use super::helpers::extract_token_claims; 2 use crate::oauth::OAuthError; 3 + use crate::rate_limit::{OAuthIntrospectLimit, OAuthRateLimited}; 4 + use crate::state::AppState; 5 + use crate::util::pds_hostname; 6 use axum::extract::State; 7 + use axum::http::StatusCode; 8 use axum::{Form, Json}; 9 use chrono::Utc; 10 use serde::{Deserialize, Serialize}; ··· 19 20 pub async fn revoke_token( 21 State(state): State<AppState>, 22 + _rate_limit: OAuthRateLimited<OAuthIntrospectLimit>, 23 Form(request): Form<RevokeRequest>, 24 ) -> Result<StatusCode, OAuthError> { 25 if let Some(token) = &request.token { 26 let refresh_token = RefreshToken::from(token.clone()); 27 if let Some((db_id, _)) = state ··· 83 84 pub async fn introspect_token( 85 State(state): State<AppState>, 86 + _rate_limit: OAuthRateLimited<OAuthIntrospectLimit>, 87 Form(request): Form<IntrospectRequest>, 88 ) -> Result<Json<IntrospectResponse>, OAuthError> { 89 let inactive_response = IntrospectResponse { 90 active: false, 91 scope: None, ··· 112 if token_data.expires_at < Utc::now() { 113 return Ok(Json(inactive_response)); 114 } 115 + let pds_hostname = pds_hostname(); 116 let issuer = format!("https://{}", pds_hostname); 117 Ok(Json(IntrospectResponse { 118 active: true, ··· 127 exp: Some(token_info.exp), 128 iat: Some(token_info.iat), 129 nbf: Some(token_info.iat), 130 + sub: Some(token_data.did.to_string()), 131 aud: Some(issuer.clone()), 132 iss: Some(issuer), 133 jti: Some(token_info.jti),
+3 -26
crates/tranquil-pds/src/oauth/endpoints/token/mod.rs
··· 4 mod types; 5 6 use crate::oauth::OAuthError; 7 - use crate::state::{AppState, RateLimitKind}; 8 use axum::body::Bytes; 9 use axum::{Json, extract::State, http::HeaderMap}; 10 ··· 17 ClientAuthParams, GrantType, TokenGrant, TokenRequest, TokenResponse, ValidatedTokenRequest, 18 }; 19 20 - fn extract_client_ip(headers: &HeaderMap) -> String { 21 - if let Some(forwarded) = headers.get("x-forwarded-for") 22 - && let Ok(value) = forwarded.to_str() 23 - && let Some(first_ip) = value.split(',').next() 24 - { 25 - return first_ip.trim().to_string(); 26 - } 27 - if let Some(real_ip) = headers.get("x-real-ip") 28 - && let Ok(value) = real_ip.to_str() 29 - { 30 - return value.trim().to_string(); 31 - } 32 - "unknown".to_string() 33 - } 34 - 35 pub async fn token_endpoint( 36 State(state): State<AppState>, 37 headers: HeaderMap, 38 body: Bytes, 39 ) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> { ··· 53 .to_string(), 54 )); 55 }; 56 - let client_ip = extract_client_ip(&headers); 57 - if !state 58 - .check_rate_limit(RateLimitKind::OAuthToken, &client_ip) 59 - .await 60 - { 61 - tracing::warn!(ip = %client_ip, "OAuth token rate limit exceeded"); 62 - return Err(OAuthError::InvalidRequest( 63 - "Too many requests. Please try again later.".to_string(), 64 - )); 65 - } 66 let dpop_proof = headers 67 .get("DPoP") 68 .and_then(|v| v.to_str().ok())
··· 4 mod types; 5 6 use crate::oauth::OAuthError; 7 + use crate::rate_limit::{OAuthRateLimited, OAuthTokenLimit}; 8 + use crate::state::AppState; 9 use axum::body::Bytes; 10 use axum::{Json, extract::State, http::HeaderMap}; 11 ··· 18 ClientAuthParams, GrantType, TokenGrant, TokenRequest, TokenResponse, ValidatedTokenRequest, 19 }; 20 21 pub async fn token_endpoint( 22 State(state): State<AppState>, 23 + _rate_limit: OAuthRateLimited<OAuthTokenLimit>, 24 headers: HeaderMap, 25 body: Bytes, 26 ) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> { ··· 40 .to_string(), 41 )); 42 }; 43 let dpop_proof = headers 44 .get("DPoP") 45 .and_then(|v| v.to_str().ok())
+9 -7
crates/tranquil-pds/src/oauth/mod.rs
··· 10 } 11 12 pub use tranquil_oauth::{ 13 - AuthFlowState, AuthorizationRequestParameters, AuthorizationServerMetadata, 14 - AuthorizedClientData, ClientAuth, ClientMetadata, ClientMetadataCache, Code, DPoPClaims, 15 - DPoPJwk, DPoPProofHeader, DPoPProofPayload, DPoPVerifier, DPoPVerifyResult, DeviceData, 16 - DeviceId, JwkPublicKey, Jwks, OAuthClientMetadata, OAuthError, ParResponse, 17 - ProtectedResourceMetadata, RefreshToken, RefreshTokenState, RequestData, RequestId, SessionId, 18 - TokenData, TokenId, TokenRequest, TokenResponse, compute_access_token_hash, 19 - compute_jwk_thumbprint, verify_client_auth, 20 }; 21 22 pub use scopes::{AccountAction, AccountAttr, RepoAction, ScopeError, ScopePermissions};
··· 10 } 11 12 pub use tranquil_oauth::{ 13 + AuthFlow, AuthFlowWithUser, AuthorizationRequestParameters, AuthorizationServerMetadata, 14 + AuthorizedClientData, ClientAuth, ClientMetadata, ClientMetadataCache, Code, 15 + CodeChallengeMethod, DPoPClaims, DPoPJwk, DPoPProofHeader, DPoPProofPayload, DPoPVerifier, 16 + DPoPVerifyResult, DeviceData, DeviceId, FlowAuthenticated, FlowAuthorized, FlowExpired, 17 + FlowNotAuthenticated, FlowNotAuthorized, FlowPending, JwkPublicKey, Jwks, OAuthClientMetadata, 18 + OAuthError, ParResponse, Prompt, ProtectedResourceMetadata, RefreshToken, RefreshTokenState, 19 + RequestData, RequestId, ResponseMode, ResponseType, SessionId, TokenData, TokenId, 20 + TokenRequest, TokenResponse, compute_access_token_hash, compute_jwk_thumbprint, 21 + verify_client_auth, 22 }; 23 24 pub use scopes::{AccountAction, AccountAttr, RepoAction, ScopeError, ScopePermissions};
+18 -14
crates/tranquil-pds/src/oauth/verify.rs
··· 20 use crate::state::AppState; 21 22 pub struct OAuthTokenInfo { 23 - pub did: String, 24 - pub token_id: String, 25 - pub client_id: String, 26 pub scope: Option<String>, 27 pub dpop_jkt: Option<String>, 28 - pub controller_did: Option<String>, 29 } 30 31 pub struct VerifyResult { ··· 48 has_dpop_proof = dpop_proof.is_some(), 49 "Verifying OAuth access token" 50 ); 51 - let token_id = TokenId::from(token_info.token_id.clone()); 52 let token_data = oauth_repo 53 .get_token_by_id(&token_id) 54 .await ··· 154 if exp < now { 155 return Err(OAuthError::ExpiredToken("Token has expired".to_string())); 156 } 157 - let token_id = payload 158 .get("sid") 159 .and_then(|j| j.as_str()) 160 - .ok_or_else(|| OAuthError::InvalidToken("Missing sid claim".to_string()))? 161 - .to_string(); 162 - let did = payload 163 .get("sub") 164 .and_then(|s| s.as_str()) 165 - .ok_or_else(|| OAuthError::InvalidToken("Missing sub claim".to_string()))? 166 - .to_string(); 167 let scope = payload 168 .get("scope") 169 .and_then(|s| s.as_str()) ··· 173 .and_then(|c| c.get("jkt")) 174 .and_then(|j| j.as_str()) 175 .map(|s| s.to_string()); 176 - let client_id = payload 177 .get("client_id") 178 .and_then(|c| c.as_str()) 179 - .map(|s| s.to_string()) 180 .unwrap_or_default(); 181 let controller_did = payload 182 .get("act") 183 .and_then(|a| a.get("sub")) 184 .and_then(|s| s.as_str()) 185 - .map(|s| s.to_string()); 186 Ok(OAuthTokenInfo { 187 did, 188 token_id,
··· 20 use crate::state::AppState; 21 22 pub struct OAuthTokenInfo { 23 + pub did: Did, 24 + pub token_id: TokenId, 25 + pub client_id: ClientId, 26 pub scope: Option<String>, 27 pub dpop_jkt: Option<String>, 28 + pub controller_did: Option<Did>, 29 } 30 31 pub struct VerifyResult { ··· 48 has_dpop_proof = dpop_proof.is_some(), 49 "Verifying OAuth access token" 50 ); 51 + let token_id = token_info.token_id.clone(); 52 let token_data = oauth_repo 53 .get_token_by_id(&token_id) 54 .await ··· 154 if exp < now { 155 return Err(OAuthError::ExpiredToken("Token has expired".to_string())); 156 } 157 + let token_id_str = payload 158 .get("sid") 159 .and_then(|j| j.as_str()) 160 + .ok_or_else(|| OAuthError::InvalidToken("Missing sid claim".to_string()))?; 161 + let token_id = TokenId::new(token_id_str); 162 + let did_str = payload 163 .get("sub") 164 .and_then(|s| s.as_str()) 165 + .ok_or_else(|| OAuthError::InvalidToken("Missing sub claim".to_string()))?; 166 + let did: Did = did_str 167 + .parse() 168 + .map_err(|_| OAuthError::InvalidToken("Invalid sub claim (not a valid DID)".to_string()))?; 169 let scope = payload 170 .get("scope") 171 .and_then(|s| s.as_str()) ··· 175 .and_then(|c| c.get("jkt")) 176 .and_then(|j| j.as_str()) 177 .map(|s| s.to_string()); 178 + let client_id_str = payload 179 .get("client_id") 180 .and_then(|c| c.as_str()) 181 .unwrap_or_default(); 182 + let client_id = ClientId::new(client_id_str); 183 let controller_did = payload 184 .get("act") 185 .and_then(|a| a.get("sub")) 186 .and_then(|s| s.as_str()) 187 + .map(|s| s.parse::<Did>()) 188 + .transpose() 189 + .map_err(|_| OAuthError::InvalidToken("Invalid act.sub claim (not a valid DID)".to_string()))?; 190 Ok(OAuthTokenInfo { 191 did, 192 token_id,
+272
crates/tranquil-pds/src/rate_limit/extractor.rs
···
··· 1 + use std::marker::PhantomData; 2 + 3 + use axum::{ 4 + extract::FromRequestParts, 5 + http::request::Parts, 6 + response::{IntoResponse, Response}, 7 + }; 8 + 9 + use crate::api::error::ApiError; 10 + use crate::oauth::OAuthError; 11 + use crate::state::{AppState, RateLimitKind}; 12 + use crate::util::extract_client_ip; 13 + 14 + pub trait RateLimitPolicy: Send + Sync + 'static { 15 + const KIND: RateLimitKind; 16 + } 17 + 18 + pub struct LoginLimit; 19 + impl RateLimitPolicy for LoginLimit { 20 + const KIND: RateLimitKind = RateLimitKind::Login; 21 + } 22 + 23 + pub struct AccountCreationLimit; 24 + impl RateLimitPolicy for AccountCreationLimit { 25 + const KIND: RateLimitKind = RateLimitKind::AccountCreation; 26 + } 27 + 28 + pub struct PasswordResetLimit; 29 + impl RateLimitPolicy for PasswordResetLimit { 30 + const KIND: RateLimitKind = RateLimitKind::PasswordReset; 31 + } 32 + 33 + pub struct ResetPasswordLimit; 34 + impl RateLimitPolicy for ResetPasswordLimit { 35 + const KIND: RateLimitKind = RateLimitKind::ResetPassword; 36 + } 37 + 38 + pub struct RefreshSessionLimit; 39 + impl RateLimitPolicy for RefreshSessionLimit { 40 + const KIND: RateLimitKind = RateLimitKind::RefreshSession; 41 + } 42 + 43 + pub struct OAuthTokenLimit; 44 + impl RateLimitPolicy for OAuthTokenLimit { 45 + const KIND: RateLimitKind = RateLimitKind::OAuthToken; 46 + } 47 + 48 + pub struct OAuthAuthorizeLimit; 49 + impl RateLimitPolicy for OAuthAuthorizeLimit { 50 + const KIND: RateLimitKind = RateLimitKind::OAuthAuthorize; 51 + } 52 + 53 + pub struct OAuthParLimit; 54 + impl RateLimitPolicy for OAuthParLimit { 55 + const KIND: RateLimitKind = RateLimitKind::OAuthPar; 56 + } 57 + 58 + pub struct OAuthIntrospectLimit; 59 + impl RateLimitPolicy for OAuthIntrospectLimit { 60 + const KIND: RateLimitKind = RateLimitKind::OAuthIntrospect; 61 + } 62 + 63 + pub struct AppPasswordLimit; 64 + impl RateLimitPolicy for AppPasswordLimit { 65 + const KIND: RateLimitKind = RateLimitKind::AppPassword; 66 + } 67 + 68 + pub struct EmailUpdateLimit; 69 + impl RateLimitPolicy for EmailUpdateLimit { 70 + const KIND: RateLimitKind = RateLimitKind::EmailUpdate; 71 + } 72 + 73 + pub struct TotpVerifyLimit; 74 + impl RateLimitPolicy for TotpVerifyLimit { 75 + const KIND: RateLimitKind = RateLimitKind::TotpVerify; 76 + } 77 + 78 + pub struct HandleUpdateLimit; 79 + impl RateLimitPolicy for HandleUpdateLimit { 80 + const KIND: RateLimitKind = RateLimitKind::HandleUpdate; 81 + } 82 + 83 + pub struct HandleUpdateDailyLimit; 84 + impl RateLimitPolicy for HandleUpdateDailyLimit { 85 + const KIND: RateLimitKind = RateLimitKind::HandleUpdateDaily; 86 + } 87 + 88 + pub struct VerificationCheckLimit; 89 + impl RateLimitPolicy for VerificationCheckLimit { 90 + const KIND: RateLimitKind = RateLimitKind::VerificationCheck; 91 + } 92 + 93 + pub struct SsoInitiateLimit; 94 + impl RateLimitPolicy for SsoInitiateLimit { 95 + const KIND: RateLimitKind = RateLimitKind::SsoInitiate; 96 + } 97 + 98 + pub struct SsoCallbackLimit; 99 + impl RateLimitPolicy for SsoCallbackLimit { 100 + const KIND: RateLimitKind = RateLimitKind::SsoCallback; 101 + } 102 + 103 + pub struct SsoUnlinkLimit; 104 + impl RateLimitPolicy for SsoUnlinkLimit { 105 + const KIND: RateLimitKind = RateLimitKind::SsoUnlink; 106 + } 107 + 108 + pub struct OAuthRegisterCompleteLimit; 109 + impl RateLimitPolicy for OAuthRegisterCompleteLimit { 110 + const KIND: RateLimitKind = RateLimitKind::OAuthRegisterComplete; 111 + } 112 + 113 + pub trait RateLimitRejection: IntoResponse + Send + 'static { 114 + fn new() -> Self; 115 + } 116 + 117 + pub struct ApiRateLimitRejection; 118 + 119 + impl RateLimitRejection for ApiRateLimitRejection { 120 + fn new() -> Self { 121 + Self 122 + } 123 + } 124 + 125 + impl IntoResponse for ApiRateLimitRejection { 126 + fn into_response(self) -> Response { 127 + ApiError::RateLimitExceeded(None).into_response() 128 + } 129 + } 130 + 131 + pub struct OAuthRateLimitRejection; 132 + 133 + impl RateLimitRejection for OAuthRateLimitRejection { 134 + fn new() -> Self { 135 + Self 136 + } 137 + } 138 + 139 + impl IntoResponse for OAuthRateLimitRejection { 140 + fn into_response(self) -> Response { 141 + OAuthError::RateLimited.into_response() 142 + } 143 + } 144 + 145 + impl From<OAuthRateLimitRejection> for OAuthError { 146 + fn from(_: OAuthRateLimitRejection) -> Self { 147 + OAuthError::RateLimited 148 + } 149 + } 150 + 151 + pub struct RateLimitedInner<P: RateLimitPolicy, R: RateLimitRejection> { 152 + client_ip: String, 153 + _marker: PhantomData<(P, R)>, 154 + } 155 + 156 + impl<P: RateLimitPolicy, R: RateLimitRejection> RateLimitedInner<P, R> { 157 + pub fn client_ip(&self) -> &str { 158 + &self.client_ip 159 + } 160 + } 161 + 162 + impl<P: RateLimitPolicy, R: RateLimitRejection> FromRequestParts<AppState> 163 + for RateLimitedInner<P, R> 164 + { 165 + type Rejection = R; 166 + 167 + async fn from_request_parts( 168 + parts: &mut Parts, 169 + state: &AppState, 170 + ) -> Result<Self, Self::Rejection> { 171 + let client_ip = extract_client_ip(&parts.headers, None); 172 + 173 + if !state.check_rate_limit(P::KIND, &client_ip).await { 174 + tracing::warn!( 175 + ip = %client_ip, 176 + kind = ?P::KIND, 177 + "Rate limit exceeded" 178 + ); 179 + return Err(R::new()); 180 + } 181 + 182 + Ok(RateLimitedInner { 183 + client_ip, 184 + _marker: PhantomData, 185 + }) 186 + } 187 + } 188 + 189 + pub type RateLimited<P> = RateLimitedInner<P, ApiRateLimitRejection>; 190 + pub type OAuthRateLimited<P> = RateLimitedInner<P, OAuthRateLimitRejection>; 191 + 192 + #[derive(Debug)] 193 + pub struct UserRateLimitError { 194 + pub kind: RateLimitKind, 195 + pub message: Option<String>, 196 + } 197 + 198 + impl UserRateLimitError { 199 + pub fn new(kind: RateLimitKind) -> Self { 200 + Self { 201 + kind, 202 + message: None, 203 + } 204 + } 205 + 206 + pub fn with_message(kind: RateLimitKind, message: impl Into<String>) -> Self { 207 + Self { 208 + kind, 209 + message: Some(message.into()), 210 + } 211 + } 212 + } 213 + 214 + impl std::fmt::Display for UserRateLimitError { 215 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 216 + match &self.message { 217 + Some(msg) => write!(f, "{}", msg), 218 + None => write!(f, "Rate limit exceeded for {:?}", self.kind), 219 + } 220 + } 221 + } 222 + 223 + impl std::error::Error for UserRateLimitError {} 224 + 225 + impl IntoResponse for UserRateLimitError { 226 + fn into_response(self) -> Response { 227 + ApiError::RateLimitExceeded(self.message).into_response() 228 + } 229 + } 230 + 231 + pub struct UserRateLimitProof<P: RateLimitPolicy> { 232 + _marker: PhantomData<P>, 233 + } 234 + 235 + impl<P: RateLimitPolicy> UserRateLimitProof<P> { 236 + fn new() -> Self { 237 + Self { 238 + _marker: PhantomData, 239 + } 240 + } 241 + } 242 + 243 + pub async fn check_user_rate_limit<P: RateLimitPolicy>( 244 + state: &AppState, 245 + user_key: &str, 246 + ) -> Result<UserRateLimitProof<P>, UserRateLimitError> { 247 + if !state.check_rate_limit(P::KIND, user_key).await { 248 + tracing::warn!( 249 + key = %user_key, 250 + kind = ?P::KIND, 251 + "User rate limit exceeded" 252 + ); 253 + return Err(UserRateLimitError::new(P::KIND)); 254 + } 255 + Ok(UserRateLimitProof::new()) 256 + } 257 + 258 + pub async fn check_user_rate_limit_with_message<P: RateLimitPolicy>( 259 + state: &AppState, 260 + user_key: &str, 261 + error_message: impl Into<String>, 262 + ) -> Result<UserRateLimitProof<P>, UserRateLimitError> { 263 + if !state.check_rate_limit(P::KIND, user_key).await { 264 + tracing::warn!( 265 + key = %user_key, 266 + kind = ?P::KIND, 267 + "User rate limit exceeded" 268 + ); 269 + return Err(UserRateLimitError::with_message(P::KIND, error_message)); 270 + } 271 + Ok(UserRateLimitProof::new()) 272 + }
+5 -102
crates/tranquil-pds/src/rate_limit.rs crates/tranquil-pds/src/rate_limit/mod.rs
··· 1 - use axum::{ 2 - Json, 3 - body::Body, 4 - extract::ConnectInfo, 5 - http::{HeaderMap, Request, StatusCode}, 6 - middleware::Next, 7 - response::{IntoResponse, Response}, 8 - }; 9 use governor::{ 10 Quota, RateLimiter, 11 clock::DefaultClock, 12 state::{InMemoryState, NotKeyed, keyed::DefaultKeyedStateStore}, 13 }; 14 - use std::{net::SocketAddr, num::NonZeroU32, sync::Arc}; 15 16 pub type KeyedRateLimiter = RateLimiter<String, DefaultKeyedStateStore<String>, DefaultClock>; 17 pub type GlobalRateLimiter = RateLimiter<NotKeyed, InMemoryState, DefaultClock>; ··· 166 } 167 } 168 169 - pub fn extract_client_ip(headers: &HeaderMap, addr: Option<SocketAddr>) -> String { 170 - if let Some(forwarded) = headers.get("x-forwarded-for") 171 - && let Ok(value) = forwarded.to_str() 172 - && let Some(first_ip) = value.split(',').next() 173 - { 174 - return first_ip.trim().to_string(); 175 - } 176 - 177 - if let Some(real_ip) = headers.get("x-real-ip") 178 - && let Ok(value) = real_ip.to_str() 179 - { 180 - return value.trim().to_string(); 181 - } 182 - 183 - addr.map(|a| a.ip().to_string()) 184 - .unwrap_or_else(|| "unknown".to_string()) 185 - } 186 - 187 - fn rate_limit_response() -> Response { 188 - ( 189 - StatusCode::TOO_MANY_REQUESTS, 190 - Json(serde_json::json!({ 191 - "error": "RateLimitExceeded", 192 - "message": "Too many requests. Please try again later." 193 - })), 194 - ) 195 - .into_response() 196 - } 197 - 198 - pub async fn login_rate_limit( 199 - ConnectInfo(addr): ConnectInfo<SocketAddr>, 200 - axum::extract::State(limiters): axum::extract::State<Arc<RateLimiters>>, 201 - request: Request<Body>, 202 - next: Next, 203 - ) -> Response { 204 - let client_ip = extract_client_ip(request.headers(), Some(addr)); 205 - 206 - if limiters.login.check_key(&client_ip).is_err() { 207 - tracing::warn!(ip = %client_ip, "Login rate limit exceeded"); 208 - return rate_limit_response(); 209 - } 210 - 211 - next.run(request).await 212 - } 213 - 214 - pub async fn oauth_token_rate_limit( 215 - ConnectInfo(addr): ConnectInfo<SocketAddr>, 216 - axum::extract::State(limiters): axum::extract::State<Arc<RateLimiters>>, 217 - request: Request<Body>, 218 - next: Next, 219 - ) -> Response { 220 - let client_ip = extract_client_ip(request.headers(), Some(addr)); 221 - 222 - if limiters.oauth_token.check_key(&client_ip).is_err() { 223 - tracing::warn!(ip = %client_ip, "OAuth token rate limit exceeded"); 224 - return rate_limit_response(); 225 - } 226 - 227 - next.run(request).await 228 - } 229 - 230 - pub async fn password_reset_rate_limit( 231 - ConnectInfo(addr): ConnectInfo<SocketAddr>, 232 - axum::extract::State(limiters): axum::extract::State<Arc<RateLimiters>>, 233 - request: Request<Body>, 234 - next: Next, 235 - ) -> Response { 236 - let client_ip = extract_client_ip(request.headers(), Some(addr)); 237 - 238 - if limiters.password_reset.check_key(&client_ip).is_err() { 239 - tracing::warn!(ip = %client_ip, "Password reset rate limit exceeded"); 240 - return rate_limit_response(); 241 - } 242 - 243 - next.run(request).await 244 - } 245 - 246 - pub async fn account_creation_rate_limit( 247 - ConnectInfo(addr): ConnectInfo<SocketAddr>, 248 - axum::extract::State(limiters): axum::extract::State<Arc<RateLimiters>>, 249 - request: Request<Body>, 250 - next: Next, 251 - ) -> Response { 252 - let client_ip = extract_client_ip(request.headers(), Some(addr)); 253 - 254 - if limiters.account_creation.check_key(&client_ip).is_err() { 255 - tracing::warn!(ip = %client_ip, "Account creation rate limit exceeded"); 256 - return rate_limit_response(); 257 - } 258 - 259 - next.run(request).await 260 - } 261 - 262 #[cfg(test)] 263 mod tests { 264 use super::*;
··· 1 + mod extractor; 2 + 3 + pub use extractor::*; 4 + 5 use governor::{ 6 Quota, RateLimiter, 7 clock::DefaultClock, 8 state::{InMemoryState, NotKeyed, keyed::DefaultKeyedStateStore}, 9 }; 10 + use std::{num::NonZeroU32, sync::Arc}; 11 12 pub type KeyedRateLimiter = RateLimiter<String, DefaultKeyedStateStore<String>, DefaultClock>; 13 pub type GlobalRateLimiter = RateLimiter<NotKeyed, InMemoryState, DefaultClock>; ··· 162 } 163 } 164 165 #[cfg(test)] 166 mod tests { 167 use super::*;
+2 -1
crates/tranquil-pds/src/sso/config.rs
··· 1 use std::sync::OnceLock; 2 use tranquil_db_traits::SsoProviderType; 3 ··· 50 }; 51 52 if config.is_any_enabled() { 53 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_default(); 54 if hostname.is_empty() || hostname == "localhost" { 55 panic!( 56 "PDS_HOSTNAME must be set to a valid hostname when SSO is enabled. \
··· 1 + use crate::util::pds_hostname; 2 use std::sync::OnceLock; 3 use tranquil_db_traits::SsoProviderType; 4 ··· 51 }; 52 53 if config.is_any_enabled() { 54 + let hostname = pds_hostname(); 55 if hostname.is_empty() || hostname == "localhost" { 56 panic!( 57 "PDS_HOSTNAME must be set to a valid hostname when SSO is enabled. \
+33 -62
crates/tranquil-pds/src/sso/endpoints.rs
··· 13 use crate::api::error::ApiError; 14 use crate::auth::extractor::extract_bearer_token_from_header; 15 use crate::auth::{generate_app_password, validate_bearer_token_cached}; 16 - use crate::rate_limit::extract_client_ip; 17 - use crate::state::{AppState, RateLimitKind}; 18 19 fn generate_state() -> String { 20 use rand::RngCore; ··· 71 72 pub async fn sso_initiate( 73 State(state): State<AppState>, 74 headers: HeaderMap, 75 Json(input): Json<SsoInitiateRequest>, 76 ) -> Result<Json<SsoInitiateResponse>, ApiError> { 77 - let client_ip = extract_client_ip(&headers, None); 78 - if !state 79 - .check_rate_limit(RateLimitKind::SsoInitiate, &client_ip) 80 - .await 81 - { 82 - tracing::warn!(ip = %client_ip, "SSO initiate rate limit exceeded"); 83 - return Err(ApiError::RateLimitExceeded(None)); 84 - } 85 - 86 if input.provider.len() > 20 { 87 return Err(ApiError::SsoProviderNotFound); 88 } ··· 217 218 pub async fn sso_callback( 219 State(state): State<AppState>, 220 - headers: HeaderMap, 221 Query(query): Query<SsoCallbackQuery>, 222 ) -> Response { 223 tracing::debug!( 224 has_code = query.code.is_some(), 225 has_state = query.state.is_some(), ··· 227 "SSO callback received" 228 ); 229 230 - let client_ip = extract_client_ip(&headers, None); 231 - if !state 232 - .check_rate_limit(RateLimitKind::SsoCallback, &client_ip) 233 - .await 234 - { 235 - tracing::warn!(ip = %client_ip, "SSO callback rate limit exceeded"); 236 - return redirect_to_error("Too many requests. Please try again later."); 237 - } 238 - 239 if let Some(ref error) = query.error { 240 tracing::warn!( 241 error = %error, ··· 329 match auth_state.action.as_str() { 330 "login" => { 331 handle_sso_login( 332 - &state, 333 &auth_state.request_uri, 334 auth_state.provider, 335 &user_info, ··· 341 Some(d) => d, 342 None => return redirect_to_error("Not authenticated"), 343 }; 344 - handle_sso_link(&state, did, auth_state.provider, &user_info).await 345 } 346 "register" => { 347 handle_sso_register( 348 - &state, 349 &auth_state.request_uri, 350 auth_state.provider, 351 &user_info, ··· 358 359 pub async fn sso_callback_post( 360 State(state): State<AppState>, 361 - headers: HeaderMap, 362 Form(form): Form<SsoCallbackForm>, 363 ) -> Response { 364 tracing::debug!( ··· 376 error_description: form.error_description, 377 }; 378 379 - sso_callback(State(state), headers, Query(query)).await 380 } 381 382 fn generate_registration_token() -> String { ··· 682 auth: crate::auth::Auth<crate::auth::Active>, 683 Json(input): Json<UnlinkAccountRequest>, 684 ) -> Result<Json<UnlinkAccountResponse>, ApiError> { 685 - if !state 686 - .check_rate_limit(RateLimitKind::SsoUnlink, auth.did.as_str()) 687 - .await 688 - { 689 - tracing::warn!(did = %auth.did, "SSO unlink rate limit exceeded"); 690 - return Err(ApiError::RateLimitExceeded(None)); 691 - } 692 693 let id = uuid::Uuid::parse_str(&input.id).map_err(|_| ApiError::InvalidId)?; 694 ··· 746 747 pub async fn get_pending_registration( 748 State(state): State<AppState>, 749 - headers: HeaderMap, 750 Query(query): Query<PendingRegistrationQuery>, 751 ) -> Result<Json<PendingRegistrationResponse>, ApiError> { 752 - let client_ip = extract_client_ip(&headers, None); 753 - if !state 754 - .check_rate_limit(RateLimitKind::SsoCallback, &client_ip) 755 - .await 756 - { 757 - tracing::warn!(ip = %client_ip, "SSO pending registration rate limit exceeded"); 758 - return Err(ApiError::RateLimitExceeded(None)); 759 - } 760 - 761 if query.token.len() > 100 { 762 return Err(ApiError::InvalidRequest("Invalid token".into())); 763 } ··· 810 } 811 }; 812 813 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 814 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 815 let full_handle = format!("{}.{}", validated, hostname_for_handles); 816 let handle_typed = crate::types::Handle::new_unchecked(&full_handle); 817 ··· 866 867 pub async fn complete_registration( 868 State(state): State<AppState>, 869 - headers: HeaderMap, 870 Json(input): Json<CompleteRegistrationInput>, 871 ) -> Result<Json<CompleteRegistrationResponse>, ApiError> { 872 use jacquard_common::types::{integer::LimitedU32, string::Tid}; 873 use jacquard_repo::{mst::Mst, storage::BlockStore}; 874 use k256::ecdsa::SigningKey; ··· 876 use serde_json::json; 877 use std::sync::Arc; 878 879 - let client_ip = extract_client_ip(&headers, None); 880 - if !state 881 - .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 882 - .await 883 - { 884 - tracing::warn!(ip = %client_ip, "SSO registration rate limit exceeded"); 885 - return Err(ApiError::RateLimitExceeded(None)); 886 - } 887 - 888 if input.token.len() > 100 { 889 return Err(ApiError::InvalidRequest("Invalid token".into())); 890 } ··· 899 .await? 900 .ok_or(ApiError::SsoSessionExpired)?; 901 902 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 903 - let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); 904 905 let handle = match crate::api::validation::validate_short_handle(&input.handle) { 906 Ok(h) => format!("{}.{}", h, hostname_for_handles), ··· 977 let handle_typed = crate::types::Handle::new_unchecked(&handle); 978 let reserved = state 979 .user_repo 980 - .reserve_handle(&handle_typed, &client_ip) 981 .await 982 .unwrap_or(false); 983 ··· 1315 return Err(ApiError::InternalError(None)); 1316 } 1317 1318 - let hostname = 1319 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1320 if let Err(e) = crate::comms::comms_repo::enqueue_welcome( 1321 state.user_repo.as_ref(), 1322 state.infra_repo.as_ref(), 1323 user_id.unwrap_or(uuid::Uuid::nil()), 1324 - &hostname, 1325 ) 1326 .await 1327 { ··· 1367 verification_channel, 1368 &verification_recipient, 1369 &formatted_token, 1370 - &hostname, 1371 ) 1372 .await 1373 {
··· 13 use crate::api::error::ApiError; 14 use crate::auth::extractor::extract_bearer_token_from_header; 15 use crate::auth::{generate_app_password, validate_bearer_token_cached}; 16 + use crate::rate_limit::{ 17 + AccountCreationLimit, RateLimited, SsoCallbackLimit, SsoInitiateLimit, SsoUnlinkLimit, 18 + check_user_rate_limit_with_message, 19 + }; 20 + use crate::state::AppState; 21 + use crate::util::{pds_hostname, pds_hostname_without_port}; 22 23 fn generate_state() -> String { 24 use rand::RngCore; ··· 75 76 pub async fn sso_initiate( 77 State(state): State<AppState>, 78 + _rate_limit: RateLimited<SsoInitiateLimit>, 79 headers: HeaderMap, 80 Json(input): Json<SsoInitiateRequest>, 81 ) -> Result<Json<SsoInitiateResponse>, ApiError> { 82 if input.provider.len() > 20 { 83 return Err(ApiError::SsoProviderNotFound); 84 } ··· 213 214 pub async fn sso_callback( 215 State(state): State<AppState>, 216 + _rate_limit: RateLimited<SsoCallbackLimit>, 217 Query(query): Query<SsoCallbackQuery>, 218 ) -> Response { 219 + sso_callback_internal(&state, query).await 220 + } 221 + 222 + async fn sso_callback_internal(state: &AppState, query: SsoCallbackQuery) -> Response { 223 tracing::debug!( 224 has_code = query.code.is_some(), 225 has_state = query.state.is_some(), ··· 227 "SSO callback received" 228 ); 229 230 if let Some(ref error) = query.error { 231 tracing::warn!( 232 error = %error, ··· 320 match auth_state.action.as_str() { 321 "login" => { 322 handle_sso_login( 323 + state, 324 &auth_state.request_uri, 325 auth_state.provider, 326 &user_info, ··· 332 Some(d) => d, 333 None => return redirect_to_error("Not authenticated"), 334 }; 335 + handle_sso_link(state, did, auth_state.provider, &user_info).await 336 } 337 "register" => { 338 handle_sso_register( 339 + state, 340 &auth_state.request_uri, 341 auth_state.provider, 342 &user_info, ··· 349 350 pub async fn sso_callback_post( 351 State(state): State<AppState>, 352 + _rate_limit: RateLimited<SsoCallbackLimit>, 353 Form(form): Form<SsoCallbackForm>, 354 ) -> Response { 355 tracing::debug!( ··· 367 error_description: form.error_description, 368 }; 369 370 + sso_callback_internal(&state, query).await 371 } 372 373 fn generate_registration_token() -> String { ··· 673 auth: crate::auth::Auth<crate::auth::Active>, 674 Json(input): Json<UnlinkAccountRequest>, 675 ) -> Result<Json<UnlinkAccountResponse>, ApiError> { 676 + let _rate_limit = check_user_rate_limit_with_message::<SsoUnlinkLimit>( 677 + &state, 678 + auth.did.as_str(), 679 + "Too many unlink attempts. Please try again later.", 680 + ) 681 + .await?; 682 683 let id = uuid::Uuid::parse_str(&input.id).map_err(|_| ApiError::InvalidId)?; 684 ··· 736 737 pub async fn get_pending_registration( 738 State(state): State<AppState>, 739 + _rate_limit: RateLimited<SsoCallbackLimit>, 740 Query(query): Query<PendingRegistrationQuery>, 741 ) -> Result<Json<PendingRegistrationResponse>, ApiError> { 742 if query.token.len() > 100 { 743 return Err(ApiError::InvalidRequest("Invalid token".into())); 744 } ··· 791 } 792 }; 793 794 + let hostname_for_handles = pds_hostname_without_port(); 795 let full_handle = format!("{}.{}", validated, hostname_for_handles); 796 let handle_typed = crate::types::Handle::new_unchecked(&full_handle); 797 ··· 846 847 pub async fn complete_registration( 848 State(state): State<AppState>, 849 + rate_limit: RateLimited<AccountCreationLimit>, 850 Json(input): Json<CompleteRegistrationInput>, 851 ) -> Result<Json<CompleteRegistrationResponse>, ApiError> { 852 + let client_ip = rate_limit.client_ip(); 853 use jacquard_common::types::{integer::LimitedU32, string::Tid}; 854 use jacquard_repo::{mst::Mst, storage::BlockStore}; 855 use k256::ecdsa::SigningKey; ··· 857 use serde_json::json; 858 use std::sync::Arc; 859 860 if input.token.len() > 100 { 861 return Err(ApiError::InvalidRequest("Invalid token".into())); 862 } ··· 871 .await? 872 .ok_or(ApiError::SsoSessionExpired)?; 873 874 + let hostname = pds_hostname(); 875 + let hostname_for_handles = pds_hostname_without_port(); 876 877 let handle = match crate::api::validation::validate_short_handle(&input.handle) { 878 Ok(h) => format!("{}.{}", h, hostname_for_handles), ··· 949 let handle_typed = crate::types::Handle::new_unchecked(&handle); 950 let reserved = state 951 .user_repo 952 + .reserve_handle(&handle_typed, client_ip) 953 .await 954 .unwrap_or(false); 955 ··· 1287 return Err(ApiError::InternalError(None)); 1288 } 1289 1290 + let hostname = pds_hostname(); 1291 if let Err(e) = crate::comms::comms_repo::enqueue_welcome( 1292 state.user_repo.as_ref(), 1293 state.infra_repo.as_ref(), 1294 user_id.unwrap_or(uuid::Uuid::nil()), 1295 + hostname, 1296 ) 1297 .await 1298 { ··· 1338 verification_channel, 1339 &verification_recipient, 1340 &formatted_token, 1341 + hostname, 1342 ) 1343 .await 1344 {
+9
crates/tranquil-pds/src/state.rs
··· 1 use crate::appview::DidResolver; 2 use crate::cache::{Cache, DistributedRateLimiter, create_cache}; 3 use crate::circuit_breaker::CircuitBreakers; 4 use crate::config::AuthConfig; ··· 7 use crate::sso::{SsoConfig, SsoManager}; 8 use crate::storage::{BackupStorage, BlobStorage, create_backup_storage, create_blob_storage}; 9 use crate::sync::firehose::SequencedEvent; 10 use sqlx::PgPool; 11 use std::error::Error; 12 use std::sync::Arc; ··· 41 pub did_resolver: Arc<DidResolver>, 42 pub sso_repo: Arc<dyn SsoRepository>, 43 pub sso_manager: SsoManager, 44 } 45 46 pub enum RateLimitKind { 47 Login, 48 AccountCreation, ··· 180 let did_resolver = Arc::new(DidResolver::new()); 181 let sso_config = SsoConfig::init(); 182 let sso_manager = SsoManager::from_config(sso_config); 183 184 Self { 185 user_repo: repos.user.clone(), ··· 204 distributed_rate_limiter, 205 did_resolver, 206 sso_manager, 207 } 208 } 209
··· 1 use crate::appview::DidResolver; 2 + use crate::auth::webauthn::WebAuthnConfig; 3 use crate::cache::{Cache, DistributedRateLimiter, create_cache}; 4 use crate::circuit_breaker::CircuitBreakers; 5 use crate::config::AuthConfig; ··· 8 use crate::sso::{SsoConfig, SsoManager}; 9 use crate::storage::{BackupStorage, BlobStorage, create_backup_storage, create_blob_storage}; 10 use crate::sync::firehose::SequencedEvent; 11 + use crate::util::pds_hostname; 12 use sqlx::PgPool; 13 use std::error::Error; 14 use std::sync::Arc; ··· 43 pub did_resolver: Arc<DidResolver>, 44 pub sso_repo: Arc<dyn SsoRepository>, 45 pub sso_manager: SsoManager, 46 + pub webauthn_config: Arc<WebAuthnConfig>, 47 } 48 49 + #[derive(Debug, Clone, Copy)] 50 pub enum RateLimitKind { 51 Login, 52 AccountCreation, ··· 184 let did_resolver = Arc::new(DidResolver::new()); 185 let sso_config = SsoConfig::init(); 186 let sso_manager = SsoManager::from_config(sso_config); 187 + let webauthn_config = Arc::new( 188 + WebAuthnConfig::new(pds_hostname()) 189 + .expect("Failed to create WebAuthn config at startup"), 190 + ); 191 192 Self { 193 user_repo: repos.user.clone(), ··· 212 distributed_rate_limiter, 213 did_resolver, 214 sso_manager, 215 + webauthn_config, 216 } 217 } 218
+2 -2
crates/tranquil-pds/src/sync/deprecated.rs
··· 20 21 async fn check_admin_or_self(state: &AppState, headers: &HeaderMap, did: &str) -> bool { 22 let extracted = match crate::auth::extract_auth_token_from_header( 23 - headers.get("Authorization").and_then(|h| h.to_str().ok()), 24 ) { 25 Some(t) => t, 26 None => return false, 27 }; 28 - let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok()); 29 let http_uri = "/"; 30 match crate::auth::validate_token_with_dpop( 31 state.user_repo.as_ref(),
··· 20 21 async fn check_admin_or_self(state: &AppState, headers: &HeaderMap, did: &str) -> bool { 22 let extracted = match crate::auth::extract_auth_token_from_header( 23 + crate::util::get_header_str(headers, "Authorization"), 24 ) { 25 Some(t) => t, 26 None => return false, 27 }; 28 + let dpop_proof = crate::util::get_header_str(headers, "DPoP"); 29 let http_uri = "/"; 30 match crate::auth::validate_token_with_dpop( 31 state.user_repo.as_ref(),
+21 -4
crates/tranquil-pds/src/util.rs
··· 4 use rand::Rng; 5 use serde_json::Value as JsonValue; 6 use std::collections::BTreeMap; 7 use std::str::FromStr; 8 use std::sync::OnceLock; 9 ··· 11 const DEFAULT_MAX_BLOB_SIZE: usize = 10 * 1024 * 1024 * 1024; 12 13 static MAX_BLOB_SIZE: OnceLock<usize> = OnceLock::new(); 14 15 pub fn get_max_blob_size() -> usize { 16 *MAX_BLOB_SIZE.get_or_init(|| { ··· 69 .unwrap_or_default() 70 } 71 72 - pub fn extract_client_ip(headers: &HeaderMap) -> String { 73 if let Some(forwarded) = headers.get("x-forwarded-for") 74 && let Ok(value) = forwarded.to_str() 75 && let Some(first_ip) = value.split(',').next() ··· 81 { 82 return value.trim().to_string(); 83 } 84 - "unknown".to_string() 85 } 86 87 - pub fn pds_hostname() -> String { 88 - std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) 89 } 90 91 pub fn pds_public_url() -> String {
··· 4 use rand::Rng; 5 use serde_json::Value as JsonValue; 6 use std::collections::BTreeMap; 7 + use std::net::SocketAddr; 8 use std::str::FromStr; 9 use std::sync::OnceLock; 10 ··· 12 const DEFAULT_MAX_BLOB_SIZE: usize = 10 * 1024 * 1024 * 1024; 13 14 static MAX_BLOB_SIZE: OnceLock<usize> = OnceLock::new(); 15 + static PDS_HOSTNAME: OnceLock<String> = OnceLock::new(); 16 + static PDS_HOSTNAME_WITHOUT_PORT: OnceLock<String> = OnceLock::new(); 17 18 pub fn get_max_blob_size() -> usize { 19 *MAX_BLOB_SIZE.get_or_init(|| { ··· 72 .unwrap_or_default() 73 } 74 75 + pub fn get_header_str<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> { 76 + headers.get(name).and_then(|h| h.to_str().ok()) 77 + } 78 + 79 + pub fn extract_client_ip(headers: &HeaderMap, addr: Option<SocketAddr>) -> String { 80 if let Some(forwarded) = headers.get("x-forwarded-for") 81 && let Ok(value) = forwarded.to_str() 82 && let Some(first_ip) = value.split(',').next() ··· 88 { 89 return value.trim().to_string(); 90 } 91 + addr.map(|a| a.ip().to_string()) 92 + .unwrap_or_else(|| "unknown".to_string()) 93 + } 94 + 95 + pub fn pds_hostname() -> &'static str { 96 + PDS_HOSTNAME.get_or_init(|| { 97 + std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) 98 + }) 99 } 100 101 + pub fn pds_hostname_without_port() -> &'static str { 102 + PDS_HOSTNAME_WITHOUT_PORT.get_or_init(|| { 103 + let hostname = pds_hostname(); 104 + hostname.split(':').next().unwrap_or(hostname).to_string() 105 + }) 106 } 107 108 pub fn pds_public_url() -> String {

History

3 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
fix: better type-safety
expand 0 comments
pull request successfully merged
1 commit
expand
fix: better type-safety
expand 0 comments
lewis.moe submitted #0
1 commit
expand
fix: better type-safety
expand 0 comments