Skip to content

Commit

Permalink
feat(E2EI): add proxy to circumvent CSP headers (#16374)
Browse files Browse the repository at this point in the history
* feat: add router for oidc client to circumvent CSP headers

* fix: type error

* chore: renamed files for clarity

* chore: fix lock files

* fix: lockfiles again
  • Loading branch information
aweiss-dev authored Dec 14, 2023
1 parent 0ce9f9c commit 06a48e8
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 8 deletions.
5 changes: 4 additions & 1 deletion server/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {ClientConfig, ServerConfig} from './config';
import {HealthCheckRoute} from './routes/_health/HealthRoute';
import {AppleAssociationRoute} from './routes/appleassociation/AppleAssociationRoute';
import {ConfigRoute} from './routes/config/ConfigRoute';
import {OIDCProxyRoute, OIDCProxyRoutePath} from './routes/E2EIProxy/OIDCProxyRoute';
import {InternalErrorRoute, NotFoundRoute} from './routes/error/ErrorRoutes';
import {GoogleWebmasterRoute} from './routes/googlewebmaster/GoogleWebmasterRoute';
import {RedirectRoutes} from './routes/RedirectRoutes';
Expand Down Expand Up @@ -73,6 +74,7 @@ class Server {
this.app.use(ConfigRoute(this.config, this.clientConfig));
this.app.use(GoogleWebmasterRoute(this.config));
this.app.use(AppleAssociationRoute());
this.app.use(OIDCProxyRoute());
this.app.use(NotFoundRoute());
this.app.use(InternalErrorRoute());
}
Expand Down Expand Up @@ -188,7 +190,8 @@ class Server {
req.path.startsWith('/join') ||
req.path.startsWith('/auth') ||
req.path.startsWith('/google') ||
req.path.startsWith('/apple-app-site-association');
req.path.startsWith('/apple-app-site-association') ||
req.path.startsWith(OIDCProxyRoutePath);

if (ignoredPath) {
return next();
Expand Down
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"geolite2": "1.3.0",
"hbs": "4.2.0",
"helmet": "7.1.0",
"http-proxy-middleware": "2.0.6",
"http-status-codes": "2.3.0",
"logdown": "3.3.1",
"maxmind": "4.3.10",
Expand Down
111 changes: 111 additions & 0 deletions server/routes/E2EIProxy/OIDCProxyRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {Router} from 'express';
import {createProxyMiddleware} from 'http-proxy-middleware';

const targetURLParam = 'targetUrl';

export const OIDCProxyRoutePath = '/oidcProxy';

export const OIDCProxyRoute = () => {
// Function to validate the URL
const isValidUrl = (urlString: string) => {
try {
new URL(urlString);
return true;
} catch (e) {

Check warning on line 33 in server/routes/E2EIProxy/OIDCProxyRoute.ts

View workflow job for this annotation

GitHub Actions / lint

Identifier name 'e' is too short (< 2)
return false;
}
};

// @ts-ignore
return Router().use(OIDCProxyRoutePath, (req, res, next) => {
const targetUrl = req.query[targetURLParam];

if (typeof targetUrl !== 'string' || !isValidUrl(targetUrl)) {
return res.status(400).send('Invalid URL');

Check warning on line 43 in server/routes/E2EIProxy/OIDCProxyRoute.ts

View workflow job for this annotation

GitHub Actions / lint

No magic number: 400
}

// Get all query parameters except the targetURLParam
const queryParams = req.query;
delete queryParams[targetURLParam];

// Append the query parameters to the target URL
const targetUrlWithQueryParams = new URL(targetUrl);
Object.keys(queryParams).forEach(key => {
targetUrlWithQueryParams.searchParams.append(key, queryParams[key] as string);
});

// Configure the dynamic proxy middleware
const proxy = createProxyMiddleware({
target: targetUrlWithQueryParams.href,
changeOrigin: true,
ignorePath: true,
// logLevel: 'debug',
selfHandleResponse: true, // Handle response manually
followRedirects: true,
onProxyRes: (proxyRes, req, res) => {
if (targetUrlWithQueryParams.href.includes('/dex/auth')) {
// Exception 1: Redirect to the target URL if the response is a redirect to the OIDC auth endpoint
res.redirect(targetUrlWithQueryParams.href);
// Exception 2: Modify the response if the target URL is the OIDC discovery URL
} else if (req.originalUrl.includes('.well-known/openid-configuration')) {
let body = '';

proxyRes.on('data', chunk => {
body += chunk;
});

proxyRes.on('end', () => {
try {
// Parse the body as JSON
const json = JSON.parse(body);

// Modify URLs in the JSON response
Object.keys(json).forEach(key => {
if (typeof json[key] === 'string' && json[key].startsWith('https://')) {
const originalUrl = new URL(json[key]);
const refererUrl = new URL(req.headers.referer);

json[key] = `${refererUrl.origin}${OIDCProxyRoutePath}?${targetURLParam}=${encodeURIComponent(
originalUrl.href,
)}`;
}
});
// Send the modified response back to the client
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(json));
} catch (error) {
console.error('Error processing proxy response:', error);
res.status(500).send('Internal Server Error');

Check warning on line 97 in server/routes/E2EIProxy/OIDCProxyRoute.ts

View workflow job for this annotation

GitHub Actions / lint

No magic number: 500
}
});
} else {
// Default: Send the response back to the client
res.setHeader('Content-Type', proxyRes.headers['content-type']);
proxyRes.pipe(res);
}
},
});

// Apply the proxy middleware
return proxy(req, res, next);
});
};
64 changes: 62 additions & 2 deletions server/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,15 @@ __metadata:
languageName: node
linkType: hard

"@types/http-proxy@npm:^1.17.8":
version: 1.17.14
resolution: "@types/http-proxy@npm:1.17.14"
dependencies:
"@types/node": "*"
checksum: 491320bce3565bbb6c7d39d25b54bce626237cfb6b09e60ee7f77b56ae7c6cbad76f08d47fe01eaa706781124ee3dfad9bb737049254491efd98ed1f014c4e83
languageName: node
linkType: hard

"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1":
version: 2.0.4
resolution: "@types/istanbul-lib-coverage@npm:2.0.4"
Expand Down Expand Up @@ -2275,6 +2284,13 @@ __metadata:
languageName: node
linkType: hard

"eventemitter3@npm:^4.0.0":
version: 4.0.7
resolution: "eventemitter3@npm:4.0.7"
checksum: 1875311c42fcfe9c707b2712c32664a245629b42bb0a5a84439762dd0fd637fc54d078155ea83c2af9e0323c9ac13687e03cfba79b03af9f40c89b4960099374
languageName: node
linkType: hard

"execa@npm:^5.0.0":
version: 5.1.1
resolution: "execa@npm:5.1.1"
Expand Down Expand Up @@ -2439,7 +2455,7 @@ __metadata:
languageName: node
linkType: hard

"follow-redirects@npm:^1.14.0":
"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.0":
version: 1.15.3
resolution: "follow-redirects@npm:1.15.3"
peerDependenciesMeta:
Expand Down Expand Up @@ -2869,6 +2885,35 @@ __metadata:
languageName: node
linkType: hard

"http-proxy-middleware@npm:2.0.6":
version: 2.0.6
resolution: "http-proxy-middleware@npm:2.0.6"
dependencies:
"@types/http-proxy": ^1.17.8
http-proxy: ^1.18.1
is-glob: ^4.0.1
is-plain-obj: ^3.0.0
micromatch: ^4.0.2
peerDependencies:
"@types/express": ^4.17.13
peerDependenciesMeta:
"@types/express":
optional: true
checksum: 2ee85bc878afa6cbf34491e972ece0f5be0a3e5c98a60850cf40d2a9a5356e1fc57aab6cff33c1fc37691b0121c3a42602d2b1956c52577e87a5b77b62ae1c3a
languageName: node
linkType: hard

"http-proxy@npm:^1.18.1":
version: 1.18.1
resolution: "http-proxy@npm:1.18.1"
dependencies:
eventemitter3: ^4.0.0
follow-redirects: ^1.0.0
requires-port: ^1.0.0
checksum: f5bd96bf83e0b1e4226633dbb51f8b056c3e6321917df402deacec31dd7fe433914fc7a2c1831cf7ae21e69c90b3a669b8f434723e9e8b71fd68afe30737b6a5
languageName: node
linkType: hard

"http-status-codes@npm:2.3.0":
version: 2.3.0
resolution: "http-status-codes@npm:2.3.0"
Expand Down Expand Up @@ -3070,6 +3115,13 @@ __metadata:
languageName: node
linkType: hard

"is-plain-obj@npm:^3.0.0":
version: 3.0.0
resolution: "is-plain-obj@npm:3.0.0"
checksum: a6ebdf8e12ab73f33530641972a72a4b8aed6df04f762070d823808303e4f76d87d5ea5bd76f96a7bbe83d93f04ac7764429c29413bd9049853a69cb630fb21c
languageName: node
linkType: hard

"is-stream@npm:^2.0.0":
version: 2.0.1
resolution: "is-stream@npm:2.0.1"
Expand Down Expand Up @@ -3876,7 +3928,7 @@ __metadata:
languageName: node
linkType: hard

"micromatch@npm:^4.0.4":
"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4":
version: 4.0.5
resolution: "micromatch@npm:4.0.5"
dependencies:
Expand Down Expand Up @@ -4775,6 +4827,13 @@ __metadata:
languageName: node
linkType: hard

"requires-port@npm:^1.0.0":
version: 1.0.0
resolution: "requires-port@npm:1.0.0"
checksum: eee0e303adffb69be55d1a214e415cf42b7441ae858c76dfc5353148644f6fd6e698926fc4643f510d5c126d12a705e7c8ed7e38061113bdf37547ab356797ff
languageName: node
linkType: hard

"resolve-cwd@npm:^3.0.0":
version: 3.0.0
resolution: "resolve-cwd@npm:3.0.0"
Expand Down Expand Up @@ -5574,6 +5633,7 @@ __metadata:
geolite2: 1.3.0
hbs: 4.2.0
helmet: 7.1.0
http-proxy-middleware: 2.0.6
http-status-codes: 2.3.0
jest: 29.7.0
logdown: 3.3.1
Expand Down
14 changes: 9 additions & 5 deletions src/script/E2EIdentity/OIDCService/OIDCService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class OIDCService {
oidcClient: {id, secret},
} = config;
const dexioConfig: UserManagerSettings = {
authority: authorityUrl,
authority: `/oidcProxy?targetUrl=${authorityUrl}`,
client_id: id,
redirect_uri: redirectUri,
response_type: 'code',
Expand All @@ -54,13 +54,17 @@ export class OIDCService {
await this.userManager.signinRedirect();
}

public handleAuthentication(): Promise<User> {
public async handleAuthentication(): Promise<User | undefined> {
// Remove the hash (hash router) from the url before processing
const url = window.location.href.replace('/#', '');

return this.userManager.signinRedirectCallback(url).then(user => {
return user;
});
const user = await this.userManager.signinCallback(url);

if (!user) {
return undefined;
}

return user;
}

public clearProgress(): Promise<void> {
Expand Down

0 comments on commit 06a48e8

Please sign in to comment.