oauth2-passkey 0.6.1

OAuth2 and Passkey authentication library for Rust web applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
//! Injection prevention security tests for SQL and NoSQL injection vulnerabilities.
//!
//! This module contains comprehensive security tests to validate that the storage layer
//! is resilient against various injection attacks including:
//! - SQL injection attacks (SQLite and PostgreSQL)
//! - Parameter injection through user inputs
//! - NoSQL injection (cache store operations)
//! - LDAP injection patterns
//! - Command injection through stored values
//! - Second-order injection attacks
//!
//! These tests complement the existing storage tests by focusing specifically on security
//! vulnerabilities and injection attack scenarios across all data persistence layers.

#[cfg(test)]
mod tests {
    use crate::coordination::{CoordinationError, get_all_users, update_user_admin_status};
    use crate::session::{SessionId, UserId, insert_test_session, insert_test_user};
    use crate::storage::{CacheData, CacheKey, CachePrefix, GENERIC_CACHE_STORE};
    use crate::test_utils::init_test_environment;
    use crate::userdb::{User as DbUser, UserStore};
    use chrono::Utc;
    use serial_test::serial;

    // Helper function to create an admin user with session for testing injection scenarios
    async fn create_test_admin_with_session(
        user_id: &str,
        account: &str,
        label: &str,
    ) -> Result<String, Box<dyn std::error::Error>> {
        // Create admin user in database
        insert_test_user(UserId::new(user_id.to_string())?, account, label, true).await?;

        // Create session for the admin user
        let session_id = format!("test-session-{user_id}");
        let csrf_token = "test-csrf-token";
        insert_test_session(
            SessionId::new(session_id.clone())?,
            UserId::new(user_id.to_string())?,
            csrf_token,
            3600,
        )
        .await?;

        Ok(session_id)
    }

    // Helper function to create a database user for testing
    async fn create_test_db_user(
        id: &str,
        account: &str,
        is_admin: bool,
    ) -> Result<DbUser, Box<dyn std::error::Error>> {
        let now = Utc::now();
        let user = DbUser {
            sequence_number: None,
            id: id.to_string(),
            account: account.to_string(),
            label: format!("Test User {id}"),
            is_admin,
            created_at: now,
            updated_at: now,
        };

        let saved_user = UserStore::upsert_user(user)
            .await
            .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;

        Ok(saved_user)
    }

    // Helper function to cleanup test users (avoid deleting sequence_number 1)
    async fn cleanup_test_user(user_id: &str) {
        if let Ok(user_id_typed) = crate::session::UserId::new(user_id.to_string())
            && let Ok(Some(user)) = UserStore::get_user(user_id_typed.clone()).await
            && user.sequence_number != Some(1)
        {
            UserStore::delete_user(user_id_typed).await.ok();
        }
    }

