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
5 changes: 5 additions & 0 deletions .changeset/calm-wolves-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/server": patch
---

🔒️ prevent challenge replay on failed auth
5 changes: 5 additions & 0 deletions .changeset/clear-guests-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/server": patch
---

🥅 harden siwe no credential error handling
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"foundryup",
"franm",
"fullwidth",
"getdel",
"ghsa",
"ghsas",
"gitmoji",
Expand Down
51 changes: 27 additions & 24 deletions server/api/auth/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,33 +312,38 @@ Submit the signed SIWE message to prove ownership of an Ethereum address. The se
columns: { publicKey: true, account: true, factory: true, transports: true, counter: true },
where: eq(credentials.id, assertion.id),
}),
redis.get(sessionId),
redis.getdel(sessionId),
Comment thread
cruzdanilo marked this conversation as resolved.
]);
if (!challenge) return c.json({ code: "no authentication", legacy: "no authentication" }, 400);
if (!credential) {
Comment thread
cruzdanilo marked this conversation as resolved.
if (assertion.method !== "siwe") return c.json({ code: "no credential", legacy: "no credential" }, 400);
const message = parseSiweMessage(challenge);
if (
!validateSiweMessage({ message, address: assertion.id, nonce: sessionId, domain, scheme }) ||
!(await publicClient.verifySiweMessage({
message: challenge,
address: assertion.id,
signature: assertion.signature,
}))
) {
return c.json({ code: "bad authentication", legacy: "bad authentication" }, 400);
try {
const message = parseSiweMessage(challenge);
if (
!validateSiweMessage({ message, address: assertion.id, nonce: sessionId, domain, scheme }) ||
!(await publicClient.verifySiweMessage({
message: challenge,
address: assertion.id,
signature: assertion.signature,
}))
) {
return c.json({ code: "bad authentication", legacy: "bad authentication" }, 400);
}
const result = await createCredential(c, assertion.id, { source: c.req.header("Client-Fid") });
const account = deriveAddress(result.factory, { x: result.x, y: result.y });
const intercomToken = await getIntercomToken(account, result.auth);
return c.json(
{
...result,
expires: result.auth,
intercomToken,
} satisfies InferOutput<typeof LegacyAuthentication>,
200,
);
} catch (error) {
captureException(error, { level: "error", tags: { unhandled: true } });
return c.json({ code: "ouch", legacy: "ouch" }, 500);
}
const result = await createCredential(c, assertion.id, { source: c.req.header("Client-Fid") });
const account = deriveAddress(result.factory, { x: result.x, y: result.y });
const intercomToken = await getIntercomToken(account, result.auth);
return c.json(
{
...result,
expires: result.auth,
intercomToken,
} satisfies InferOutput<typeof LegacyAuthentication>,
200,
);
}
setUser({ id: parse(Address, credential.account) });

Expand Down Expand Up @@ -383,8 +388,6 @@ Submit the signed SIWE message to prove ownership of an Ethereum address. The se
} catch (error) {
captureException(error, { level: "error", tags: { unhandled: true } });
return c.json({ code: "ouch", legacy: "ouch" }, 500);
} finally {
await redis.del(sessionId);
}

const expires = new Date(Date.now() + AUTH_EXPIRY);
Expand Down
31 changes: 17 additions & 14 deletions server/api/auth/registration.ts
Comment thread
cruzdanilo marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ export default new Hono()
setContext("auth", attestation);
const sessionId = c.req.header("x-session-id") ?? c.req.valid("cookie").session_id;
if (!sessionId) return c.json({ code: "bad session" }, 400);
const challenge = await redis.get(sessionId);
const challenge = await redis.getdel(sessionId);
if (!challenge) return c.json({ code: "no registration", legacy: "no registration" }, 400);

let webauthn: undefined | WebAuthnCredential;
Expand All @@ -323,7 +323,7 @@ export default new Hono()
signature: attestation.signature,
}))
) {
return c.json({ code: "bad authentication", legacy: "bad authentication" }, 400);
return c.json({ code: "bad registration", legacy: "bad registration" }, 400);
}
break;
}
Expand Down Expand Up @@ -357,20 +357,23 @@ export default new Hono()
} catch (error) {
captureException(error, { level: "error", tags: { unhandled: true } });
return c.json({ code: "ouch", legacy: "ouch" }, 500);
} finally {
await redis.del(sessionId);
}

const result = await createCredential(c, attestation.id, { webauthn, source: c.req.header("Client-Fid") });
const account = deriveAddress(result.factory, { x: result.x, y: result.y });
const intercomToken = await getIntercomToken(account, new Date(Date.now() + AUTH_EXPIRY));
return c.json(
{
...result,
intercomToken,
} satisfies InferOutput<typeof Authentication>,
200,
);
try {
const result = await createCredential(c, attestation.id, { webauthn, source: c.req.header("Client-Fid") });
const account = deriveAddress(result.factory, { x: result.x, y: result.y });
const intercomToken = await getIntercomToken(account, new Date(Date.now() + AUTH_EXPIRY));
return c.json(
{
...result,
intercomToken,
} satisfies InferOutput<typeof Authentication>,
200,
);
} catch (error) {
captureException(error, { level: "error", tags: { unhandled: true } });
return c.json({ code: "ouch", legacy: "ouch" }, 500);
}
},
);

Expand Down
Loading