Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions api/auth/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,35 @@ module.exports = async function handler(req, res) {
return fail(405, 'method_not_allowed', 'Method not allowed. Use POST.');
}

const body = req.body || {};
const message = typeof body.message === 'string' ? body.message : '';
const signature = typeof body.signature === 'string' ? body.signature : '';
const body = req.body;
if (!body || typeof body !== 'object' || Array.isArray(body)) {
return fail(400, 'malformed_request', 'Request body must be a JSON object.');
}

if (!Object.prototype.hasOwnProperty.call(body, 'message')) {
return fail(400, 'missing_message', 'Missing SIWE message.');
}
if (!Object.prototype.hasOwnProperty.call(body, 'signature')) {
return fail(400, 'missing_signature', 'Missing SIWE signature.');
}

if (typeof body.message !== 'string') {
return fail(400, 'invalid_message_type', 'SIWE message must be a string.');
}
if (typeof body.signature !== 'string') {
return fail(400, 'invalid_signature_type', 'SIWE signature must be a string.');
}

const message = body.message;
const signature = body.signature;
if (!message) return fail(400, 'missing_message', 'Missing SIWE message.');
if (!signature) return fail(400, 'missing_signature', 'Missing SIWE signature.');

let SiweMessage;
try {
({ SiweMessage } = require('siwe'));
} catch {
return fail(503, 'dependency_unavailable', 'SIWE verification dependency unavailable on server.');
return fail(503, 'dependency_unavailable', 'SIWE dependency is unavailable');
}

try {
Expand Down
154 changes: 77 additions & 77 deletions tests/api-auth.test.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,77 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const nonceHandler = require('../api/auth/nonce');
const verifyHandler = require('../api/auth/verify');
function makeRes() {
return {
statusCode: 200,
headers: {},
body: null,
setHeader(name, value) { this.headers[name.toLowerCase()] = value; },
status(code) { this.statusCode = code; return this; },
json(payload) { this.body = payload; return this; },
};
}
test('GET /api/auth/nonce returns nonce and randomness', async () => {
const r1 = makeRes(); const r2 = makeRes();
await nonceHandler({ method: 'GET', headers: {} }, r1);
await nonceHandler({ method: 'GET', headers: {} }, r2);
assert.equal(r1.statusCode, 200);
assert.equal(r1.body.ok, true);
assert.match(r1.body.nonce, /^[a-f0-9]{32,}$/);
assert.notEqual(r1.body.nonce, r2.body.nonce);
});
test('POST /api/auth/verify rejects missing signature', async () => {
const res = makeRes();
await verifyHandler({ method: 'POST', body: { message: 'x' }, headers: { host: 'localhost:3000' } }, res);
assert.equal(res.statusCode, 400);
assert.equal(res.body.ok, false);
assert.equal(res.body.error, 'missing_signature');
});
test('POST /api/auth/verify rejects malformed message/signature', async () => {
const res = makeRes();
await verifyHandler({ method: 'POST', body: { message: 'invalid', signature: '0xdeadbeef' }, headers: { host: 'localhost:3000' } }, res);
assert.equal(res.body.ok, false);
assert.equal(res.body.status, 'AUTH_FAILED');
assert.ok(['malformed_message', 'dependency_unavailable'].includes(res.body.error));
});
test('POST /api/auth/verify rejects statement mismatch', async () => {
const res = makeRes();
const message = `www.commandlayer.org wants you to sign in with your Ethereum account:
0x0000000000000000000000000000000000000001
Different statement.
URI: https://www.commandlayer.org
Version: 1
Chain ID: 1
Nonce: abcdefgh
Issued At: 2026-01-01T00:00:00.000Z`;
await verifyHandler({ method: 'POST', body: { message, signature: '0xdeadbeef' }, headers: { host: 'www.commandlayer.org' } }, res);
if (res.statusCode === 503) {
assert.equal(res.body.error, 'dependency_unavailable');
return;
}
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'statement_mismatch');
});
test('POST /api/auth/verify rejects malformed SIWE payload or surfaces missing dependency', async () => {
const res = makeRes();
await verifyHandler({ method: 'POST', body: { message: 'x', signature: '0xy' }, headers: { host: 'localhost:3000' } }, res);
assert.equal(res.body.ok, false);
assert.equal(res.body.status, 'AUTH_FAILED');
assert.ok(
(res.statusCode === 400 && res.body.error === 'malformed_message') ||
(res.statusCode === 503 && res.body.error === 'dependency_unavailable')
);
});
'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');

const nonceHandler = require('../api/auth/nonce');
const verifyHandler = require('../api/auth/verify');

function makeRes() {
return {
statusCode: 200,
headers: {},
body: null,
setHeader(name, value) { this.headers[name.toLowerCase()] = value; },
status(code) { this.statusCode = code; return this; },
json(payload) { this.body = payload; return this; },
};
}

test('GET /api/auth/nonce returns nonce and randomness', async () => {
const r1 = makeRes(); const r2 = makeRes();
await nonceHandler({ method: 'GET', headers: {} }, r1);
await nonceHandler({ method: 'GET', headers: {} }, r2);
assert.equal(r1.statusCode, 200);
assert.equal(r1.body.ok, true);
assert.match(r1.body.nonce, /^[a-f0-9]{32,}$/);
assert.notEqual(r1.body.nonce, r2.body.nonce);
});

test('POST /api/auth/verify rejects missing signature', async () => {
const res = makeRes();
await verifyHandler({ method: 'POST', body: { message: 'x' }, headers: { host: 'localhost:3000' } }, res);
assert.equal(res.statusCode, 400);
assert.equal(res.body.ok, false);
assert.equal(res.body.error, 'missing_signature');
});

test('POST /api/auth/verify rejects malformed message/signature', async () => {
const res = makeRes();
await verifyHandler({ method: 'POST', body: { message: 'invalid', signature: '0xdeadbeef' }, headers: { host: 'localhost:3000' } }, res);
assert.equal(res.body.ok, false);
assert.equal(res.body.status, 'AUTH_FAILED');
assert.ok(['malformed_message', 'dependency_unavailable'].includes(res.body.error));
});

test('POST /api/auth/verify rejects statement mismatch', async () => {
const res = makeRes();
const message = `www.commandlayer.org wants you to sign in with your Ethereum account:
0x0000000000000000000000000000000000000001

Different statement.

URI: https://www.commandlayer.org
Version: 1
Chain ID: 1
Nonce: abcdefgh
Issued At: 2026-01-01T00:00:00.000Z`;
await verifyHandler({ method: 'POST', body: { message, signature: '0xdeadbeef' }, headers: { host: 'www.commandlayer.org' } }, res);
if (res.statusCode === 503) {
assert.equal(res.body.error, 'dependency_unavailable');
return;
}
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'statement_mismatch');
});


test('POST /api/auth/verify rejects malformed SIWE payload or surfaces missing dependency', async () => {
const res = makeRes();
await verifyHandler({ method: 'POST', body: { message: 'x', signature: '0xy' }, headers: { host: 'localhost:3000' } }, res);
assert.equal(res.body.ok, false);
assert.equal(res.body.status, 'AUTH_FAILED');
assert.ok(
(res.statusCode === 400 && res.body.error === 'malformed_message') ||
(res.statusCode === 503 && res.body.error === 'dependency_unavailable')
);
});
Loading