فهرست منبع

Fix streaming URL token preservation across reloads (#3212)

maziggy 19 ساعت پیش
والد
کامیت
f9449ed3c3
2فایلهای تغییر یافته به همراه62 افزوده شده و 1 حذف شده
  1. 58 0
      frontend/src/__tests__/contexts/AuthContext.test.tsx
  2. 4 1
      frontend/src/contexts/AuthContext.tsx

+ 58 - 0
frontend/src/__tests__/contexts/AuthContext.test.tsx

@@ -39,6 +39,64 @@ function createWrapper() {
 }
 
 describe('AuthContext', () => {
+  describe('streaming URL tokens', () => {
+    beforeEach(() => {
+      setAuthToken(null);
+      server.use(http.get('/api/v1/auth/status', () =>
+        HttpResponse.json({ auth_enabled: true, requires_setup: false })
+      ));
+    });
+
+    afterEach(() => {
+      window.history.replaceState({}, '', '/');
+      setAuthToken(null);
+    });
+
+    it.each(['/overlay/2', '/overlay/2/', '/camwall', '/camwall/'])(
+      'preserves the scoped token across remounts on %s', async (path) => {
+        const url = `${path}?artwork=2&token=streaming-token#preview`;
+        window.history.replaceState({}, '', url);
+        const authMe = vi.fn(() => HttpResponse.json({}, { status: 401 }));
+        server.use(http.get('/api/v1/auth/me', authMe));
+        for (let load = 0; load < 2; load++) {
+          const { result, unmount } = renderHook(() => useAuth(), { wrapper: createWrapper() });
+          await waitFor(() => expect(result.current.loading).toBe(false));
+          expect(window.location.pathname + window.location.search + window.location.hash).toBe(url);
+          expect(getAuthToken()).toBeNull();
+          expect(sessionStorage.getItem('auth_token')).toBeNull();
+          unmount();
+        }
+        expect(authMe).not.toHaveBeenCalled();
+      }
+    );
+
+    it.each(['/overlay/2', '/camwall'])('does not replace an existing login on %s', async (path) => {
+      setAuthToken('valid-token', 'persistent');
+      window.history.replaceState({}, '', `${path}?token=streaming-token`);
+      server.use(http.get('/api/v1/auth/me', () => HttpResponse.json({
+        id: 1, username: 'alice', is_active: true, permissions: [], groups: [],
+      })));
+      const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
+      await waitFor(() => expect(result.current.loading).toBe(false));
+      expect(getAuthToken()).toBe('valid-token');
+      expect(sessionStorage.getItem('auth_token')).toBe('valid-token');
+      expect(result.current.user?.username).toBe('alice');
+    });
+
+    it('still bootstraps and removes a SpoolBuddy login token', async () => {
+      window.history.replaceState({}, '', '/spoolbuddy?token=kiosk-api-key&view=ams#top');
+      server.use(http.get('/api/v1/auth/me', () => HttpResponse.json({
+        id: 1, username: 'kiosk', is_active: true, permissions: [], groups: [],
+      })));
+      const { result } = renderHook(() => useAuth(), { wrapper: createWrapper() });
+      await waitFor(() => expect(result.current.loading).toBe(false));
+      expect(getAuthToken()).toBe('kiosk-api-key');
+      expect(localStorage.setItem).toHaveBeenCalledWith('auth_token', 'kiosk-api-key');
+      expect(window.location.search).toBe('?view=ams');
+      expect(window.location.hash).toBe('#top');
+    });
+  });
+
   describe('when auth is disabled', () => {
     beforeEach(() => {
       server.use(

+ 4 - 1
frontend/src/contexts/AuthContext.tsx

@@ -39,7 +39,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
       // token has been verified by the server (L-4: prevents session fixation
       // where an attacker-crafted URL immediately persists a forged/stolen token).
       const urlParams = new URLSearchParams(window.location.search);
-      const urlToken = urlParams.get('token');
+      // Streaming pages own their scoped URL tokens. Consuming one as a login
+      // token removes authentication on reload and can overwrite a user session.
+      const isStreamingPage = /^\/(?:overlay\/[^/]+|camwall)\/?$/i.test(window.location.pathname);
+      const urlToken = isStreamingPage ? null : urlParams.get('token');
       if (urlToken) {
         setAuthToken(urlToken, 'session'); // session-only until server confirms it's valid
         urlParams.delete('token');