Skip to content

Commit e203560

Browse files
authored
Merge pull request #42 from GBSL-Informatik/feature/configurable-cors
feature: configurable cors config
2 parents 16fa6a4 + 618694a commit e203560

File tree

6 files changed

+71
-20
lines changed

6 files changed

+71
-20
lines changed

.example.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
DATABASE_URL="postgresql://user:pw@localhost:5432/teaching_website"
22
USER_ID="b6651212-0765-4d1c-ba5c-71632bf53d2a"
33
USER_EMAIL="[email protected]"
4-
FRONTEND_URL="http://localhost:3000"
4+
ALLOWED_ORIGINS="http://localhost:3000"
5+
ALLOW_SUBDOMAINS="false"
56
MSAL_CLIENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
67
MSAL_TENANT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
78
ADMIN_USER_GROUP_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ to format all typescript files.
7171
| `USER_ID` | The UUID of the user to be created on seeding. \* | `fc0dfc19-d4a3-4354-afef-b5706046b368` |
7272
| `NO_AUTH` | If set (and not running `production` mode), clients can authenticate as any user by supplying `{'email': '[email protected]'}` in the `Auhorization` header, for any user email in the database\*\* | `NO_AUTH=true` |
7373
| `PORT` | (optional) The port the server should listen on. | `3002` (default) |
74-
| `FRONTEND_URL` | The URL of the frontend. | `http://localhost:3000` |
74+
| `ALLOWED_ORIGINS` | A comma-separated list of origins allowed to access the api. E.g. teaching-dev.gbsl.website | `localhost:3000` |
75+
| `ALLOW_SUBDOMAINS` | Wheter subdomains from `ALLOWED_DOMAINS` should be granted access too. | `false` |
7576
| `SESSION_SECRET` | The secret for the session cookie.\*\*\* | `secret` |
7677
| `MSAL_CLIENT_ID` | The client id for the web api from Azure. | |
7778
| `MSAL_TENANT_ID` | The Tenant ID from your Azure instance | |
@@ -261,7 +262,8 @@ dokku config:set dev-teaching-api MSAL_CLIENT_ID="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXX
261262
dokku config:set dev-teaching-api MSAL_TENANT_ID="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
262263
dokku config:set --no-restart dev-teaching-api DOKKU_LETSENCRYPT_EMAIL="[email protected]"
263264
dokku config:set dev-teaching-api SESSION_SECRET="$(openssl rand -base64 32)"
264-
dokku config:set dev-teaching-api FRONTEND_URL="https://..."
265+
dokku config:set dev-teaching-api ALLOWED_ORIGINS="tdev.tld"
266+
dokku config:set dev-teaching-api ALLOW_SUBDOMAINS="false"
265267

266268
mkdir /home/dokku/dev-teaching-api/nginx.conf.d/
267269
echo 'client_max_body_size 5m;' > /home/dokku/dev-teaching-api/nginx.conf.d/upload.conf

bin/create-dokku.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ read -p "Enter MSAL_TENANT_ID: " MSAL_TENANT_ID
1515
# Prompt for DOKKU_LETSENCRYPT_EMAIL
1616
read -p "Enter DOKKU_LETSENCRYPT_EMAIL: " LETSENCRYPT_EMAIL
1717

18-
# Prompt for FRONTEND_URL
19-
read -p "Enter FRONTEND_URL: " FRONTEND_URL
18+
# Prompt for D
19+
read -p "Enter ALLOWED_ORIGINS: " ALLOWED_ORIGINS
2020

2121
# Prompt for AWS backup credentials
2222
read -p "Enter AWS access key ID for backup: " AWS_ACCESS_KEY
@@ -43,7 +43,7 @@ dokku config:set $APP_NAME MSAL_CLIENT_ID="$MSAL_CLIENT_ID"
4343
dokku config:set $APP_NAME MSAL_TENANT_ID="$MSAL_TENANT_ID"
4444
dokku config:set --no-restart $APP_NAME DOKKU_LETSENCRYPT_EMAIL="$LETSENCRYPT_EMAIL"
4545
dokku config:set $APP_NAME SESSION_SECRET="$SESSION_SECRET"
46-
dokku config:set $APP_NAME FRONTEND_URL="$FRONTEND_URL"
46+
dokku config:set $APP_NAME ALLOWED_ORIGINS="$ALLOWED_ORIGINS"
4747

4848
# Configure nginx for file uploads
4949
mkdir -p /home/dokku/$APP_NAME/nginx.conf.d/

src/app.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import connectPgSimple from 'connect-pg-simple';
1616
import Logger from './utils/logger';
1717
import type { ClientToServerEvents, ServerToClientEvents } from './routes/socketEventTypes';
1818
import type { Server } from 'socket.io';
19+
import { CORS_ORIGIN, SAME_SITE } from './utils/originConfig';
1920

