Skip to content
Merged

lol #87

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
25 changes: 19 additions & 6 deletions packages/auth/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,30 @@ export const authConfig: NextAuthConfig = {
const { createTransport } = await import("nodemailer");
const transport = createTransport(provider.server);

// Extract the raw token from NextAuth's callback URL
const parsedUrl = new URL(url);
const host = parsedUrl.host;
const token = parsedUrl.searchParams.get("token") || "";
const callbackUrl = parsedUrl.searchParams.get("callbackUrl") || "/dashboard";

// Build /verify URL — user clicks a button on this page to complete sign-in.
// This prevents email scanners from consuming the one-time token.
// The verify page redirects to our custom /api/auth/verify-email endpoint.
// Generate our own random token and store it directly in the DB.
// This bypasses NextAuth's internal token hashing which causes
// Verification_Failed errors in our deployment environment.
const { randomBytes } = await import("crypto");
const customToken = randomBytes(32).toString("hex");
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours

// Dynamically import DB to store our custom token
const { db, verificationTokens } = await import("@query/db");
if (db) {
await db.insert(verificationTokens).values({
identifier,
token: `custom:${customToken}`,
expires,
});
}

// Build /verify URL with our custom token
const verifyUrl = new URL("/verify", parsedUrl.origin);
verifyUrl.searchParams.set("token", token);
verifyUrl.searchParams.set("token", customToken);
verifyUrl.searchParams.set("email", identifier);
verifyUrl.searchParams.set("callbackUrl", callbackUrl);
const safeUrl = verifyUrl.toString();
Expand Down
27 changes: 12 additions & 15 deletions sites/mainweb/app/(portal)/api/auth/verify-email/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,39 @@ import { db, verificationTokens, users, sessions } from "@query/db";
import { eq, and } from "drizzle-orm";

/**
* Custom email verification endpoint that bypasses NextAuth's callback.
* Custom email verification endpoint.
*
* Our sendVerificationRequest stores a separate token that we control.
* This endpoint looks up that token directly — no dependency on NextAuth's
* internal hashing mechanism.
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const rawToken = searchParams.get("token");
const tokenParam = searchParams.get("token");
const email = searchParams.get("email");
const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";

// Use NEXTAUTH_URL for redirects (request.url resolves to internal host on Cloud Run)
const baseUrl = process.env.NEXTAUTH_URL || process.env.AUTH_URL || "https://datasciencegt.org";

if (!rawToken || !email) {
if (!tokenParam || !email) {
return NextResponse.redirect(`${baseUrl}/auth/error?error=Configuration`);
}

const secret = process.env.AUTH_SECRET || process.env.NEXTAUTH_SECRET || "";

try {
// Hash the token the same way @auth/core does (Web Crypto SHA-256)
const data = new TextEncoder().encode(`${rawToken}${secret}`);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashedToken = Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");

if (!db) {
return NextResponse.redirect(`${baseUrl}/auth/error?error=Configuration`);
}

// Look up and delete the token (one-time use)
// Look up the token directly — our sendVerificationRequest stores
// the token value as-is (no hashing) with a "custom:" prefix
const customTokenValue = `custom:${tokenParam}`;

const result = await db
.delete(verificationTokens)
.where(
and(
eq(verificationTokens.identifier, email),
eq(verificationTokens.token, hashedToken)
eq(verificationTokens.token, customTokenValue)
)
)
.returning();
Expand Down
Loading