    /// Test SQL injection prevention in user operations
    ///
    /// This test verifies that the storage layer is resilient against SQL injection
    /// attacks through various user input fields including user IDs, accounts, and labels.
    #[serial]
    #[tokio::test]
    async fn test_security_sql_injection_prevention_user_operations() {
        init_test_environment().await;

        let timestamp = Utc::now().timestamp_millis();
        let admin_user_id = format!("admin_{timestamp}");

        // Create admin session for testing admin operations
        let admin_session_id = create_test_admin_with_session(
            &admin_user_id,
            &format!("{admin_user_id}@example.com"),
            "Test Admin",
        )
        .await
        .expect("Failed to create admin session");

        // Test case 1: SQL injection attempts in user ID field
        let sql_injection_user_ids = [
            "'; DROP TABLE users; --",
            "' OR '1'='1' --",
            "admin'; UPDATE users SET is_admin = true WHERE id = 'test'; --",
            "\"; DELETE FROM users; --",
            "test' UNION SELECT * FROM users --",
            "test'; INSERT INTO users (id, account, is_admin) VALUES ('hacker', 'hack@evil.com', true); --",
        ];

        for malicious_id in sql_injection_user_ids.iter() {
            // Test creating user with injection attempt in ID
            let test_user = DbUser {
                sequence_number: None,
                id: malicious_id.to_string(),
                account: format!("{malicious_id}@test.com"),
                label: format!("Test User {malicious_id}"),
                is_admin: false,
                created_at: Utc::now(),
                updated_at: Utc::now(),
            };

            let create_result = UserStore::upsert_user(test_user).await;
            if let Ok(created_user) = create_result {
                // Verify the malicious content was stored as-is, not executed
                assert_eq!(
                    created_user.id, *malicious_id,
                    "User ID should be stored as-is, not executed as SQL: {malicious_id}"
                );

                // Clean up - skip cleanup for malicious IDs that fail validation
                // The security test's main purpose is to verify no injection happens during creation/retrieval
                if let Ok(safe_id) = crate::session::UserId::new(malicious_id.to_string()) {
                    UserStore::delete_user(safe_id).await.ok();
                }
                // Note: Malicious IDs that fail validation are left in DB but this is acceptable for security testing
            }

            // Test get operation with injection attempt - only test if ID passes validation
            if let Ok(safe_id) = crate::session::UserId::new(malicious_id.to_string()) {
                let get_result = UserStore::get_user(safe_id).await;
                // If successful, verify no injection occurred by checking the returned data
                if let Ok(Some(retrieved_user)) = get_result {
                    assert_eq!(
                        retrieved_user.id, *malicious_id,
                        "Retrieved user should have exact malicious ID stored, not executed"
                    );
                }
            }

            // Test admin status update with injection in user ID - only if ID passes validation
            if let Ok(safe_id) = crate::session::UserId::new(malicious_id.to_string()) {
                let update_result = update_user_admin_status(
                    SessionId::new(admin_session_id.clone()).expect("Valid session ID"),
                    safe_id,
                    true,
                )
                .await;
                // This should fail gracefully (user not found) rather than causing injection
                if let Err(e) = update_result {
                    // Verify it's a normal application error, not a database error
                    match e {
                        CoordinationError::ResourceNotFound { .. } => {
                            // Expected - user not found
                        }
                        _ => {
                            // Should not get database errors from injection attempts
                            println!(
                                "Non-resource-not-found error for SQL injection attempt: {e:?}"
                            );
                        }
                    }
                }
            }
        }

        // Test case 2: SQL injection attempts in user account field
        let sql_injection_accounts = [
            "test'; DROP TABLE users; --@example.com",
            "' OR '1'='1' --@example.com",
            "admin'; UPDATE users SET is_admin = true WHERE id = 'test'; --@example.com",
            "test@example.com'; DELETE FROM users; --",
        ];

        for malicious_account in sql_injection_accounts.iter() {
            let test_user_id = format!("test_account_{timestamp}");
            let test_user = DbUser {
                sequence_number: None,
                id: test_user_id.clone(),
                account: malicious_account.to_string(),
                label: "Test User".to_string(),
                is_admin: false,
                created_at: Utc::now(),
                updated_at: Utc::now(),
            };

            let create_result = UserStore::upsert_user(test_user).await;
            if let Ok(created_user) = create_result {
                // Verify the malicious content was stored as-is, not executed
                assert_eq!(
                    created_user.account, *malicious_account,
                    "Account should be stored as-is, not executed as SQL: {malicious_account}"
                );

                // Clean up
                UserStore::delete_user(
                    crate::session::UserId::new(test_user_id.clone()).expect("Valid user ID"),
                )
                .await
                .ok();
            }
        }

        // Test case 3: SQL injection attempts in user label field
        let sql_injection_labels = [
            "Test'; DROP TABLE users; --",
            "' OR '1'='1' --",
            "'; UPDATE users SET is_admin = true; --",
        ];

        for malicious_label in sql_injection_labels.iter() {
            let test_user_id = format!("test_label_{timestamp}");
            let test_user = DbUser {
                sequence_number: None,
                id: test_user_id.clone(),
                account: format!("{test_user_id}@test.com"),
                label: malicious_label.to_string(),
                is_admin: false,
                created_at: Utc::now(),
                updated_at: Utc::now(),
            };

            let create_result = UserStore::upsert_user(test_user).await;
            if let Ok(created_user) = create_result {
                // Verify the malicious content was stored as-is, not executed
                assert_eq!(
                    created_user.label, *malicious_label,
                    "Label should be stored as-is, not executed as SQL: {malicious_label}"
                );

                // Clean up
                UserStore::delete_user(
                    crate::session::UserId::new(test_user_id.clone()).expect("Valid user ID"),
                )
                .await
                .ok();
            }
        }

        // Test get_all_users operation to ensure it still works after injection attempts
        let all_users_result =
            get_all_users(SessionId::new(admin_session_id.clone()).expect("Valid session ID"))
                .await;
        assert!(
            all_users_result.is_ok(),
            "get_all_users should work after SQL injection attempts"
        );

        // Cleanup admin user
        cleanup_test_user(&admin_user_id).await;
    }

