admin / Synapse-Sonar
publicAttack Surface Simulation
Synapse-Sonar / synapse-sonar / lib / authOptions.ts
2983 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | import type { NextAuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import bcrypt from "bcryptjs"; import { authenticator } from "otplib"; import { prisma } from "@/lib/prisma"; import { decryptJson, EncryptedBlob } from "@/lib/crypto"; /** * Tenant-aware credential auth. The login form must supply tenant slug, email, * password, and (when MFA is enabled) a TOTP token. The resolved tenantId/role * are carried in the JWT so every API route can scope by them. */ export const authOptions: NextAuthOptions = { session: { strategy: "jwt" }, pages: { signIn: "/login" }, providers: [ CredentialsProvider({ name: "Credentials", credentials: { username: { label: "Username", type: "text" }, password: { label: "Password", type: "password" }, totp: { label: "MFA Code", type: "text" }, }, async authorize(credentials) { if (!credentials?.username || !credentials.password) { return null; } // Username is the global login identifier; the tenant is derived from it. const user = await prisma.user.findUnique({ where: { username: credentials.username }, }); if (!user || user.status !== "ACTIVE" || !user.passwordHash) return null; const ok = await bcrypt.compare(credentials.password, user.passwordHash); if (!ok) return null; // Enforce MFA ONLY when the user has enabled it. Users without MFA are // never asked for a code. Throw distinct errors so the UI can tell // "needs a code" apart from "wrong password". if (user.mfaEnabled && user.mfaSecret) { if (!credentials.totp) { throw new Error("MFA_REQUIRED"); } const secret = decryptJson<{ secret: string }>( user.mfaSecret as unknown as EncryptedBlob ).secret; if (!authenticator.check(credentials.totp, secret)) { throw new Error("MFA_INVALID"); } } await prisma.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() }, }); return { id: user.id, username: user.username, email: user.email ?? undefined, name: user.name ?? undefined, tenantId: user.tenantId, role: user.role, } as any; }, }), ], callbacks: { async jwt({ token, user }) { if (user) { token.tenantId = (user as any).tenantId; token.role = (user as any).role; token.username = (user as any).username; } return token; }, async session({ session, token }) { if (session.user) { (session.user as any).id = token.sub; (session.user as any).tenantId = token.tenantId; (session.user as any).role = token.role; (session.user as any).username = token.username; } return session; }, }, }; |