Skip to content
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ R2_CDN_URL=
# Allowed CORS Origins, split by comma
CORS_ORIGINS=https://app.jetkvm.com,http://localhost:5173

# Allowed account emails, split by comma (leave empty to allow all)
ALLOWED_IDENTITIES=

# Real IP Header for the reverse proxy (e.g. X-Real-IP), leave empty if not needed
REAL_IP_HEADER=

Expand Down
21 changes: 21 additions & 0 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import { type NextFunction, type Request, type Response } from "express";
import * as jose from "jose";
import { UnauthorizedError } from "./errors";

const ALLOWED_IDENTITIES = process.env.ALLOWED_IDENTITIES?.split(",")
.map((identity) => identity.trim().toLowerCase())
.filter(Boolean);

const getAllowedIdentities = () => {
if (!ALLOWED_IDENTITIES) return null;
return ALLOWED_IDENTITIES.length > 0 ? new Set(ALLOWED_IDENTITIES) : null;
};

export const isIdentityAllowed = (identity?: string | null) => {
const allowedIdentities = getAllowedIdentities();
const identityNormalized = identity?.trim().toLowerCase();
if (!allowedIdentities) return true;
if (!identityNormalized) return false;
return allowedIdentities.has(identityNormalized);
};

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

const email = (payload as { email?: string }).email;
if (!isIdentityAllowed(email)) {
throw new UnauthorizedError("Account is not in the allowlist", "account_not_allowed");
}

next();
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ declare global {
// Real IP
REAL_IP_HEADER: string;
ICE_SERVERS: string;

ALLOWED_IDENTITIES?: string;
}
}
}
Expand Down
12 changes: 11 additions & 1 deletion src/oidc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { generators, Issuer } from "openid-client";
import express from "express";
import { prisma } from "./db";
import { BadRequestError } from "./errors";
import { BadRequestError, UnauthorizedError } from "./errors";
import { isIdentityAllowed } from "./auth";
import * as crypto from "crypto";

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

if (!userInfo.email) {
req.session = null;
throw new BadRequestError("Missing email claim in user info", "missing_email_claim");
}
if (!isIdentityAllowed(userInfo.email)) {
req.session = null;
throw new UnauthorizedError("Account is not in the allowlist", "account_not_allowed");
}

req.session!.id_token = tokenSet.id_token;

await prisma.user.upsert({
Expand Down