Skip to content

Improve email whitelisting and extend to GitHub auth #2090

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod";
import { SecretStoreOptionsSchema } from "./services/secrets/secretStoreOptionsSchema.server";
import { isValidDatabaseUrl } from "./utils/db";
import { isValidRegex } from "./utils/regex";
import { BoolEnv } from "./utils/boolEnv";

const EnvironmentSchema = z.object({
NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]),
Expand Down Expand Up @@ -50,7 +51,7 @@ const EnvironmentSchema = z.object({
RESEND_API_KEY: z.string().optional(),
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().optional(),
SMTP_SECURE: z.coerce.boolean().optional(),
SMTP_SECURE: BoolEnv.optional(),
SMTP_USER: z.string().optional(),
SMTP_PASSWORD: z.string().optional(),

Expand Down Expand Up @@ -338,7 +339,7 @@ const EnvironmentSchema = z.object({
ALERT_RESEND_API_KEY: z.string().optional(),
ALERT_SMTP_HOST: z.string().optional(),
ALERT_SMTP_PORT: z.coerce.number().optional(),
ALERT_SMTP_SECURE: z.coerce.boolean().optional(),
ALERT_SMTP_SECURE: BoolEnv.optional(),
ALERT_SMTP_USER: z.string().optional(),
ALERT_SMTP_PASSWORD: z.string().optional(),
ALERT_RATE_LIMITER_EMISSION_INTERVAL: z.coerce.number().int().default(2_500),
Expand Down Expand Up @@ -378,7 +379,7 @@ const EnvironmentSchema = z.object({
MAX_SEQUENTIAL_INDEX_FAILURE_COUNT: z.coerce.number().default(96),

LOOPS_API_KEY: z.string().optional(),
MARQS_DISABLE_REBALANCING: z.coerce.boolean().default(false),
MARQS_DISABLE_REBALANCING: BoolEnv.default(false),
MARQS_VISIBILITY_TIMEOUT_MS: z.coerce
.number()
.int()
Expand Down Expand Up @@ -456,7 +457,7 @@ const EnvironmentSchema = z.object({
.number()
.int()
.default(60_000 * 10),
RUN_ENGINE_DEBUG_WORKER_NOTIFICATIONS: z.coerce.boolean().default(false),
RUN_ENGINE_DEBUG_WORKER_NOTIFICATIONS: BoolEnv.default(false),
RUN_ENGINE_PARENT_QUEUE_LIMIT: z.coerce.number().int().default(1000),
RUN_ENGINE_CONCURRENCY_LIMIT_BIAS: z.coerce.number().default(0.75),
RUN_ENGINE_AVAILABLE_CAPACITY_BIAS: z.coerce.number().default(0.3),
Expand Down
24 changes: 12 additions & 12 deletions apps/webapp/app/models/user.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
getDashboardPreferences,
} from "~/services/dashboardPreferences.server";
export type { User } from "@trigger.dev/database";

import { assertEmailAllowed } from "~/utils/email";
type FindOrCreateMagicLink = {
authenticationMethod: "MAGIC_LINK";
email: string;
Expand Down Expand Up @@ -38,31 +38,29 @@ export async function findOrCreateUser(input: FindOrCreateUser): Promise<LoggedI
}
}

export async function findOrCreateMagicLinkUser(
input: FindOrCreateMagicLink
): Promise<LoggedInUser> {
if (env.WHITELISTED_EMAILS && !new RegExp(env.WHITELISTED_EMAILS).test(input.email)) {
throw new Error("This email is unauthorized");
}
export async function findOrCreateMagicLinkUser({
email,
}: FindOrCreateMagicLink): Promise<LoggedInUser> {
assertEmailAllowed(email);

const existingUser = await prisma.user.findFirst({
where: {
email: input.email,
email,
},
});

const adminEmailRegex = env.ADMIN_EMAILS ? new RegExp(env.ADMIN_EMAILS) : undefined;
const makeAdmin = adminEmailRegex ? adminEmailRegex.test(input.email) : false;
const makeAdmin = adminEmailRegex ? adminEmailRegex.test(email) : false;

const user = await prisma.user.upsert({
where: {
email: input.email,
email,
},
update: {
email: input.email,
email,
},
create: {
email: input.email,
email,
authenticationMethod: "MAGIC_LINK",
admin: makeAdmin, // only on create, to prevent automatically removing existing admins
},
Expand All @@ -79,6 +77,8 @@ export async function findOrCreateGithubUser({
authenticationProfile,
authenticationExtraParams,
}: FindOrCreateGithub): Promise<LoggedInUser> {
assertEmailAllowed(email);

const name = authenticationProfile._json.name;
let avatarUrl: string | undefined = undefined;
if (authenticationProfile.photos[0]) {
Expand Down
24 changes: 22 additions & 2 deletions apps/webapp/app/routes/login._index/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
import { LoginPageLayout } from "~/components/LoginPageLayout";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormError } from "~/components/primitives/FormError";
import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { TextLink } from "~/components/primitives/TextLink";
import { isGithubAuthSupported } from "~/services/auth.server";
import { commitSession, setRedirectTo } from "~/services/redirectTo.server";
import { getUserId } from "~/services/session.server";
import { getUserSession } from "~/services/sessionStorage.server";
import { requestUrl } from "~/utils/requestUrl.server";

export const meta: MetaFunction = ({ matches }) => {
Expand Down Expand Up @@ -48,17 +50,34 @@ export async function loader({ request }: LoaderFunctionArgs) {
const session = await setRedirectTo(request, redirectTo);

return typedjson(
{ redirectTo, showGithubAuth: isGithubAuthSupported },
{
redirectTo,
showGithubAuth: isGithubAuthSupported,
authError: null,
},
{
headers: {
"Set-Cookie": await commitSession(session),
},
}
);
} else {
const session = await getUserSession(request);
const error = session.get("auth:error");

let authError: string | undefined;
if (error) {
if ("message" in error) {
authError = error.message;
} else {
authError = JSON.stringify(error, null, 2);
}
}

return typedjson({
redirectTo: null,
showGithubAuth: isGithubAuthSupported,
authError,
});
}
}
Expand All @@ -81,7 +100,7 @@ export default function LoginPage() {
Create an account or login
</Paragraph>
<Fieldset className="w-full">
<div className="flex flex-col gap-y-2">
<div className="flex flex-col items-center gap-y-2">
{data.showGithubAuth && (
<Button
type="submit"
Expand All @@ -103,6 +122,7 @@ export default function LoginPage() {
<EnvelopeIcon className="mr-2 size-5 text-text-bright" />
Continue with Email
</LinkButton>
{data.authError && <FormError>{data.authError}</FormError>}
</div>
<Paragraph variant="extra-small" className="mt-2 text-center">
By signing up you agree to our{" "}
Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/services/email.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { EmailClient, MailTransportOptions } from "emails";
import type { SendEmailOptions } from "remix-auth-email-link";
import { redirect } from "remix-typedjson";
import { env } from "~/env.server";
import type { User } from "~/models/user.server";
import type { AuthUser } from "./authUser";
import { workerQueue } from "./worker.server";
import { logger } from "./logger.server";
import { singleton } from "~/utils/singleton";
import { assertEmailAllowed } from "~/utils/email";

const client = singleton(
"email-client",
Expand Down Expand Up @@ -66,6 +66,8 @@ function buildTransportOptions(alerts?: boolean): MailTransportOptions {
}

export async function sendMagicLinkEmail(options: SendEmailOptions<AuthUser>): Promise<void> {
assertEmailAllowed(options.emailAddress);

// Auto redirect when in development mode
if (env.NODE_ENV === "development") {
throw redirect(options.magicLink);
Expand Down
9 changes: 9 additions & 0 deletions apps/webapp/app/utils/boolEnv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from "zod";

export const BoolEnv = z.preprocess((val) => {
if (typeof val !== "string") {
return val;
}

return ["true", "1"].includes(val.toLowerCase().trim());
}, z.boolean());
13 changes: 13 additions & 0 deletions apps/webapp/app/utils/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { env } from "~/env.server";

export function assertEmailAllowed(email: string) {
if (!env.WHITELISTED_EMAILS) {
return;
}

const regexp = new RegExp(env.WHITELISTED_EMAILS);

if (!regexp.test(email)) {
throw new Error("This email is unauthorized");
}
}