fix(owner): preserve password whitespace during login

This commit is contained in:
gadgethd
2026-09-23 11:49:25 +00:00
parent 771fc6f613
commit 2be3357af6
4 changed files with 72 additions and 9 deletions
+33
View File
@@ -82,3 +82,36 @@ test('owner session route rejects cookies from an older credential generation',
ownerServer.restore();
}
});
test('owner login verifies the exact password string, including surrounding whitespace', async () => {
let receivedCredentials: [string, string] | undefined;
const ownerServer = await startOwnerRoutes({
verifyCredentials: async (username, password) => {
receivedCredentials = [username, password];
return true;
},
});
try {
const csrfResponse = await fetch(`${ownerServer.baseUrl}/owner/csrf`);
const { csrfToken } = await csrfResponse.json() as { csrfToken: string };
const csrfCookie = csrfResponse.headers.get('set-cookie')?.split(';', 1)[0];
assert.ok(csrfCookie);
const response = await fetch(`${ownerServer.baseUrl}/owner/login`, {
method: 'POST',
headers: {
'content-type': 'application/json',
cookie: csrfCookie,
'x-csrf-token': csrfToken,
},
body: JSON.stringify({ mqttUsername: ' owner ', mqttPassword: ' password ' }),
});
assert.equal(response.status, 200);
assert.deepEqual(receivedCredentials, ['owner', ' password ']);
} finally {
await ownerServer.close();
ownerServer.restore();
}
});
+9 -9
View File
@@ -22,6 +22,8 @@ import {
lastHopSeriesCache,
linkBadge,
nodeRoleLabel,
ownerLoginCredentials,
ownerLoginValidationError,
publishOwnerSession,
type LastHopStrengthPoint,
type MappedPeer,
@@ -128,13 +130,14 @@ export const OwnerPortalPage: React.FC = () => {
const handleLogin = (event: FormEvent) => {
event.preventDefault();
if (!mqttUsername.trim() || !mqttPassword) {
setError('Enter your MQTT username and password.');
const validationError = ownerLoginValidationError(mqttUsername, mqttPassword);
if (validationError) {
setError(validationError);
return;
}
setSubmitting(true);
setError(null);
const loginUsername = mqttUsername.trim();
const credentials = ownerLoginCredentials(mqttUsername, mqttPassword);
fetchOwnerCsrfToken()
.then((csrfToken) => fetchJson<OwnerSessionResponse>('/api/owner/login', {
method: 'POST',
@@ -142,10 +145,7 @@ export const OwnerPortalPage: React.FC = () => {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify({
mqttUsername: loginUsername,
mqttPassword,
}),
body: JSON.stringify(credentials),
}, {
timeoutMs: 15_000,
maxBytes: 2 * 1024 * 1024,
@@ -155,8 +155,8 @@ export const OwnerPortalPage: React.FC = () => {
.then((json) => {
lastHopSeriesCache.clear();
setDashboard(json.dashboard);
setOwnerSessionKey(json.mqttUsername ?? loginUsername);
publishOwnerSession(json.mqttUsername ?? loginUsername);
setOwnerSessionKey(json.mqttUsername ?? credentials.mqttUsername);
publishOwnerSession(json.mqttUsername ?? credentials.mqttUsername);
if (json.dashboard.nodes[0]?.node_id) {
setSelectedNodeId(json.dashboard.nodes[0].node_id);
}
@@ -9,8 +9,20 @@ import {
isOwnerSessionResponse,
isValidMapCoord,
nodeRoleLabel,
ownerLoginCredentials,
ownerLoginValidationError,
} from './ownerPortalModel.js';
test('owner login trims only the username and preserves password whitespace', () => {
assert.equal(ownerLoginValidationError(' owner ', ' password '), null);
assert.deepEqual(ownerLoginCredentials(' owner ', ' password '), {
mqttUsername: 'owner',
mqttPassword: ' password ',
});
assert.match(ownerLoginValidationError('owner', '\tpassword') ?? '', /invalid characters/);
assert.match(ownerLoginValidationError('owner', 'x'.repeat(129)) ?? '', /too long/);
});
test('owner map coordinate policy rejects placeholders and invalid coordinates', () => {
assert.equal(isValidMapCoord(51.5, -0.1), true);
assert.equal(isValidMapCoord(0, 0), false);
@@ -21,6 +21,24 @@ export function nodeRoleLabel(role: number | null): string {
return 'Repeater';
}
export type OwnerLoginCredentials = { mqttUsername: string; mqttPassword: string };
export function ownerLoginValidationError(mqttUsername: string, mqttPassword: string): string | null {
const normalizedUsername = mqttUsername.trim();
if (!normalizedUsername || !mqttPassword) return 'Enter your MQTT username and password.';
if (normalizedUsername.length > 128 || mqttPassword.length > 128) {
return 'MQTT username or password is too long.';
}
if (/[\u0000-\u001F\u007F]/.test(normalizedUsername) || /[\u0000-\u001F\u007F]/.test(mqttPassword)) {
return 'MQTT username or password contains invalid characters.';
}
return null;
}
export function ownerLoginCredentials(mqttUsername: string, mqttPassword: string): OwnerLoginCredentials {
return { mqttUsername: mqttUsername.trim(), mqttPassword };
}
export type OwnerDashboard = {
nodes: OwnerNode[];
};