sfsso-frappe

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-auth

2. 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_LABEL di-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