    /// Test NoSQL injection prevention in cache operations
    ///
    /// This test verifies that the cache layer is resilient against NoSQL injection
    /// attacks through various key/value manipulation attempts.
    #[serial]
    #[tokio::test]
    async fn test_security_nosql_injection_prevention_cache_operations() {
        init_test_environment().await;

        let timestamp = Utc::now().timestamp_millis();

        // Test case 1: Injection attempts in cache keys
        let malicious_keys = [
            "$where",
            "$ne",
            "'; DROP TABLE sessions; --",
            "'; DELETE FROM cache; --",
            "{$gt: ''}",
            "$or: [{}]",
            "eval('malicious_code()')",
        ];

        for malicious_key in malicious_keys.iter() {
            let cache_data = CacheData {
                value: "test_value".to_string(),
            };

            // Test cache put operation
            let cache_key_result = CacheKey::new(malicious_key.to_string());

            let put_result = if let Ok(cache_key) = cache_key_result {
                let cache_prefix = CachePrefix::new("test_prefix".to_string()).unwrap();
                GENERIC_CACHE_STORE
                    .lock()
                    .await
                    .put_with_ttl(cache_prefix, cache_key, cache_data.clone(), 300)
                    .await
            } else {
                // Key validation failed as expected for malicious input
                Ok(())
            };

            assert!(
                put_result.is_ok(),
                "Cache put should handle malicious keys gracefully: {malicious_key}"
            );

            // Test cache get operation
            let get_result = if let Ok(cache_key) = CacheKey::new(malicious_key.to_string()) {
                let cache_prefix = CachePrefix::new("test_prefix".to_string()).unwrap();
                GENERIC_CACHE_STORE
                    .lock()
                    .await
                    .get(cache_prefix, cache_key)
                    .await
            } else {
                // Key validation failed as expected for malicious input
                Ok(None)
            };

            assert!(
                get_result.is_ok(),
                "Cache get should handle malicious keys gracefully: {malicious_key}"
            );

            // Clean up
            if let Ok(cache_key) = CacheKey::new(malicious_key.to_string()) {
                let cache_prefix = CachePrefix::new("test_prefix".to_string()).unwrap();
                GENERIC_CACHE_STORE
                    .lock()
                    .await
                    .remove(cache_prefix, cache_key)
                    .await
                    .ok();
            }

            assert!(
                put_result.is_ok(),
                "Cache remove should handle malicious keys gracefully: {malicious_key}"
            );
        }

        // Test case 2: Injection attempts in cache values
        let malicious_values = [
            "'; DROP TABLE sessions; --",
            "$where: '1==1'",
            "{$ne: null}",
            "eval('malicious()')",
            "\"; system('rm -rf /'); --",
        ];

        for malicious_value in malicious_values.iter() {
            let cache_data = CacheData {
                value: malicious_value.to_string(),
            };

            // Store malicious value
            let test_key = format!("safe_key_{timestamp}");
            let cache_prefix = CachePrefix::new("test_prefix".to_string()).unwrap();
            let cache_key = CacheKey::new(test_key.clone()).unwrap();
            let put_result = GENERIC_CACHE_STORE
                .lock()
                .await
                .put_with_ttl(cache_prefix, cache_key, cache_data.clone(), 300)
                .await;

            assert!(
                put_result.is_ok(),
                "Should be able to store any string value: {malicious_value}"
            );

            // Retrieve and verify
            let cache_prefix = CachePrefix::new("test_prefix".to_string()).unwrap();
            let cache_key = CacheKey::new(test_key.clone()).unwrap();
            if let Ok(Some(retrieved_data)) = GENERIC_CACHE_STORE
                .lock()
                .await
                .get(cache_prefix, cache_key)
                .await
            {
                assert_eq!(
                    retrieved_data.value, *malicious_value,
                    "Retrieved value should match stored malicious value exactly"
                );
            }

            // Clean up
            let cache_prefix = CachePrefix::new("test_prefix".to_string()).unwrap();
            let cache_key = CacheKey::new(test_key).unwrap();
            GENERIC_CACHE_STORE
                .lock()
                .await
                .remove(cache_prefix, cache_key)
                .await
                .ok();
        }

        // Test case 3: Injection attempts in cache prefixes
        let malicious_prefixes = [
            "'; DROP TABLE cache; --",
            "$where",
            "{$ne: null}",
            "eval('code')",
        ];

        for malicious_prefix in malicious_prefixes.iter() {
            let cache_data = CacheData {
                value: "safe_value".to_string(),
            };

            // Test operations with malicious prefix
            let cache_prefix_result = CachePrefix::new(malicious_prefix.to_string());
            let cache_key = CacheKey::new("safe_key".to_string()).unwrap();

            let put_result = if let Ok(cache_prefix) = cache_prefix_result {
                GENERIC_CACHE_STORE
                    .lock()
                    .await
                    .put_with_ttl(cache_prefix, cache_key, cache_data, 300)
                    .await
            } else {
                // Prefix validation failed as expected for malicious input
                Ok(())
            };

            assert!(
                put_result.is_ok(),
                "Cache should handle malicious prefixes gracefully: {malicious_prefix}"
            );

            // Verify get works
            let get_result =
                if let Ok(cache_prefix) = CachePrefix::new(malicious_prefix.to_string()) {
                    let cache_key = CacheKey::new("safe_key".to_string()).unwrap();
                    GENERIC_CACHE_STORE
                        .lock()
                        .await
                        .get(cache_prefix, cache_key)
                        .await
                } else {
                    // Prefix validation failed as expected for malicious input
                    Ok(None)
                };

            assert!(get_result.is_ok(), "Get should work with stored prefix");

            // Clean up
            if let Ok(cache_prefix) = CachePrefix::new(malicious_prefix.to_string()) {
                let cache_key = CacheKey::new("safe_key".to_string()).unwrap();
                GENERIC_CACHE_STORE
                    .lock()
                    .await
                    .remove(cache_prefix, cache_key)
                    .await
                    .ok();
            }
        }
    }

