diff --git a/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts b/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts index 68ec4e1a153..8b7b7e521fd 100644 --- a/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts +++ b/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts @@ -10,24 +10,21 @@ import { shouldSkipBackendSelfPairing, } from "./handshake-auth-helpers.js"; +function createRateLimiter(): AuthRateLimiter { + return { + check: () => ({ allowed: true, remaining: 1, retryAfterMs: 0 }), + reset: () => {}, + recordFailure: () => {}, + size: () => 0, + prune: () => {}, + dispose: () => {}, + }; +} + describe("handshake auth helpers", () => { it("pins browser-origin loopback clients to the synthetic rate-limit ip", () => { - const rateLimiter: AuthRateLimiter = { - check: () => ({ allowed: true, remaining: 1, retryAfterMs: 0 }), - reset: () => {}, - recordFailure: () => {}, - size: () => 0, - prune: () => {}, - dispose: () => {}, - }; - const browserRateLimiter: AuthRateLimiter = { - check: () => ({ allowed: true, remaining: 1, retryAfterMs: 0 }), - reset: () => {}, - recordFailure: () => {}, - size: () => 0, - prune: () => {}, - dispose: () => {}, - }; + const rateLimiter = createRateLimiter(); + const browserRateLimiter = createRateLimiter(); const resolved = resolveHandshakeBrowserSecurityContext({ requestOrigin: "https://app.example", clientIp: "127.0.0.1", diff --git a/src/gateway/server/ws-connection/handshake-auth-helpers.ts b/src/gateway/server/ws-connection/handshake-auth-helpers.ts index cce5b979b3e..8529cf55082 100644 --- a/src/gateway/server/ws-connection/handshake-auth-helpers.ts +++ b/src/gateway/server/ws-connection/handshake-auth-helpers.ts @@ -91,6 +91,23 @@ function resolveSignatureToken(connectParams: ConnectParams): string | null { ); } +function buildUnauthorizedHandshakeContext(params: { + authProvided: AuthProvidedKind; + canRetryWithDeviceToken: boolean; + recommendedNextStep: + | "retry_with_device_token" + | "update_auth_configuration" + | "update_auth_credentials" + | "wait_then_retry" + | "review_auth_configuration"; +}) { + return { + authProvided: params.authProvided, + canRetryWithDeviceToken: params.canRetryWithDeviceToken, + recommendedNextStep: params.recommendedNextStep, + }; +} + export function resolveDeviceSignaturePayloadVersion(params: { device: { id: string; @@ -104,7 +121,7 @@ export function resolveDeviceSignaturePayloadVersion(params: { nonce: string; }): "v3" | "v2" | null { const signatureToken = resolveSignatureToken(params.connectParams); - const payloadV3 = buildDeviceAuthPayloadV3({ + const basePayload = { deviceId: params.device.id, clientId: params.connectParams.client.id, clientMode: params.connectParams.client.mode, @@ -113,6 +130,9 @@ export function resolveDeviceSignaturePayloadVersion(params: { signedAtMs: params.signedAtMs, token: signatureToken, nonce: params.nonce, + }; + const payloadV3 = buildDeviceAuthPayloadV3({ + ...basePayload, platform: params.connectParams.client.platform, deviceFamily: params.connectParams.client.deviceFamily, }); @@ -120,16 +140,7 @@ export function resolveDeviceSignaturePayloadVersion(params: { return "v3"; } - const payloadV2 = buildDeviceAuthPayload({ - deviceId: params.device.id, - clientId: params.connectParams.client.id, - clientMode: params.connectParams.client.mode, - role: params.role, - scopes: params.scopes, - signedAtMs: params.signedAtMs, - token: signatureToken, - nonce: params.nonce, - }); + const payloadV2 = buildDeviceAuthPayload(basePayload); if (verifyDeviceSignature(params.device.publicKey, payloadV2, params.device.signature)) { return "v2"; } @@ -171,41 +182,41 @@ export function resolveUnauthorizedHandshakeContext(params: { authProvided === "token" && !params.connectAuth?.deviceToken; if (canRetryWithDeviceToken) { - return { + return buildUnauthorizedHandshakeContext({ authProvided, canRetryWithDeviceToken, recommendedNextStep: "retry_with_device_token", - }; + }); } switch (params.failedAuth.reason) { case "token_missing": case "token_missing_config": case "password_missing": case "password_missing_config": - return { + return buildUnauthorizedHandshakeContext({ authProvided, canRetryWithDeviceToken, recommendedNextStep: "update_auth_configuration", - }; + }); case "token_mismatch": case "password_mismatch": case "device_token_mismatch": - return { + return buildUnauthorizedHandshakeContext({ authProvided, canRetryWithDeviceToken, recommendedNextStep: "update_auth_credentials", - }; + }); case "rate_limited": - return { + return buildUnauthorizedHandshakeContext({ authProvided, canRetryWithDeviceToken, recommendedNextStep: "wait_then_retry", - }; + }); default: - return { + return buildUnauthorizedHandshakeContext({ authProvided, canRetryWithDeviceToken, recommendedNextStep: "review_auth_configuration", - }; + }); } }