Created
March 19, 2025 15:52
-
-
Save keithbloom/86a740582a58e01c2252bce60c4e6f5a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const express = require('express'); | |
const { createProxyMiddleware } = require('http-proxy-middleware'); | |
const cors = require('cors'); | |
const cookieParser = require('cookie-parser'); | |
const session = require('express-session'); | |
const path = require('path'); | |
const app = express(); | |
const PORT = process.env.PORT || 3000; | |
// Parse cookies | |
app.use(cookieParser()); | |
// Session management (to store API cookies) | |
app.use(session({ | |
secret: 'your-secret-key', | |
resave: false, | |
saveUninitialized: true, | |
cookie: { | |
secure: process.env.NODE_ENV === 'production', | |
sameSite: 'lax' | |
} | |
})); | |
// Enable CORS with dynamic origin | |
app.use(cors({ | |
origin: true, // Allow the request origin dynamically | |
credentials: true // Allow cookies to be sent | |
})); | |
app.use(express.json()); | |
app.use(express.urlencoded({ extended: true })); | |
// Create the proxy middleware for API requests | |
const apiProxy = createProxyMiddleware({ | |
target: 'https://api.com', | |
changeOrigin: true, | |
pathRewrite: { | |
'^/api': '' // Remove the /api prefix when forwarding | |
}, | |
// Important: cookies will be automatically included | |
withCredentials: true, | |
// Handle redirects | |
onProxyRes: (proxyRes, req, res) => { | |
// Handle redirects from api.com | |
const statusCode = proxyRes.statusCode; | |
if (statusCode >= 300 && statusCode < 400) { | |
const location = proxyRes.headers.location; | |
if (location) { | |
try { | |
// Parse the redirect URL | |
const redirectUrl = new URL(location); | |
// Check if it's redirecting to api.com | |
if (redirectUrl.hostname === 'api.com' || redirectUrl.hostname.endsWith('.api.com')) { | |
// Get the host from the original request | |
const currentHost = req.get('host'); // This includes domain and port if present | |
const protocol = req.protocol; | |
// Replace the hostname with current host and add /api prefix to the path | |
redirectUrl.protocol = protocol; | |
redirectUrl.hostname = currentHost.split(':')[0]; // Remove port if present in hostname | |
if (currentHost.includes(':')) { | |
redirectUrl.port = currentHost.split(':')[1]; // Set port if it was present | |
} else { | |
redirectUrl.port = ''; // Clear port if none was present | |
} | |
// Add /api prefix only for API endpoints | |
if (!redirectUrl.pathname.startsWith('/auth')) { | |
redirectUrl.pathname = '/api' + redirectUrl.pathname; | |
} | |
// Update the location header | |
proxyRes.headers.location = redirectUrl.toString(); | |
} | |
} catch (error) { | |
console.error('Error processing redirect:', error); | |
} | |
} | |
} | |
// Check for authentication errors | |
if (statusCode === 401 || statusCode === 403) { | |
// Clear the session and force re-authentication | |
req.session.apiCookies = null; | |
} | |
} | |
}); | |
// Create a proxy for full page content from api.com | |
const createPageProxy = (req, res, next) => { | |
const pageProxy = createProxyMiddleware({ | |
target: 'https://api.com', | |
changeOrigin: true, | |
withCredentials: true, | |
selfHandleResponse: true, // We'll handle the response | |
onProxyRes: (proxyRes, req, res) => { | |
let responseBody = ''; | |
proxyRes.on('data', (chunk) => { | |
responseBody += chunk; | |
}); | |
proxyRes.on('end', () => { | |
// Rewrite any api.com URLs in the response to our domain | |
const currentHost = req.get('host'); | |
const protocol = req.protocol; | |
const ourBaseUrl = `${protocol}://${currentHost}`; | |
// Replace absolute URLs in HTML content | |
let modifiedBody = responseBody; | |
if (proxyRes.headers['content-type'] && | |
proxyRes.headers['content-type'].includes('text/html')) { | |
// Replace links and form actions | |
modifiedBody = responseBody | |
.replace(/https?:\/\/api\.com/g, ourBaseUrl) | |
.replace(/action="\/([^"]*)"/g, `action="/api/$1"`) | |
.replace(/href="\/([^"]*)"/g, (match, path) => { | |
// Don't add /api prefix to auth paths | |
if (path.startsWith('auth')) { | |
return `href="/${path}"`; | |
} | |
return `href="/api/${path}"`; | |
}); | |
} | |
// Set headers and send the modified response | |
Object.keys(proxyRes.headers).forEach(key => { | |
// Skip the content-length as we modified the body | |
if (key !== 'content-length') { | |
res.setHeader(key, proxyRes.headers[key]); | |
} | |
}); | |
res.send(modifiedBody); | |
}); | |
} | |
}); | |
return pageProxy(req, res, next); | |
}; | |
// Initial setup middleware to handle authentication for API requests | |
app.use('/api', (req, res, next) => { | |
// If we have API cookies already stored in session, use them | |
if (req.session.apiCookies) { | |
req.apiCookies = req.session.apiCookies; | |
return next(); | |
} | |
// Store the original request URL | |
req.session.originalUrl = req.originalUrl; | |
// Redirect to the authentication handler | |
res.redirect('/auth/initiate'); | |
}); | |
// Authentication flow routes | |
app.get('/auth/initiate', (req, res) => { | |
// Get the current host information | |
const host = req.get('host'); | |
const protocol = req.protocol; | |
const callbackUrl = `${protocol}://${host}/auth/callback`; | |
// Redirect to API.com login page with our callback | |
res.redirect(`https://api.com/login?redirect_uri=${encodeURIComponent(callbackUrl)}`); | |
}); | |
app.get('/auth/callback', (req, res) => { | |
// At this point, the browser should have cookies for api.com | |
// Extract cookies via iframe | |
const extractorHtml = ` | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Authentication</title> | |
<script> | |
// Function to extract cookies | |
function extractCookies() { | |
// Create a fetch request to api.com that will include cookies | |
fetch('https://api.com/user', { | |
method: 'GET', | |
credentials: 'include', | |
mode: 'cors' | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
// Send the cookies back to our server | |
return fetch('/auth/store-cookies', { | |
method: 'POST', | |
credentials: 'include', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
// We can't directly access the cookies due to security, | |
// but we can get user data which confirms authentication | |
userData: data | |
}) | |
}); | |
}) | |
.then(() => { | |
// Redirect to the original URL | |
window.location.href = '${req.session.originalUrl || '/'}'; | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
document.getElementById('message').textContent = 'Authentication failed. Please try again.'; | |
}); | |
} | |
// Run when page loads | |
window.onload = extractCookies; | |
</script> | |
</head> | |
<body> | |
<h1>Authentication in progress...</h1> | |
<p id="message">Please wait while we complete the authentication process.</p> | |
</body> | |
</html> | |
`; | |
res.send(extractorHtml); | |
}); | |
// Endpoint to store the API session information | |
app.post('/auth/store-cookies', (req, res) => { | |
// Create a timestamp to track when we got these cookies | |
req.session.apiAuthTime = Date.now(); | |
// Store the session info | |
req.session.apiCookies = { authenticated: true }; | |
// Store user data if available | |
if (req.body.userData) { | |
req.session.userData = req.body.userData; | |
} | |
res.json({ success: true }); | |
}); | |
// Use the proxy for all requests to /api | |
app.use('/api', apiProxy); | |
// Known API pages that should be proxied | |
const apiPaths = ['/dashboard', '/profile', '/settings', '/account']; | |
// Special handler for API pages | |
app.get(apiPaths, (req, res, next) => { | |
return createPageProxy(req, res, next); | |
}); | |
// Serve static files from the 'public' directory | |
// This must come after API routes to avoid conflicts | |
app.use(express.static(path.join(__dirname, 'public'))); | |
// For Single Page Applications, serve index.html for all other routes | |
app.get('*', (req, res) => { | |
res.sendFile(path.join(__dirname, 'public', 'index.html')); | |
}); | |
// Start the server | |
app.listen(PORT, () => { | |
console.log(`Server is running on port ${PORT}`); | |
console.log(`Access your application at http://localhost:${PORT}`); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment