Vibe-guided bskyoauth and custom repo example code in Golang 馃 probably not safe to use in prod
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}