UNPKG

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
// 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 });