refactor: share handshake auth helper builders

This commit is contained in:
Peter Steinberger 2026-03-13 21:33:28 +00:00
parent 6ecc184637
commit 4674fbf923
2 changed files with 45 additions and 37 deletions

View File

@ -10,24 +10,21 @@ import {
shouldSkipBackendSelfPairing, shouldSkipBackendSelfPairing,
} from "./handshake-auth-helpers.js"; } 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", () => { describe("handshake auth helpers", () => {
it("pins browser-origin loopback clients to the synthetic rate-limit ip", () => { it("pins browser-origin loopback clients to the synthetic rate-limit ip", () => {
const rateLimiter: AuthRateLimiter = { const rateLimiter = createRateLimiter();
check: () => ({ allowed: true, remaining: 1, retryAfterMs: 0 }), const browserRateLimiter = createRateLimiter();
reset: () => {},
recordFailure: () => {},
size: () => 0,
prune: () => {},
dispose: () => {},
};
const browserRateLimiter: AuthRateLimiter = {
check: () => ({ allowed: true, remaining: 1, retryAfterMs: 0 }),
reset: () => {},
recordFailure: () => {},
size: () => 0,
prune: () => {},
dispose: () => {},
};
const resolved = resolveHandshakeBrowserSecurityContext({ const resolved = resolveHandshakeBrowserSecurityContext({
requestOrigin: "https://app.example", requestOrigin: "https://app.example",
clientIp: "127.0.0.1", clientIp: "127.0.0.1",

View File

@ -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: { export function resolveDeviceSignaturePayloadVersion(params: {
device: { device: {
id: string; id: string;
@ -104,7 +121,7 @@ export function resolveDeviceSignaturePayloadVersion(params: {
nonce: string; nonce: string;
}): "v3" | "v2" | null { }): "v3" | "v2" | null {
const signatureToken = resolveSignatureToken(params.connectParams); const signatureToken = resolveSignatureToken(params.connectParams);
const payloadV3 = buildDeviceAuthPayloadV3({ const basePayload = {
deviceId: params.device.id, deviceId: params.device.id,
clientId: params.connectParams.client.id, clientId: params.connectParams.client.id,
clientMode: params.connectParams.client.mode, clientMode: params.connectParams.client.mode,
@ -113,6 +130,9 @@ export function resolveDeviceSignaturePayloadVersion(params: {
signedAtMs: params.signedAtMs, signedAtMs: params.signedAtMs,
token: signatureToken, token: signatureToken,
nonce: params.nonce, nonce: params.nonce,
};
const payloadV3 = buildDeviceAuthPayloadV3({
...basePayload,
platform: params.connectParams.client.platform, platform: params.connectParams.client.platform,
deviceFamily: params.connectParams.client.deviceFamily, deviceFamily: params.connectParams.client.deviceFamily,
}); });
@ -120,16 +140,7 @@ export function resolveDeviceSignaturePayloadVersion(params: {
return "v3"; return "v3";
} }
const payloadV2 = buildDeviceAuthPayload({ const payloadV2 = buildDeviceAuthPayload(basePayload);
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,
});
if (verifyDeviceSignature(params.device.publicKey, payloadV2, params.device.signature)) { if (verifyDeviceSignature(params.device.publicKey, payloadV2, params.device.signature)) {
return "v2"; return "v2";
} }
@ -171,41 +182,41 @@ export function resolveUnauthorizedHandshakeContext(params: {
authProvided === "token" && authProvided === "token" &&
!params.connectAuth?.deviceToken; !params.connectAuth?.deviceToken;
if (canRetryWithDeviceToken) { if (canRetryWithDeviceToken) {
return { return buildUnauthorizedHandshakeContext({
authProvided, authProvided,
canRetryWithDeviceToken, canRetryWithDeviceToken,
recommendedNextStep: "retry_with_device_token", recommendedNextStep: "retry_with_device_token",
}; });
} }
switch (params.failedAuth.reason) { switch (params.failedAuth.reason) {
case "token_missing": case "token_missing":
case "token_missing_config": case "token_missing_config":
case "password_missing": case "password_missing":
case "password_missing_config": case "password_missing_config":
return { return buildUnauthorizedHandshakeContext({
authProvided, authProvided,
canRetryWithDeviceToken, canRetryWithDeviceToken,
recommendedNextStep: "update_auth_configuration", recommendedNextStep: "update_auth_configuration",
}; });
case "token_mismatch": case "token_mismatch":
case "password_mismatch": case "password_mismatch":
case "device_token_mismatch": case "device_token_mismatch":
return { return buildUnauthorizedHandshakeContext({
authProvided, authProvided,
canRetryWithDeviceToken, canRetryWithDeviceToken,
recommendedNextStep: "update_auth_credentials", recommendedNextStep: "update_auth_credentials",
}; });
case "rate_limited": case "rate_limited":
return { return buildUnauthorizedHandshakeContext({
authProvided, authProvided,
canRetryWithDeviceToken, canRetryWithDeviceToken,
recommendedNextStep: "wait_then_retry", recommendedNextStep: "wait_then_retry",
}; });
default: default:
return { return buildUnauthorizedHandshakeContext({
authProvided, authProvided,
canRetryWithDeviceToken, canRetryWithDeviceToken,
recommendedNextStep: "review_auth_configuration", recommendedNextStep: "review_auth_configuration",
}; });
} }
} }