auth-yes/server/routes/auth/register.ts
google-labs-jules[bot] 589e146ecc feat(routes): decompose admin and auth monolith routes
Extracted domain-specific sub-routers from monolithic `server/routes/admin.ts` and `server/routes/auth.ts` into isolated modules within `server/routes/admin/` and `server/routes/auth/` respectively. The original entry routers were updated to import and assemble these sub-routers without breaking their current HTTP interface or rate limiting/authorization middleware. Testing and linting were run ensuring perfect functionality and 100% test passing score.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-26 04:37:26 +00:00

370 lines
12 KiB
TypeScript

import { Hono } from "jsr:@hono/hono@4";
import { deleteCookie, getCookie, setCookie } from "jsr:@hono/hono@4/cookie";
import { encodeBase64Url } from "jsr:@std/encoding@1/base64url";
import {
generateRegistrationOptions,
MetadataService,
verifyRegistrationResponse,
} from "jsr:@simplewebauthn/server@13";
import type { RegistrationResponseJSON } from "jsr:@simplewebauthn/server@13";
import { sqlWrapper } from "../../db.ts";
import { valkey } from "../../valkey.ts";
import { auditWrapper } from "../../audit.ts";
import { getClientIp, publicRateLimiter } from "../../middleware.ts";
import { getCookieDomain } from "./utils.ts";
export const registerAuthRoutes = new Hono();
const rpName = "Auth-Yes Identity Provider";
const rpID = Deno.env.get("RP_ID") ||
(import.meta.main ? undefined : "localhost");
const origin = Deno.env.get("ORIGIN") ||
(import.meta.main ? undefined : "http://localhost");
const requireHardwareToken = Deno.env.get("REQUIRE_HARDWARE_TOKEN") === "true";
registerAuthRoutes.use("/api/register/*", publicRateLimiter);
registerAuthRoutes.get("/.well-known/webauthn", (c) => {
if (!origin) {
return c.json({ origins: [] });
}
return c.json({ origins: [origin] });
});
// Start a WebAuthn registration ceremony
registerAuthRoutes.post("/api/register/challenge", async (c) => {
const { username, inviteCode } = await c.req.json();
if (!username || !inviteCode) {
return c.json({ error: "Username and inviteCode required" }, 400);
}
// Validate invite code early (checks expiration and max_uses bounds)
const invite = await sqlWrapper
.sql`SELECT id, max_uses, uses_count, auto_activate FROM invites WHERE code = ${inviteCode} AND (max_uses IS NULL OR uses_count < max_uses) AND expires_at > NOW()`
.then((res: any) => res[0]);
if (!invite) {
return c.json(
{ error: "Invalid, expired, or fully claimed invite code" },
400,
);
}
// Prevent hijacking an existing user's account if they already exist
const existingUser = await sqlWrapper
.sql`SELECT id FROM users WHERE username = ${username}`.then((res: any) =>
res[0]
);
if (existingUser) {
return c.json({ error: "Username already exists" }, 409);
}
const newUserId = crypto.randomUUID();
const userIdBytes = new TextEncoder().encode(newUserId);
if (!rpID) throw new Error("rpID is missing");
const options = await generateRegistrationOptions({
rpName,
rpID,
userName: username,
userID: userIdBytes,
attestationType: "direct",
authenticatorSelection: {
residentKey: "required",
requireResidentKey: true,
userVerification: "preferred",
},
timeout: 60000,
extensions: {
["prf" as string]: {},
} as any,
});
setCookie(c, "expected_registration_challenge", options.challenge, {
httpOnly: true,
secure: true,
sameSite: "Lax",
maxAge: 300,
});
setCookie(c, "registration_user_id", newUserId, {
httpOnly: true,
secure: true,
sameSite: "Lax",
maxAge: 300,
});
return c.json({ options, username });
});
// Verify registration and create UUID/session
registerAuthRoutes.post("/api/register/verify", async (c) => {
try {
const { response, username, inviteCode, upgrade_session } = await c.req
.json();
if (!inviteCode && !upgrade_session) {
return c.json({ error: "inviteCode or upgrade_session required" }, 400);
}
const expectedChallenge = getCookie(c, "expected_registration_challenge");
const registrationUserId = getCookie(c, "registration_user_id");
if (!expectedChallenge || !registrationUserId) {
return c.json({
error: "Missing or expired registration challenge/user ID",
}, 400);
}
// Prevent race condition account hijacking
let user = await sqlWrapper
.sql`SELECT id FROM users WHERE username = ${username}`
.then(
(res: any) => res[0],
);
if (user) {
return c.json({ error: "Username already exists" }, 409);
}
if (!origin || !rpID) throw new Error("Missing origin or rpID");
let verification;
try {
verification = await verifyRegistrationResponse({
response: response as RegistrationResponseJSON,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
requireUserVerification: false,
});
} catch (error: any) {
return c.json({ error: error.message }, 400);
}
const { verified, registrationInfo } = verification;
if (!verified || !registrationInfo) {
return c.json({ error: "Verification failed" }, 400);
}
// Enterprise Allow-List Verification
const allowlistCount = await sqlWrapper
.sql`SELECT COUNT(*) as count FROM aaguid_allowlist`.then((res: any) =>
Number(res[0].count)
);
if (allowlistCount > 0 && registrationInfo.aaguid) {
const isAllowed = await sqlWrapper
.sql`SELECT id FROM aaguid_allowlist WHERE aaguid = ${registrationInfo.aaguid}`
.then((res: any) => res[0]);
if (!isAllowed) {
auditWrapper.auditLog(null, "failed_attestation_allowlist", null, {
aaguid: registrationInfo.aaguid,
}, getClientIp(c));
return c.json({
error: "Authenticator AAGUID is not in the enterprise allow-list.",
}, 403);
}
}
// Optional Strict Hardware Attestation (e.g. YubiKey-only)
if (requireHardwareToken) {
if (
!registrationInfo.aaguid ||
registrationInfo.aaguid === "00000000-0000-0000-0000-000000000000"
) {
auditWrapper.auditLog(null, "registration_failed_attestation", null, {
username,
reason: "No AAGUID provided",
}, getClientIp(c));
return c.json({
error:
"Hardware attestation failed: No AAGUID provided. Only certified hardware security keys are permitted.",
}, 403);
}
let mdsStatement;
try {
mdsStatement = await MetadataService.getStatement(
registrationInfo.aaguid,
);
} catch (mdsError) {
console.warn("[Auth API] MetadataService lookup error:", mdsError);
}
if (!mdsStatement) {
auditWrapper.auditLog(null, "registration_failed_attestation", null, {
username,
aaguid: registrationInfo.aaguid,
reason: "AAGUID not found in MDS3",
}, getClientIp(c));
return c.json({
error:
`Hardware attestation failed: Authenticator AAGUID (${registrationInfo.aaguid}) not found in FIDO MDS3 blob. Only certified hardware security keys are permitted.`,
}, 403);
}
// @ts-ignore: TypeScript definition might be out of date for FIDO MDS3 (1)
if (mdsStatement.keyProtection?.includes(0x0001)) {
auditWrapper.auditLog(null, "registration_failed_attestation", null, {
username,
aaguid: registrationInfo.aaguid,
reason: "Software passkey detected",
}, getClientIp(c));
return c.json({
error:
"Hardware attestation failed: Authenticator is flagged as a software-based passkey. Only certified hardware security keys are permitted.",
}, 403);
}
}
const credentialID = registrationInfo.credential.id;
const credentialPublicKey = registrationInfo.credential.publicKey;
const counter = registrationInfo.credential.counter;
const base64CredentialID = typeof credentialID === "string"
? credentialID
: encodeBase64Url(new Uint8Array(credentialID as unknown as ArrayBuffer));
const base64PublicKey = encodeBase64Url(
new Uint8Array(credentialPublicKey as unknown as ArrayBuffer),
);
const prfEnabled =
(response.clientExtensionResults as any)?.prf?.enabled === true;
let prfSalt = null;
if (prfEnabled) {
const saltBytes = crypto.getRandomValues(new Uint8Array(32));
prfSalt = encodeBase64Url(saltBytes);
}
if (upgrade_session) {
// Ephemeral Guest Sandbox in-flight promotion
const sessionDataStr = await valkey.get(upgrade_session);
if (!sessionDataStr) {
return c.json({ error: "Invalid or expired guest session" }, 400);
}
const sessionData = JSON.parse(sessionDataStr);
if (
!sessionData || !sessionData.uuid ||
sessionData.account_status !== "guest"
) {
return c.json({ error: "Invalid guest session state" }, 400);
}
const guestUuid = sessionData.uuid;
const insertRes = await sqlWrapper
.sql`INSERT INTO users (id, username, account_status) VALUES (${guestUuid}, ${username}, 'active') RETURNING id`;
user = insertRes[0];
await sqlWrapper.sql`
INSERT INTO passkeys (user_id, credential_id, public_key, counter, prf_enabled, prf_salt)
VALUES (${user.id}, ${base64CredentialID}, ${base64PublicKey}, ${counter}, ${prfEnabled}, ${prfSalt})
`;
// Promote Valkey session
await valkey.setex(
upgrade_session,
28800, // Upgrade TTL to 8 hours
JSON.stringify({ uuid: guestUuid, username, account_status: "active" }),
);
// Register session in PostgreSQL
const expiresAt = new Date(Date.now() + 8 * 60 * 60 * 1000);
await sqlWrapper.sql`
INSERT INTO sessions (id, user_id, expires_at)
VALUES (${upgrade_session}, ${user.id}, ${expiresAt})
`;
} else {
// Standard Registration Flow
const invite = await sqlWrapper
.sql`SELECT id, app_id, role, max_uses, uses_count, auto_activate FROM invites WHERE code = ${inviteCode} AND (max_uses IS NULL OR uses_count < max_uses) AND expires_at > NOW()`
.then((res: any) => res[0]);
if (!invite) {
return c.json(
{ error: "Invalid, expired, or fully claimed invite code" },
400,
);
}
const initialStatus = invite.auto_activate === false
? "pending"
: "active";
const insertRes = await sqlWrapper
.sql`INSERT INTO users (id, username, account_status) VALUES (${registrationUserId}, ${username}, ${initialStatus}) RETURNING id`;
user = insertRes[0];
await sqlWrapper.sql`
INSERT INTO passkeys (user_id, credential_id, public_key, counter, prf_enabled, prf_salt)
VALUES (${user.id}, ${base64CredentialID}, ${base64PublicKey}, ${counter}, ${prfEnabled}, ${prfSalt})
`;
await sqlWrapper.sql`
UPDATE invites
SET uses_count = uses_count + 1,
used_at = NOW(),
used_by = ${user.id}
WHERE id = ${invite.id}
`;
await sqlWrapper.sql`
INSERT INTO invite_redemptions (invite_id, user_id)
VALUES (${invite.id}, ${user.id})
`;
if (invite.app_id) {
await sqlWrapper.sql`
INSERT INTO grants (user_id, app_id, role)
VALUES (${user.id}, ${invite.app_id}, ${invite.role})
`;
} else if (invite.role === "admin") {
const adminApp = await sqlWrapper
.sql`SELECT id FROM apps WHERE name = 'Auth-Yes Management Console'`
.then((res: any) => res[0]);
if (adminApp) {
await sqlWrapper.sql`
INSERT INTO grants (user_id, app_id, role)
VALUES (${user.id}, ${adminApp.id}, 'admin')
ON CONFLICT (user_id, app_id) DO UPDATE SET role = 'admin'
`;
}
}
}
auditWrapper.auditLog(
user.id,
"user_registered",
null,
{ username, inviteCode },
getClientIp(c),
);
// Set response cookie to clear out the challenge
setCookie(c, "expected_registration_challenge", "", {
httpOnly: true,
secure: true,
sameSite: "Lax",
maxAge: 0,
});
setCookie(c, "registration_user_id", "", {
httpOnly: true,
secure: true,
sameSite: "Lax",
maxAge: 0,
});
const cookieDomain = getCookieDomain(rpID);
if (cookieDomain) {
deleteCookie(c, "session_id", { domain: cookieDomain, path: "/" });
}
deleteCookie(c, "session_id", { path: "/" });
return c.json({ success: true });
} catch (error: any) {
console.error(
"[Auth API] Uncaught Exception in /api/register/verify:",
error,
);
return c.json({ error: error.message || "Internal server error" }, 500);
}
});