feat: add Hanzo IAM OIDC SSO provider

- Add HanzoIAMProvider to NextAuth config (OIDC with userinfo endpoint)
- Add "Sign in with Hanzo" button to login page (above passkey/Google)
- Add IS_HANZO_IAM_ENABLED constant
- Env vars: HANZO_IAM_URL, HANZO_IAM_CLIENT_ID, HANZO_IAM_CLIENT_SECRET
- IAM app registered as app-captable in init_data.json
This commit is contained in:
Hanzo Dev
2026-03-01 15:36:41 -08:00
parent e5204b5edb
commit 40d4c5e562
5 changed files with 65 additions and 4 deletions
+8 -1
View File
@@ -58,4 +58,11 @@ yarn-error.log*
# Sentry Config File
.env.sentry-build-plugin
notes.md
notes.md
AGENTS.md
CLAUDE.md
GEMINI.md
GROK.md
QWEN.md
+2 -2
View File
@@ -1,5 +1,5 @@
import SignInForm from "@/components/onboarding/signin";
import { IS_GOOGLE_AUTH_ENABLED } from "@/constants/auth";
import { IS_GOOGLE_AUTH_ENABLED, IS_HANZO_IAM_ENABLED } from "@/constants/auth";
import { getServerComponentAuthSession } from "@/server/auth";
import type { Metadata } from "next";
import { redirect } from "next/navigation";
@@ -19,5 +19,5 @@ export default async function SignIn() {
return redirect("/onboarding");
}
return <SignInForm isGoogleAuthEnabled={IS_GOOGLE_AUTH_ENABLED} />;
return <SignInForm isGoogleAuthEnabled={IS_GOOGLE_AUTH_ENABLED} isHanzoIAMEnabled={IS_HANZO_IAM_ENABLED} />;
}
+20 -1
View File
@@ -35,9 +35,10 @@ const loginSchema = z.object({
interface LoginFormProps {
isGoogleAuthEnabled: boolean;
isHanzoIAMEnabled?: boolean;
}
const SignInForm = ({ isGoogleAuthEnabled }: LoginFormProps) => {
const SignInForm = ({ isGoogleAuthEnabled, isHanzoIAMEnabled }: LoginFormProps) => {
const router = useRouter();
const [isPasskeyLoading, setIsPasskeyLoading] = useState<boolean>(false);
@@ -108,11 +109,29 @@ const SignInForm = ({ isGoogleAuthEnabled }: LoginFormProps) => {
await signIn("google", { callbackUrl: "/onboarding" });
}
async function signInWithHanzo() {
await signIn("hanzo-iam", { callbackUrl: "/onboarding" });
}
return (
<div className="flex h-screen items-center justify-center bg-gradient-to-br from-indigo-50 via-white to-cyan-100">
<div className="grid w-full max-w-md grid-cols-1 gap-5 rounded-xl border bg-white p-10 shadow">
<AuthFormHeader page="signin" />
<>
{isHanzoIAMEnabled && (
<Button
disabled={isSubmitting}
type="button"
onClick={signInWithHanzo}
className="bg-red-500 hover:bg-red-600 text-white"
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
Sign in with <span className="font-bold">Hanzo</span>
</Button>
)}
<Button
disabled={isSubmitting}
loading={isPasskeyLoading}
+2
View File
@@ -3,3 +3,5 @@ import { env } from "@/env";
export const IS_GOOGLE_AUTH_ENABLED = !!(
env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET
);
export const IS_HANZO_IAM_ENABLED = !!process.env.HANZO_IAM_CLIENT_ID;
+33
View File
@@ -18,11 +18,43 @@ import {
import type { MemberStatusEnum } from "@/prisma/enums";
import { type TPrismaOrTransaction, db } from "@/server/db";
import { verifyAuthenticationResponse } from "@simplewebauthn/server";
import type { OAuthConfig } from "next-auth/providers/oauth";
import CredentialsProvider from "next-auth/providers/credentials";
import GoogleProvider from "next-auth/providers/google";
import { cache } from "react";
import { getUserByEmail, getUserById } from "./user";
const HANZO_IAM_URL = process.env.HANZO_IAM_URL;
const HANZO_IAM_CLIENT_ID = process.env.HANZO_IAM_CLIENT_ID;
const HANZO_IAM_CLIENT_SECRET = process.env.HANZO_IAM_CLIENT_SECRET;
function HanzoIAMProvider(): OAuthConfig<any> {
const issuer = HANZO_IAM_URL || "https://hanzo.id";
return {
id: "hanzo-iam",
name: process.env.HANZO_IAM_PROVIDER_NAME || "Hanzo",
type: "oauth",
wellKnown: `${issuer}/.well-known/openid-configuration`,
clientId: HANZO_IAM_CLIENT_ID || "",
clientSecret: HANZO_IAM_CLIENT_SECRET || "",
authorization: { params: { scope: "openid profile email" } },
idToken: false,
userinfo: { url: `${issuer}/oauth/userinfo` },
profile(profile) {
return {
id: profile.sub,
name:
profile.displayName ||
profile.name ||
profile.preferred_username,
email: profile.email,
image: profile.avatar || profile.picture,
};
},
allowDangerousEmailAccountLinking: true,
};
}
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
export const JWT_SECRET = new TextEncoder().encode(env.NEXTAUTH_SECRET);
@@ -151,6 +183,7 @@ export const authOptions: NextAuthOptions = {
strategy: "jwt",
},
providers: [
...(HANZO_IAM_CLIENT_ID ? [HanzoIAMProvider()] : []),
CredentialsProvider({
name: "Credentials",
credentials: {