mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-20 09:07:49 +01:00
allow controlling cors from config and add cors middleware to traefik
This commit is contained in:
parent
7ff5376d13
commit
ab18e15a71
8 changed files with 98 additions and 43 deletions
|
@ -1,15 +1,15 @@
|
||||||
app:
|
app:
|
||||||
dashboard_url: http://localhost
|
dashboard_url: http://localhost:3002
|
||||||
base_domain: localhost
|
base_domain: localhost
|
||||||
log_level: debug
|
log_level: info
|
||||||
save_logs: false
|
save_logs: false
|
||||||
|
|
||||||
server:
|
server:
|
||||||
external_port: 3000
|
external_port: 3000
|
||||||
internal_port: 3001
|
internal_port: 3001
|
||||||
next_port: 3002
|
next_port: 3002
|
||||||
internal_hostname: localhost
|
internal_hostname: pangolin
|
||||||
secure_cookies: false
|
secure_cookies: true
|
||||||
session_cookie_name: p_session
|
session_cookie_name: p_session
|
||||||
resource_session_cookie_name: p_resource_session
|
resource_session_cookie_name: p_resource_session
|
||||||
resource_access_token_param: p_token
|
resource_access_token_param: p_token
|
||||||
|
@ -38,4 +38,6 @@ users:
|
||||||
password: Password123!
|
password: Password123!
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: false
|
require_email_verification: true
|
||||||
|
disable_signup_without_invite: true
|
||||||
|
disable_user_create_org: true
|
||||||
|
|
|
@ -4,6 +4,21 @@ http:
|
||||||
redirectScheme:
|
redirectScheme:
|
||||||
scheme: https
|
scheme: https
|
||||||
permanent: true
|
permanent: true
|
||||||
|
cors:
|
||||||
|
headers:
|
||||||
|
accessControlAllowMethods:
|
||||||
|
- GET
|
||||||
|
- PUT
|
||||||
|
- POST
|
||||||
|
- DELETE
|
||||||
|
- PATCH
|
||||||
|
accessControlAllowHeaders:
|
||||||
|
- Content-Type
|
||||||
|
- X-CSRF-Token
|
||||||
|
accessControlAllowOriginList:
|
||||||
|
- https://{{.DashboardDomain}}
|
||||||
|
accessControlAllowCredentials: false
|
||||||
|
|
||||||
|
|
||||||
routers:
|
routers:
|
||||||
# HTTP to HTTPS redirect router
|
# HTTP to HTTPS redirect router
|
||||||
|
@ -14,6 +29,7 @@ http:
|
||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- redirect-to-https
|
- redirect-to-https
|
||||||
|
- cors
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
|
@ -21,6 +37,8 @@ http:
|
||||||
service: next-service
|
service: next-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- cors
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
@ -30,6 +48,8 @@ http:
|
||||||
service: api-service
|
service: api-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- cors
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
@ -39,6 +59,8 @@ http:
|
||||||
service: api-service
|
service: api-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- cors
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
|
|
@ -20,23 +20,30 @@ const externalPort = config.getRawConfig().server.external_port;
|
||||||
export function createApiServer() {
|
export function createApiServer() {
|
||||||
const apiServer = express();
|
const apiServer = express();
|
||||||
|
|
||||||
// Middleware setup
|
|
||||||
apiServer.set("trust proxy", 1);
|
apiServer.set("trust proxy", 1);
|
||||||
if (dev) {
|
|
||||||
apiServer.use(
|
|
||||||
cors({
|
|
||||||
origin: `http://localhost:${config.getRawConfig().server.next_port}`,
|
|
||||||
credentials: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const corsOptions = {
|
|
||||||
origin: config.getRawConfig().app.dashboard_url,
|
|
||||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
||||||
allowedHeaders: ["Content-Type", "X-CSRF-Token"]
|
|
||||||
};
|
|
||||||
|
|
||||||
apiServer.use(cors(corsOptions));
|
const corsConfig = config.getRawConfig().server.cors;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
...(corsConfig?.origins
|
||||||
|
? { origin: corsConfig.origins }
|
||||||
|
: {
|
||||||
|
origin: (origin: any, callback: any) => {
|
||||||
|
callback(null, true);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...(corsConfig?.methods && { methods: corsConfig.methods }),
|
||||||
|
...(corsConfig?.allowed_headers && {
|
||||||
|
allowedHeaders: corsConfig.allowed_headers
|
||||||
|
}),
|
||||||
|
credentials: !(corsConfig?.credentials === false)
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.debug("Using CORS options", options);
|
||||||
|
|
||||||
|
apiServer.use(cors(options));
|
||||||
|
|
||||||
|
if (!dev) {
|
||||||
apiServer.use(helmet());
|
apiServer.use(helmet());
|
||||||
apiServer.use(csrfProtectionMiddleware);
|
apiServer.use(csrfProtectionMiddleware);
|
||||||
}
|
}
|
||||||
|
@ -47,7 +54,8 @@ export function createApiServer() {
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
apiServer.use(
|
apiServer.use(
|
||||||
rateLimitMiddleware({
|
rateLimitMiddleware({
|
||||||
windowMin: config.getRawConfig().rate_limits.global.window_minutes,
|
windowMin:
|
||||||
|
config.getRawConfig().rate_limits.global.window_minutes,
|
||||||
max: config.getRawConfig().rate_limits.global.max_requests,
|
max: config.getRawConfig().rate_limits.global.max_requests,
|
||||||
type: "IP_AND_PATH"
|
type: "IP_AND_PATH"
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
encodeBase32LowerCaseNoPadding,
|
encodeBase32LowerCaseNoPadding,
|
||||||
encodeHexLowerCase,
|
encodeHexLowerCase
|
||||||
} from "@oslojs/encoding";
|
} from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { Session, sessions, User, users } from "@server/db/schema";
|
import { Session, sessions, User, users } from "@server/db/schema";
|
||||||
|
@ -9,8 +9,10 @@ import { eq } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import type { RandomReader } from "@oslojs/crypto/random";
|
import type { RandomReader } from "@oslojs/crypto/random";
|
||||||
import { generateRandomString } from "@oslojs/crypto/random";
|
import { generateRandomString } from "@oslojs/crypto/random";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name;
|
export const SESSION_COOKIE_NAME =
|
||||||
|
config.getRawConfig().server.session_cookie_name;
|
||||||
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||||
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
||||||
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
|
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
|
||||||
|
@ -24,25 +26,25 @@ export function generateSessionToken(): string {
|
||||||
|
|
||||||
export async function createSession(
|
export async function createSession(
|
||||||
token: string,
|
token: string,
|
||||||
userId: string,
|
userId: string
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token)),
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const session: Session = {
|
const session: Session = {
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
userId,
|
userId,
|
||||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime()
|
||||||
};
|
};
|
||||||
await db.insert(sessions).values(session);
|
await db.insert(sessions).values(session);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateSessionToken(
|
export async function validateSessionToken(
|
||||||
token: string,
|
token: string
|
||||||
): Promise<SessionValidationResult> {
|
): Promise<SessionValidationResult> {
|
||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token)),
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const result = await db
|
const result = await db
|
||||||
.select({ user: users, session: sessions })
|
.select({ user: users, session: sessions })
|
||||||
|
@ -61,12 +63,12 @@ export async function validateSessionToken(
|
||||||
}
|
}
|
||||||
if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
|
if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
|
||||||
session.expiresAt = new Date(
|
session.expiresAt = new Date(
|
||||||
Date.now() + SESSION_COOKIE_EXPIRES,
|
Date.now() + SESSION_COOKIE_EXPIRES
|
||||||
).getTime();
|
).getTime();
|
||||||
await db
|
await db
|
||||||
.update(sessions)
|
.update(sessions)
|
||||||
.set({
|
.set({
|
||||||
expiresAt: session.expiresAt,
|
expiresAt: session.expiresAt
|
||||||
})
|
})
|
||||||
.where(eq(sessions.sessionId, session.sessionId));
|
.where(eq(sessions.sessionId, session.sessionId));
|
||||||
}
|
}
|
||||||
|
@ -81,26 +83,38 @@ export async function invalidateAllSessions(userId: string): Promise<void> {
|
||||||
await db.delete(sessions).where(eq(sessions.userId, userId));
|
await db.delete(sessions).where(eq(sessions.userId, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeSessionCookie(token: string): string {
|
export function serializeSessionCookie(
|
||||||
if (SECURE_COOKIES) {
|
token: string,
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
isSecure: boolean
|
||||||
|
): string {
|
||||||
|
if (isSecure) {
|
||||||
|
logger.debug("Setting cookie for secure origin");
|
||||||
|
if (SECURE_COOKIES) {
|
||||||
|
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||||
|
} else {
|
||||||
|
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBlankSessionTokenCookie(): string {
|
export function createBlankSessionTokenCookie(isSecure: boolean): string {
|
||||||
if (SECURE_COOKIES) {
|
if (isSecure) {
|
||||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
if (SECURE_COOKIES) {
|
||||||
|
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||||
|
} else {
|
||||||
|
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const random: RandomReader = {
|
const random: RandomReader = {
|
||||||
read(bytes: Uint8Array): void {
|
read(bytes: Uint8Array): void {
|
||||||
crypto.getRandomValues(bytes);
|
crypto.getRandomValues(bytes);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function generateId(length: number): string {
|
export function generateId(length: number): string {
|
||||||
|
|
|
@ -38,7 +38,13 @@ const environmentSchema = z.object({
|
||||||
secure_cookies: z.boolean(),
|
secure_cookies: z.boolean(),
|
||||||
session_cookie_name: z.string(),
|
session_cookie_name: z.string(),
|
||||||
resource_session_cookie_name: z.string(),
|
resource_session_cookie_name: z.string(),
|
||||||
resource_access_token_param: z.string()
|
resource_access_token_param: z.string(),
|
||||||
|
cors: z.object({
|
||||||
|
origins: z.array(z.string()).optional(),
|
||||||
|
methods: z.array(z.string()).optional(),
|
||||||
|
allowed_headers: z.array(z.string()).optional(),
|
||||||
|
credentials: z.boolean().optional(),
|
||||||
|
}).optional()
|
||||||
}),
|
}),
|
||||||
traefik: z.object({
|
traefik: z.object({
|
||||||
http_entrypoint: z.string(),
|
http_entrypoint: z.string(),
|
||||||
|
|
|
@ -120,7 +120,8 @@ export async function login(
|
||||||
|
|
||||||
const token = generateSessionToken();
|
const token = generateSessionToken();
|
||||||
await createSession(token, existingUser.userId);
|
await createSession(token, existingUser.userId);
|
||||||
const cookie = serializeSessionCookie(token);
|
const isSecure = req.protocol === "https";
|
||||||
|
const cookie = serializeSessionCookie(token, isSecure);
|
||||||
|
|
||||||
res.appendHeader("Set-Cookie", cookie);
|
res.appendHeader("Set-Cookie", cookie);
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,8 @@ export async function logout(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invalidateSession(sessionId);
|
await invalidateSession(sessionId);
|
||||||
res.setHeader("Set-Cookie", createBlankSessionTokenCookie());
|
const isSecure = req.protocol === "https";
|
||||||
|
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
|
||||||
|
|
||||||
return response<null>(res, {
|
return response<null>(res, {
|
||||||
data: null,
|
data: null,
|
||||||
|
|
|
@ -158,7 +158,8 @@ export async function signup(
|
||||||
|
|
||||||
const token = generateSessionToken();
|
const token = generateSessionToken();
|
||||||
await createSession(token, userId);
|
await createSession(token, userId);
|
||||||
const cookie = serializeSessionCookie(token);
|
const isSecure = req.protocol === "https";
|
||||||
|
const cookie = serializeSessionCookie(token, isSecure);
|
||||||
res.appendHeader("Set-Cookie", cookie);
|
res.appendHeader("Set-Cookie", cookie);
|
||||||
|
|
||||||
if (config.getRawConfig().flags?.require_email_verification) {
|
if (config.getRawConfig().flags?.require_email_verification) {
|
||||||
|
|
Loading…
Reference in a new issue