UNPKG

saget-auth-middleware

Version:

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

349 lines (309 loc) 10.5 kB
import { logger } from '../../logger.js'; /** * Enhanced TokenUtils class with better error handling and reduced duplication */ export class TokenUtils { constructor(config) { if (!config) { throw new Error('TokenUtils requires a configuration object'); } this.config = config; } /** * Get tokens from request object (server-side version) * @param {Request} req - The request object * @returns {object} { accessToken, refreshToken } */ getTokensFromRequest(req) { if (!req) { logger.error('Request object is required for server-side token retrieval'); return { accessToken: null, refreshToken: null }; } try { const accessToken = this.getTokenFromRequest(req); const refreshToken = this.getRefreshTokenFromRequest(req); return { accessToken, refreshToken }; } catch (error) { logger.error('Error retrieving tokens from request:', error.message); return { accessToken: null, refreshToken: null }; } } /** * Get access token from cookies or localStorage (via headers) * @param {Request} req - Request object * @returns {string|null} Access token */ getTokenFromRequest(req) { return this.getTokenFromSource(req, { cookieName: this.config.COOKIE_ACCESS_TOKEN_NAME, headerName: this.config.LOCAL_STORAGE_ACCESS_TOKEN_KEY, tokenType: 'access' }); } /** * Get refresh token from cookies or localStorage (via headers) * @param {Request} req - Request object * @returns {string|null} Refresh token */ getRefreshTokenFromRequest(req) { return this.getTokenFromSource(req, { cookieName: this.config.COOKIE_REFRESH_TOKEN_NAME, headerName: this.config.LOCAL_STORAGE_REFRESH_TOKEN_KEY, tokenType: 'refresh' }); } /** * Generic method to get token from various sources * @private * @param {Request} req - Request object * @param {object} options - Token source options * @returns {string|null} Token value */ getTokenFromSource(req, { cookieName, headerName, tokenType }) { try { // First try cookies const cookieToken = this.getTokenFromCookies(req, cookieName); if (cookieToken) { logger.debug(`Found ${tokenType} token in cookies`); return cookieToken; } // Then try localStorage via custom header const headerToken = this.getTokenFromHeaders(req, headerName); if (headerToken) { logger.debug(`Found ${tokenType} token in localStorage header`); return headerToken; } logger.debug(`No ${tokenType} token found`); return null; } catch (error) { logger.error(`Error getting ${tokenType} token:`, error.message); return null; } } /** * Get token from cookies * @private * @param {Request} req - Request object * @param {string} cookieName - Cookie name * @returns {string|null} Token from cookies */ getTokenFromCookies(req, cookieName) { try { return req.cookies?.get(cookieName)?.value || null; } catch (error) { logger.debug('Error reading from cookies:', error.message); return null; } } /** * Get token from headers * @private * @param {Request} req - Request object * @param {string} headerName - Header name * @returns {string|null} Token from headers */ getTokenFromHeaders(req, headerName) { try { return req.headers?.get(headerName) || null; } catch (error) { logger.debug('Error reading from headers:', error.message); return null; } } /** * Set tokens in response cookies and headers with enhanced error handling * @param {object} params - Parameters object * @param {NextResponse} params.response - Response object * @param {string} params.accessToken - Access token * @param {string} params.refreshToken - Refresh token */ setTokensInResponse({ response, accessToken, refreshToken }) { if (!response || !accessToken || !refreshToken) { logger.error('Missing required parameters for setting tokens'); return; } logger.info('Setting tokens in response'); try { const cookieOptions = this.getCookieOptions(); // Set access token cookie (30 minutes) this.setTokenCookie(response, { name: this.config.COOKIE_ACCESS_TOKEN_NAME, value: accessToken, options: { ...cookieOptions, maxAge: 30 * 60 } }); // Set refresh token cookie (6 hours) this.setTokenCookie(response, { name: this.config.COOKIE_REFRESH_TOKEN_NAME, value: refreshToken, options: { ...cookieOptions, maxAge: 6 * 60 * 60 } }); // Set headers for client-side handling this.setTokenHeaders(response, accessToken, refreshToken); logger.info('Tokens set successfully in response'); } catch (error) { logger.error('Error setting tokens in response:', error.message); this.setFallbackHeaders(response, accessToken, refreshToken); } } /** * Set individual token cookie with error handling * @private * @param {NextResponse} response - Response object * @param {object} params - Cookie parameters */ setTokenCookie(response, { name, value, options }) { try { response.cookies.set(name, value, options); logger.debug(`${name} cookie set successfully`); } catch (error) { logger.error(`Error setting ${name} cookie:`, error.message); throw error; } } /** * Set token headers for client-side handling * @private * @param {NextResponse} response - Response object * @param {string} accessToken - Access token * @param {string} refreshToken - Refresh token */ setTokenHeaders(response, accessToken, refreshToken) { const headers = { [this.config.NEXT_PUBLIC_COOKIE_ACCESS_TOKEN_NAME]: accessToken, [this.config.NEXT_PUBLIC_COOKIE_REFRESH_TOKEN_NAME]: refreshToken, [this.config.NEXT_PUBLIC_COOKIE_DOMAIN]: this.config.COOKIE_DOMAIN, [this.config.LOCAL_STORAGE_ACCESS_TOKEN_KEY]: accessToken, [this.config.LOCAL_STORAGE_REFRESH_TOKEN_KEY]: refreshToken }; Object.entries(headers).forEach(([key, value]) => { if (value) { response.headers.set(key, value); } }); } /** * Set fallback headers when cookie setting fails * @private * @param {NextResponse} response - Response object * @param {string} accessToken - Access token * @param {string} refreshToken - Refresh token */ setFallbackHeaders(response, accessToken, refreshToken) { logger.warn('Using fallback headers for token storage'); const fallbackHeaders = { 'x-set-access-token': accessToken, 'x-set-refresh-token': refreshToken, 'x-localstorage-access-key': this.config.LOCAL_STORAGE_ACCESS_TOKEN_KEY, 'x-localstorage-refresh-key': this.config.LOCAL_STORAGE_REFRESH_TOKEN_KEY }; Object.entries(fallbackHeaders).forEach(([key, value]) => { response.headers.set(key, value); }); } /** * Get cookie options with domain validation * @private * @returns {object} Cookie options */ getCookieOptions() { const validatedDomain = this.config.validateCookieDomain(this.config.COOKIE_DOMAIN); const options = { httpOnly: false, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/" }; if (validatedDomain) { options.domain = validatedDomain; } return options; } /** * Decode JWT from middleware (NextRequest) with enhanced error handling * @param {Request} req - Request object * @returns {object|null} Decoded payload */ async getPayload(req) { try { const token = this.getTokenFromRequest(req); if (!token) { logger.debug('No token found for payload extraction'); return null; } return await this.verifyAndDecodeToken(token); } catch (error) { logger.error('Error getting payload from request:', error.message); return null; } } /** * Decode JWT from headers server-side with enhanced error handling * @param {Headers|IncomingHttpHeaders} headers - Headers object * @returns {object|null} Decoded payload */ async getPayloadFromHeaders(headers) { try { const token = this.extractTokenFromHeaders(headers); if (!token) { logger.debug('No token found in headers for payload extraction'); return null; } return await this.verifyAndDecodeToken(token); } catch (error) { logger.error('Error getting payload from headers:', error.message); return null; } } /** * Extract token from headers object * @private * @param {Headers|IncomingHttpHeaders} headers - Headers object * @returns {string|null} Extracted token */ extractTokenFromHeaders(headers) { // Try cookies first const cookieHeader = headers.get ? headers.get('cookie') : headers.cookie; if (cookieHeader) { const token = this.parseTokenFromCookieHeader(cookieHeader); if (token) return token; } // Try localStorage header const headerToken = headers.get ? headers.get(this.config.LOCAL_STORAGE_ACCESS_TOKEN_KEY) : headers[this.config.LOCAL_STORAGE_ACCESS_TOKEN_KEY]; return headerToken || null; } /** * Parse token from cookie header string * @private * @param {string} cookieHeader - Cookie header string * @returns {string|null} Parsed token */ parseTokenFromCookieHeader(cookieHeader) { try { const tokenCookie = cookieHeader .split(';') .find(c => c.trim().startsWith(`${this.config.COOKIE_ACCESS_TOKEN_NAME}=`)); return tokenCookie ? tokenCookie.split('=')[1] : null; } catch (error) { logger.debug('Error parsing cookie header:', error.message); return null; } } /** * Verify and decode JWT token * @private * @param {string} token - JWT token * @returns {object|null} Decoded payload */ async verifyAndDecodeToken(token) { try { const { jwtVerify } = await import('jose'); const payload = await jwtVerify(token, this.config.JWT_SECRET); return payload?.payload || null; } catch (error) { logger.debug('JWT verification failed:', error.message); return null; } } }