diff --git a/api/auth/verify.js b/api/auth/verify.js index ef7ed0b..0bec004 100644 --- a/api/auth/verify.js +++ b/api/auth/verify.js @@ -58,9 +58,27 @@ 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.'); @@ -68,7 +86,7 @@ module.exports = async function handler(req, res) { 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 { diff --git a/tests/api-auth.test.js b/tests/api-auth.test.js index 2814f2f..436e3c0 100644 --- a/tests/api-auth.test.js +++ b/tests/api-auth.test.js @@ -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') + ); +});