UNPKG

saget-auth-midleware

Version:

SSO Middleware untuk validasi authentifikasi domain malinau.go.id dan semua subdomain pada aplikasi Next.js 14 & 15

520 lines (429 loc) 13.2 kB
# sso-malinau-nextjs-middleware File middleware untuk validasi authentifikasi SSO Kabupaten Malinau pada aplikasi Next.js (App Router) ## Instalasi ```bash npm i sso-malinau-nextjs-middleware ``` ## Konfigurasi Environment Variables Buat file `.env.local` di root project Next.js Anda: ```env SSO_APP_KEY=sso SSO_JWT_SECRET=your-jwt-secret-here # Ganti dengan secret dari SSO Server Anda COOKIE_ACCESS_TOKEN_NAME=access_token # Ganti dengan nama cookie access token Anda COOKIE_REFRESH_TOKEN_NAME=refresh_token # Ganti dengan nama cookie refresh token Anda SSO_LOGIN_URL=https://sso.malinau.go.id/login SSO_API_URL=https://sso.malinau.go.id/api-v1 NEXT_REDIRECT_URL=http://localhost:3000 ## Ganti dengan URL aplikasi Anda ``` ## Penggunaan ### 1. Setup Middleware (JavaScript) Buat file `middleware.js` di root project Next.js: ```javascript // middleware.js import SSOMiddleware from 'sso-malinau-nextjs-middleware'; export function middleware(request) { // Proteksi semua route kecuali public routes if (request.nextUrl.pathname.startsWith('/api/auth') || request.nextUrl.pathname.startsWith('/login') || request.nextUrl.pathname === '/') { return; } return SSOMiddleware.withSSOValidation((req) => { // Middleware berhasil, lanjutkan ke route console.log('User authenticated:', req.user.email); console.log('User role:', req.role); console.log('User subrole:', req.subrole); })(request); } export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - api/auth (auth routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) */ '/((?!api/auth|_next/static|_next/image|favicon.ico).*)', ], }; ``` ### 2. Setup Middleware (TypeScript) Buat file `middleware.ts` di root project Next.js: ```typescript // middleware.ts import { NextRequest, NextResponse } from 'next/server'; import SSOMiddleware from 'sso-malinau-nextjs-middleware'; // Type definitions untuk payload SSO interface UserProfile { id: string; userId: string; name: string; externalId: string | null; address: string; village: string | null; district: string | null; city: string; province: string; } interface UserApplication { id: string; applicationName: string; applicationKey: string; organization: string; organizationAddress: string | null; organizationContact: string | null; url: string; callbackUrl: string; isPublic: boolean; status: string; createdAt: string; updatedAt: string; role: string; subrole: string; } interface SSOUser { id: string; phone: string; email: string; type: string; identity: string; status: string; lastLogin: string; createdAt: string; updatedAt: string; profile: UserProfile; application: UserApplication; } interface SSOPayload { user: SSOUser; iat: number; exp: number; } // Extend NextRequest untuk menambahkan properties SSO declare module 'next/server' { interface NextRequest { user?: SSOUser; role?: string; subrole?: string; } } export function middleware(request: NextRequest) { // Proteksi semua route kecuali public routes if (request.nextUrl.pathname.startsWith('/api/auth') || request.nextUrl.pathname.startsWith('/login') || request.nextUrl.pathname === '/') { return; } return SSOMiddleware.withSSOValidation((req: NextRequest) => { // Middleware berhasil, lanjutkan ke route console.log('User authenticated:', req.user?.email); console.log('User role:', req.role); console.log('User subrole:', req.subrole); return NextResponse.next(); })(request); } export const config = { matcher: [ '/((?!api/auth|_next/static|_next/image|favicon.ico).*)', ], }; ``` ### 3. Menggunakan Data User di API Routes (JavaScript) ```javascript // app/api/profile/route.js import SSOMiddleware from 'sso-malinau-nextjs-middleware'; export async function GET(request) { try { // Ambil payload user dari JWT const payload = await SSOMiddleware.getPayload(request); if (!payload) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); } return Response.json({ user: payload.user, role: payload.user.application.role, subrole: payload.user.application.subrole }); } catch (error) { return Response.json({ error: 'Internal Server Error' }, { status: 500 }); } } ``` ### 4. Menggunakan Data User di API Routes (TypeScript) ```typescript // app/api/profile/route.ts import { NextRequest } from 'next/server'; import SSOMiddleware from 'sso-malinau-nextjs-middleware'; export async function GET(request: NextRequest) { try { // Ambil payload user dari JWT const payload = await SSOMiddleware.getPayload(request); if (!payload) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); } return Response.json({ user: payload.user, role: payload.user.application.role, subrole: payload.user.application.subrole }); } catch (error) { return Response.json({ error: 'Internal Server Error' }, { status: 500 }); } } ``` ### 5. Menggunakan Data User di Server Components (JavaScript) ```javascript // app/dashboard/page.js import { cookies } from 'next/headers'; import SSOMiddleware from 'sso-malinau-nextjs-middleware'; export default async function DashboardPage() { const cookieStore = cookies(); // Buat mock request object untuk getPayload const mockRequest = { cookies: { get: (name) => cookieStore.get(name) } }; const payload = await SSOMiddleware.getPayload(mockRequest); if (!payload) { return <div>Unauthorized</div>; } return ( <div> <h1>Dashboard</h1> <div> <h2>User Information</h2> <p>Name: {payload.user.profile.name}</p> <p>Email: {payload.user.email}</p> <p>Phone: {payload.user.phone}</p> <p>Role: {payload.user.application.role}</p> <p>Subrole: {payload.user.application.subrole}</p> <p>Organization: {payload.user.application.organization}</p> </div> </div> ); } ``` ### 6. Menggunakan Data User di Server Components (TypeScript) ```typescript // app/dashboard/page.tsx import { cookies } from 'next/headers'; import SSOMiddleware from 'sso-malinau-nextjs-middleware'; interface MockRequest { cookies: { get: (name: string) => { value: string } | undefined; }; } export default async function DashboardPage() { const cookieStore = cookies(); // Buat mock request object untuk getPayload const mockRequest: MockRequest = { cookies: { get: (name: string) => cookieStore.get(name) } }; const payload = await SSOMiddleware.getPayload(mockRequest); if (!payload) { return <div>Unauthorized</div>; } return ( <div> <h1>Dashboard</h1> <div> <h2>User Information</h2> <p>Name: {payload.user.profile.name}</p> <p>Email: {payload.user.email}</p> <p>Phone: {payload.user.phone}</p> <p>Role: {payload.user.application.role}</p> <p>Subrole: {payload.user.application.subrole}</p> <p>Organization: {payload.user.application.organization}</p> </div> </div> ); } ``` ### 7. Menggunakan Data User di Client Components (JavaScript) ```javascript // app/components/UserProfile.js 'use client'; import { useState, useEffect } from 'react'; export default function UserProfile() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch('/api/profile') .then(res => res.json()) .then(data => { setUser(data.user); setLoading(false); }) .catch(err => { console.error('Error fetching user:', err); setLoading(false); }); }, []); if (loading) return <div>Loading...</div>; if (!user) return <div>No user data</div>; return ( <div className="user-profile"> <h3>Profile Information</h3> <div> <p><strong>Name:</strong> {user.profile.name}</p> <p><strong>Email:</strong> {user.email}</p> <p><strong>Phone:</strong> {user.phone}</p> <p><strong>Address:</strong> {user.profile.address}</p> <p><strong>City:</strong> {user.profile.city}</p> <p><strong>Province:</strong> {user.profile.province}</p> <p><strong>Role:</strong> {user.application.role}</p> <p><strong>Subrole:</strong> {user.application.subrole}</p> <p><strong>Organization:</strong> {user.application.organization}</p> </div> </div> ); } ``` ### 8. Menggunakan Data User di Client Components (TypeScript) ```typescript // app/components/UserProfile.tsx 'use client'; import { useState, useEffect } from 'react'; interface UserProfile { id: string; userId: string; name: string; externalId: string | null; address: string; village: string | null; district: string | null; city: string; province: string; } interface UserApplication { id: string; applicationName: string; applicationKey: string; organization: string; organizationAddress: string | null; organizationContact: string | null; url: string; callbackUrl: string; isPublic: boolean; status: string; createdAt: string; updatedAt: string; role: string; subrole: string; } interface User { id: string; phone: string; email: string; type: string; identity: string; status: string; lastLogin: string; createdAt: string; updatedAt: string; profile: UserProfile; application: UserApplication; } export default function UserProfile() { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState<boolean>(true); useEffect(() => { fetch('/api/profile') .then(res => res.json()) .then(data => { setUser(data.user); setLoading(false); }) .catch(err => { console.error('Error fetching user:', err); setLoading(false); }); }, []); if (loading) return <div>Loading...</div>; if (!user) return <div>No user data</div>; return ( <div className="user-profile"> <h3>Profile Information</h3> <div> <p><strong>Name:</strong> {user.profile.name}</p> <p><strong>Email:</strong> {user.email}</p> <p><strong>Phone:</strong> {user.phone}</p> <p><strong>Address:</strong> {user.profile.address}</p> <p><strong>City:</strong> {user.profile.city}</p> <p><strong>Province:</strong> {user.profile.province}</p> <p><strong>Role:</strong> {user.application.role}</p> <p><strong>Subrole:</strong> {user.application.subrole}</p> <p><strong>Organization:</strong> {user.application.organization}</p> </div> </div> ); } ``` ## Struktur Payload SSO Payload yang dikembalikan oleh SSO memiliki struktur sebagai berikut: ```json { "user": { "id": "c3d76c14-6e10-4c37-9a47-31852e28fd5d", "phone": "085157541166", "email": "pkpvicky@gmail.com", "type": "NIK", "identity": "12345678", "status": "VALIDATED", "lastLogin": "2025-06-29T18:08:22.699Z", "createdAt": "2025-06-25T08:23:34.013Z", "updatedAt": "2025-06-29T18:08:22.699Z", "profile": { "id": "861a6a92-9fc2-44c5-8f9c-53cc61708db2", "userId": "c3d76c14-6e10-4c37-9a47-31852e28fd5d", "name": "Vicky", "externalId": null, "address": "Jl. Proklamasi", "village": null, "district": null, "city": "Pangkalpinang", "province": "Babel" }, "applications": [ { "id": "976bc488-f77d-4491-958f-bfecc9fa9da1", "applicationKey": "sso", "url": "https://sso.malinau.go.id", "callbackUrl": "/auth/callback", "isPublic": true, "role": "SUPER ADMIN", "subrole": "DEVELOPER SSO" } ] }, "iat": 1751220502, "exp": 1751224102 } ``` ## API Methods ### `sso.withSSOValidation(handler)` Middleware utama untuk validasi SSO. Menambahkan properties berikut ke request object: - `req.user` - Data user lengkap - `req.role` - Role user dalam aplikasi - `req.subrole` - Subrole user dalam aplikasi ### `sso.getPayload(request)` Mengambil payload JWT dari request. Mengembalikan object payload atau `null` jika tidak valid. ### `sso.getPayloadFromHeaders(request)` Mengambil payload JWT dari headers (untuk server-side usage). ## Troubleshooting ### 1. Error "Module not found: Can't resolve 'next/server'" Pastikan Anda menggunakan Next.js versi 12 atau lebih baru. ### 2. Error "ERR_REQUIRE_ESM" Pastikan Anda menggunakan import statement, bukan require. ### 3. Token tidak valid Pastikan environment variables sudah dikonfigurasi dengan benar, terutama `SSO_JWT_SECRET` dan `SSO_APP_KEY`. ### 4. Redirect loop Pastikan route login dan callback tidak diproteksi oleh middleware. ## Contoh Implementasi Lengkap Untuk contoh implementasi lengkap, lihat folder `examples/` di repository ini. ## Support Untuk pertanyaan dan dukungan, silakan hubungi tim Diskominfo Kabupaten Malinau.