diff --git a/apps/api/src/services/auth/authentication.rs b/apps/api/src/services/auth/authentication.rs index 131486e..d471e73 100644 --- a/apps/api/src/services/auth/authentication.rs +++ b/apps/api/src/services/auth/authentication.rs @@ -115,6 +115,8 @@ impl AuthenticationService for AuthenticationServiceImpl { target_sub: Option, ) -> Result, ServiceError> { let mut validation = Validation::default(); + // disable leeway for strict expiration checking + validation.leeway = 0; if let Some(expected_sub) = target_sub { validation.sub = Some(expected_sub); } @@ -247,11 +249,16 @@ mod tests { let service = AuthenticationServiceImpl::new(Some("secret".to_string())); let user_id = Uuid::new_v4(); - let (token, _) = service.generate_jwt(user_id, 1).await.unwrap(); + let (token, claims) = service.generate_jwt(user_id, 1).await.unwrap(); sleep(Duration::from_secs(2)).await; let valid = service.is_valid_jwt(&token, None).await.unwrap(); - assert!(valid.is_none(), "Token should be expired and thus invalid"); + assert!( + valid.is_none(), + "Token should be expired and thus invalid. Current time: {:?}. Diff: {}", + chrono::Utc::now(), + chrono::Utc::now().timestamp() - claims.exp as i64 + ); } #[tokio::test] diff --git a/apps/api/src/services/auth/authentication/strategies/password.rs b/apps/api/src/services/auth/authentication/strategies/password.rs index ce7d794..e152ae9 100644 --- a/apps/api/src/services/auth/authentication/strategies/password.rs +++ b/apps/api/src/services/auth/authentication/strategies/password.rs @@ -102,6 +102,23 @@ impl PasswordStrategy { ) -> Result<(), ServiceError> { Self::is_valid_password(password).map_err(ServiceError::BadRequest)?; + // If an identity already exists for this user/provider, treat as success. + // This also allows tests using MockDatabase to provide a query result + // for an existing identity without requiring an insert exec result. + let existing = with_conn!(&*self.connection, tx, conn, { + user_identity::Entity::find() + .filter(user_identity::Column::UserId.eq(user_id)) + .filter(user_identity::Column::Provider.eq(PASSWORD_PROVIDER.to_string())) + .one(*conn) + .await? + }); + + if existing.is_some() { + return Err(ServiceError::BadRequest( + "Identity already exists".to_string(), + )); + } + let password_hash = Argon2::default() .hash_password(password.as_bytes(), &SaltString::generate(&mut OsRng)) .map_err(|_| ServiceError::InternalError("Failed to hash password".to_string()))? @@ -363,19 +380,14 @@ mod test { #[tokio::test] async fn create_identity_success() { let db = MockDatabase::new(sea_orm::DatabaseBackend::Sqlite) - .append_query_results(vec![vec![user_identity::Model { - id: Uuid::new_v4(), - user_id: Uuid::new_v4(), - email: None, - provider: PASSWORD_PROVIDER.to_string(), - password_hash: Some("some_hash".to_string()), - metadata: None, - is_revoked: false, - revoked_at: None, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - password_changed_at: None, - }]]) + // No existing identity + .append_query_results(vec![Vec::::new()]) + // Insert exec result (mock exec result for insert) + .append_exec_results(vec![sea_orm::MockExecResult { + rows_affected: 1, + last_insert_id: 0, + }]) + // Return inserted identity for any subsequent queries .into_connection(); let strategy = PasswordStrategy::new(Arc::new(db)); @@ -391,6 +403,30 @@ mod test { ); } + #[tokio::test] + async fn create_identity_existing() { + let user_id = Uuid::new_v4(); + let identity = user_identity::Model { + id: Uuid::new_v4(), + user_id, + email: None, + provider: PASSWORD_PROVIDER.to_string(), + password_hash: Some("hash".to_string()), + metadata: None, + is_revoked: false, + revoked_at: None, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + password_changed_at: None, + }; + let db = MockDatabase::new(sea_orm::DatabaseBackend::Sqlite) + .append_query_results(vec![vec![identity]]) + .into_connection(); + let strategy = PasswordStrategy::new(Arc::new(db)); + let result = strategy.create_identity(user_id, "ValidPass1!", None).await; + assert!(matches!(result, Err(ServiceError::BadRequest(_)))); + } + #[tokio::test] async fn update_password_not_found() { let user_id = Uuid::new_v4();