app/routes/api.auth.login.tsx (96 lines of code) (raw):
import { LoaderFunction, redirect } from "@remix-run/node";
import serverConfig from "~/lib/server/config";
import { generateRandomString, createCodeChallenge } from "~/lib/oauth2Utils";
export const loader: LoaderFunction = async ({ request }) => {
console.log("🔥 OAuth2 login route hit");
// Check if OAuth2 is enabled
if (!serverConfig.OAUTH2.ENABLED) {
throw new Response("OAuth2 not configured", { status: 400 });
}
// In development mode with demo credentials, provide helpful message
if (
process.env.NODE_ENV === "development" &&
(!serverConfig.OAUTH2.CLIENT_ID ||
serverConfig.OAUTH2.CLIENT_ID === "demo-client-id")
) {
const helpMessage =
"OAuth2 is enabled for demo purposes, but requires real HuggingFace OAuth app credentials. " +
"Create an OAuth app at https://huggingface.co/settings/applications/new and set OPENID_CLIENT_ID and OPENID_CLIENT_SECRET in your .env file.";
// Return HTML page for popup
return new Response(
`
<!DOCTYPE html>
<html>
<head>
<title>OAuth2 Configuration Required</title>
<style>
body { font-family: system-ui, sans-serif; text-align: center; padding: 50px; }
.error { color: #DC2626; }
.info { color: #6B7280; margin-top: 20px; }
</style>
</head>
<body>
<div class="error">
<h2>âš OAuth2 Configuration Required</h2>
<p>OAuth2 is enabled for demo purposes, but requires real HuggingFace OAuth app credentials.</p>
<div class="info">
<p>To enable OAuth2:</p>
<p>1. Create an OAuth app at <a href="https://huggingface.co/settings/applications/new" target="_blank">HuggingFace Settings</a></p>
<p>2. Set OPENID_CLIENT_ID and OPENID_CLIENT_SECRET in your .env file</p>
</div>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'OAUTH2_ERROR',
error: '${helpMessage}'
}, window.location.origin);
// window.close();
}
</script>
</body>
</html>
`,
{
status: 200,
headers: { "Content-Type": "text/html" },
}
);
}
console.log("OAuth2 client ID:", serverConfig.OAUTH2.CLIENT_ID);
const url = new URL(request.url);
// const returnTo = url.searchParams.get("returnTo") || "/";
const returnTo = "/api/auth/done"; // Default return URL
// Generate PKCE parameters for security
const state = generateRandomString(32);
const codeVerifier = generateRandomString(128);
const codeChallenge = await createCodeChallenge(codeVerifier);
console.log(
"Generated serverConfig.OAUTH2.CALLBACK_URL:",
serverConfig.OAUTH2.CALLBACK_URL
);
// Build OAuth2 authorization URL
const authUrl = new URL("/oauth/authorize", serverConfig.OAUTH2.PROVIDER_URL);
authUrl.searchParams.set("client_id", serverConfig.OAUTH2.CLIENT_ID!);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("scope", serverConfig.OAUTH2.SCOPES);
authUrl.searchParams.set("redirect_uri", serverConfig.OAUTH2.CALLBACK_URL);
authUrl.searchParams.set("state", state);
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");
console.log("Redirecting to OAuth2 authorization URL:", authUrl.toString());
// Create response with redirect and secure cookies for PKCE
const response = redirect(authUrl.toString());
// Store PKCE data and return URL in secure HttpOnly cookies
const cookieOptions = "Path=/; HttpOnly; SameSite=Lax; Max-Age=600"; // 10 minutes
response.headers.append(
"Set-Cookie",
`oauth_state=${state}; ${cookieOptions}`
);
response.headers.append(
"Set-Cookie",
`oauth_code_verifier=${codeVerifier}; ${cookieOptions}`
);
response.headers.append(
"Set-Cookie",
`oauth_return_to=${encodeURIComponent(returnTo)}; ${cookieOptions}`
);
console.log("OAuth2 login cookies set:", {
state,
codeVerifier,
returnTo,
});
return response;
};