2021
const AccessRules = createAccessRules(authConfig.accessMatrix);
2122

@@ -29,17 +30,6 @@ const app = express();
2930
export const API_VERSION = 'v1';
3031
export const API_URL = `/api/${API_VERSION}`;
3132

32-
const HOSTNAME = new URL(process.env.FRONTEND_URL || 'http://localhost:3000').hostname;
33-
const domainParts = HOSTNAME.split('.');
34-
const domain = domainParts.slice(domainParts.length - 2).join('.'); /** foo.bar.ch --> domain is bar.ch */
35-
const CORS_APP = domain.split('.')[1]
36-
? new RegExp(`https://(.*\.)?${domain.split('.')[0]}\\.${domain.split('.')[1]}$`, 'i')
37-
: 'http://localhost:3000';
38-
const CORS_NETLIFY = process.env.NETLIFY_PROJECT_NAME
39-
? new RegExp(`https://deploy-preview-\\d+--${process.env.NETLIFY_PROJECT_NAME}\\.netlify\\.app$`, 'i')
40-
: undefined;
41-
export const CORS_ORIGIN = [HOSTNAME, CORS_APP, CORS_NETLIFY].filter((rule) => !!rule) as (string | RegExp)[];
42-
4333
/**
4434
* this is not needed when running behind a reverse proxy
4535
* as is the case with dokku (nginx)
@@ -85,8 +75,7 @@ export const sessionMiddleware = session({
8575
cookie: {
8676
secure: process.env.NODE_ENV === 'production',
8777
httpOnly: true,
88-
sameSite: process.env.NETLIFY_PROJECT_NAME ? 'none' : 'strict',
89-
domain: domain.length > 0 ? domain : undefined,
78+
sameSite: SAME_SITE,
9079
maxAge: SESSION_MAX_AGE // 30 days
9180
}
9281
});

src/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import app, { configure, CORS_ORIGIN, sessionMiddleware } from './app';
1+
import app, { configure, sessionMiddleware } from './app';
22
import http from 'http';
33
import Logger from './utils/logger';
44
import { Server } from 'socket.io';
@@ -8,6 +8,7 @@ import EventRouter from './routes/socketEvents';
88
import { NextFunction, Request, Response } from 'express';
99
import * as Sentry from '@sentry/node';
1010
import { HTTP403Error } from './utils/errors/Errors';
11+
import { CORS_ORIGIN } from './utils/originConfig';
1112

1213
const PORT = process.env.PORT || 3002;
1314

src/utils/originConfig.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const DEV_ORIGIN = 'localhost:3000';
2+
3+
/**
4+
* splits the origins and removes duplicates
5+
*/
6+
const splitOrigins = (origins: string) => {
7+
return [
8+
...new Set(
9+
origins
10+
.split(',')
11+
.map((origin) => origin.trim())
12+
.filter(Boolean)
13+
)
14+
];
15+
};
16+
17+
// Read CORS configuration from environment variables
18+
const allowedOrigins = process.env.ALLOWED_ORIGINS ? splitOrigins(process.env.ALLOWED_ORIGINS) : [DEV_ORIGIN];
19+
const allowSubdomains = process.env.ALLOW_SUBDOMAINS === 'true';
20+
const netlifyProjectName = process.env.NETLIFY_PROJECT_NAME;
21+
22+
// Build the CORS origin array
23+
export const CORS_ORIGIN: (string | RegExp)[] = [];
24+
25+
const isLoclhost = (origin: string) => {
26+
return origin.startsWith('localhost') || origin.startsWith('127.0.0.1');
27+
};
28+
29+
// Process additional allowed origins
30+
allowedOrigins.forEach((origin) => {
31+
origin = origin.trim();
32+
33+
if (!origin) {
34+
return;
35+
}
36+
if (isLoclhost(origin)) {
37+
return CORS_ORIGIN.push(`http://${origin}`);
38+
}
39+
40+
if (allowSubdomains) {
41+
// Escape dots and create regex for domain with optional subdomains
42+
const escapedDomain = origin.replace(/\./g, '\\.');
43+
CORS_ORIGIN.push(new RegExp(`^https?://(.*\\.)?${escapedDomain}$`, 'i'));
44+
} else {
45+
// Add exact domain match (ensuring it has protocol)
46+
if (!origin.startsWith('http')) {
47+
origin = `https://${origin}`;
48+
}
49+
CORS_ORIGIN.push(origin);
50+
}
51+
});
52+
53+
// Add Netlify deploy previews if enabled
54+
if (netlifyProjectName) {
55+
CORS_ORIGIN.push(new RegExp(`https://deploy-preview-\\d+--${netlifyProjectName}\\.netlify\\.app$`, 'i'));
56+
}
57+
58+
export const SAME_SITE = allowedOrigins.length > 1 ? 'none' : 'strict';

0 commit comments

Comments
 (0)