Files
triggerdotdev--trigger.dev/apps/webapp/app/routes/auth.sso.ts
T
2026-07-13 13:32:57 +08:00

79 lines
3.1 KiB
TypeScript

import { redirect, type ActionFunctionArgs } from "@remix-run/node";
import { tryCatch } from "@trigger.dev/core/v3";
import { SSO_FLOWS, type SsoFlow } from "@trigger.dev/plugins";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
import { ssoController } from "~/services/sso.server";
import {
checkSsoEmailRateLimit,
checkSsoIpRateLimit,
SsoRateLimitError,
} from "~/services/ssoRateLimiter.server";
import { extractClientIp } from "~/utils/extractClientIp.server";
import { sanitizeRedirectPath } from "~/utils";
const VALID_FLOWS: ReadonlySet<SsoFlow> = new Set<SsoFlow>(SSO_FLOWS);
function isSsoFlow(value: string): value is SsoFlow {
return VALID_FLOWS.has(value as SsoFlow);
}
export async function action({ request }: ActionFunctionArgs) {
if (request.method !== "POST") {
return new Response(null, { status: 405 });
}
const form = await request.formData();
const rawEmail = form.get("email");
if (typeof rawEmail !== "string" || rawEmail.trim().length === 0) {
return redirect("/login/sso?error=missing_email");
}
const email = rawEmail.toLowerCase().trim();
const rawRedirectTo = form.get("redirectTo");
const redirectTo =
sanitizeRedirectPath(typeof rawRedirectTo === "string" ? rawRedirectTo : null) ?? "/";
const rawFlow = (form.get("flow") as string | null) ?? "user_initiated";
const flow: SsoFlow = isSsoFlow(rawFlow) ? rawFlow : "user_initiated";
if (env.LOGIN_RATE_LIMITS_ENABLED) {
const xff = request.headers.get("x-forwarded-for");
const clientIp = extractClientIp(xff);
const [rateError] = await tryCatch(
Promise.all([
clientIp ? checkSsoIpRateLimit(clientIp) : Promise.resolve(),
checkSsoEmailRateLimit(email),
])
);
if (rateError) {
if (rateError instanceof SsoRateLimitError) {
logger.warn("SSO login rate limit exceeded", { clientIp, email });
} else {
logger.error("SSO login rate limiter failed", { clientIp, email, error: rateError });
}
return redirect(`/login/sso?email=${encodeURIComponent(email)}&error=rate_limited`);
}
}
// decideRouteForEmail is the auto-discovery gate — "should I redirect
// a magic-link / OAuth attempt to SSO?" That gate requires
// enforced=true. user_initiated means the user explicitly chose SSO,
// so enforcement is irrelevant; we just need a configured domain,
// which beginAuthorization itself validates (returns
// no_org_for_domain / no_active_connection).
if (flow !== "user_initiated") {
const decision = await ssoController.decideRouteForEmail(email);
if (decision.isErr() || decision.value.kind === "no_sso") {
return redirect(`/login/sso?email=${encodeURIComponent(email)}&error=no_sso_for_domain`);
}
}
const begun = await ssoController.beginAuthorization({ email, redirectTo, flow });
if (begun.isErr()) {
logger.warn("SSO beginAuthorization failed", { reason: begun.error, email, flow });
return redirect(`/login/sso?email=${encodeURIComponent(email)}&error=${begun.error}`);
}
return redirect(begun.value.url);
}