mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
hanzo.id is an OIDC provider and returns an id_token. The provider declared idToken: false, so next-auth used the plain-OAuth2 callback and openid-client threw on a token response it could not account for: id_token detected in the response, you must use client.callback() instead of client.oauthCallback() A signing-in user saw only /api/auth/signin?error=OAuthCallback and a red "Try signing in with a different account." — which points at the account, and the account was never the problem. What made this easy to call working: the sign-in HANDOFF is healthy. The button reaches hanzo.id with the right client_id and an allowlisted redirect_uri, and a real login page renders. Only the trip back was broken, and nothing before the callback shows it. Verifying a login means completing one. profile() reads sub/name/email/picture/owner, all of which the id_token carries, so the mapping is unchanged; userinfo stays declared for the claims the token does not hold.
162 lines
4.7 KiB
TypeScript
162 lines
4.7 KiB
TypeScript
import { NextApiRequest, NextApiResponse } from "next";
|
|
|
|
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
|
import NextAuth, { type NextAuthOptions } from "next-auth";
|
|
import type { OAuthConfig } from "next-auth/providers/oauth";
|
|
|
|
import { identifyUser, trackAnalytics } from "@/lib/analytics";
|
|
import prisma from "@/lib/prisma";
|
|
import { CustomUser } from "@/lib/types";
|
|
|
|
const VERCEL_DEPLOYMENT = !!process.env.VERCEL_URL;
|
|
|
|
const IAM_URL = process.env.IAM_URL;
|
|
const IAM_CLIENT_ID = process.env.IAM_CLIENT_ID;
|
|
const IAM_CLIENT_SECRET = process.env.IAM_CLIENT_SECRET;
|
|
|
|
function HanzoIAMProvider(): OAuthConfig<any> {
|
|
const issuer = IAM_URL || "https://hanzo.id";
|
|
return {
|
|
id: "hanzo-iam",
|
|
name: process.env.IAM_PROVIDER_NAME || "Hanzo",
|
|
type: "oauth",
|
|
wellKnown: `${issuer}/.well-known/openid-configuration`,
|
|
clientId: IAM_CLIENT_ID || "",
|
|
clientSecret: IAM_CLIENT_SECRET || "",
|
|
authorization: { params: { scope: "openid profile email" } },
|
|
// hanzo.id IS an OIDC provider and returns an id_token, so the callback
|
|
// has to be the OIDC one. With this false, next-auth took the plain-OAuth2
|
|
// path and openid-client threw on the token response it could not account
|
|
// for — "id_token detected in the response, you must use client.callback()
|
|
// instead of client.oauthCallback()" — which surfaced to a signing-in user
|
|
// as `/api/auth/signin?error=OAuthCallback` and nothing else. The sign-in
|
|
// handoff LOOKED healthy the whole time: the button reached hanzo.id and
|
|
// rendered a real login. Only the trip back was broken.
|
|
idToken: true,
|
|
userinfo: { url: `${issuer}/v1/iam/oauth/userinfo` },
|
|
profile(profile) {
|
|
return {
|
|
id: profile.sub,
|
|
name:
|
|
profile.displayName ||
|
|
profile.name ||
|
|
profile.preferred_username,
|
|
email: profile.email,
|
|
image: profile.avatar || profile.picture,
|
|
organization: profile.owner || profile.organization || profile.org,
|
|
};
|
|
},
|
|
allowDangerousEmailAccountLinking: true,
|
|
};
|
|
}
|
|
|
|
export const authOptions: NextAuthOptions = {
|
|
pages: {
|
|
error: "/login",
|
|
},
|
|
providers: [HanzoIAMProvider()],
|
|
adapter: PrismaAdapter(prisma),
|
|
session: { strategy: "jwt" },
|
|
cookies: {
|
|
sessionToken: {
|
|
name: `${VERCEL_DEPLOYMENT ? "__Secure-" : ""}next-auth.session-token`,
|
|
options: {
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
path: "/",
|
|
domain: VERCEL_DEPLOYMENT ? ".dataroom.hanzo.ai" : undefined,
|
|
secure: VERCEL_DEPLOYMENT,
|
|
},
|
|
},
|
|
},
|
|
callbacks: {
|
|
jwt: async (params) => {
|
|
const { token, user, trigger } = params;
|
|
if (!token.email) {
|
|
return {};
|
|
}
|
|
if (user) {
|
|
token.user = user;
|
|
// Persist IAM organization claim from initial sign-in
|
|
if ((user as any).organization) {
|
|
token.organization = (user as any).organization;
|
|
}
|
|
}
|
|
// refresh the user data
|
|
if (trigger === "update") {
|
|
const user = token?.user as CustomUser;
|
|
const refreshedUser = await prisma.user.findUnique({
|
|
where: { id: user.id },
|
|
});
|
|
if (refreshedUser) {
|
|
token.user = refreshedUser;
|
|
} else {
|
|
return {};
|
|
}
|
|
|
|
if (refreshedUser?.email !== user.email) {
|
|
if (user.id && refreshedUser.email) {
|
|
await prisma.account.deleteMany({
|
|
where: { userId: user.id },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return token;
|
|
},
|
|
session: async ({ session, token }) => {
|
|
(session.user as CustomUser) = {
|
|
id: token.sub,
|
|
// @ts-ignore
|
|
...(token || session).user,
|
|
organization: token.organization as string | undefined,
|
|
};
|
|
return session;
|
|
},
|
|
},
|
|
events: {
|
|
async createUser(message) {
|
|
await identifyUser(message.user.email ?? message.user.id);
|
|
await trackAnalytics({
|
|
event: "User Signed Up",
|
|
email: message.user.email,
|
|
userId: message.user.id,
|
|
});
|
|
},
|
|
},
|
|
};
|
|
|
|
const getAuthOptions = (req: NextApiRequest): NextAuthOptions => {
|
|
return {
|
|
...authOptions,
|
|
callbacks: {
|
|
...authOptions.callbacks,
|
|
signIn: async ({ user }) => {
|
|
if (!user.email) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
},
|
|
events: {
|
|
...authOptions.events,
|
|
signIn: async (message) => {
|
|
await Promise.allSettled([
|
|
identifyUser(message.user.email ?? message.user.id),
|
|
trackAnalytics({
|
|
event: "User Signed In",
|
|
email: message.user.email,
|
|
}),
|
|
]);
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
export default async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse,
|
|
) {
|
|
return NextAuth(req, res, getAuthOptions(req));
|
|
}
|