Merge pull request #22 from b4arena/fix/password-mode-device-signing

fix: exclude token from device signing payload in password auth mode
This commit is contained in:
MarlburroW
2026-03-09 22:00:24 +01:00
committed by GitHub
2 changed files with 48 additions and 1 deletions

View File

@@ -1,5 +1,12 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { GatewayClient } from '../gateway';
import type { DeviceIdentity } from '../deviceIdentity';
import * as deviceIdentityModule from '../deviceIdentity';
vi.mock('../deviceIdentity', () => ({
buildDeviceAuthPayload: vi.fn(),
signPayload: vi.fn(),
}));
/* ------------------------------------------------------------------ */
/* Minimal WebSocket mock */
@@ -50,6 +57,7 @@ beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).WebSocket = MockWebSocket;
vi.useFakeTimers();
vi.clearAllMocks();
});
afterEach(() => {
@@ -297,6 +305,45 @@ describe('GatewayClient', () => {
gw.disconnect();
});
it('password mode with deviceIdentity: signs with token:null and sends auth.password', async () => {
const buildPayload = vi.mocked(deviceIdentityModule.buildDeviceAuthPayload);
const sign = vi.mocked(deviceIdentityModule.signPayload);
buildPayload.mockReturnValue('mock-device-payload');
sign.mockResolvedValue('mock-sig');
// In password mode authToken holds the password string, not a JWT/token.
// buildDeviceAuthPayload must receive token:null so the gateway signature
// verification matches (gateway sees no token segment in the connect request).
const gw = new GatewayClient('ws://test:1234', 'my-secret-password', 'password');
const mockIdentity: DeviceIdentity = {
id: 'device-id-abc',
publicKeyRaw: 'pubkey-raw-abc',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
keyPair: { privateKey: {} as CryptoKey, publicKey: {} as CryptoKey },
};
gw.setDeviceIdentity(mockIdentity);
gw.connect();
await vi.advanceTimersByTimeAsync(1);
const ws = MockWebSocket.instances[0]!;
ws._receive({ type: 'event', event: 'connect.challenge', payload: { nonce: 'nonce-xyz' } });
await vi.advanceTimersByTimeAsync(0);
// buildDeviceAuthPayload must be called with token: null (not the password)
expect(buildPayload).toHaveBeenCalledWith(
expect.objectContaining({ token: null }),
);
// The connect request must use auth.password, not auth.token
const req = JSON.parse(ws.sent[0]!);
expect(req.method).toBe('connect');
expect(req.params.auth.password).toBe('my-secret-password');
expect(req.params.auth.token).toBeUndefined();
gw.disconnect();
});
it('emits pairing status on NOT_PAIRED error', async () => {
const gw = new GatewayClient('ws://test:1234', 'tok');
const statuses: string[] = [];

View File

@@ -137,7 +137,7 @@ export class GatewayClient {
role,
scopes,
signedAtMs,
token: this.authToken || null,
token: this.authMode === 'password' ? null : (this.authToken || null),
nonce,
});
const signature = await signPayload(this.deviceIdentity.keyPair.privateKey, payload);