Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ outputs/*

e2e/mock-api-v2/html/*
e2e/**/.playwright/*
e2e/certs/


# bundle analysis
Expand All @@ -82,4 +83,4 @@ test-output

# Gemini local knowledge base files
GEMINI.md
**/GEMINI.md
**/GEMINI.md
4 changes: 4 additions & 0 deletions contributing_docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
- [Private vs. Public Packages](./releases.md#adding-a-package-to-the-repository)
- [Ignoring Packages from Releases](./releases.md#adding-a-package-to-the-repository)

### Local HTTPS

- [E2E HTTPS Bootstrap](./local-https.md#local-https-for-e2e-apps)

## 🚀 Quick Links

- [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary)
Expand Down
68 changes: 68 additions & 0 deletions contributing_docs/local-https.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Local HTTPS for E2E apps

This guide explains how to generate and refresh the TLS certificates used by the shared HTTPS reverse proxy in the `e2e` stack.

## Why this exists

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.

## One-time prerequisites

1. Install [`mkcert`](https://github.com/FiloSottile/mkcert):
- macOS: `brew install mkcert nss`
- Windows (Powershell): `choco install mkcert` or `scoop install mkcert`
- Linux: use your package manager or download the binary
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.

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`.

## Bootstrap the certificate

From the repository root run:

```bash
pnpm run setup:https
```

`pnpm run setup:https` is a thin wrapper around `scripts/bootstrap-https.sh`. The script:

- Ensures `mkcert` is installed
- Installs the mkcert root CA into the system trust store if it is not present
- Creates (or refreshes) `e2e/certs/proxy-cert.pem` and `e2e/certs/proxy-key.pem` with SANs for `localhost`, `127.0.0.1`, and `::1`

> The files can safely be committed to your local clone—they should **not** be committed to git.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify the wording about certificate files.

The statement "The files can safely be committed to your local clone—they should not be committed to git" is confusing because a "local clone" is still a git repository. This creates a contradiction.

Apply this diff to clarify:

-> The files can safely be committed to your local clone—they should **not** be committed to git.
+> The files will exist in your local working directory but should **not** be committed to git (they are already in `.gitignore`).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
> The files can safely be committed to your local clone—they should **not** be committed to git.
> The files will exist in your local working directory but should **not** be committed to git (they are already in `.gitignore`).
🤖 Prompt for AI Agents
In contributing_docs/local-https.md around line 33, the sentence "The files can
safely be committed to your local clone—they should **not** be committed to git"
is contradictory and unclear; replace it with a clarified sentence that
explicitly states these certificate files should remain in your working
directory but must not be committed to the repository or pushed to remotes
(e.g., "You may keep these files locally in your working copy, but do not commit
or push them to the repository — add them to .gitignore"). Also update guidance
to recommend adding the specific cert filenames to .gitignore and, if present,
mention secure storage alternatives (e.g., system keychain or secret manager).


## Regenerating or customizing

- Re-run `pnpm run setup:https` at any time; it overwrites the pem files in place.
- 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.).

## Docker integration

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.

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.

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.

## Troubleshooting

- **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.
- **mkcert not found**: Ensure it is on your `PATH`. Open a new shell after installation.
- **Permission issues on install**: `mkcert -install` modifies OS certificate stores and may require elevated privileges. Run the script again with the necessary rights.

With the certificate in place, the HTTPS proxy is ready and the E2E apps can rely on secure origins without per-app TLS configuration.

## Running Applications Behind the Proxy

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.

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.

Here is an example command:

```bash
pnpm nx run @forgerock/davinci-app:nxServe --port=5173 --host=0.0.0.0
```

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.
20 changes: 20 additions & 0 deletions e2e/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
https-proxy:
build:
context: ./https-proxy
image: ping-local-https
ports:
- '8443:8443'
environment:
LISTEN_PORT: 8443
SSL_CERT_PATH: /etc/nginx/tls/proxy-cert.pem
SSL_CERT_KEY_PATH: /etc/nginx/tls/proxy-key.pem
DAVINCI_UPSTREAM: host.docker.internal:5829
OIDC_UPSTREAM: host.docker.internal:5173
PROTECT_UPSTREAM: host.docker.internal:4300
DEVICE_UPSTREAM: host.docker.internal:4301
MOCK_API_UPSTREAM: host.docker.internal:9443
volumes:
- ./certs:/etc/nginx/tls:ro
extra_hosts:
- 'host.docker.internal:host-gateway'
18 changes: 18 additions & 0 deletions e2e/https-proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM nginx:1.27-alpine

RUN apk add --no-cache gettext

ENV LISTEN_PORT=8443 \
SSL_CERT_PATH=/etc/nginx/tls/proxy-cert.pem \
SSL_CERT_KEY_PATH=/etc/nginx/tls/proxy-key.pem \
DAVINCI_UPSTREAM=host.docker.internal:5829 \
OIDC_UPSTREAM=host.docker.internal:5173 \
PROTECT_UPSTREAM=host.docker.internal:4300 \
DEVICE_UPSTREAM=host.docker.internal:4301 \
MOCK_API_UPSTREAM=host.docker.internal:9443

COPY default.conf.template /etc/nginx/templates/default.conf.template
COPY docker-entrypoint.d/ /docker-entrypoint.d/

EXPOSE 8443

73 changes: 73 additions & 0 deletions e2e/https-proxy/default.conf.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
listen ${LISTEN_PORT} ssl http2;
listen [::]:${LISTEN_PORT} ssl http2;
server_name _;

ssl_certificate ${SSL_CERT_PATH};
ssl_certificate_key ${SSL_CERT_KEY_PATH};
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;

keepalive_timeout 65;

if ($scheme = http) {
return 301 https://$host$request_uri;
}

add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;

client_max_body_size 20m;

location /davinci/ {
rewrite ^/davinci/(.*)$ /$1 break;
proxy_pass http://${DAVINCI_UPSTREAM};
}

location /ping-am/ {
proxy_pass http://${OIDC_UPSTREAM};
}

location /ping-one/ {
proxy_pass http://${OIDC_UPSTREAM};
}

location /protect/ {
rewrite ^/protect/(.*)$ /$1 break;
proxy_pass http://${PROTECT_UPSTREAM};
}

location /device-client/ {
rewrite ^/device-client/(.*)$ /$1 break;
proxy_pass http://${DEVICE_UPSTREAM};
}

location /mock-api/ {
rewrite ^/mock-api/(.*)$ /$1 break;
proxy_pass http://${MOCK_API_UPSTREAM};
}

# Fallback to oidc app for everything else
location / {
proxy_pass http://${OIDC_UPSTREAM};
}
}
17 changes: 17 additions & 0 deletions e2e/https-proxy/docker-entrypoint.d/10-generate-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/sh
set -eu

template="/etc/nginx/templates/default.conf.template"
output="/etc/nginx/conf.d/default.conf"

echo "[https-proxy] Rendering nginx config..."
envsubst '\
${LISTEN_PORT} \
${SSL_CERT_PATH} \
${SSL_CERT_KEY_PATH} \
${DAVINCI_UPSTREAM} \
${OIDC_UPSTREAM} \
${PROTECT_UPSTREAM} \
${DEVICE_UPSTREAM} \
${MOCK_API_UPSTREAM} \
' < "$template" > "$output"
2 changes: 1 addition & 1 deletion e2e/oidc-app/src/ping-am/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const wellknown = urlParams.get('wellknown');

const config = {
clientId: clientId || 'WebOAuthClient',
redirectUri: 'http://localhost:8443/ping-am/',
redirectUri: 'https://localhost:8443/ping-am',
scope: 'openid profile email',
serverConfig: {
wellknown:
Expand Down
2 changes: 1 addition & 1 deletion e2e/oidc-app/src/ping-one/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const wellknown = urlParams.get('wellknown');

const config = {
clientId: clientId || '654b14e2-7cc5-4977-8104-c4113e43c537',
redirectUri: 'http://localhost:8443/ping-one/',
redirectUri: 'https://localhost:8443/ping-one',
scope: 'openid revoke profile email',
serverConfig: {
wellknown:
Expand Down
8 changes: 4 additions & 4 deletions e2e/oidc-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ export default defineConfig(() => ({
cacheDir: '../../node_modules/.vite/e2e/oidc-app',
publicDir: __dirname + '/public',
server: {
port: 8443,
host: 'localhost',
port: 5173,
host: '0.0.0.0',
},
preview: {
port: 8443,
host: 'localhost',
port: 5173,
host: '0.0.0.0',
},
plugins: [],
// Uncomment this if you are using workers.
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
"lint": "nx affected --target=lint",
"local-release": "pnpm ts-node tools/release/release.ts",
"nx": "nx",
"https-proxy:up": "docker compose -f e2e/docker-compose.yml up --build",
"postinstall": "ts-patch install",
"preinstall": "npx only-allow pnpm",
"prepare": "node .husky/install.mjs",
"setup:https": "scripts/bootstrap-https.sh",
"serve": "nx serve",
"test": "CI=true nx affected:test",
"test:e2e": "CI=true nx affected:e2e",
Expand Down
26 changes: 26 additions & 0 deletions scripts/bootstrap-https.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail

CERT_DIR="$(git rev-parse --show-toplevel)/e2e/certs"
DOMAIN_LIST=("localhost" "127.0.0.1" "::1")

mkdir -p "${CERT_DIR}"

if ! command -v mkcert >/dev/null 2>&1; then
echo "mkcert not found; install it first." >&2
exit 1
fi

pushd "${CERT_DIR}" >/dev/null

if [ ! -f "$(mkcert -CAROOT)/rootCA.pem" ]; then
echo "Installing mkcert root CA..."
mkcert -install
fi

echo "Generating proxy certificate..."
mkcert -cert-file proxy-cert.pem -key-file proxy-key.pem "${DOMAIN_LIST[@]}"

popd >/dev/null

echo "Certificate material written to ${CERT_DIR}"
Loading