Skip to content

Commit

Permalink
Add missed files
Browse files Browse the repository at this point in the history
  • Loading branch information
davidje13 committed Nov 16, 2024
1 parent 926871d commit 84f5f8e
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 0 deletions.
95 changes: 95 additions & 0 deletions backend/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { WebSocketExpress, type Router } from 'websocket-express';
import ab from 'authentication-backend';
import type { ClientConfig } from './shared/api-entities';
import type { ConfigT } from './config';
import type { UserAuthService } from './services/UserAuthService';
import { addNoCacheHeaders } from './headers';
import { json } from './helpers/json';

const JSON_BODY = WebSocketExpress.json({ limit: 5 * 1024 });

interface AuthBackend {
addRoutes(app: Router): void;
clientConfig: ClientConfig['sso'];
}

export function getAuthBackend(
config: ConfigT,
userAuthService: UserAuthService,
): AuthBackend {
if (config.insecure.sharedAccount.enabled) {
if (Object.values(config.sso).some((v) => v.clientId !== '')) {
throw new Error('Cannot combine insecure login with SSO');
}
const path = config.insecure.sharedAccount.authUrl;
if (!path.startsWith('/') || path.length < 2) {
throw new Error(
'Insecure login path must start with / and cannot be empty',
);
}
return getInsecureAuthBackend(path, userAuthService);
}

const sso = ab.buildAuthenticationBackend(
config.sso,
userAuthService.grantLoginToken,
);
return {
addRoutes: (app) => app.useHTTP('/api/sso', sso.router),
clientConfig: sso.service.clientConfig,
};
}

function getInsecureAuthBackend(
path: string,
userAuthService: UserAuthService,
): AuthBackend {
return {
addRoutes: (app) => {
app.useHTTP('/api/sso/public', JSON_BODY, (req, res) => {
try {
const { externalToken } = json.extractObject(req.body, {
externalToken: json.string,
});
res.status(200).json({ userToken: externalToken });
} catch (e) {
if (!(e instanceof Error)) {
res.status(500).json({ error: 'Internal error' });
} else {
res.status(400).json({ error: e.message });
}
}
});

app.useHTTP(path, (req, res) => {
addNoCacheHeaders(res);

const { redirect_uri: redirectUri, state } = req.query;

if (
typeof redirectUri !== 'string' ||
!redirectUri ||
typeof state !== 'string' ||
!state
) {
res.status(400).json({ error: 'Bad request' });
return;
}

const userToken = userAuthService.grantLoginToken(
'everybody',
'public',
);
const target = new URL(redirectUri);
target.hash = new URLSearchParams({
state,
token: userToken,
}).toString();

res.header('Location', target.toString());
res.status(303).end();
});
},
clientConfig: { public: { authUrl: path, clientId: '' } },
};
}
83 changes: 83 additions & 0 deletions backend/src/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type express from 'express';

const devMode = process.env['NODE_ENV'] === 'development';

const CSP_DOMAIN_PLACEHOLDER = /\(domain\)/g;
const CSP = [
"base-uri 'self'",
"default-src 'self'",
"object-src 'none'",
`script-src 'self'${devMode ? " 'unsafe-eval'" : ''}`,
`style-src 'self'${
devMode
? " 'unsafe-inline'"
: " 'sha256-dhQFgDyZCSW+FVxPjFWZQkEnh+5DHADvj1I8rpzmaGU='"
}`,
'trusted-types dynamic-import',
"require-trusted-types-for 'script'",
// https://github.com/w3c/webappsec-csp/issues/7 (2023: still required for Mobile Safari)
`connect-src 'self' wss://(domain)${devMode ? ' ws://(domain)' : ''}`,
"img-src 'self' data: https://*.giphy.com",
"form-action 'none'",
"frame-ancestors 'none'",
].join('; ');

const PERMISSIONS_POLICY = [
'accelerometer=()',
'autoplay=()',
'camera=()',
'geolocation=()',
'gyroscope=()',
'interest-cohort=()',
'magnetometer=()',
'microphone=()',
'payment=()',
'sync-xhr=()',
'usb=()',
].join(', ');

export function addSecurityHeaders(
req: express.Request,
res: express.Response,
) {
res.header('x-frame-options', 'DENY');
res.header('x-xss-protection', '1; mode=block');
res.header('x-content-type-options', 'nosniff');
res.header(
'content-security-policy',
CSP.replace(CSP_DOMAIN_PLACEHOLDER, getHost(req)),
);
res.header('permissions-policy', PERMISSIONS_POLICY);
res.header('referrer-policy', 'no-referrer');
res.header('cross-origin-opener-policy', 'same-origin');
// Note: CORP causes manifest icons to fail to load in Chrome Devtools,
// but does not break actual functionality
// See: https://issues.chromium.org/issues/41451129
res.header('cross-origin-resource-policy', 'same-origin');
res.header('cross-origin-embedder-policy', 'require-corp');
}

export function removeHtmlSecurityHeaders(res: express.Response) {
res.removeHeader('content-security-policy');
res.removeHeader('permissions-policy');
res.removeHeader('referrer-policy');
res.removeHeader('cross-origin-opener-policy');
res.removeHeader('cross-origin-embedder-policy');
}

export function addNoCacheHeaders(res: express.Response) {
res.header('cache-control', 'no-cache, no-store');
res.header('expires', '0');
res.header('pragma', 'no-cache');
}

function getHost(req: { hostname: string }): string {
const raw: string = req.hostname;
if (raw.includes(':')) {
return raw;
}
// Bug in express 4.x: hostname does not include port
// fixed in 5, but not released yet
// https://expressjs.com/en/guide/migrating-5.html#req.host
return `${raw}:*`;
}

0 comments on commit 84f5f8e

Please sign in to comment.