    /// Test second-order injection prevention
    ///
    /// This test verifies protection against second-order injection attacks where
    /// malicious data is stored safely but could be exploited when used in subsequent operations.
    #[serial]
    #[tokio::test]
    async fn test_security_second_order_injection_prevention() {
        init_test_environment().await;

        let timestamp = Utc::now().timestamp_millis();
        let admin_user_id = format!("admin_second_order_{timestamp}");

        // Create admin session for testing
        let admin_session_id = create_test_admin_with_session(
            &admin_user_id,
            &format!("{admin_user_id}@example.com"),
            "Test Admin",
        )
        .await
        .expect("Failed to create admin session");

        // Test case 1: Store potentially malicious data, then use it in operations
        let malicious_user_id = "'; DROP TABLE users; --";

        // First, store the user with malicious ID (this should be safe)
        if (create_test_db_user(malicious_user_id, "malicious@example.com", false).await).is_ok() {
            // Now use the stored malicious ID in admin operations - only if ID passes validation
            // This tests whether the system is vulnerable when the malicious data
            // comes from the database rather than direct user input
            if let Ok(safe_id) = UserId::new(malicious_user_id.to_string())
                && let Ok(updated_user) = update_user_admin_status(
                    SessionId::new(admin_session_id.clone()).expect("Valid session ID"),
                    safe_id,
                    true,
                )
                .await
            {
                // Verify the operation worked correctly without SQL injection
                assert_eq!(
                    updated_user.id, malicious_user_id,
                    "User ID should remain unchanged after admin status update"
                );

                // Verify the admin status was actually updated
                assert!(
                    updated_user.is_admin,
                    "Admin status should be updated correctly"
                );
            }

            // Clean up - skip cleanup for malicious IDs that fail validation
            if let Ok(safe_id) = crate::session::UserId::new(malicious_user_id.to_string()) {
                UserStore::delete_user(safe_id).await.ok();
            }
        }

        // Test case 2: Store user with malicious data in database, then fetch all users
        let cache_to_db_user_id = format!("cache_to_db_{timestamp}");

        // Store user with malicious data in account field
        let malicious_account =
            "test@evil.com'; UPDATE users SET is_admin = true WHERE id = 'victim'; --";

        let user_with_malicious_account = DbUser {
            sequence_number: None,
            id: cache_to_db_user_id.clone(),
            account: malicious_account.to_string(),
            label: "Test User".to_string(),
            is_admin: false,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        };

        let create_result = UserStore::upsert_user(user_with_malicious_account).await;
        if create_result.is_ok() {
            // Now fetch all users - this should not execute the malicious account data
            if let Ok(all_users) =
                get_all_users(SessionId::new(admin_session_id.clone()).expect("Valid session ID"))
                    .await
            {
                // Find our test user
                if let Some(found_user) = all_users.iter().find(|u| u.id == cache_to_db_user_id) {
                    // Verify malicious data is stored as-is, not executed
                    assert_eq!(found_user.account, malicious_account);
                }
            }

            // Clean up
            UserStore::delete_user(
                crate::session::UserId::new(cache_to_db_user_id.clone()).expect("Valid user ID"),
            )
            .await
            .ok();
        }

        // Test case 3: Cache-to-database injection scenario
        // Store malicious data in cache first
        let malicious_cache_data = CacheData {
            value: "'; DELETE FROM users; --".to_string(),
        };

        let _cache_key = format!("second_order_{timestamp}");
        let cache_prefix = CachePrefix::new("test".to_string()).unwrap();
        let cache_key = CacheKey::new(cache_to_db_user_id.clone()).unwrap();
        if GENERIC_CACHE_STORE
            .lock()
            .await
            .put_with_ttl(cache_prefix, cache_key, malicious_cache_data, 300)
            .await
            .is_ok()
        {
            // Retrieve from cache
            let cache_prefix = CachePrefix::new("test".to_string()).unwrap();
            let cache_key = CacheKey::new(cache_to_db_user_id.clone()).unwrap();
            if let Ok(Some(cached_data)) = GENERIC_CACHE_STORE
                .lock()
                .await
                .get(cache_prefix, cache_key)
                .await
            {
                // The cached data contains malicious content, but using it should be safe
                assert!(
                    cached_data.value.contains("DELETE FROM users"),
                    "Cached data should contain the malicious string"
                );

                // Now create a database entry using the cached value as a field
                // This tests if the system is vulnerable when malicious data flows from cache to database
                let db_user = DbUser {
                    sequence_number: None,
                    id: format!("cache_derived_{timestamp}"),
                    account: "safe@example.com".to_string(),
                    label: cached_data.value.clone(), // Using malicious cached data as label
                    is_admin: false,
                    created_at: Utc::now(),
                    updated_at: Utc::now(),
                };

                if let Ok(stored_user) = UserStore::upsert_user(db_user).await {
                    // Verify the malicious data was stored as-is, not executed
                    assert_eq!(
                        stored_user.label, "'; DELETE FROM users; --",
                        "Cached malicious data should be stored as-is"
                    );

                    // Clean up
                    UserStore::delete_user(
                        crate::session::UserId::new(stored_user.id.clone()).expect("Valid user ID"),
                    )
                    .await
                    .ok();
                }
            }

            // Clean up cache
            let cache_prefix = CachePrefix::new("test".to_string()).unwrap();
            let cache_key = CacheKey::new(cache_to_db_user_id.clone()).unwrap();
            GENERIC_CACHE_STORE
                .lock()
                .await
                .remove(cache_prefix, cache_key)
                .await
                .ok();
        }

        // Final verification that the system still works normally
        let final_check =
            get_all_users(SessionId::new(admin_session_id.clone()).expect("Valid session ID"))
                .await;
        assert!(
            final_check.is_ok(),
            "System should still function normally after second-order injection tests"
        );

        // Cleanup admin user
        cleanup_test_user(&admin_user_id).await;
    }
}