Skip to content

Instantly share code, notes, and snippets.

@keithbloom
Created March 19, 2025 15:52
Show Gist options
  • Save keithbloom/86a740582a58e01c2252bce60c4e6f5a to your computer and use it in GitHub Desktop.
Save keithbloom/86a740582a58e01c2252bce60c4e6f5a to your computer and use it in GitHub Desktop.
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