Skip to content

Commit 8112c12

Browse files
authored
Improve email whitelisting and extend to GitHub auth (#2090)
* make github auth respect email whitelist * display github login errors * fix smtp secure env vars * prevent sending magic link if email not allowed * fix MARQS_DISABLE_REBALANCING and RUN_ENGINE_DEBUG_WORKER_NOTIFICATIONS
1 parent c8d252e commit 8112c12

File tree

6 files changed

+64
-19
lines changed

6 files changed

+64
-19
lines changed

apps/webapp/app/env.server.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { z } from "zod";
22
import { SecretStoreOptionsSchema } from "./services/secrets/secretStoreOptionsSchema.server";
33
import { isValidDatabaseUrl } from "./utils/db";
44
import { isValidRegex } from "./utils/regex";
5+
import { BoolEnv } from "./utils/boolEnv";
56

67
const EnvironmentSchema = z.object({
78
NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]),
@@ -50,7 +51,7 @@ const EnvironmentSchema = z.object({
5051
RESEND_API_KEY: z.string().optional(),
5152
SMTP_HOST: z.string().optional(),
5253
SMTP_PORT: z.coerce.number().optional(),
53-
SMTP_SECURE: z.coerce.boolean().optional(),
54+
SMTP_SECURE: BoolEnv.optional(),
5455
SMTP_USER: z.string().optional(),
5556
SMTP_PASSWORD: z.string().optional(),
5657

@@ -338,7 +339,7 @@ const EnvironmentSchema = z.object({
338339
ALERT_RESEND_API_KEY: z.string().optional(),
339340
ALERT_SMTP_HOST: z.string().optional(),
340341
ALERT_SMTP_PORT: z.coerce.number().optional(),
341-
ALERT_SMTP_SECURE: z.coerce.boolean().optional(),
342+
ALERT_SMTP_SECURE: BoolEnv.optional(),
342343
ALERT_SMTP_USER: z.string().optional(),
343344
ALERT_SMTP_PASSWORD: z.string().optional(),
344345
ALERT_RATE_LIMITER_EMISSION_INTERVAL: z.coerce.number().int().default(2_500),
@@ -378,7 +379,7 @@ const EnvironmentSchema = z.object({
378379
MAX_SEQUENTIAL_INDEX_FAILURE_COUNT: z.coerce.number().default(96),
379380

380381
LOOPS_API_KEY: z.string().optional(),
381-
MARQS_DISABLE_REBALANCING: z.coerce.boolean().default(false),
382+
MARQS_DISABLE_REBALANCING: BoolEnv.default(false),
382383
MARQS_VISIBILITY_TIMEOUT_MS: z.coerce
383384
.number()
384385
.int()
@@ -456,7 +457,7 @@ const EnvironmentSchema = z.object({
456457
.number()
457458
.int()
458459
.default(60_000 * 10),
459-
RUN_ENGINE_DEBUG_WORKER_NOTIFICATIONS: z.coerce.boolean().default(false),
460+
RUN_ENGINE_DEBUG_WORKER_NOTIFICATIONS: BoolEnv.default(false),
460461
RUN_ENGINE_PARENT_QUEUE_LIMIT: z.coerce.number().int().default(1000),
461462
RUN_ENGINE_CONCURRENCY_LIMIT_BIAS: z.coerce.number().default(0.75),
462463
RUN_ENGINE_AVAILABLE_CAPACITY_BIAS: z.coerce.number().default(0.3),

apps/webapp/app/models/user.server.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
getDashboardPreferences,
88
} from "~/services/dashboardPreferences.server";
99
export type { User } from "@trigger.dev/database";
10-
10+
import { assertEmailAllowed } from "~/utils/email";
1111
type FindOrCreateMagicLink = {
1212
authenticationMethod: "MAGIC_LINK";
1313
email: string;
@@ -38,31 +38,29 @@ export async function findOrCreateUser(input: FindOrCreateUser): Promise<LoggedI
3838
}
3939
}
4040

41-
export async function findOrCreateMagicLinkUser(
42-
input: FindOrCreateMagicLink
43-
): Promise<LoggedInUser> {
44-
if (env.WHITELISTED_EMAILS && !new RegExp(env.WHITELISTED_EMAILS).test(input.email)) {
45-
throw new Error("This email is unauthorized");
46-
}
41+
export async function findOrCreateMagicLinkUser({
42+
email,
43+
}: FindOrCreateMagicLink): Promise<LoggedInUser> {
44+
assertEmailAllowed(email);
4745

4846
const existingUser = await prisma.user.findFirst({
4947
where: {
50-
email: input.email,
48+
email,
5149
},
5250
});
5351

5452
const adminEmailRegex = env.ADMIN_EMAILS ? new RegExp(env.ADMIN_EMAILS) : undefined;
55-
const makeAdmin = adminEmailRegex ? adminEmailRegex.test(input.email) : false;
53+
const makeAdmin = adminEmailRegex ? adminEmailRegex.test(email) : false;
5654

5755
const user = await prisma.user.upsert({
5856
where: {
59-
email: input.email,
57+
email,
6058
},
6159
update: {
62-
email: input.email,
60+
email,
6361
},
6462
create: {
65-
email: input.email,
63+
email,
6664
authenticationMethod: "MAGIC_LINK",
6765
admin: makeAdmin, // only on create, to prevent automatically removing existing admins
6866
},
@@ -79,6 +77,8 @@ export async function findOrCreateGithubUser({
7977
authenticationProfile,
8078
authenticationExtraParams,
8179
}: FindOrCreateGithub): Promise<LoggedInUser> {
80+
assertEmailAllowed(email);
81+
8282
const name = authenticationProfile._json.name;
8383
let avatarUrl: string | undefined = undefined;
8484
if (authenticationProfile.photos[0]) {

apps/webapp/app/routes/login._index/route.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
66
import { LoginPageLayout } from "~/components/LoginPageLayout";
77
import { Button, LinkButton } from "~/components/primitives/Buttons";
88
import { Fieldset } from "~/components/primitives/Fieldset";
9+
import { FormError } from "~/components/primitives/FormError";
910
import { Header1 } from "~/components/primitives/Headers";
1011
import { Paragraph } from "~/components/primitives/Paragraph";
1112
import { TextLink } from "~/components/primitives/TextLink";
1213
import { isGithubAuthSupported } from "~/services/auth.server";
1314
import { commitSession, setRedirectTo } from "~/services/redirectTo.server";
1415
import { getUserId } from "~/services/session.server";
16+
import { getUserSession } from "~/services/sessionStorage.server";
1517
import { requestUrl } from "~/utils/requestUrl.server";
1618

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

5052
return typedjson(
51-
{ redirectTo, showGithubAuth: isGithubAuthSupported },
53+
{
54+
redirectTo,
55+
showGithubAuth: isGithubAuthSupported,
56+
authError: null,
57+
},
5258
{
5359
headers: {
5460
"Set-Cookie": await commitSession(session),
5561
},
5662
}
5763
);
5864
} else {
65+
const session = await getUserSession(request);
66+
const error = session.get("auth:error");
67+
68+
let authError: string | undefined;
69+
if (error) {
70+
if ("message" in error) {
71+
authError = error.message;
72+
} else {
73+
authError = JSON.stringify(error, null, 2);
74+
}
75+
}
76+
5977
return typedjson({
6078
redirectTo: null,
6179
showGithubAuth: isGithubAuthSupported,
80+
authError,
6281
});
6382
}
6483
}
@@ -81,7 +100,7 @@ export default function LoginPage() {
81100
Create an account or login
82101
</Paragraph>
83102
<Fieldset className="w-full">
84-
<div className="flex flex-col gap-y-2">
103+
<div className="flex flex-col items-center gap-y-2">
85104
{data.showGithubAuth && (
86105
<Button
87106
type="submit"
@@ -103,6 +122,7 @@ export default function LoginPage() {
103122
<EnvelopeIcon className="mr-2 size-5 text-text-bright" />
104123
Continue with Email
105124
</LinkButton>
125+
{data.authError && <FormError>{data.authError}</FormError>}
106126
</div>
107127
<Paragraph variant="extra-small" className="mt-2 text-center">
108128
By signing up you agree to our{" "}

apps/webapp/app/services/email.server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { EmailClient, MailTransportOptions } from "emails";
33
import type { SendEmailOptions } from "remix-auth-email-link";
44
import { redirect } from "remix-typedjson";
55
import { env } from "~/env.server";
6-
import type { User } from "~/models/user.server";
76
import type { AuthUser } from "./authUser";
87
import { workerQueue } from "./worker.server";
98
import { logger } from "./logger.server";
109
import { singleton } from "~/utils/singleton";
10+
import { assertEmailAllowed } from "~/utils/email";
1111

1212
const client = singleton(
1313
"email-client",
@@ -66,6 +66,8 @@ function buildTransportOptions(alerts?: boolean): MailTransportOptions {
6666
}
6767

6868
export async function sendMagicLinkEmail(options: SendEmailOptions<AuthUser>): Promise<void> {
69+
assertEmailAllowed(options.emailAddress);
70+
6971
// Auto redirect when in development mode
7072
if (env.NODE_ENV === "development") {
7173
throw redirect(options.magicLink);

apps/webapp/app/utils/boolEnv.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { z } from "zod";
2+
3+
export const BoolEnv = z.preprocess((val) => {
4+
if (typeof val !== "string") {
5+
return val;
6+
}
7+
8+
return ["true", "1"].includes(val.toLowerCase().trim());
9+
}, z.boolean());

apps/webapp/app/utils/email.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { env } from "~/env.server";
2+
3+
export function assertEmailAllowed(email: string) {
4+
if (!env.WHITELISTED_EMAILS) {
5+
return;
6+
}
7+
8+
const regexp = new RegExp(env.WHITELISTED_EMAILS);
9+
10+
if (!regexp.test(email)) {
11+
throw new Error("This email is unauthorized");
12+
}
13+
}

0 commit comments

Comments
 (0)