Skip to content

Commit b5bf689

Browse files
committed
feat(e2e): implement local https proxy for e2e testing
Implements a local HTTPS development environment using an nginx reverse proxy to support E2E testing of features that require a secure context, such as WebAuthn. Key changes include: - Added an nginx reverse proxy to terminate SSL for local development. - Simplified and corrected the nginx configuration for robustness and proper path-forwarding for multi-page applications. - Updated the OIDC application to use `https` in its redirect URIs and corrected mismatches. - Introduced `pnpm` scripts (`setup:https`, `https-proxy:up`) to streamline setup and usage. - Added documentation for the new local HTTPS setup, including the requirement for `--host=0.0.0.0` when running applications behind the proxy.
1 parent 722b73b commit b5bf689

File tree

12 files changed

+236
-7
lines changed

12 files changed

+236
-7
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ outputs/*
6060

6161
e2e/mock-api-v2/html/*
6262
e2e/**/.playwright/*
63+
e2e/certs/
6364

6465

6566
# bundle analysis
@@ -82,4 +83,4 @@ test-output
8283

8384
# Gemini local knowledge base files
8485
GEMINI.md
85-
**/GEMINI.md
86+
**/GEMINI.md

contributing_docs/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
- [Private vs. Public Packages](./releases.md#adding-a-package-to-the-repository)
3737
- [Ignoring Packages from Releases](./releases.md#adding-a-package-to-the-repository)
3838

39+
### Local HTTPS
40+
41+
- [E2E HTTPS Bootstrap](./local-https.md#local-https-for-e2e-apps)
42+
3943
## 🚀 Quick Links
4044

4145
- [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary)

contributing_docs/local-https.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Local HTTPS for E2E apps
2+
3+
This guide explains how to generate and refresh the TLS certificates used by the shared HTTPS reverse proxy in the `e2e` stack.
4+
5+
## Why this exists
6+
7+
Some capabilities (for example WebAuthn) require that the browser sees a fully trusted HTTPS origin. Instead of teaching every test app to serve HTTPS, we terminate TLS once at a lightweight proxy container and keep the individual apps on HTTP. The proxy reads a single certificate/key pair from `e2e/certs` and routes traffic (e.g., `/davinci`, `/ping-am`) to the existing services.
8+
9+
## One-time prerequisites
10+
11+
1. Install [`mkcert`](https://github.com/FiloSottile/mkcert):
12+
- macOS: `brew install mkcert nss`
13+
- Windows (Powershell): `choco install mkcert` or `scoop install mkcert`
14+
- Linux: use your package manager or download the binary
15+
2. Trust the local root into the OS/browser store. Run `mkcert -install` (the script below will do this automatically if it has not been run before). Administrator/root approval may be needed.
16+
17+
If your device already trusts the Ping internal CA that issues the certificates, you can skip `mkcert` and instead place the relevant certificate/key in `e2e/certs`. For the default workflow we ship, we rely on `mkcert`.
18+
19+
## Bootstrap the certificate
20+
21+
From the repository root run:
22+
23+
```bash
24+
pnpm run setup:https
25+
```
26+
27+
`pnpm run setup:https` is a thin wrapper around `scripts/bootstrap-https.sh`. The script:
28+
29+
- Ensures `mkcert` is installed
30+
- Installs the mkcert root CA into the system trust store if it is not present
31+
- Creates (or refreshes) `e2e/certs/proxy-cert.pem` and `e2e/certs/proxy-key.pem` with SANs for `localhost`, `127.0.0.1`, and `::1`
32+
33+
> The files can safely be committed to your local clone—they should **not** be committed to git.
34+
35+
## Regenerating or customizing
36+
37+
- Re-run `pnpm run setup:https` at any time; it overwrites the pem files in place.
38+
- To add additional hostnames (for example `dev.ping.local`), edit `DOMAIN_LIST` inside `scripts/bootstrap-https.sh` before re-running the script. Make sure the new hostnames resolve to your proxy (via `/etc/hosts`, corporate DNS, etc.).
39+
40+
## Docker integration
41+
42+
The `e2e/docker-compose.yml` proxy service mounts the mkcert outputs directly (`./certs/proxy-cert.pem` and `./certs/proxy-key.pem`) into `/etc/nginx/tls/`, matching the defaults baked into the Docker image.
43+
44+
When you run `pnpm https-proxy:up` (or directly `docker compose -f e2e/docker-compose.yml up`), the proxy serves `https://localhost:8443/...` using the freshly generated certificate. Because the root CA is trusted, browsers treat the origin as fully secure and WebAuthn flows work without security errors.
45+
46+
If you are using a corporate-managed device, your IT team can distribute the mkcert root CA (or an equivalent internal CA) via MDM so that new developers do not need to run `mkcert -install` manually.
47+
48+
## Troubleshooting
49+
50+
- **Browser warning persists**: Confirm the mkcert root CA is installed in the OS trust store (run `mkcert -CAROOT` to locate it). Remove any stale certificates and rerun the bootstrap script.
51+
- **mkcert not found**: Ensure it is on your `PATH`. Open a new shell after installation.
52+
- **Permission issues on install**: `mkcert -install` modifies OS certificate stores and may require elevated privileges. Run the script again with the necessary rights.
53+
54+
With the certificate in place, the HTTPS proxy is ready and the E2E apps can rely on secure origins without per-app TLS configuration.
55+
56+
## Running Applications Behind the Proxy
57+
58+
When running an application that needs to be accessed by the HTTPS proxy, you must ensure that its development server is accessible from within the Docker network.
59+
60+
For Vite-based applications (like `oidc-app` or `davinci-app`), you need to start the dev server with the `--host=0.0.0.0` flag. This tells the server to listen on all available network interfaces, not just `localhost`. This is essential for the `nginx` proxy container to be able to connect to your application's dev server.
61+
62+
Here is an example command:
63+
64+
```bash
65+
pnpm nx run @forgerock/davinci-app:nxServe --port=5173 --host=0.0.0.0
66+
```
67+
68+
If you forget to add `--host=0.0.0.0`, the proxy will not be able to reach your application, and you will see a "502 Bad Gateway" error in your browser.

e2e/docker-compose.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
services:
2+
https-proxy:
3+
build:
4+
context: ./https-proxy
5+
image: ping-local-https
6+
ports:
7+
- '8443:8443'
8+
environment:
9+
LISTEN_PORT: 8443
10+
SSL_CERT_PATH: /etc/nginx/tls/proxy-cert.pem
11+
SSL_CERT_KEY_PATH: /etc/nginx/tls/proxy-key.pem
12+
DAVINCI_UPSTREAM: host.docker.internal:5829
13+
OIDC_UPSTREAM: host.docker.internal:5173
14+
PROTECT_UPSTREAM: host.docker.internal:4300
15+
DEVICE_UPSTREAM: host.docker.internal:4301
16+
MOCK_API_UPSTREAM: host.docker.internal:9443
17+
volumes:
18+
- ./certs:/etc/nginx/tls:ro
19+
extra_hosts:
20+
- 'host.docker.internal:host-gateway'

e2e/https-proxy/Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM nginx:1.27-alpine
2+
3+
RUN apk add --no-cache gettext
4+
5+
ENV LISTEN_PORT=8443 \
6+
SSL_CERT_PATH=/etc/nginx/tls/proxy-cert.pem \
7+
SSL_CERT_KEY_PATH=/etc/nginx/tls/proxy-key.pem \
8+
DAVINCI_UPSTREAM=host.docker.internal:5829 \
9+
OIDC_UPSTREAM=host.docker.internal:5173 \
10+
PROTECT_UPSTREAM=host.docker.internal:4300 \
11+
DEVICE_UPSTREAM=host.docker.internal:4301 \
12+
MOCK_API_UPSTREAM=host.docker.internal:9443
13+
14+
COPY default.conf.template /etc/nginx/templates/default.conf.template
15+
COPY docker-entrypoint.d/ /docker-entrypoint.d/
16+
17+
EXPOSE 8443
18+
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
map $http_upgrade $connection_upgrade {
2+
default upgrade;
3+
'' close;
4+
}
5+
6+
server {
7+
listen ${LISTEN_PORT} ssl http2;
8+
listen [::]:${LISTEN_PORT} ssl http2;
9+
server_name _;
10+
11+
ssl_certificate ${SSL_CERT_PATH};
12+
ssl_certificate_key ${SSL_CERT_KEY_PATH};
13+
ssl_session_cache shared:SSL:10m;
14+
ssl_session_timeout 10m;
15+
ssl_protocols TLSv1.2 TLSv1.3;
16+
ssl_prefer_server_ciphers on;
17+
18+
keepalive_timeout 65;
19+
20+
if ($scheme = http) {
21+
return 301 https://$host$request_uri;
22+
}
23+
24+
add_header Strict-Transport-Security "max-age=31536000" always;
25+
add_header X-Content-Type-Options nosniff;
26+
add_header X-Frame-Options SAMEORIGIN;
27+
add_header X-XSS-Protection "1; mode=block";
28+
29+
proxy_set_header Host $host;
30+
proxy_set_header X-Real-IP $remote_addr;
31+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
32+
proxy_set_header X-Forwarded-Proto $scheme;
33+
proxy_http_version 1.1;
34+
proxy_set_header Upgrade $http_upgrade;
35+
proxy_set_header Connection $connection_upgrade;
36+
proxy_cache_bypass $http_upgrade;
37+
proxy_redirect off;
38+
39+
client_max_body_size 20m;
40+
41+
location /davinci/ {
42+
rewrite ^/davinci/(.*)$ /$1 break;
43+
proxy_pass http://${DAVINCI_UPSTREAM};
44+
}
45+
46+
location /ping-am/ {
47+
proxy_pass http://${OIDC_UPSTREAM};
48+
}
49+
50+
location /ping-one/ {
51+
proxy_pass http://${OIDC_UPSTREAM};
52+
}
53+
54+
location /protect/ {
55+
rewrite ^/protect/(.*)$ /$1 break;
56+
proxy_pass http://${PROTECT_UPSTREAM};
57+
}
58+
59+
location /device-client/ {
60+
rewrite ^/device-client/(.*)$ /$1 break;
61+
proxy_pass http://${DEVICE_UPSTREAM};
62+
}
63+
64+
location /mock-api/ {
65+
rewrite ^/mock-api/(.*)$ /$1 break;
66+
proxy_pass http://${MOCK_API_UPSTREAM};
67+
}
68+
69+
# Fallback to oidc app for everything else
70+
location / {
71+
proxy_pass http://${OIDC_UPSTREAM};
72+
}
73+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
template="/etc/nginx/templates/default.conf.template"
5+
output="/etc/nginx/conf.d/default.conf"
6+
7+
echo "[https-proxy] Rendering nginx config..."
8+
envsubst '\
9+
${LISTEN_PORT} \
10+
${SSL_CERT_PATH} \
11+
${SSL_CERT_KEY_PATH} \
12+
${DAVINCI_UPSTREAM} \
13+
${OIDC_UPSTREAM} \
14+
${PROTECT_UPSTREAM} \
15+
${DEVICE_UPSTREAM} \
16+
${MOCK_API_UPSTREAM} \
17+
' < "$template" > "$output"

e2e/oidc-app/src/ping-am/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const wellknown = urlParams.get('wellknown');
1414

1515
const config = {
1616
clientId: clientId || 'WebOAuthClient',
17-
redirectUri: 'http://localhost:8443/ping-am/',
17+
redirectUri: 'https://localhost:8443/ping-am',
1818
scope: 'openid profile email',
1919
serverConfig: {
2020
wellknown:

e2e/oidc-app/src/ping-one/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const wellknown = urlParams.get('wellknown');
1414

1515
const config = {
1616
clientId: clientId || '654b14e2-7cc5-4977-8104-c4113e43c537',
17-
redirectUri: 'http://localhost:8443/ping-one/',
17+
redirectUri: 'https://localhost:8443/ping-one',
1818
scope: 'openid revoke profile email',
1919
serverConfig: {
2020
wellknown:

e2e/oidc-app/vite.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ export default defineConfig(() => ({
1010
cacheDir: '../../node_modules/.vite/e2e/oidc-app',
1111
publicDir: __dirname + '/public',
1212
server: {
13-
port: 8443,
14-
host: 'localhost',
13+
port: 5173,
14+
host: '0.0.0.0',
1515
},
1616
preview: {
17-
port: 8443,
18-
host: 'localhost',
17+
port: 5173,
18+
host: '0.0.0.0',
1919
},
2020
plugins: [],
2121
// Uncomment this if you are using workers.

0 commit comments

Comments
 (0)