refactor: extract websocket handshake auth helpers
This commit is contained in:
122
src/gateway/server/ws-connection/handshake-auth-helpers.test.ts
Normal file
122
src/gateway/server/ws-connection/handshake-auth-helpers.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AuthRateLimiter } from "../../auth-rate-limit.js";
|
||||
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../protocol/client-info.js";
|
||||
import type { ConnectParams } from "../../protocol/index.js";
|
||||
import {
|
||||
BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP,
|
||||
resolveHandshakeBrowserSecurityContext,
|
||||
resolveUnauthorizedHandshakeContext,
|
||||
shouldAllowSilentLocalPairing,
|
||||
shouldSkipBackendSelfPairing,
|
||||
} from "./handshake-auth-helpers.js";
|
||||
|
||||
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 resolved = resolveHandshakeBrowserSecurityContext({
|
||||
requestOrigin: "https://app.example",
|
||||
clientIp: "127.0.0.1",
|
||||
rateLimiter,
|
||||
browserRateLimiter,
|
||||
});
|
||||
|
||||
expect(resolved).toMatchObject({
|
||||
hasBrowserOriginHeader: true,
|
||||
enforceOriginCheckForAnyClient: true,
|
||||
rateLimitClientIp: BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP,
|
||||
authRateLimiter: browserRateLimiter,
|
||||
});
|
||||
});
|
||||
|
||||
it("recommends device-token retry only for shared-token mismatch with device identity", () => {
|
||||
const resolved = resolveUnauthorizedHandshakeContext({
|
||||
connectAuth: { token: "shared-token" },
|
||||
failedAuth: { ok: false, reason: "token_mismatch" },
|
||||
hasDeviceIdentity: true,
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
authProvided: "token",
|
||||
canRetryWithDeviceToken: true,
|
||||
recommendedNextStep: "retry_with_device_token",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats explicit device-token mismatch as credential update guidance", () => {
|
||||
const resolved = resolveUnauthorizedHandshakeContext({
|
||||
connectAuth: { deviceToken: "device-token" },
|
||||
failedAuth: { ok: false, reason: "device_token_mismatch" },
|
||||
hasDeviceIdentity: true,
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
authProvided: "device-token",
|
||||
canRetryWithDeviceToken: false,
|
||||
recommendedNextStep: "update_auth_credentials",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows silent local pairing only for not-paired and scope upgrades", () => {
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
isLocalClient: true,
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "not-paired",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAllowSilentLocalPairing({
|
||||
isLocalClient: true,
|
||||
hasBrowserOriginHeader: false,
|
||||
isControlUi: false,
|
||||
isWebchat: false,
|
||||
reason: "metadata-upgrade",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("skips backend self-pairing only for local shared-secret backend clients", () => {
|
||||
const connectParams = {
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT,
|
||||
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||
},
|
||||
} as ConnectParams;
|
||||
|
||||
expect(
|
||||
shouldSkipBackendSelfPairing({
|
||||
connectParams,
|
||||
isLocalClient: true,
|
||||
hasBrowserOriginHeader: false,
|
||||
sharedAuthOk: true,
|
||||
authMethod: "token",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldSkipBackendSelfPairing({
|
||||
connectParams,
|
||||
isLocalClient: false,
|
||||
hasBrowserOriginHeader: false,
|
||||
sharedAuthOk: true,
|
||||
authMethod: "token",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
211
src/gateway/server/ws-connection/handshake-auth-helpers.ts
Normal file
211
src/gateway/server/ws-connection/handshake-auth-helpers.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { verifyDeviceSignature } from "../../../infra/device-identity.js";
|
||||
import type { AuthRateLimiter } from "../../auth-rate-limit.js";
|
||||
import type { GatewayAuthResult } from "../../auth.js";
|
||||
import { buildDeviceAuthPayload, buildDeviceAuthPayloadV3 } from "../../device-auth.js";
|
||||
import { isLoopbackAddress } from "../../net.js";
|
||||
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../protocol/client-info.js";
|
||||
import type { ConnectParams } from "../../protocol/index.js";
|
||||
import type { AuthProvidedKind } from "./auth-messages.js";
|
||||
|
||||
export const BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP = "198.18.0.1";
|
||||
|
||||
export type HandshakeBrowserSecurityContext = {
|
||||
hasBrowserOriginHeader: boolean;
|
||||
enforceOriginCheckForAnyClient: boolean;
|
||||
rateLimitClientIp: string | undefined;
|
||||
authRateLimiter?: AuthRateLimiter;
|
||||
};
|
||||
|
||||
type HandshakeConnectAuth = {
|
||||
token?: string;
|
||||
bootstrapToken?: string;
|
||||
deviceToken?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export function resolveHandshakeBrowserSecurityContext(params: {
|
||||
requestOrigin?: string;
|
||||
clientIp: string | undefined;
|
||||
rateLimiter?: AuthRateLimiter;
|
||||
browserRateLimiter?: AuthRateLimiter;
|
||||
}): HandshakeBrowserSecurityContext {
|
||||
const hasBrowserOriginHeader = Boolean(
|
||||
params.requestOrigin && params.requestOrigin.trim() !== "",
|
||||
);
|
||||
return {
|
||||
hasBrowserOriginHeader,
|
||||
enforceOriginCheckForAnyClient: hasBrowserOriginHeader,
|
||||
rateLimitClientIp:
|
||||
hasBrowserOriginHeader && isLoopbackAddress(params.clientIp)
|
||||
? BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP
|
||||
: params.clientIp,
|
||||
authRateLimiter:
|
||||
hasBrowserOriginHeader && params.browserRateLimiter
|
||||
? params.browserRateLimiter
|
||||
: params.rateLimiter,
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldAllowSilentLocalPairing(params: {
|
||||
isLocalClient: boolean;
|
||||
hasBrowserOriginHeader: boolean;
|
||||
isControlUi: boolean;
|
||||
isWebchat: boolean;
|
||||
reason: "not-paired" | "role-upgrade" | "scope-upgrade" | "metadata-upgrade";
|
||||
}): boolean {
|
||||
return (
|
||||
params.isLocalClient &&
|
||||
(!params.hasBrowserOriginHeader || params.isControlUi || params.isWebchat) &&
|
||||
(params.reason === "not-paired" || params.reason === "scope-upgrade")
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldSkipBackendSelfPairing(params: {
|
||||
connectParams: ConnectParams;
|
||||
isLocalClient: boolean;
|
||||
hasBrowserOriginHeader: boolean;
|
||||
sharedAuthOk: boolean;
|
||||
authMethod: GatewayAuthResult["method"];
|
||||
}): boolean {
|
||||
const isGatewayBackendClient =
|
||||
params.connectParams.client.id === GATEWAY_CLIENT_IDS.GATEWAY_CLIENT &&
|
||||
params.connectParams.client.mode === GATEWAY_CLIENT_MODES.BACKEND;
|
||||
if (!isGatewayBackendClient) {
|
||||
return false;
|
||||
}
|
||||
const usesSharedSecretAuth = params.authMethod === "token" || params.authMethod === "password";
|
||||
return (
|
||||
params.isLocalClient &&
|
||||
!params.hasBrowserOriginHeader &&
|
||||
params.sharedAuthOk &&
|
||||
usesSharedSecretAuth
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSignatureToken(connectParams: ConnectParams): string | null {
|
||||
return (
|
||||
connectParams.auth?.token ??
|
||||
connectParams.auth?.deviceToken ??
|
||||
connectParams.auth?.bootstrapToken ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveDeviceSignaturePayloadVersion(params: {
|
||||
device: {
|
||||
id: string;
|
||||
signature: string;
|
||||
publicKey: string;
|
||||
};
|
||||
connectParams: ConnectParams;
|
||||
role: string;
|
||||
scopes: string[];
|
||||
signedAtMs: number;
|
||||
nonce: string;
|
||||
}): "v3" | "v2" | null {
|
||||
const signatureToken = resolveSignatureToken(params.connectParams);
|
||||
const payloadV3 = buildDeviceAuthPayloadV3({
|
||||
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,
|
||||
platform: params.connectParams.client.platform,
|
||||
deviceFamily: params.connectParams.client.deviceFamily,
|
||||
});
|
||||
if (verifyDeviceSignature(params.device.publicKey, payloadV3, params.device.signature)) {
|
||||
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,
|
||||
});
|
||||
if (verifyDeviceSignature(params.device.publicKey, payloadV2, params.device.signature)) {
|
||||
return "v2";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveAuthProvidedKind(
|
||||
connectAuth: HandshakeConnectAuth | null | undefined,
|
||||
): AuthProvidedKind {
|
||||
return connectAuth?.password
|
||||
? "password"
|
||||
: connectAuth?.token
|
||||
? "token"
|
||||
: connectAuth?.bootstrapToken
|
||||
? "bootstrap-token"
|
||||
: connectAuth?.deviceToken
|
||||
? "device-token"
|
||||
: "none";
|
||||
}
|
||||
|
||||
export function resolveUnauthorizedHandshakeContext(params: {
|
||||
connectAuth: HandshakeConnectAuth | null | undefined;
|
||||
failedAuth: GatewayAuthResult;
|
||||
hasDeviceIdentity: boolean;
|
||||
}): {
|
||||
authProvided: AuthProvidedKind;
|
||||
canRetryWithDeviceToken: boolean;
|
||||
recommendedNextStep:
|
||||
| "retry_with_device_token"
|
||||
| "update_auth_configuration"
|
||||
| "update_auth_credentials"
|
||||
| "wait_then_retry"
|
||||
| "review_auth_configuration";
|
||||
} {
|
||||
const authProvided = resolveAuthProvidedKind(params.connectAuth);
|
||||
const canRetryWithDeviceToken =
|
||||
params.failedAuth.reason === "token_mismatch" &&
|
||||
params.hasDeviceIdentity &&
|
||||
authProvided === "token" &&
|
||||
!params.connectAuth?.deviceToken;
|
||||
if (canRetryWithDeviceToken) {
|
||||
return {
|
||||
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 {
|
||||
authProvided,
|
||||
canRetryWithDeviceToken,
|
||||
recommendedNextStep: "update_auth_configuration",
|
||||
};
|
||||
case "token_mismatch":
|
||||
case "password_mismatch":
|
||||
case "device_token_mismatch":
|
||||
return {
|
||||
authProvided,
|
||||
canRetryWithDeviceToken,
|
||||
recommendedNextStep: "update_auth_credentials",
|
||||
};
|
||||
case "rate_limited":
|
||||
return {
|
||||
authProvided,
|
||||
canRetryWithDeviceToken,
|
||||
recommendedNextStep: "wait_then_retry",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
authProvided,
|
||||
canRetryWithDeviceToken,
|
||||
recommendedNextStep: "review_auth_configuration",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { verifyDeviceBootstrapToken } from "../../../infra/device-bootstrap.js";
|
||||
import {
|
||||
deriveDeviceIdFromPublicKey,
|
||||
normalizeDevicePublicKeyBase64Url,
|
||||
verifyDeviceSignature,
|
||||
} from "../../../infra/device-identity.js";
|
||||
import {
|
||||
approveDevicePairing,
|
||||
@@ -33,11 +32,7 @@ import {
|
||||
CANVAS_CAPABILITY_TTL_MS,
|
||||
mintCanvasCapabilityToken,
|
||||
} from "../../canvas-capability.js";
|
||||
import {
|
||||
buildDeviceAuthPayload,
|
||||
buildDeviceAuthPayloadV3,
|
||||
normalizeDeviceMetadataForAuth,
|
||||
} from "../../device-auth.js";
|
||||
import { normalizeDeviceMetadataForAuth } from "../../device-auth.js";
|
||||
import {
|
||||
isLocalishHost,
|
||||
isLoopbackAddress,
|
||||
@@ -46,7 +41,7 @@ import {
|
||||
} from "../../net.js";
|
||||
import { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
|
||||
import { checkBrowserOrigin } from "../../origin-check.js";
|
||||
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../protocol/client-info.js";
|
||||
import { GATEWAY_CLIENT_IDS } from "../../protocol/client-info.js";
|
||||
import {
|
||||
ConnectErrorDetailCodes,
|
||||
resolveDeviceAuthConnectErrorDetailCode,
|
||||
@@ -83,143 +78,30 @@ import {
|
||||
} from "../health-state.js";
|
||||
import type { GatewayWsClient } from "../ws-types.js";
|
||||
import { resolveConnectAuthDecision, resolveConnectAuthState } from "./auth-context.js";
|
||||
import { formatGatewayAuthFailureMessage, type AuthProvidedKind } from "./auth-messages.js";
|
||||
import { formatGatewayAuthFailureMessage } from "./auth-messages.js";
|
||||
import {
|
||||
evaluateMissingDeviceIdentity,
|
||||
isTrustedProxyControlUiOperatorAuth,
|
||||
resolveControlUiAuthPolicy,
|
||||
shouldSkipControlUiPairing,
|
||||
} from "./connect-policy.js";
|
||||
import {
|
||||
resolveDeviceSignaturePayloadVersion,
|
||||
resolveHandshakeBrowserSecurityContext,
|
||||
resolveUnauthorizedHandshakeContext,
|
||||
shouldAllowSilentLocalPairing,
|
||||
shouldSkipBackendSelfPairing,
|
||||
} from "./handshake-auth-helpers.js";
|
||||
import { isUnauthorizedRoleError, UnauthorizedFloodGuard } from "./unauthorized-flood-guard.js";
|
||||
|
||||
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
|
||||
|
||||
const DEVICE_SIGNATURE_SKEW_MS = 2 * 60 * 1000;
|
||||
const BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP = "198.18.0.1";
|
||||
|
||||
export type WsOriginCheckMetrics = {
|
||||
hostHeaderFallbackAccepted: number;
|
||||
};
|
||||
|
||||
type HandshakeBrowserSecurityContext = {
|
||||
hasBrowserOriginHeader: boolean;
|
||||
enforceOriginCheckForAnyClient: boolean;
|
||||
rateLimitClientIp: string | undefined;
|
||||
authRateLimiter?: AuthRateLimiter;
|
||||
};
|
||||
|
||||
function resolveHandshakeBrowserSecurityContext(params: {
|
||||
requestOrigin?: string;
|
||||
hasProxyHeaders: boolean;
|
||||
clientIp: string | undefined;
|
||||
rateLimiter?: AuthRateLimiter;
|
||||
browserRateLimiter?: AuthRateLimiter;
|
||||
}): HandshakeBrowserSecurityContext {
|
||||
const hasBrowserOriginHeader = Boolean(
|
||||
params.requestOrigin && params.requestOrigin.trim() !== "",
|
||||
);
|
||||
return {
|
||||
hasBrowserOriginHeader,
|
||||
enforceOriginCheckForAnyClient: hasBrowserOriginHeader,
|
||||
rateLimitClientIp:
|
||||
hasBrowserOriginHeader && isLoopbackAddress(params.clientIp)
|
||||
? BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP
|
||||
: params.clientIp,
|
||||
authRateLimiter:
|
||||
hasBrowserOriginHeader && params.browserRateLimiter
|
||||
? params.browserRateLimiter
|
||||
: params.rateLimiter,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldAllowSilentLocalPairing(params: {
|
||||
isLocalClient: boolean;
|
||||
hasBrowserOriginHeader: boolean;
|
||||
isControlUi: boolean;
|
||||
isWebchat: boolean;
|
||||
reason: "not-paired" | "role-upgrade" | "scope-upgrade" | "metadata-upgrade";
|
||||
}): boolean {
|
||||
return (
|
||||
params.isLocalClient &&
|
||||
(!params.hasBrowserOriginHeader || params.isControlUi || params.isWebchat) &&
|
||||
(params.reason === "not-paired" || params.reason === "scope-upgrade")
|
||||
);
|
||||
}
|
||||
|
||||
function shouldSkipBackendSelfPairing(params: {
|
||||
connectParams: ConnectParams;
|
||||
isLocalClient: boolean;
|
||||
hasBrowserOriginHeader: boolean;
|
||||
sharedAuthOk: boolean;
|
||||
authMethod: GatewayAuthResult["method"];
|
||||
}): boolean {
|
||||
const isGatewayBackendClient =
|
||||
params.connectParams.client.id === GATEWAY_CLIENT_IDS.GATEWAY_CLIENT &&
|
||||
params.connectParams.client.mode === GATEWAY_CLIENT_MODES.BACKEND;
|
||||
if (!isGatewayBackendClient) {
|
||||
return false;
|
||||
}
|
||||
const usesSharedSecretAuth = params.authMethod === "token" || params.authMethod === "password";
|
||||
return (
|
||||
params.isLocalClient &&
|
||||
!params.hasBrowserOriginHeader &&
|
||||
params.sharedAuthOk &&
|
||||
usesSharedSecretAuth
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDeviceSignaturePayloadVersion(params: {
|
||||
device: {
|
||||
id: string;
|
||||
signature: string;
|
||||
publicKey: string;
|
||||
};
|
||||
connectParams: ConnectParams;
|
||||
role: string;
|
||||
scopes: string[];
|
||||
signedAtMs: number;
|
||||
nonce: string;
|
||||
}): "v3" | "v2" | null {
|
||||
const payloadV3 = buildDeviceAuthPayloadV3({
|
||||
deviceId: params.device.id,
|
||||
clientId: params.connectParams.client.id,
|
||||
clientMode: params.connectParams.client.mode,
|
||||
role: params.role,
|
||||
scopes: params.scopes,
|
||||
signedAtMs: params.signedAtMs,
|
||||
token:
|
||||
params.connectParams.auth?.token ??
|
||||
params.connectParams.auth?.deviceToken ??
|
||||
params.connectParams.auth?.bootstrapToken ??
|
||||
null,
|
||||
nonce: params.nonce,
|
||||
platform: params.connectParams.client.platform,
|
||||
deviceFamily: params.connectParams.client.deviceFamily,
|
||||
});
|
||||
if (verifyDeviceSignature(params.device.publicKey, payloadV3, params.device.signature)) {
|
||||
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:
|
||||
params.connectParams.auth?.token ??
|
||||
params.connectParams.auth?.deviceToken ??
|
||||
params.connectParams.auth?.bootstrapToken ??
|
||||
null,
|
||||
nonce: params.nonce,
|
||||
});
|
||||
if (verifyDeviceSignature(params.device.publicKey, payloadV2, params.device.signature)) {
|
||||
return "v2";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePinnedClientMetadata(params: {
|
||||
claimedPlatform?: string;
|
||||
claimedDeviceFamily?: string;
|
||||
@@ -362,7 +244,6 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
const unauthorizedFloodGuard = new UnauthorizedFloodGuard();
|
||||
const browserSecurity = resolveHandshakeBrowserSecurityContext({
|
||||
requestOrigin,
|
||||
hasProxyHeaders,
|
||||
clientIp,
|
||||
rateLimiter,
|
||||
browserRateLimiter,
|
||||
@@ -589,57 +470,21 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
clientIp: browserRateLimitClientIp,
|
||||
});
|
||||
const rejectUnauthorized = (failedAuth: GatewayAuthResult) => {
|
||||
const canRetryWithDeviceToken =
|
||||
failedAuth.reason === "token_mismatch" &&
|
||||
Boolean(device) &&
|
||||
hasSharedAuth &&
|
||||
!connectParams.auth?.deviceToken;
|
||||
const recommendedNextStep = (() => {
|
||||
if (canRetryWithDeviceToken) {
|
||||
return "retry_with_device_token";
|
||||
}
|
||||
switch (failedAuth.reason) {
|
||||
case "token_missing":
|
||||
case "token_missing_config":
|
||||
case "password_missing":
|
||||
case "password_missing_config":
|
||||
return "update_auth_configuration";
|
||||
case "token_mismatch":
|
||||
case "password_mismatch":
|
||||
case "device_token_mismatch":
|
||||
return "update_auth_credentials";
|
||||
case "rate_limited":
|
||||
return "wait_then_retry";
|
||||
default:
|
||||
return "review_auth_configuration";
|
||||
}
|
||||
})();
|
||||
const { authProvided, canRetryWithDeviceToken, recommendedNextStep } =
|
||||
resolveUnauthorizedHandshakeContext({
|
||||
connectAuth: connectParams.auth,
|
||||
failedAuth,
|
||||
hasDeviceIdentity: Boolean(device),
|
||||
});
|
||||
markHandshakeFailure("unauthorized", {
|
||||
authMode: resolvedAuth.mode,
|
||||
authProvided: connectParams.auth?.password
|
||||
? "password"
|
||||
: connectParams.auth?.token
|
||||
? "token"
|
||||
: connectParams.auth?.bootstrapToken
|
||||
? "bootstrap-token"
|
||||
: connectParams.auth?.deviceToken
|
||||
? "device-token"
|
||||
: "none",
|
||||
authProvided,
|
||||
authReason: failedAuth.reason,
|
||||
allowTailscale: resolvedAuth.allowTailscale,
|
||||
});
|
||||
logWsControl.warn(
|
||||
`unauthorized conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version} reason=${failedAuth.reason ?? "unknown"}`,
|
||||
);
|
||||
const authProvided: AuthProvidedKind = connectParams.auth?.password
|
||||
? "password"
|
||||
: connectParams.auth?.token
|
||||
? "token"
|
||||
: connectParams.auth?.bootstrapToken
|
||||
? "bootstrap-token"
|
||||
: connectParams.auth?.deviceToken
|
||||
? "device-token"
|
||||
: "none";
|
||||
const authMessage = formatGatewayAuthFailureMessage({
|
||||
authMode: resolvedAuth.mode,
|
||||
authProvided,
|
||||
|
||||
Reference in New Issue
Block a user