Vibe-guided bskyoauth and custom repo example code in Golang 馃 probably not safe to use in prod
at main 17 kB view raw
1package bskyoauth 2 3import ( 4 "crypto/ecdsa" 5 "crypto/elliptic" 6 "crypto/rand" 7 "net/http" 8 "net/http/httptest" 9 "strings" 10 "sync" 11 "testing" 12 "time" 13) 14 15// TestSessionHijackingPrevention verifies that session IDs are cryptographically 16// random and resistant to prediction attacks. 17func TestSessionHijackingPrevention(t *testing.T) { 18 t.Run("session IDs are cryptographically random", func(t *testing.T) { 19 // Generate many session IDs 20 ids := make(map[string]bool) 21 iterations := 10000 22 23 for i := 0; i < iterations; i++ { 24 id := GenerateSessionID() 25 26 // Check for collision (should be extremely rare with 128 bits) 27 if ids[id] { 28 t.Fatalf("Session ID collision detected: %s (iteration %d)", id, i) 29 } 30 ids[id] = true 31 32 // Verify length (32 chars = 192 bits base64url) 33 if len(id) != 32 { 34 t.Errorf("Session ID length incorrect: expected 32, got %d", len(id)) 35 } 36 37 // Verify characters are base64url safe 38 for _, c := range id { 39 if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_') { 40 t.Errorf("Session ID contains invalid character: %c", c) 41 } 42 } 43 } 44 45 if len(ids) != iterations { 46 t.Errorf("Expected %d unique session IDs, got %d", iterations, len(ids)) 47 } 48 }) 49 50 t.Run("session IDs are not predictable", func(t *testing.T) { 51 // Generate sequence of IDs and verify they don't follow a pattern 52 id1 := GenerateSessionID() 53 id2 := GenerateSessionID() 54 id3 := GenerateSessionID() 55 56 // IDs should be completely different 57 if id1 == id2 || id2 == id3 || id1 == id3 { 58 t.Error("Session IDs show pattern or repetition") 59 } 60 61 // Check Hamming distance (number of different characters) 62 // Should be high for random IDs 63 differences := 0 64 for i := 0; i < len(id1) && i < len(id2); i++ { 65 if id1[i] != id2[i] { 66 differences++ 67 } 68 } 69 70 // At least 50% of characters should differ for truly random IDs 71 if differences < len(id1)/2 { 72 t.Errorf("Session IDs too similar (only %d/%d chars differ), may indicate weak randomness", differences, len(id1)) 73 } 74 }) 75 76 t.Run("session ID has sufficient entropy", func(t *testing.T) { 77 // Session IDs should have high entropy (>= 128 bits recommended) 78 id := GenerateSessionID() 79 80 // 32 characters of base64url = log2(64^32) = 192 bits of entropy 81 // This is well above the 128-bit minimum recommended 82 expectedMinEntropy := 128.0 83 actualEntropy := float64(len(id)) * 6.0 // 6 bits per base64 character 84 85 if actualEntropy < expectedMinEntropy { 86 t.Errorf("Session ID entropy too low: %.1f bits (minimum: %.1f bits)", actualEntropy, expectedMinEntropy) 87 } 88 }) 89} 90 91// TestSessionFixationPrevention verifies that sessions are regenerated 92// appropriately to prevent session fixation attacks. 93func TestSessionFixationPrevention(t *testing.T) { 94 t.Run("new session ID generated on creation", func(t *testing.T) { 95 store := NewMemorySessionStore() 96 97 // Create session with test data 98 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 99 if err != nil { 100 t.Fatalf("Failed to generate key: %v", err) 101 } 102 103 session1 := &Session{ 104 DID: "did:plc:test123", 105 AccessToken: "token1", 106 DPoPKey: key, 107 } 108 109 session2 := &Session{ 110 DID: "did:plc:test456", 111 AccessToken: "token2", 112 DPoPKey: key, 113 } 114 115 // Store sessions with different IDs 116 id1 := GenerateSessionID() 117 id2 := GenerateSessionID() 118 119 store.Set(id1, session1) 120 store.Set(id2, session2) 121 122 // Verify sessions have different IDs 123 if id1 == id2 { 124 t.Error("Session IDs should be unique (session fixation risk)") 125 } 126 127 // Verify both can be retrieved 128 retrieved1, err := store.Get(id1) 129 if err != nil || retrieved1.DID != "did:plc:test123" { 130 t.Error("First session should be retrievable with its ID") 131 } 132 133 retrieved2, err := store.Get(id2) 134 if err != nil || retrieved2.DID != "did:plc:test456" { 135 t.Error("Second session should be retrievable with its ID") 136 } 137 }) 138 139 t.Run("old session IDs become invalid", func(t *testing.T) { 140 store := NewMemorySessionStore() 141 142 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 143 if err != nil { 144 t.Fatalf("Failed to generate key: %v", err) 145 } 146 147 // Create initial session 148 oldSessionID := GenerateSessionID() 149 session := &Session{ 150 DID: "did:plc:test123", 151 AccessToken: "old-token", 152 DPoPKey: key, 153 } 154 store.Set(oldSessionID, session) 155 156 // Simulate session regeneration (like after login) 157 newSessionID := GenerateSessionID() 158 store.Set(newSessionID, session) 159 160 // Delete old session ID 161 store.Delete(oldSessionID) 162 163 // Old session ID should no longer work 164 _, err = store.Get(oldSessionID) 165 if err == nil { 166 t.Error("Old session ID should be invalid after regeneration (session fixation prevention)") 167 } 168 169 // New session ID should work 170 retrieved, err := store.Get(newSessionID) 171 if err != nil { 172 t.Error("New session ID should be valid") 173 } 174 if retrieved.DID != "did:plc:test123" { 175 t.Error("Session data should be preserved with new ID") 176 } 177 }) 178} 179 180// TestSessionExpirationEnforcement verifies that expired sessions are properly 181// rejected at multiple layers (cookie, store, validation). 182func TestSessionExpirationEnforcement(t *testing.T) { 183 t.Run("cookie MaxAge enforces expiration", func(t *testing.T) { 184 // Test that cookie headers include MaxAge 185 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 186 http.SetCookie(w, &http.Cookie{ 187 Name: "session_id", 188 Value: "test-session", 189 Path: "/", 190 HttpOnly: true, 191 SameSite: http.SameSiteLaxMode, 192 MaxAge: 86400, // 24 hours 193 }) 194 w.WriteHeader(http.StatusOK) 195 }) 196 197 req := httptest.NewRequest("GET", "/", nil) 198 w := httptest.NewRecorder() 199 handler(w, req) 200 201 // Check cookie header 202 cookies := w.Result().Cookies() 203 if len(cookies) != 1 { 204 t.Fatal("Expected one cookie") 205 } 206 207 cookie := cookies[0] 208 if cookie.MaxAge != 86400 { 209 t.Errorf("Cookie MaxAge should be 86400 seconds, got %d", cookie.MaxAge) 210 } 211 }) 212 213 t.Run("session store TTL enforces expiration", func(t *testing.T) { 214 // Create store with short TTL 215 store := NewMemorySessionStoreWithTTL(100*time.Millisecond, 50*time.Millisecond) 216 defer store.Stop() 217 218 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 219 if err != nil { 220 t.Fatalf("Failed to generate key: %v", err) 221 } 222 223 // Store session 224 sessionID := GenerateSessionID() 225 session := &Session{ 226 DID: "did:plc:test123", 227 AccessToken: "token", 228 DPoPKey: key, 229 } 230 store.Set(sessionID, session) 231 232 // Should be valid immediately 233 _, err = store.Get(sessionID) 234 if err != nil { 235 t.Error("Session should be valid immediately after creation") 236 } 237 238 // Wait for expiration 239 time.Sleep(150 * time.Millisecond) 240 241 // Should be expired now 242 _, err = store.Get(sessionID) 243 if err == nil { 244 t.Error("Session should be expired after TTL") 245 } 246 }) 247 248 t.Run("expired sessions are rejected", func(t *testing.T) { 249 store := NewMemorySessionStoreWithTTL(50*time.Millisecond, 25*time.Millisecond) 250 defer store.Stop() 251 252 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 253 if err != nil { 254 t.Fatalf("Failed to generate key: %v", err) 255 } 256 257 // Create multiple sessions 258 for i := 0; i < 5; i++ { 259 sessionID := GenerateSessionID() 260 session := &Session{ 261 DID: "did:plc:test" + string(rune(i)), 262 AccessToken: "token" + string(rune(i)), 263 DPoPKey: key, 264 } 265 store.Set(sessionID, session) 266 } 267 268 // Wait for all to expire 269 time.Sleep(100 * time.Millisecond) 270 271 // Try to use expired session - should fail 272 expiredID := GenerateSessionID() 273 _, err = store.Get(expiredID) 274 if err == nil { 275 t.Error("Expired or non-existent session should be rejected") 276 } 277 }) 278 279 t.Run("cleanup goroutine removes expired sessions", func(t *testing.T) { 280 store := NewMemorySessionStoreWithTTL(50*time.Millisecond, 25*time.Millisecond) 281 defer store.Stop() 282 283 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 284 if err != nil { 285 t.Fatalf("Failed to generate key: %v", err) 286 } 287 288 // Add session 289 sessionID := GenerateSessionID() 290 session := &Session{ 291 DID: "did:plc:test123", 292 AccessToken: "token", 293 DPoPKey: key, 294 } 295 store.Set(sessionID, session) 296 297 // Wait for expiration and cleanup 298 time.Sleep(200 * time.Millisecond) 299 300 // Session should be cleaned up 301 _, err = store.Get(sessionID) 302 if err == nil { 303 t.Error("Cleanup should have removed expired session") 304 } 305 }) 306} 307 308// TestSessionConcurrentAccess verifies thread-safety of session operations 309// under concurrent access. 310func TestSessionConcurrentAccess(t *testing.T) { 311 t.Run("concurrent reads and writes are thread-safe", func(t *testing.T) { 312 store := NewMemorySessionStore() 313 314 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 315 if err != nil { 316 t.Fatalf("Failed to generate key: %v", err) 317 } 318 319 // Create initial session 320 sessionID := GenerateSessionID() 321 session := &Session{ 322 DID: "did:plc:test123", 323 AccessToken: "token", 324 DPoPKey: key, 325 } 326 store.Set(sessionID, session) 327 328 // Perform concurrent operations 329 var wg sync.WaitGroup 330 errors := make(chan error, 100) 331 332 // Concurrent reads 333 for i := 0; i < 50; i++ { 334 wg.Add(1) 335 go func() { 336 defer wg.Done() 337 _, err := store.Get(sessionID) 338 if err != nil { 339 errors <- err 340 } 341 }() 342 } 343 344 // Concurrent writes 345 for i := 0; i < 50; i++ { 346 wg.Add(1) 347 go func(idx int) { 348 defer wg.Done() 349 newSession := &Session{ 350 DID: "did:plc:test" + string(rune(idx)), 351 AccessToken: "token" + string(rune(idx)), 352 DPoPKey: key, 353 } 354 store.Set(sessionID, newSession) 355 }(i) 356 } 357 358 wg.Wait() 359 close(errors) 360 361 // Check for errors 362 for err := range errors { 363 t.Errorf("Concurrent access error: %v", err) 364 } 365 }) 366 367 t.Run("session updates during concurrent access", func(t *testing.T) { 368 store := NewMemorySessionStore() 369 370 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 371 if err != nil { 372 t.Fatalf("Failed to generate key: %v", err) 373 } 374 375 sessionID := GenerateSessionID() 376 numGoroutines := 100 377 var wg sync.WaitGroup 378 379 // Many goroutines updating the same session 380 for i := 0; i < numGoroutines; i++ { 381 wg.Add(1) 382 go func(idx int) { 383 defer wg.Done() 384 session := &Session{ 385 DID: "did:plc:updated", 386 AccessToken: "token" + string(rune(idx)), 387 DPoPKey: key, 388 } 389 store.Set(sessionID, session) 390 }(i) 391 } 392 393 wg.Wait() 394 395 // Session should still be retrievable 396 retrieved, err := store.Get(sessionID) 397 if err != nil { 398 t.Errorf("Session should be retrievable after concurrent updates: %v", err) 399 } 400 if retrieved.DID != "did:plc:updated" { 401 t.Error("Session should have been updated") 402 } 403 }) 404} 405 406// TestSessionCookieSecurityFlags verifies that session cookies have proper 407// security flags set to prevent various attacks. 408func TestSessionCookieSecurityFlags(t *testing.T) { 409 t.Run("HttpOnly flag prevents JavaScript access", func(t *testing.T) { 410 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 411 http.SetCookie(w, &http.Cookie{ 412 Name: "session_id", 413 Value: "test-session", 414 Path: "/", 415 HttpOnly: true, 416 SameSite: http.SameSiteLaxMode, 417 MaxAge: 86400, 418 }) 419 w.WriteHeader(http.StatusOK) 420 }) 421 422 req := httptest.NewRequest("GET", "/", nil) 423 w := httptest.NewRecorder() 424 handler(w, req) 425 426 cookies := w.Result().Cookies() 427 if len(cookies) != 1 { 428 t.Fatal("Expected one cookie") 429 } 430 431 if !cookies[0].HttpOnly { 432 t.Error("Cookie should have HttpOnly flag set (prevents XSS)") 433 } 434 }) 435 436 t.Run("Secure flag set for HTTPS", func(t *testing.T) { 437 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 438 // Detect HTTPS from URL or X-Forwarded-Proto header 439 isSecure := strings.HasPrefix(r.URL.String(), "https://") || 440 r.Header.Get("X-Forwarded-Proto") == "https" || 441 r.TLS != nil 442 443 http.SetCookie(w, &http.Cookie{ 444 Name: "session_id", 445 Value: "test-session", 446 Path: "/", 447 HttpOnly: true, 448 Secure: isSecure, 449 SameSite: http.SameSiteLaxMode, 450 MaxAge: 86400, 451 }) 452 w.WriteHeader(http.StatusOK) 453 }) 454 455 // Test with HTTPS 456 req := httptest.NewRequest("GET", "https://example.com/", nil) 457 req.Header.Set("X-Forwarded-Proto", "https") 458 w := httptest.NewRecorder() 459 handler(w, req) 460 461 cookies := w.Result().Cookies() 462 if len(cookies) != 1 { 463 t.Fatal("Expected one cookie") 464 } 465 466 if !cookies[0].Secure { 467 t.Error("Cookie should have Secure flag for HTTPS (prevents interception)") 468 } 469 }) 470 471 t.Run("SameSite flag prevents CSRF", func(t *testing.T) { 472 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 473 http.SetCookie(w, &http.Cookie{ 474 Name: "session_id", 475 Value: "test-session", 476 Path: "/", 477 HttpOnly: true, 478 SameSite: http.SameSiteLaxMode, 479 MaxAge: 86400, 480 }) 481 w.WriteHeader(http.StatusOK) 482 }) 483 484 req := httptest.NewRequest("GET", "/", nil) 485 w := httptest.NewRecorder() 486 handler(w, req) 487 488 cookies := w.Result().Cookies() 489 if len(cookies) != 1 { 490 t.Fatal("Expected one cookie") 491 } 492 493 if cookies[0].SameSite != http.SameSiteLaxMode { 494 t.Errorf("Cookie should have SameSite=Lax (prevents CSRF), got %v", cookies[0].SameSite) 495 } 496 }) 497 498 t.Run("localhost detection for development", func(t *testing.T) { 499 tests := []struct { 500 name string 501 host string 502 isLocalhost bool 503 }{ 504 {"localhost", "localhost:8080", true}, 505 {"127.0.0.1", "127.0.0.1:8080", true}, 506 {"IPv6 localhost", "[::1]:8080", true}, 507 {"production", "example.com", false}, 508 {"production with port", "example.com:443", false}, 509 } 510 511 for _, tt := range tests { 512 t.Run(tt.name, func(t *testing.T) { 513 host := tt.host 514 // Strip port for checking 515 if idx := strings.LastIndex(host, ":"); idx != -1 { 516 host = host[:idx] 517 } 518 // Remove IPv6 brackets 519 host = strings.TrimPrefix(host, "[") 520 host = strings.TrimSuffix(host, "]") 521 522 isLocal := host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "0.0.0.0" 523 524 if isLocal != tt.isLocalhost { 525 t.Errorf("Expected isLocalhost=%v for %s, got %v", tt.isLocalhost, tt.host, isLocal) 526 } 527 }) 528 } 529 }) 530 531 t.Run("cookie path prevents leakage", func(t *testing.T) { 532 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 533 http.SetCookie(w, &http.Cookie{ 534 Name: "session_id", 535 Value: "test-session", 536 Path: "/", 537 HttpOnly: true, 538 SameSite: http.SameSiteLaxMode, 539 MaxAge: 86400, 540 }) 541 w.WriteHeader(http.StatusOK) 542 }) 543 544 req := httptest.NewRequest("GET", "/", nil) 545 w := httptest.NewRecorder() 546 handler(w, req) 547 548 cookies := w.Result().Cookies() 549 if len(cookies) != 1 { 550 t.Fatal("Expected one cookie") 551 } 552 553 if cookies[0].Path != "/" { 554 t.Errorf("Cookie path should be '/', got '%s'", cookies[0].Path) 555 } 556 }) 557} 558 559// TestSessionStorageSecure tests that sessions stored contain appropriate data 560// and don't leak sensitive information. 561func TestSessionStorageSecure(t *testing.T) { 562 t.Run("sessions contain required fields", func(t *testing.T) { 563 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 564 if err != nil { 565 t.Fatalf("Failed to generate key: %v", err) 566 } 567 568 session := &Session{ 569 DID: "did:plc:test123", 570 AccessToken: "access-token", 571 RefreshToken: "refresh-token", 572 DPoPKey: key, 573 AccessTokenExpiresAt: time.Now().Add(12 * time.Hour), 574 } 575 576 // Verify all critical fields are present 577 if session.DID == "" { 578 t.Error("Session should have DID") 579 } 580 if session.AccessToken == "" { 581 t.Error("Session should have AccessToken") 582 } 583 if session.RefreshToken == "" { 584 t.Error("Session should have RefreshToken") 585 } 586 if session.DPoPKey == nil { 587 t.Error("Session should have DPoPKey") 588 } 589 if session.AccessTokenExpiresAt.IsZero() { 590 t.Error("Session should have AccessTokenExpiresAt") 591 } 592 }) 593 594 t.Run("session store operations are secure", func(t *testing.T) { 595 store := NewMemorySessionStore() 596 597 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 598 if err != nil { 599 t.Fatalf("Failed to generate key: %v", err) 600 } 601 602 sessionID := GenerateSessionID() 603 session := &Session{ 604 DID: "did:plc:test123", 605 AccessToken: "sensitive-token", 606 DPoPKey: key, 607 } 608 609 // Store session 610 store.Set(sessionID, session) 611 612 // Retrieve session 613 retrieved, err := store.Get(sessionID) 614 if err != nil { 615 t.Fatalf("Failed to retrieve session: %v", err) 616 } 617 618 // Verify data integrity 619 if retrieved.DID != session.DID { 620 t.Error("Session DID should match") 621 } 622 if retrieved.AccessToken != session.AccessToken { 623 t.Error("Session AccessToken should match") 624 } 625 626 // Delete session 627 store.Delete(sessionID) 628 629 // Verify deletion 630 _, err = store.Get(sessionID) 631 if err == nil { 632 t.Error("Deleted session should not be retrievable") 633 } 634 }) 635}