package auth import ( "errors" "testing" "time" "github.com/google/uuid" ) const scannerTestSecret = "scanner-secret-must-be-at-least-32-bytes-yy" func TestScannerJWT_RoundTrip(t *testing.T) { s, err := NewScannerJWTSigner(scannerTestSecret, "test-issuer", 4*time.Hour) if err != nil { t.Fatalf("new signer: %v", err) } userID := uuid.New() eventID := uuid.New() now := time.Now().UTC() tok, exp, err := s.Issue(userID, eventID, now) if err != nil { t.Fatalf("issue: %v", err) } if !exp.After(now) { t.Fatalf("expiry should be after now: exp=%v now=%v", exp, now) } got, err := s.Parse(tok) if err != nil { t.Fatalf("parse: %v", err) } if got.UserID != userID || got.EventID != eventID { t.Errorf("claims mismatch: got user=%v event=%v want user=%v event=%v", got.UserID, got.EventID, userID, eventID) } } func TestScannerJWT_Expired(t *testing.T) { s, _ := NewScannerJWTSigner(scannerTestSecret, "test-issuer", time.Second) // Issue with `now` 5 minutes in the past; the +1s TTL has long since // elapsed against the actual wall clock. pastNow := time.Now().UTC().Add(-5 * time.Minute) tok, _, err := s.Issue(uuid.New(), uuid.New(), pastNow) if err != nil { t.Fatalf("issue: %v", err) } if _, err := s.Parse(tok); !errors.Is(err, ErrExpiredJWT) { t.Errorf("parse expired: want ErrExpiredJWT, got %v", err) } } // A scanner JWT must not be acceptable as a session access token even // though both are HS256-signed with the same secret. The audience // constraint on the session signer is responsible for keeping them // apart; this test guards against accidental regressions. func TestScannerJWT_RejectedBySessionParser(t *testing.T) { const issuer = "guestguard-test" scannerSigner, _ := NewScannerJWTSigner(scannerTestSecret, issuer, time.Hour) sessionSigner, err := NewJWTSigner(scannerTestSecret, time.Hour, issuer) if err != nil { t.Fatalf("new session signer: %v", err) } tok, _, err := scannerSigner.Issue(uuid.New(), uuid.New(), time.Now().UTC()) if err != nil { t.Fatalf("issue scanner: %v", err) } // The session parser doesn't enforce audience itself, so Parse() will // succeed and return claims with the "scanner" audience populated. // Middleware (requireAuth) is what then rejects the audience — so the // guarantee we want here is that the audience claim survives parsing // so that check can fire. claims, err := sessionSigner.Parse(tok) if err != nil { t.Fatalf("session parser unexpectedly errored on a scanner JWT: %v", err) } hasScannerAud := false for _, aud := range claims.Audience { if aud == ScannerJWTAudience { hasScannerAud = true } } if !hasScannerAud { t.Errorf("scanner JWT lost its audience after session-parser parse — middleware can no longer reject it. got audience %v", claims.Audience) } } // And the reverse: a normal session token (no audience) must not be // accepted by the scanner parser, because the scanner parser pins // audience=scanner. Otherwise a stolen session token could be turned // into a fake scanner ticket. func TestScannerJWT_RejectsSessionToken(t *testing.T) { const issuer = "guestguard-test" sessionSigner, _ := NewJWTSigner(scannerTestSecret, time.Hour, issuer) scannerSigner, _ := NewScannerJWTSigner(scannerTestSecret, issuer, time.Hour) tok, _, err := sessionSigner.Issue(uuid.New(), time.Now().UTC()) if err != nil { t.Fatalf("issue session: %v", err) } if _, err := scannerSigner.Parse(tok); !errors.Is(err, ErrInvalidJWT) { t.Errorf("scanner parser must reject session token: got %v", err) } } func TestScannerJWT_SecretTooShort(t *testing.T) { if _, err := NewScannerJWTSigner("short", "issuer", time.Hour); err == nil { t.Errorf("expected too-short-secret error") } }