app/routes/api.auth.github.callback.tsx (264 lines of code) (raw):
import { LoaderFunction, redirect } from "@remix-run/node";
import serverConfig from "~/lib/server/config";
import { parseCookies } from "~/lib/server/auth";
interface GitHubUserInfo {
username: string;
name?: string;
email?: string;
avatar_url?: string;
}
export const loader: LoaderFunction = async ({ request }) => {
console.log("🔥 GitHub OAuth2 callback route hit");
// Check if GitHub OAuth2 is enabled
if (!serverConfig.GITHUB_OAUTH2.ENABLED) {
throw new Response("GitHub OAuth2 not configured", { status: 400 });
}
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const error = url.searchParams.get("error");
// Handle OAuth2 error
if (error) {
console.error("GitHub OAuth2 error:", error);
return createErrorResponse(
"GitHub OAuth2 authentication was cancelled or failed"
);
}
if (!code || !state) {
return createErrorResponse("Missing required GitHub OAuth2 parameters");
}
// Get stored state from cookies for CSRF protection
const cookieHeader = request.headers.get("Cookie");
const cookies = parseCookies(cookieHeader || "");
const storedState = cookies.github_oauth_state;
const returnTo = cookies.github_oauth_return_to
? decodeURIComponent(cookies.github_oauth_return_to)
: "/";
// Verify state parameter (CSRF protection)
if (!storedState || storedState !== state) {
console.error("GitHub OAuth2 state mismatch");
return createErrorResponse("Invalid GitHub OAuth2 state parameter");
}
try {
// Exchange authorization code for access token
const tokenResponse = await fetch(
"https://github.com/login/oauth/access_token",
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: serverConfig.GITHUB_OAUTH2.CLIENT_ID!,
client_secret: serverConfig.GITHUB_OAUTH2.CLIENT_SECRET!,
code,
redirect_uri: serverConfig.GITHUB_OAUTH2.CALLBACK_URL,
}),
}
);
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
console.error("GitHub token exchange failed:", errorText);
return createErrorResponse("Failed to obtain access token from GitHub");
}
const tokenData = await tokenResponse.json();
const accessToken = tokenData.access_token;
if (!accessToken) {
console.error("No access token in GitHub response");
return createErrorResponse("No access token received from GitHub");
}
// Get user info from GitHub API
const userResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!userResponse.ok) {
console.error("Failed to get GitHub user info");
return createErrorResponse("Failed to get user information from GitHub");
}
const userData = await userResponse.json();
// Get user's primary email
let userEmail = userData.email;
if (!userEmail) {
try {
const emailResponse = await fetch(
"https://api.github.com/user/emails",
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
}
);
if (emailResponse.ok) {
const emails = await emailResponse.json();
const primaryEmail = emails.find((email: any) => email.primary);
userEmail = primaryEmail?.email || emails[0]?.email;
}
} catch (emailError) {
console.warn("Could not fetch user email:", emailError);
}
}
const githubUserInfo: GitHubUserInfo = {
username: userData.login,
name: userData.name || userData.login,
email: userEmail,
avatar_url: userData.avatar_url,
};
// 🔧 FIX: Read existing auth cookie and merge with GitHub data
let existingCookieData: any = {};
const existingAuthCookie =
cookies[serverConfig.COOKIE_NAME] ||
cookies[encodeURIComponent(serverConfig.COOKIE_NAME)];
if (existingAuthCookie) {
try {
// Handle both encoded and non-encoded cookie values
let cookieValue = existingAuthCookie;
try {
// Try decoding first in case it's URL encoded
cookieValue = decodeURIComponent(existingAuthCookie);
} catch (decodeError) {
// If decoding fails, use original value
cookieValue = existingAuthCookie;
}
// Decode existing cookie to preserve HuggingFace session
existingCookieData = JSON.parse(atob(cookieValue));
console.log("📋 Existing auth cookie found, merging with GitHub data");
console.log("🔍 Existing cookie data:", {
hasHuggingFace: existingCookieData.hasHuggingFace,
hasOpenAI: existingCookieData.hasOpenAI,
hasHfUserInfo: !!existingCookieData.hfUserInfo,
});
} catch (error) {
console.warn("⚠️ Could not parse existing auth cookie:", error);
console.warn("⚠️ Cookie value length:", existingAuthCookie?.length);
}
} else {
console.warn(
"⚠️ No existing auth cookie found - user may not be logged in to HuggingFace"
);
}
// Merge existing credentials with new GitHub token
let mergedCredentials: any = {};
if (existingCookieData.enc) {
try {
mergedCredentials = JSON.parse(atob(existingCookieData.enc));
} catch (error) {
console.warn("⚠️ Could not parse existing credentials:", error);
}
}
// Add GitHub token to existing credentials
mergedCredentials.gh = accessToken;
// Create merged session cookie data
const expiresAt = new Date();
expiresAt.setTime(expiresAt.getTime() + 7 * 24 * 60 * 60 * 1000); // 7 days
const cookieData = {
// Preserve existing HuggingFace data
hasOpenAI: existingCookieData.hasOpenAI || false,
hasHuggingFace: existingCookieData.hasHuggingFace || false,
hasGitHub: true, // Add GitHub authentication flag
expiresAt: existingCookieData.expiresAt || expiresAt.toISOString(),
hfUserInfo: existingCookieData.hfUserInfo, // Preserve HF user info
githubUserInfo: btoa(JSON.stringify(githubUserInfo)), // Add GitHub user info
isOAuth2: existingCookieData.isOAuth2, // Preserve OAuth2 flag if it exists
isGitHubOAuth2: true, // Flag to indicate GitHub OAuth2 session
// Store merged credentials (HF + GitHub tokens)
enc: btoa(JSON.stringify(mergedCredentials)),
};
const cookieValue = btoa(JSON.stringify(cookieData));
console.log("✅ GitHub OAuth successful - Final cookie data:", {
hasHuggingFace: cookieData.hasHuggingFace,
hasGitHub: cookieData.hasGitHub,
hasOpenAI: cookieData.hasOpenAI,
hasHfUserInfo: !!cookieData.hfUserInfo,
hasGithubUserInfo: !!cookieData.githubUserInfo,
credentialsKeys: Object.keys(mergedCredentials),
});
// Create HTML response for popup
const htmlResponse = `
<!DOCTYPE html>
<html>
<head>
<title>GitHub Authentication Success</title>
<style>
body { font-family: system-ui, sans-serif; text-align: center; padding: 50px; }
.success { color: #059669; }
.loading { color: #6B7280; }
</style>
</head>
<body>
<div class="success">
<h2>✓ GitHub Authentication Successful</h2>
<p class="loading">Redirecting...</p>
</div>
<script>
// Close the popup after setting the cookie
window.close();
</script>
</body>
</html>
`;
console.log(
"GitHub OAuth2 authentication successful for user:",
githubUserInfo.username
);
// Create response with auth cookie
const response = new Response(htmlResponse, {
status: 200,
headers: {
"Content-Type": "text/html",
},
});
// Clear GitHub OAuth2 temporary cookies
const clearCookieOptions = "Path=/; HttpOnly; SameSite=Lax; Max-Age=0";
response.headers.append(
"Set-Cookie",
`github_oauth_state=; ${clearCookieOptions}`
);
response.headers.append(
"Set-Cookie",
`github_oauth_return_to=; ${clearCookieOptions}`
);
// Set auth cookie with proper settings for popup scenarios
const isSecure = request.url.startsWith("https");
const cookieOptions = [
`${serverConfig.COOKIE_NAME}=${cookieValue}`,
"Max-Age=" + 7 * 24 * 60 * 60, // 7 days
"Path=/",
"SameSite=None",
"HttpOnly=false", // Allow client-side access for existing code
"Secure",
];
if (isSecure) {
cookieOptions.push("Secure");
}
response.headers.append("Set-Cookie", cookieOptions.join("; "));
return response;
} catch (error) {
console.error("GitHub OAuth2 callback error:", error);
return createErrorResponse(
"GitHub OAuth2 authentication failed during token exchange"
);
}
};
function createErrorResponse(errorMessage: string): Response {
return new Response(
`
<!DOCTYPE html>
<html>
<head>
<title>GitHub Authentication Error</title>
<style>
body { font-family: system-ui, sans-serif; text-align: center; padding: 50px; }
.error { color: #DC2626; }
</style>
</head>
<body>
<div class="error">
<h2>⚠ GitHub Authentication Failed</h2>
<p>${errorMessage}</p>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'GITHUB_OAUTH2_ERROR',
error: '${errorMessage}'
}, window.location.origin);
window.close();
} else {
window.location.href = '/?error=' + encodeURIComponent('${errorMessage}');
}
</script>
</body>
</html>
`,
{
status: 200,
headers: { "Content-Type": "text/html" },
}
);
}