next-auth v5 beta ignores redirectProxyUrl when constructing the redirect_uri sent to Microsoft — it strips the pathname from AUTH_URL and uses only the origin. Passing redirect_uri directly in authorization.params guarantees the /hp-prod-tracker basePath is included in the callback URL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
2.9 KiB
TypeScript
86 lines
2.9 KiB
TypeScript
import NextAuth from "next-auth";
|
|
import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id";
|
|
import { PrismaAdapter } from "@auth/prisma-adapter";
|
|
import { prisma } from "@/lib/prisma";
|
|
import type { Role } from "@/generated/prisma/client";
|
|
|
|
// next-auth v5 beta ignores redirectProxyUrl when building the redirect_uri
|
|
// sent to the OAuth provider — it strips the pathname from AUTH_URL and appends
|
|
// basePath directly to the origin. We must pass redirect_uri explicitly so the
|
|
// /hp-prod-tracker basePath is included in the Microsoft callback URL.
|
|
const explicitRedirectUri = process.env.AUTH_URL
|
|
? `${process.env.AUTH_URL}/api/auth/callback/microsoft-entra-id`
|
|
: undefined;
|
|
|
|
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
// Explicit basePath prevents AUTH_URL's pathname from overriding it.
|
|
// Next.js strips the /hp-prod-tracker prefix before Auth.js sees the request,
|
|
// so internally routes must match /api/auth/*.
|
|
basePath: "/api/auth",
|
|
adapter: PrismaAdapter(prisma),
|
|
providers: [
|
|
MicrosoftEntraID({
|
|
clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID,
|
|
clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET,
|
|
issuer: `https://login.microsoftonline.com/${process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID}/v2.0`,
|
|
// Safe for Entra ID — Microsoft verifies organizational emails.
|
|
// Required to link SSO accounts to pre-seeded User records by email match.
|
|
allowDangerousEmailAccountLinking: true,
|
|
// Explicitly set redirect_uri so /hp-prod-tracker basePath is included.
|
|
// next-auth v5 beta strips the pathname from AUTH_URL otherwise.
|
|
...(explicitRedirectUri && {
|
|
authorization: { params: { redirect_uri: explicitRedirectUri } },
|
|
}),
|
|
}),
|
|
],
|
|
session: {
|
|
strategy: "database",
|
|
},
|
|
events: {
|
|
async signIn({ user }) {
|
|
if (!user.id || !user.email) return;
|
|
|
|
// Auto-assign organization by email domain match
|
|
const dbUser = await prisma.user.findUnique({
|
|
where: { id: user.id },
|
|
select: { organizationId: true, email: true },
|
|
});
|
|
|
|
if (dbUser?.organizationId || !dbUser?.email) return;
|
|
|
|
const domain = dbUser.email.split("@")[1];
|
|
if (!domain) return;
|
|
|
|
const org = await prisma.organization.findFirst({
|
|
where: { domain },
|
|
});
|
|
|
|
if (org) {
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { organizationId: org.id },
|
|
});
|
|
}
|
|
},
|
|
},
|
|
callbacks: {
|
|
async session({ session, user }) {
|
|
// Fetch user with role and org from database
|
|
const dbUser = await prisma.user.findUnique({
|
|
where: { id: user.id },
|
|
select: { role: true, organizationId: true },
|
|
});
|
|
|
|
if (dbUser) {
|
|
session.user.id = user.id;
|
|
session.user.role = dbUser.role;
|
|
session.user.organizationId = dbUser.organizationId;
|
|
}
|
|
|
|
return session;
|
|
},
|
|
},
|
|
pages: {
|
|
signIn: "/login",
|
|
},
|
|
});
|