Next.js + Auth.js v5
Integrasi paling sederhana. Karena Next.js punya server sendiri, tidak perlu backend terpisah — API routes menangani pertukaran OAuth secara otomatis melalui Auth.js.
1. Instal
npm install sso-frappe next-auth2. Environment variables
FRAPPE_SSO_BASE_URL=https://erp.example.com
FRAPPE_SSO_CLIENT_ID=<oauth-client-id>
FRAPPE_SSO_CLIENT_SECRET=<oauth-client-secret>
NEXTAUTH_URL=https://app.example.com
FRAPPE_SSO_BUTTON_LABEL=Login with Frappe
NEXT_PUBLIC_FRAPPE_SSO_BUTTON_LABEL=Login with Frappe
NEXT_PUBLIC_FRAPPE_SSO_BUTTON_LABELdi-embed saat build time. Jika label berbeda antar lingkungan, hardcode di komponen button.
3. Konfigurasi Auth.js
// auth.ts
import NextAuth from 'next-auth';
import { frappeNextAuthProvider } from 'sso-frappe/next-auth';
import type { Provider } from 'next-auth/providers';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
frappeNextAuthProvider({
baseUrl: process.env.FRAPPE_SSO_BASE_URL!,
clientId: process.env.FRAPPE_SSO_CLIENT_ID!,
clientSecret: process.env.FRAPPE_SSO_CLIENT_SECRET!,
redirectUri: `${process.env.NEXTAUTH_URL}/api/auth/callback/frappe`,
buttonLabel: process.env.FRAPPE_SSO_BUTTON_LABEL ?? 'Login with Frappe',
}) as unknown as Provider,
],
// ...callbacks, session, dll
});Route handler:
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;4. Login button
// components/LoginButton.tsx
'use client';
import { signIn } from 'next-auth/react';
export default function LoginButton() {
return (
<button onClick={() => signIn('frappe', { callbackUrl: '/' })} className="...">
Login with Frappe
</button>
);
}5. Melindungi halaman / mengambil session
// middleware.ts — proteksi route
export { auth as middleware } from '@/auth';
export const config = {
matcher: ['/dashboard/:path*'],
};// app/dashboard/page.tsx
import { auth } from '@/auth';
export default async function Dashboard() {
const session = await auth();
return <h1>Welcome, {session?.user?.name}</h1>;
}6. Auto-create user saat login pertama
Gunakan callback signIn (bukan jwt, bukan session) untuk membuat user lokal.
// auth.ts
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcryptjs';
async signIn({ user, account }) {
if (account?.provider !== 'frappe') return true;
const email = user.email;
if (!email) return false;
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) {
if (existing.status !== 'AKTIF') return false;
return true;
}
// Password: random bcrypt hash — biar kolom NOT NULL terisi,
// tapi akun tidak bisa login pakai password sungguhan.
const randomHash = await bcrypt.hash(
crypto.randomUUID() + crypto.randomUUID(),
10,
);
await prisma.user.create({
data: {
email,
name: user.name,
passwordHash: randomHash,
role: process.env.FRAPPE_SSO_DEFAULT_ROLE ?? 'user',
status: 'AKTIF',
},
});
return true;
}Lihat database schema untuk schema user_identities yang direkomendasikan.
Edge Runtime warning
Callback signIn() / jwt() / session() berjalan di Edge Runtime (middleware). Prisma dan driver DB Node-only akan crash di sana. Pindahkan operasi DB ke route API biasa.
Troubleshooting
| Gejala | Sebab / Solusi |
|---|---|
/signin?error=Configuration |
Provider tidak punya issuer dan authorization — gunakan type: 'oauth', bukan 'oidc' |
"issuer" property does not match |
Discovery Frappe mengembalikan http:// saat diakses via https — gunakan endpoint eksplisit (sudah ditangani adapter) |
Token exchange gagal: WWW-Authenticate challenge |
Frappe butuh client_secret_post, bukan Basic auth — sudah ditangani adapter |
Validasi id_token gagal |
Frappe pakai HS256, oauth4webapi mengharap RS256 — adapter men-strip id_token |
Login kembali ke /signin |
Callback NextAuth melakukan operasi DB di Edge Runtime — jaga callback bebas DB |