app/routes/api.auth.callback.tsx (435 lines of code) (raw):
import { LoaderFunction, redirect, json } from "@remix-run/node";
import serverConfig from "~/lib/server/config";
import { parseCookies } from "~/lib/server/auth";
interface HFUserInfo {
username: string;
fullName: string;
avatarUrl: string;
}
export const loader: LoaderFunction = async ({ request }) => {
// Check if OAuth2 is enabled
if (!serverConfig.OAUTH2.ENABLED) {
throw new Response("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("OAuth2 error:", error);
return new Response(
`
<!DOCTYPE html>
<html>
<head>
<title>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>⚠ Authentication Failed</h2>
<p>OAuth2 authentication was cancelled or failed.</p>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'OAUTH2_ERROR',
error: 'OAuth2 authentication failed'
}, window.location.origin);
window.close();
} else {
window.location.href = '/?error=' + encodeURIComponent('OAuth2 authentication failed');
}
</script>
</body>
</html>
`,
{
status: 200,
headers: { "Content-Type": "text/html" },
}
);
}
if (!code || !state) {
return new Response(
`
<!DOCTYPE html>
<html>
<head>
<title>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>⚠ Invalid OAuth2 Callback</h2>
<p>Missing required OAuth2 parameters.</p>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'OAUTH2_ERROR',
error: 'Invalid OAuth2 callback'
}, window.location.origin);
window.close();
} else {
window.location.href = '/?error=' + encodeURIComponent('Invalid OAuth2 callback');
}
</script>
</body>
</html>
`,
{
status: 200,
headers: { "Content-Type": "text/html" },
}
);
}
// Get stored PKCE data from cookies
const cookieHeader = request.headers.get("Cookie");
const cookies = parseCookies(cookieHeader || "");
const storedState = cookies.oauth_state;
const codeVerifier = cookies.oauth_code_verifier;
const returnTo = cookies.oauth_return_to
? decodeURIComponent(cookies.oauth_return_to)
: "/";
// Verify state parameter (CSRF protection)
if (!storedState || storedState !== state) {
console.error("OAuth2 state mismatch");
return new Response(
`
<!DOCTYPE html>
<html>
<head>
<title>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>⚠ Security Error</h2>
<p>Invalid OAuth2 state parameter.</p>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'OAUTH2_ERROR',
error: 'Invalid OAuth2 state'
}, window.location.origin);
window.close();
} else {
window.location.href = '/?error=' + encodeURIComponent('Invalid OAuth2 state');
}
</script>
</body>
</html>
`,
{
status: 200,
headers: { "Content-Type": "text/html" },
}
);
}
if (!codeVerifier) {
console.error("Missing code verifier");
return new Response(
`
<!DOCTYPE html>
<html>
<head>
<title>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>⚠ Authentication Error</h2>
<p>Missing OAuth2 verification data.</p>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'OAUTH2_ERROR',
error: 'Missing OAuth2 verification data'
}, window.location.origin);
window.close();
} else {
window.location.href = '/?error=' + encodeURIComponent('Missing OAuth2 verification data');
}
</script>
</body>
</html>
`,
{
status: 200,
headers: { "Content-Type": "text/html" },
}
);
}
try {
// Exchange authorization code for access token
const tokenResponse = await fetch(
`${serverConfig.OAUTH2.PROVIDER_URL}/oauth/token`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: serverConfig.OAUTH2.CLIENT_ID!,
client_secret: serverConfig.OAUTH2.CLIENT_SECRET!,
code,
redirect_uri: serverConfig.OAUTH2.CALLBACK_URL,
code_verifier: codeVerifier,
}),
}
);
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
console.error("Token exchange failed:", errorText);
return new Response(
`
<!DOCTYPE html>
<html>
<head>
<title>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>⚠ Token Exchange Failed</h2>
<p>Failed to obtain access token from HuggingFace.</p>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'OAUTH2_ERROR',
error: 'Failed to obtain access token'
}, window.location.origin);
window.close();
} else {
window.location.href = '/?error=' + encodeURIComponent('Failed to obtain access token');
}
</script>
</body>
</html>
`,
{
status: 200,
headers: { "Content-Type": "text/html" },
}
);
}
const tokenData = await tokenResponse.json();
const accessToken = tokenData.access_token;
if (!accessToken) {
console.error("No access token in response");
return new Response(
`
<!DOCTYPE html>
<html>
<head>
<title>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>⚠ Invalid Token Response</h2>
<p>No access token received from HuggingFace.</p>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'OAUTH2_ERROR',
error: 'Invalid token response'
}, window.location.origin);
window.close();
} else {
window.location.href = '/?error=' + encodeURIComponent('Invalid token response');
}
</script>
</body>
</html>
`,
{
status: 200,
headers: { "Content-Type": "text/html" },
}
);
}
// Get user info from HuggingFace API
const userResponse = await fetch("https://huggingface.co/api/whoami-v2", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!userResponse.ok) {
console.error("Failed to get user info");
return new Response(
`
<!DOCTYPE html>
<html>
<head>
<title>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>⚠ User Info Failed</h2>
<p>Failed to get user information from HuggingFace.</p>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'OAUTH2_ERROR',
error: 'Failed to get user information'
}, window.location.origin);
window.close();
} else {
window.location.href = '/?error=' + encodeURIComponent('Failed to get user information');
}
</script>
</body>
</html>
`,
{
status: 200,
headers: { "Content-Type": "text/html" },
}
);
}
const userData = await userResponse.json();
const userInfo: HFUserInfo = {
username: userData.name || "",
fullName: userData.fullname || userData.name || "",
avatarUrl: userData.avatarUrl || "",
};
// Create session cookie (similar to existing auth)
const expiresAt = new Date();
expiresAt.setTime(expiresAt.getTime() + 7 * 24 * 60 * 60 * 1000); // 7 days
const cookieData = {
hasOpenAI: false,
hasHuggingFace: true,
expiresAt: expiresAt.toISOString(),
hfUserInfo: btoa(JSON.stringify(userInfo)),
isOAuth2: true, // Flag to indicate OAuth2 session
// Store the access token (encrypted)
enc: btoa(
JSON.stringify({
hf: accessToken,
})
),
};
const cookieValue = btoa(JSON.stringify(cookieData));
// Create HTML response for popup with auth cookie
const htmlResponse = `
<!DOCTYPE html>
<html>
<head>
<title>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>✓ Authentication Successful</h2>
<p class="loading">Redirecting...</p>
</div>
<script>
// Just close the popup after setting the cookie
window.close();
</script>
</body>
</html>
`;
console.log(
"OAuth2 authentication successful for user:",
userInfo.username
);
// const htmlResponse = ""
// Create response with auth cookie
const response = new Response(htmlResponse, {
status: 200,
headers: {
"Content-Type": "text/html",
},
});
// Clear OAuth2 temporary cookies
const clearCookieOptions = "Path=/; HttpOnly; SameSite=Lax; Max-Age=0";
response.headers.append(
"Set-Cookie",
`oauth_state=; ${clearCookieOptions}`
);
response.headers.append(
"Set-Cookie",
`oauth_code_verifier=; ${clearCookieOptions}`
);
response.headers.append(
"Set-Cookie",
`oauth_return_to=; ${clearCookieOptions}`
);
// Set auth cookie with proper settings for popup/iframe scenarios
const isSecure = request.url.startsWith("https");
const cookieOptions = [
`${serverConfig.COOKIE_NAME}=${cookieValue}`,
"Max-Age=" + 7 * 24 * 60 * 60, // 7 days
"Path=/",
// isSecure ? "SameSite=None" : "SameSite=Lax", // None for HTTPS (required for iframes), Lax for HTTP
"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("OAuth2 callback error:", error);
return new Response(
`
<!DOCTYPE html>
<html>
<head>
<title>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>⚠ Authentication Failed</h2>
<p>OAuth2 authentication failed during token exchange.</p>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'OAUTH2_ERROR',
error: 'OAuth2 authentication failed'
}, window.location.origin);
window.close();
} else {
window.location.href = '/?error=' + encodeURIComponent('OAuth2 authentication failed');
}
</script>
</body>
</html>
`,
{
status: 200,
headers: { "Content-Type": "text/html" },
}
);
}
};