Skip to content

Commit 745266f

Browse files
authored
Merge pull request #264 from commandlayer/codex/fix-live-authentication-for-sign-in-with-ethereum
fix: enable real SIWE verification policy for claim auth
2 parents 26e808e + 26531b8 commit 745266f

3 files changed

Lines changed: 77 additions & 28 deletions

File tree

api/auth/verify.js

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,40 @@
22

33
const { URL } = require('node:url');
44

5-
const ALLOWED_CHAIN_IDS = new Set((process.env.SIWE_ALLOWED_CHAIN_IDS || '1,8453,11155111').split(',').map((v) => Number(v.trim())).filter(Number.isFinite));
6-
const ALLOWED_DOMAIN = process.env.SIWE_ALLOWED_DOMAIN || '';
7-
const ALLOWED_URI = process.env.SIWE_ALLOWED_URI || '';
85
const REQUIRED_STATEMENT = 'CommandLayer Claim activation';
96

7+
function isDev() {
8+
return process.env.NODE_ENV === 'development';
9+
}
10+
1011
function getHost(req) {
1112
return String((req.headers && (req.headers['x-forwarded-host'] || req.headers.host)) || '').split(',')[0].trim().toLowerCase();
1213
}
1314

15+
function getAllowedDomain(req) {
16+
const configured = process.env.COMMANDLAYER_SIWE_DOMAIN || process.env.SIWE_ALLOWED_DOMAIN || '';
17+
if (configured) return configured.toLowerCase();
18+
const host = getHost(req).split(':')[0];
19+
if (isDev() && (host === 'localhost' || host === '127.0.0.1')) return host;
20+
return '';
21+
}
22+
23+
function getAllowedUri(req) {
24+
const configured = process.env.COMMANDLAYER_SITE_URL || process.env.SIWE_ALLOWED_URI || '';
25+
if (configured) {
26+
try { return new URL(configured).toString(); } catch { return configured; }
27+
}
28+
const host = getHost(req);
29+
if (isDev() && host.startsWith('localhost')) return `http://${host}/`;
30+
if (isDev() && host.startsWith('127.0.0.1')) return `http://${host}/`;
31+
return '';
32+
}
33+
34+
function getAllowedChainIds() {
35+
const raw = process.env.COMMANDLAYER_SIWE_CHAIN_IDS || process.env.SIWE_ALLOWED_CHAIN_IDS || '1,8453';
36+
return new Set(raw.split(',').map((v) => Number(v.trim())).filter(Number.isFinite));
37+
}
38+
1439
module.exports = async function handler(req, res) {
1540
res.setHeader('Content-Type', 'application/json; charset=utf-8');
1641
res.setHeader('Cache-Control', 'no-store');
@@ -36,20 +61,26 @@ module.exports = async function handler(req, res) {
3661

3762
try {
3863
const parsed = new SiweMessage(message);
39-
const expectedDomain = ALLOWED_DOMAIN || getHost(req);
40-
if (expectedDomain && String(parsed.domain || '').toLowerCase() !== expectedDomain) {
64+
const expectedDomain = getAllowedDomain(req);
65+
const expectedUri = getAllowedUri(req);
66+
const allowedChains = getAllowedChainIds();
67+
68+
if (!expectedDomain) {
69+
return res.status(400).json({ ok: false, status: 'AUTH_FAILED', error: 'SIWE domain policy is not configured.' });
70+
}
71+
if (String(parsed.domain || '').toLowerCase() !== expectedDomain) {
4172
return res.status(400).json({ ok: false, status: 'AUTH_FAILED', error: 'SIWE domain mismatch.' });
4273
}
4374

44-
if (ALLOWED_URI && parsed.uri !== ALLOWED_URI) {
75+
if (expectedUri && parsed.uri !== expectedUri) {
4576
return res.status(400).json({ ok: false, status: 'AUTH_FAILED', error: 'SIWE URI mismatch.' });
4677
}
4778

48-
if (!ALLOWED_CHAIN_IDS.has(Number(parsed.chainId))) {
79+
if (!allowedChains.has(Number(parsed.chainId))) {
4980
return res.status(400).json({ ok: false, status: 'AUTH_FAILED', error: 'Unsupported SIWE chainId.' });
5081
}
5182

52-
if (!String(parsed.statement || '').toLowerCase().includes(REQUIRED_STATEMENT.toLowerCase())) {
83+
if (String(parsed.statement || '').trim() !== REQUIRED_STATEMENT) {
5384
return res.status(400).json({ ok: false, status: 'AUTH_FAILED', error: 'Invalid SIWE statement for claim activation.' });
5485
}
5586

public/claim.html

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -784,21 +784,22 @@ <h3>Payment and provisioning are coming next.</h3>
784784
}
785785

786786
async function signInWithEthereum(){
787-
if(!window.ethereum){ alert('No Ethereum wallet detected. Install a wallet extension first.'); return; }
788-
updateSiweModeHint();
789787
const statusEl=document.getElementById('siweStatus');
790788
const walletEl=document.getElementById('siweWallet');
791-
statusEl.textContent='Status: Requesting signature...';
792-
const [address]=await window.ethereum.request({ method:'eth_requestAccounts' });
793-
const nonceRes=await fetch('/api/auth/nonce',{method:'GET',headers:{'Accept':'application/json'}});
794-
const nonceData=await nonceRes.json();
795-
if(!nonceData.ok){ throw new Error('Nonce request failed'); }
796-
const domain=window.location.host;
797-
const uri=window.location.origin;
798-
const chainHex=await window.ethereum.request({ method:'eth_chainId' });
799-
const chainId=parseInt(chainHex,16);
800-
const issuedAt=new Date().toISOString();
801-
const msg=`${domain} wants you to sign in with your Ethereum account:
789+
try {
790+
if(!window.ethereum){ throw new Error('NO_WALLET'); }
791+
updateSiweModeHint();
792+
statusEl.textContent='Status: Requesting signature...';
793+
const [address]=await window.ethereum.request({ method:'eth_requestAccounts' });
794+
const nonceRes=await fetch('/api/auth/nonce',{method:'GET',headers:{'Accept':'application/json'}});
795+
const nonceData=await nonceRes.json();
796+
if(!nonceData.ok){ throw new Error('NONCE_FAILED'); }
797+
const domain=window.location.host;
798+
const uri=window.location.origin;
799+
const chainHex=await window.ethereum.request({ method:'eth_chainId' });
800+
const chainId=parseInt(chainHex,16);
801+
const issuedAt=new Date().toISOString();
802+
const msg=`${domain} wants you to sign in with your Ethereum account:
802803
${address}
803804
804805
CommandLayer Claim activation
@@ -808,13 +809,22 @@ <h3>Payment and provisioning are coming next.</h3>
808809
Chain ID: ${chainId}
809810
Nonce: ${nonceData.nonce}
810811
Issued At: ${issuedAt}`;
811-
const signature=await window.ethereum.request({method:'personal_sign',params:[msg,address]});
812-
const verifyRes=await fetch('/api/auth/verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg,signature})});
813-
const verify=await verifyRes.json();
814-
if(!verify.ok){ throw new Error(verify.error||'Authentication failed'); }
815-
state.authenticatedAddress=verify.address; state.authStatus=verify.status; state.authChainId=verify.chainId;
816-
statusEl.textContent='Status: Authenticated';
817-
walletEl.textContent=`Connected wallet: ${shortAddr(verify.address)}`;
812+
const signature=await window.ethereum.request({method:'personal_sign',params:[msg,address]});
813+
const verifyRes=await fetch('/api/auth/verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg,signature})});
814+
const verify=await verifyRes.json();
815+
if(!verify.ok){ throw new Error(verify.error||'Authentication failed'); }
816+
state.authenticatedAddress=verify.address; state.authStatus=verify.status; state.authChainId=verify.chainId;
817+
statusEl.textContent='Status: Authenticated';
818+
walletEl.textContent=`Connected wallet: ${shortAddr(verify.address)}`;
819+
} catch (err) {
820+
const msg=(err && err.message)||'';
821+
if(msg==='NO_WALLET') statusEl.textContent='Status: No wallet found.';
822+
else if(msg.includes('User rejected') || msg.includes('rejected')) statusEl.textContent='Status: Wallet rejected signature.';
823+
else if(msg.includes('dependency unavailable')) statusEl.textContent='Status: Auth dependency unavailable.';
824+
else if(msg.includes('domain mismatch')) statusEl.textContent='Status: Domain mismatch.';
825+
else if(msg.includes('signature') || msg.includes('SIWE')) statusEl.textContent='Status: Signature invalid.';
826+
else statusEl.textContent='Status: Authentication failed.';
827+
}
818828
}
819829

820830
function goStep5Auth(){

tests/api-auth.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,11 @@ test('POST /api/auth/verify rejects malformed message/signature', async () => {
4040
assert.equal(res.body.ok, false);
4141
assert.equal(res.body.status, 'AUTH_FAILED');
4242
});
43+
44+
45+
test('POST /api/auth/verify surfaces dependency unavailable when siwe is missing', async () => {
46+
const res = makeRes();
47+
await verifyHandler({ method: 'POST', body: { message: 'x', signature: '0xy' }, headers: { host: 'localhost:3000' } }, res);
48+
assert.equal(res.statusCode, 503);
49+
assert.match(res.body.error, /dependency unavailable/i);
50+
});

0 commit comments

Comments
 (0)