From 1274ee747126e40bd30cb9013d3f377ef861725b Mon Sep 17 00:00:00 2001 From: Nicholas Sliter <78503029+Nicholas-Sliter@users.noreply.github.com> Date: Sat, 7 Feb 2026 11:49:59 -0500 Subject: [PATCH] custom override auth --- .env.local | 4 +- src/pages/api/auth/[...nextauth].ts | 97 +++++++++++++++++++++++++++-- src/pages/auth/signin.jsx | 55 +++++++++++++--- src/pages/schedule/index.tsx | 3 +- 4 files changed, 145 insertions(+), 14 deletions(-) diff --git a/.env.local b/.env.local index ca0479d..c0e07be 100644 --- a/.env.local +++ b/.env.local @@ -3,4 +3,6 @@ GOOGLE_CLIENT_ID=1065709499536-f5g11l8g2v32eu02eh0q6t723uj4pcvv.apps.googleuserc GOOGLE_CLIENT_SECRET=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 HOST_NAME=localhost INTERNAL_AUTH=ArandomVeryLongSecret -NOTE=This_file_must_never_contain_production_secrets \ No newline at end of file +NOTE=This_file_must_never_contain_production_secrets +ADMIN_API_KEY=supersecretadminkey +NEXTAUTH_SECRET=anotherlongsecretfornextauth \ No newline at end of file diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index 34b5d57..23c196b 100644 --- a/src/pages/api/auth/[...nextauth].ts +++ b/src/pages/api/auth/[...nextauth].ts @@ -1,7 +1,16 @@ import NextAuth from "next-auth"; -//import Auth0Provider from "next-auth/providers/auth0"; import GoogleProvider from "next-auth/providers/google"; -import type { Session } from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import { timingSafeEqual } from "crypto"; + + +// Ensures constant-time comparison for strings +const safeCompare = (a, b) => { + if (a.length !== b.length) return false; + return timingSafeEqual(new Uint8Array(Buffer.from(a, "utf8")), new Uint8Array(Buffer.from(b, "utf8"))); +}; + + import { checkIfUserExists, @@ -9,9 +18,15 @@ import { getUserByEmail, } from "../../../lib/backend/database-utils"; import { CustomSession } from "../../../lib/common/types"; -//import { canWriteReviews } from "../../../lib/backend/utils"; async function signIn({ profile, user, account }) { + console.log("Sign-in attempt:", { profile, user, account }); + + if (account.provider === "credentials" && user.isAdminOverrideSession) { + console.log("Admin override sign-in successful for user:", user.email); + return true; // Allow sign-in for admin override sessions without further checks + } + //validate the email is an email const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -45,7 +60,17 @@ async function signIn({ profile, user, account }) { return true; } -async function session({ session, user }) { +async function session({ session, user, token }): Promise { + if (token.isAdminOverrideSession) { + console.log("Admin override session detected for user:", session.user.email); + session.user.id = token?.user?.id; + session.user.role = token?.user?.role; + session.user.authorized = token?.user?.authorized; + session.user.admin = token?.user?.admin; + session.user.banned = token?.user?.banned; + return session as CustomSession; + } + const u = await getUserByEmail(session.user.email); session.user.id = u?.userID; @@ -70,12 +95,76 @@ export default NextAuth({ clientSecret: process.env.GOOGLE_CLIENT_SECRET, checks: "state", }), + // Admin credentials provider for service accounts + CredentialsProvider({ + name: "Admin Credentials", + credentials: { + email: { label: "Assumed Email", type: "text" }, + apiKey: { label: "API Key", type: "password" }, + role: { label: "Role", type: "text" }, + }, + async authorize(credentials, req) { + console.log("Attempting admin sign in with credentials:", JSON.stringify(credentials)); + const { email, apiKey, role } = credentials; + + if (!email || !apiKey || !role) { + throw new Error("Email, API key, and role are required"); + } + + if (!["student", "faculty"].includes(role)) { + throw new Error("Invalid role"); + } + + // Compare API key with secret + if (safeCompare(apiKey, process.env.ADMIN_API_KEY)) { + + // Email lookup for id matching, otherwise return a default admin user with id 0 (not ideal, but allows for flexibility in admin accounts without database entries) + const existingUser = await getUserByEmail(email); + if (existingUser) { + return { + id: existingUser.userID, + email, + role: existingUser.userType, + authorized: existingUser.canReadReviews as boolean, + admin: existingUser.admin, + banned: existingUser.banned, + isAdminOverrideSession: true, // Custom flag to identify admin sessions + }; + } + + // Return a user object with admin role + return { + id: '00000000-0000-0000-0000-000000000000', + email, + role: "admin", + authorized: true, + admin: true, + banned: false, + isAdminOverrideSession: true, // Custom flag to identify admin sessions + }; + } + + throw new Error("Invalid API key"); + + } + }), ], secret: process.env.NEXTAUTH_SECRET, callbacks: { signIn: signIn, session: session, redirect: redirect, + jwt: async ({ token, user, account }) => { + // If this is an admin override session, add the flag to the token + if ((user as any)?.isAdminOverrideSession) { + token.isAdminOverrideSession = true; + } + if (user) { + token.user = user; + } + + return token; + } }, pages: { newUser: "/auth/signup", diff --git a/src/pages/auth/signin.jsx b/src/pages/auth/signin.jsx index f4d7ddd..55f9b33 100644 --- a/src/pages/auth/signin.jsx +++ b/src/pages/auth/signin.jsx @@ -1,7 +1,10 @@ import { getProviders, signIn, useSession } from "next-auth/react"; import Router from "next/router"; + export default function SignIn({ providers }) { + const url = typeof window !== "undefined" ? window.location.href : ""; + const adminLogin = new URLSearchParams(url.split("?")[1]).get("adminLogin") === "true"; const { data: session, status } = useSession() if (status === "authenticated") { @@ -9,15 +12,53 @@ export default function SignIn({ providers }) { } + return ( <> - {Object.values(providers).map((provider) => ( -
- -
- ))} + {Object.values(providers).map((provider) => { + if (!adminLogin && provider.id === "credentials") { + return null; // Don't show credentials provider on regular login page + } + + if (adminLogin && provider.id === "credentials") { + // Render login form + return ( +
+

Admin Login

+
{ + e.preventDefault(); + const email = e.target.email.value; + const apiKey = e.target.apiKey.value; + const role = e.target.role.value; + signIn(provider.id, { email, apiKey, role }); + } + }> +
+ + +
+
+ + +
+
+ + +
+ +
+
+ ) + } + + return ( +
+ +
+ ) + })} ); } diff --git a/src/pages/schedule/index.tsx b/src/pages/schedule/index.tsx index add6258..03fae39 100644 --- a/src/pages/schedule/index.tsx +++ b/src/pages/schedule/index.tsx @@ -28,10 +28,9 @@ import useLocalStorage from "../../hooks/useLocalStorage"; import useSetAnalyticsFlag from "../../hooks/useSetAnalyticsFlag"; export async function getServerSideProps(context) { - const session = await getSession(context) as CustomSession; const currentTerms = await getAvailableTermsForSchedulePlanning(); - const term = (context.query.term ?? currentTerms[0]) as string; + const term = (context?.query?.term ?? currentTerms[0]) as string; const authorized = session?.user?.authorized ?? false;