saget-auth-midleware
Version:
SSO Middleware untuk validasi authentifikasi domain malinau.go.id dan semua subdomain pada aplikasi Next.js 14 & 15
235 lines (205 loc) • 8.02 kB
JavaScript
// index.js
const { NextResponse } = require('next/server');
class SSOMiddleware {
constructor() {
// Validasi environment variables
if (!process.env.SSO_LOGIN_URL) {
console.log('WARNING: SSO_LOGIN_URL environment variable is empty or not set');
}
if (!process.env.SSO_APP_KEY) {
console.log('WARNING: SSO_APP_KEY environment variable is empty or not set');
}
if (!process.env.SSO_JWT_SECRET) {
console.log('WARNING: SSO_JWT_SECRET environment variable is empty or not set');
}
if (!process.env.COOKIE_ACCESS_TOKEN_NAME) {
console.log('WARNING: COOKIE_ACCESS_TOKEN_NAME environment variable is empty or not set');
}
if (!process.env.COOKIE_REFRESH_TOKEN_NAME) {
console.log('WARNING: COOKIE_REFRESH_TOKEN_NAME environment variable is empty or not set');
}
if (!process.env.SSO_API_URL) {
console.log('WARNING: SSO_API_URL environment variable is empty or not set');
}
if (!process.env.COOKIE_DOMAIN) {
console.log('WARNING: COOKIE_DOMAIN environment variable is empty or not set');
}
if (!process.env.NEXT_REDIRECT_URL) {
console.log('WARNING: NEXT_REDIRECT_URL environment variable is empty or not set');
}
this.SSO_LOGIN_URL = process.env.SSO_LOGIN_URL;
this.APP_KEY = process.env.SSO_APP_KEY;
this.JWT_SECRET = new TextEncoder().encode(process.env.SSO_JWT_SECRET);
this.COOKIE_ACCESS_TOKEN_NAME = process.env.COOKIE_ACCESS_TOKEN_NAME;
this.COOKIE_REFRESH_TOKEN_NAME = process.env.COOKIE_REFRESH_TOKEN_NAME;
this.SSO_API_URL = process.env.SSO_API_URL;
this.NEXT_REDIRECT_URL = process.env.NEXT_REDIRECT_URL;
this.COOKIE_DOMAIN = process.env.COOKIE_DOMAIN;
}
/**
* Middleware utama untuk validasi SSO
* @param {Function} handler - Next.js middleware handler
* @returns {Function} middleware
*/
withSSOValidation(handler) {
return async (req) => {
const url = new URL(req.url);
const token = req.cookies.get(this.COOKIE_ACCESS_TOKEN_NAME)?.value;
if (!token) return this.redirectToSSO(url);
try {
const { jwtVerify } = await import('jose');
let payload;
try {
payload = await jwtVerify(token, this.JWT_SECRET);
} catch (jwtError) {
// Handle JWT-specific errors
if (jwtError.code === 'ERR_JWT_EXPIRED' || jwtError.message.includes('exp')) {
console.log('JWT token expired, attempting refresh...');
// Token expired, try refresh token
const refreshToken = req.cookies.get(this.COOKIE_REFRESH_TOKEN_NAME)?.value;
if (!refreshToken) {
console.warn('No refresh token available, redirecting to SSO');
return this.redirectToSSO(url);
}
try {
const refreshRes = await fetch(
`${this.SSO_API_URL}/api-v1/auth/refresh-token`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ refreshToken }),
}
);
if (refreshRes.ok) {
const resJson = await refreshRes.json();
console.log("🔁 Refresh token result:", resJson);
// Set new cookies and redirect
const response = NextResponse.redirect(new URL(req.url));
response.cookies.set(
this.COOKIE_ACCESS_TOKEN_NAME,
resJson.result.accessToken,
{
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
domain: this.COOKIE_DOMAIN,
maxAge: 60 * 30, // 30 minutes
}
);
response.cookies.set(
this.COOKIE_REFRESH_TOKEN_NAME,
resJson.result.refreshToken,
{
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
domain: this.COOKIE_DOMAIN,
maxAge: 6 * 60 * 60, // 6 hours
}
);
console.log("🔁 Token refreshed successfully.");
return response;
} else {
console.warn("Refresh token invalid. Redirecting to login.");
return this.redirectToSSO(url);
}
} catch (refreshError) {
console.error("Refresh token error:", refreshError);
return this.redirectToSSO(url);
}
} else {
// Other JWT errors (invalid signature, malformed, etc.)
console.error('JWT validation failed:', jwtError.message);
return this.redirectToSSO(url);
}
}
// Ambil aplikasi (bisa array atau object)
const apps = payload.payload.user?.applications;
let app = null;
if (Array.isArray(apps)) {
app = apps.find((a) => a.applicationKey === this.APP_KEY);
} else {
app = payload.payload.user?.application;
}
if (!app) {
return this.redirectToSSO(url, payload.payload);
}
console.log("Request on:", app);
// Tambahkan informasi ke request jika perlu
req.user = payload.payload.user;
req.role = app.role;
req.subrole = app.subrole;
return handler(req);
} catch (err) {
console.error('SSO middleware error:', err);
return this.redirectToSSO(url);
}
};
}
/**
* Redirect ke halaman login SSO dengan query params
* @param {URL} currentUrl
* @param {object} [payload]
* @returns {NextResponse}
*/
redirectToSSO(currentUrl, payload = {}) {
const loginUrl = new URL(this.SSO_LOGIN_URL);
loginUrl.searchParams.set('redirect_uri', currentUrl.toString());
loginUrl.searchParams.set('app_key', this.APP_KEY);
loginUrl.searchParams.set('userId', payload?.user?.id || '');
loginUrl.searchParams.set('tokenTime', Math.floor(Date.now() / 1000));
console.log('Redirecting to:', loginUrl.toString());
return NextResponse.redirect(loginUrl);
}
/**
* Mendekode JWT dari middleware (NextRequest)
* @param {Request} req
* @returns {object|null}
*/
async getPayload(req) {
try {
const token = req.cookies.get(this.COOKIE_ACCESS_TOKEN_NAME)?.value;
if (!token) return null;
const { jwtVerify } = await import('jose');
const payload = await jwtVerify(token, this.JWT_SECRET);
return payload?.payload;
} catch (err) {
console.error('Error decoding token:', err);
return null;
}
}
/**
* Mendekode JWT dari headers server-side (API Route, getServerSideProps, dsb)
* @param {Headers|IncomingHttpHeaders} headers
* @returns {object|null}
*/
async getPayloadFromHeaders(headers) {
try {
const cookieHeader = headers.get
? headers.get('cookie')
: headers.cookie;
if (!cookieHeader) return null;
const token = cookieHeader
.split(';')
.find((c) => c.trim().startsWith(`${this.COOKIE_ACCESS_TOKEN_NAME}=`))
?.split('=')[1];
if (!token) return null;
const { jwtVerify } = await import('jose');
const payload = await jwtVerify(token, this.JWT_SECRET);
return payload?.payload;
} catch (err) {
console.error('Error decoding token from headers:', err);
return null;
}
}
}
// Export instance untuk kemudahan penggunaan
module.exports = new SSOMiddleware();
module.exports.default = new SSOMiddleware();
module.exports.SSOMiddleware = SSOMiddleware;
// Untuk kompatibilitas ES6
Object.defineProperty(module.exports, '__esModule', { value: true });