Fixed an issue where the user would be redirected to a localhost address because an auth url was not set in the env.
This commit is contained in:
8 files changed
+420
-71
No files matched your search
+10
-67
@@ -3,44 +3,10 @@ import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import Google from "next-auth/providers/google";
|
||||
import { authConfig } from "@/auth.config";
|
||||
import { finalizeOtp, verifyOtp } from "@/lib/auth/otp.service";
|
||||
import { authorizeCredentials } from "@/lib/auth/authorize-credentials";
|
||||
import { prisma } from "@/lib/db/prisma";
|
||||
import { checkRateLimit } from "@/lib/security/rate-limit";
|
||||
import { secretsMatch } from "@/lib/security/secrets-match";
|
||||
|
||||
/**
|
||||
* Two credential shapes, both ending in the same finalizeOtp() call and
|
||||
* the same `{id, email, name, image}` return shape — see
|
||||
* src/lib/auth/otp.service.ts's own header for the full verify/finalize
|
||||
* design this builds on.
|
||||
*
|
||||
* `{ email, code }` — the normal path, used by sign-in, registration, and
|
||||
* (since the "administrator invite verifies via OTP too" work) invitation
|
||||
* acceptance as well: verifyOtp then finalizeOtp back to back in this one
|
||||
* request (functionally "verify and consume in one shot" from the
|
||||
* caller's point of view).
|
||||
*
|
||||
* `{ email, finalize: "true", token }` — used only by the installer's own
|
||||
* immediate post-completion sign-in, where the administrator's code was
|
||||
* already consumed by installApplication's own earlier verifyOtp call
|
||||
* (inside its transaction) and can't be re-entered (one-time use). `token`
|
||||
* must match the live INSTALLATION_TOKEN — required because
|
||||
* /api/auth/callback/credentials is a public route regardless of this
|
||||
* change (true of any Credentials provider), so this path must
|
||||
* independently prove the caller already holds a secret an attacker can't
|
||||
* have, not just trust that it's being called "from the right place".
|
||||
* Without this, a race window (someone else's verifiedAt-but-not-yet-
|
||||
* consumedAt row, mid-wizard) would be forgeable via a direct POST to that
|
||||
* route with just a guessed email. (Invitation acceptance no longer uses
|
||||
* this path — it has its own real OTP code to re-enter here instead, so
|
||||
* there's nothing left to trust a bare token for there.)
|
||||
*/
|
||||
function verifyFinalizeToken(token: string): boolean {
|
||||
if (!token) return false;
|
||||
|
||||
const installToken = process.env.INSTALLATION_TOKEN;
|
||||
return !!installToken && secretsMatch(token, installToken);
|
||||
}
|
||||
export { authorizeCredentials } from "@/lib/auth/authorize-credentials";
|
||||
|
||||
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
...authConfig,
|
||||
@@ -58,40 +24,17 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
email: { label: "Email", type: "email" },
|
||||
code: { label: "Code", type: "text" },
|
||||
// Only ever set by the installer's own trusted server-side sign-in
|
||||
// call — never rendered on any real form. See this file's own
|
||||
// header for the full finalize/token design.
|
||||
// call — never rendered on any real form. See
|
||||
// src/lib/auth/authorize-credentials.ts's own header for the full
|
||||
// finalize/token design.
|
||||
finalize: { label: "Finalize", type: "text" },
|
||||
token: { label: "Token", type: "text" },
|
||||
},
|
||||
authorize: async (raw) => {
|
||||
const email = typeof raw?.email === "string" ? raw.email.trim().toLowerCase() : "";
|
||||
if (!email) return null;
|
||||
|
||||
// Keyed by the attempted email, not IP — this is what actually
|
||||
// stops credential stuffing against one account regardless of how
|
||||
// many source addresses it's spread across. 30 attempts per 15
|
||||
// minutes is well above anything a real forgetful user would ever
|
||||
// hit, while still making online guessing impractical.
|
||||
const { allowed } = checkRateLimit(`signin:${email}`, { maxAttempts: 30, windowMs: 15 * 60 * 1000 });
|
||||
if (!allowed) return null;
|
||||
|
||||
if (raw?.finalize === "true") {
|
||||
const token = typeof raw?.token === "string" ? raw.token : "";
|
||||
if (!verifyFinalizeToken(token)) return null;
|
||||
if (!(await finalizeOtp(prisma, email))) return null;
|
||||
} else {
|
||||
const code = typeof raw?.code === "string" ? raw.code : "";
|
||||
if (!code) return null;
|
||||
const verified = await verifyOtp(prisma, email, code);
|
||||
if (!verified.ok) return null;
|
||||
if (!(await finalizeOtp(prisma, email))) return null;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
if (!user) return null;
|
||||
|
||||
return { id: user.id, email: user.email, name: user.name, image: user.image };
|
||||
},
|
||||
// Kept in its own module (not inlined here) so it's importable from
|
||||
// a plain Vitest test without dragging in this file's NextAuth(...)
|
||||
// construction, which needs the Next.js runtime — see
|
||||
// authorize-credentials.ts's own header.
|
||||
authorize: authorizeCredentials,
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
|
||||
Reference in new issue
Block a user