feat: enforce strict expiration checking for JWT and handle existing user identities in password strategy
All checks were successful
Test / test-frontend (pull_request) Successful in 20s
Test / frontend-build (pull_request) Successful in 22s
Verify / verify-generated-code (pull_request) Successful in 58s
Test / test (pull_request) Successful in 47s
Verify / verify-openapi-spec (pull_request) Successful in 57s
Verify / verify-frontend-api-client (pull_request) Successful in 16s
Test / lint (pull_request) Successful in 1m0s
All checks were successful
Test / test-frontend (pull_request) Successful in 20s
Test / frontend-build (pull_request) Successful in 22s
Verify / verify-generated-code (pull_request) Successful in 58s
Test / test (pull_request) Successful in 47s
Verify / verify-openapi-spec (pull_request) Successful in 57s
Verify / verify-frontend-api-client (pull_request) Successful in 16s
Test / lint (pull_request) Successful in 1m0s
This commit is contained in:
@@ -115,6 +115,8 @@ impl AuthenticationService for AuthenticationServiceImpl {
|
||||
target_sub: Option<String>,
|
||||
) -> Result<Option<Claims>, 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]
|
||||
|
||||
@@ -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::<sea_orm::MockRow>::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();
|
||||
|
||||
Reference in New Issue
Block a user