saget-auth-midleware
v1.0.26
Published
SSO Middleware untuk validasi authentifikasi domain malinau.go.id dan semua subdomain pada aplikasi Next.js 14 & 15
Maintainers
Readme
sso-malinau-nextjs-middleware
File middleware untuk validasi authentifikasi SSO Kabupaten Malinau pada aplikasi Next.js (App Router)
Instalasi
npm i sso-malinau-nextjs-middlewareKonfigurasi Environment Variables
Buat file .env.local di root project Next.js Anda:
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 AndaPenggunaan
1. Setup Middleware (JavaScript)
Buat file middleware.js di root project Next.js:
// 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:
// 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)
// 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)
// 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)
// 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)
// 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)
// 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)
// 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:
{
"user": {
"id": "c3d76c14-6e10-4c37-9a47-31852e28fd5d",
"phone": "085157541166",
"email": "[email protected]",
"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 lengkapreq.role- Role user dalam aplikasireq.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.
