Skip to content

Commit 8596c72

Browse files
authored
feat: implement user account allowlist (#51)
1 parent dd96373 commit 8596c72

4 files changed

Lines changed: 37 additions & 1 deletion

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ R2_CDN_URL=
3232
# Allowed CORS Origins, split by comma
3333
CORS_ORIGINS=https://app.jetkvm.com,http://localhost:5173
3434

35+
# Allowed account emails, split by comma (leave empty to allow all)
36+
ALLOWED_IDENTITIES=
37+
3538
# Real IP Header for the reverse proxy (e.g. X-Real-IP), leave empty if not needed
3639
REAL_IP_HEADER=
3740

src/auth.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@ import { type NextFunction, type Request, type Response } from "express";
22
import * as jose from "jose";
33
import { UnauthorizedError } from "./errors";
44

5+
const ALLOWED_IDENTITIES = process.env.ALLOWED_IDENTITIES?.split(",")
6+
.map((identity) => identity.trim().toLowerCase())
7+
.filter(Boolean);
8+
9+
const getAllowedIdentities = () => {
10+
if (!ALLOWED_IDENTITIES) return null;
11+
return ALLOWED_IDENTITIES.length > 0 ? new Set(ALLOWED_IDENTITIES) : null;
12+
};
13+
14+
export const isIdentityAllowed = (identity?: string | null) => {
15+
const allowedIdentities = getAllowedIdentities();
16+
const identityNormalized = identity?.trim().toLowerCase();
17+
if (!allowedIdentities) return true;
18+
if (!identityNormalized) return false;
19+
return allowedIdentities.has(identityNormalized);
20+
};
521

622
export const verifyToken = async (idToken: string) => {
723
const JWKS = jose.createRemoteJWKSet(
@@ -33,5 +49,10 @@ export const authenticated = async (req: Request, res: Response, next: NextFunct
3349
throw new UnauthorizedError();
3450
}
3551

52+
const email = (payload as { email?: string }).email;
53+
if (!isIdentityAllowed(email)) {
54+
throw new UnauthorizedError("Account is not in the allowlist", "account_not_allowed");
55+
}
56+
3657
next();
3758
};

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ declare global {
4545
// Real IP
4646
REAL_IP_HEADER: string;
4747
ICE_SERVERS: string;
48+
49+
ALLOWED_IDENTITIES?: string;
4850
}
4951
}
5052
}

src/oidc.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { generators, Issuer } from "openid-client";
22
import express from "express";
33
import { prisma } from "./db";
4-
import { BadRequestError } from "./errors";
4+
import { BadRequestError, UnauthorizedError } from "./errors";
5+
import { isIdentityAllowed } from "./auth";
56
import * as crypto from "crypto";
67

78
const API_HOSTNAME = process.env.API_HOSTNAME;
@@ -88,6 +89,15 @@ export const Callback = async (req: express.Request, res: express.Response) => {
8889
throw new BadRequestError("Missing ID Token", "missing_id_token");
8990
}
9091

92+
if (!userInfo.email) {
93+
req.session = null;
94+
throw new BadRequestError("Missing email claim in user info", "missing_email_claim");
95+
}
96+
if (!isIdentityAllowed(userInfo.email)) {
97+
req.session = null;
98+
throw new UnauthorizedError("Account is not in the allowlist", "account_not_allowed");
99+
}
100+
91101
req.session!.id_token = tokenSet.id_token;
92102

93103
await prisma.user.upsert({

0 commit comments

Comments
 (0)