Devserver and proxy configs for multi-realm
- devserver bootstraps via one-shot 'paskia init' when no database exists (multi --rp-id, --rp-name/--auth-host/--origin apply to the default realm), then runs plain 'paskia' serve which reads all realm configuration from the database; legacy *.paskiadb is adopted by serve without init. - Caddy origins iterate all bootstrap rp-ids. - vite.config.js accepts a comma-separated PASKIA_AUTH_HOST list and proxies /.well-known/webauthn to the backend so ROR works in dev. - caddy/auth/setup forwards /.well-known/openid-configuration and /.well-known/webauthn to paskia (they must not be swallowed by a static /.well-known/* file handler); Caddyfile.dev updated to match the generated dev config.
This commit is contained in:
+5
-2
@@ -1,9 +1,12 @@
|
|||||||
localhost {
|
localhost {
|
||||||
# Forwards API by caddy, bypassing the Vite dev proxy
|
# WebSockets bypass directly to backend (workaround for bun proxy bug)
|
||||||
# Avoids bug https://github.com/oven-sh/bun/issues/9882
|
# Avoids bug https://github.com/oven-sh/bun/issues/9882
|
||||||
handle /api/* {
|
handle /auth/ws/* {
|
||||||
reverse_proxy :4402 # directly to backend
|
reverse_proxy :4402 # directly to backend
|
||||||
}
|
}
|
||||||
|
# Everything else goes to or via Vite
|
||||||
|
# (Vite proxies /auth/api, /.well-known/openid-configuration and
|
||||||
|
# /.well-known/webauthn to the backend)
|
||||||
handle {
|
handle {
|
||||||
reverse_proxy :4403 # vite dev server
|
reverse_proxy :4403 # vite dev server
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,10 @@ header -Remote-*
|
|||||||
handle @auth_api {
|
handle @auth_api {
|
||||||
reverse_proxy {$AUTH_UPSTREAM::4401}
|
reverse_proxy {$AUTH_UPSTREAM::4401}
|
||||||
}
|
}
|
||||||
|
# Paskia-served well-known endpoints: OIDC discovery and WebAuthn Related
|
||||||
|
# Origin Requests (must reach paskia even when other /.well-known/* files
|
||||||
|
# are served statically)
|
||||||
|
@auth_wellknown path /.well-known/openid-configuration /.well-known/webauthn
|
||||||
|
handle @auth_wellknown {
|
||||||
|
reverse_proxy {$AUTH_UPSTREAM::4401}
|
||||||
|
}
|
||||||
|
|||||||
+10
-5
@@ -6,8 +6,12 @@ import { existsSync, renameSync, mkdirSync } from 'node:fs'
|
|||||||
import sirv from 'sirv'
|
import sirv from 'sirv'
|
||||||
import fastapiVue from './vite-plugin-fastapi.js'
|
import fastapiVue from './vite-plugin-fastapi.js'
|
||||||
|
|
||||||
// Auth host mode: when set, clients accessing the auth host get /auth/ at / and /auth/admin/ at /admin/
|
// Auth host mode: when set, clients accessing an auth host get /auth/ at / and /auth/admin/ at /admin/
|
||||||
const authHost = process.env.PASKIA_AUTH_HOST
|
// Comma-separated list of bare hostnames (one per realm with a dedicated auth host)
|
||||||
|
const authHosts = (process.env.PASKIA_AUTH_HOST || '')
|
||||||
|
.split(',')
|
||||||
|
.map(h => h.trim().replace(/^https?:\/\//, '').split(':')[0].split('/')[0])
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
export default defineConfig(({ command }) => ({
|
export default defineConfig(({ command }) => ({
|
||||||
appType: 'mpa',
|
appType: 'mpa',
|
||||||
@@ -17,6 +21,7 @@ export default defineConfig(({ command }) => ({
|
|||||||
"/auth/api",
|
"/auth/api",
|
||||||
"/auth/ws",
|
"/auth/ws",
|
||||||
"/.well-known/openid-configuration",
|
"/.well-known/openid-configuration",
|
||||||
|
"/.well-known/webauthn",
|
||||||
// Passphrase links: /auth/word1.word2.word3.word4.word5
|
// Passphrase links: /auth/word1.word2.word3.word4.word5
|
||||||
"^/auth/[a-z]+\\.[a-z]+\\.[a-z]+\\.[a-z]+\\.[a-z]+$",
|
"^/auth/[a-z]+\\.[a-z]+\\.[a-z]+\\.[a-z]+\\.[a-z]+$",
|
||||||
// Passphrase links: /word1.word2.word3.word4.word5
|
// Passphrase links: /word1.word2.word3.word4.word5
|
||||||
@@ -25,13 +30,13 @@ export default defineConfig(({ command }) => ({
|
|||||||
vue(),
|
vue(),
|
||||||
// Auth host routing: rewrite paths when accessing dedicated auth host
|
// Auth host routing: rewrite paths when accessing dedicated auth host
|
||||||
// Must run before serve-examples to handle / correctly
|
// Must run before serve-examples to handle / correctly
|
||||||
authHost && {
|
authHosts.length && {
|
||||||
name: 'auth-host-routing',
|
name: 'auth-host-routing',
|
||||||
configureServer(server) {
|
configureServer(server) {
|
||||||
server.middlewares.use((req, _res, next) => {
|
server.middlewares.use((req, _res, next) => {
|
||||||
const host = req.headers.host?.split(':')[0]
|
const host = req.headers.host?.split(':')[0]
|
||||||
// Check if request is coming to the auth host
|
// Check if request is coming to the auth host
|
||||||
if (host === authHost) {
|
if (authHosts.includes(host)) {
|
||||||
// Only rewrite specific paths that should map to /auth/*
|
// Only rewrite specific paths that should map to /auth/*
|
||||||
// Rewrite / and /index.html to /auth/
|
// Rewrite / and /index.html to /auth/
|
||||||
if (req.url === '/' || req.url === '/index.html') {
|
if (req.url === '/' || req.url === '/index.html') {
|
||||||
@@ -67,7 +72,7 @@ export default defineConfig(({ command }) => ({
|
|||||||
server.middlewares.use((req, _res, next) => {
|
server.middlewares.use((req, _res, next) => {
|
||||||
// Skip redirect to examples on auth host (handled by auth-host-routing)
|
// Skip redirect to examples on auth host (handled by auth-host-routing)
|
||||||
const host = req.headers.host?.split(':')[0]
|
const host = req.headers.host?.split(':')[0]
|
||||||
if (authHost && host === authHost) {
|
if (authHosts.includes(host)) {
|
||||||
next()
|
next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-18
@@ -6,6 +6,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -13,6 +14,9 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
import tracerite
|
import tracerite
|
||||||
|
|
||||||
|
from paskia.db.legacy import find_legacy_databases
|
||||||
|
from paskia.db.paths import db_file_path
|
||||||
|
|
||||||
# Import utilities from scripts/fastapi-vue (not a package, so we adjust sys.path)
|
# Import utilities from scripts/fastapi-vue (not a package, so we adjust sys.path)
|
||||||
sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue")))
|
sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue")))
|
||||||
from devutil import ( # noqa: E402
|
from devutil import ( # noqa: E402
|
||||||
@@ -135,6 +139,40 @@ async def run_caddy(
|
|||||||
return proc
|
return proc
|
||||||
|
|
||||||
|
|
||||||
|
def _split_multi(values: list[str] | None) -> list[str]:
|
||||||
|
"""Split repeatable/comma-separated CLI values into a flat list."""
|
||||||
|
result = []
|
||||||
|
for value in values or []:
|
||||||
|
result.extend(part.strip() for part in value.split(",") if part.strip())
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_database(rp_ids: list[str], args: argparse.Namespace, listen: str) -> None:
|
||||||
|
"""Bootstrap paskia.kantadb via 'paskia init' when no database exists.
|
||||||
|
|
||||||
|
Realm options are init-only; 'paskia' (serve) reads all configuration
|
||||||
|
from the database. A legacy *.paskiadb database is adopted by serve,
|
||||||
|
so no init is run in that case either.
|
||||||
|
"""
|
||||||
|
if db_file_path().exists() or find_legacy_databases():
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = [sys.executable, "-m", "paskia", "init", f"--listen={listen}"]
|
||||||
|
for rp_id in rp_ids:
|
||||||
|
cmd.extend(["--rp-id", rp_id])
|
||||||
|
if args.rp_name:
|
||||||
|
cmd.extend(["--rp-name", args.rp_name])
|
||||||
|
if args.auth_host:
|
||||||
|
cmd.extend(["--auth-host", args.auth_host])
|
||||||
|
for origin in _split_multi(args.origins):
|
||||||
|
cmd.extend(["--origin", origin])
|
||||||
|
|
||||||
|
logger.info(">>> paskia init (first run)")
|
||||||
|
proc = subprocess.run(cmd, check=False) # noqa: S603
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise SystemExit(proc.returncode)
|
||||||
|
|
||||||
|
|
||||||
async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None:
|
async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None:
|
||||||
"""Run the development server with all components."""
|
"""Run the development server with all components."""
|
||||||
reporoot = Path(__file__).parent.parent
|
reporoot = Path(__file__).parent.parent
|
||||||
@@ -146,13 +184,10 @@ async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None:
|
|||||||
viteurl, npm_install, vite = setup_vite(args.listen, DEFAULT_VITE_PORT)
|
viteurl, npm_install, vite = setup_vite(args.listen, DEFAULT_VITE_PORT)
|
||||||
backurl, paskia = setup_cli("paskia", args.backend, DEFAULT_DEV_PORT)
|
backurl, paskia = setup_cli("paskia", args.backend, DEFAULT_DEV_PORT)
|
||||||
|
|
||||||
# Build paskia command with options
|
rp_ids = _split_multi(args.rp_id) or ["localhost"]
|
||||||
paskia.extend(["--rp-id", args.rp_id])
|
ensure_database(rp_ids, args, listen=backurl.removeprefix("http://"))
|
||||||
if args.auth_host:
|
|
||||||
paskia.extend(["--auth-host", args.auth_host])
|
# Serve: no realm options — all configuration lives in the database
|
||||||
if args.origins:
|
|
||||||
for origin in args.origins:
|
|
||||||
paskia.extend(["--origin", origin])
|
|
||||||
paskia.extend(remaining)
|
paskia.extend(remaining)
|
||||||
|
|
||||||
# Set environment for subprocesses
|
# Set environment for subprocesses
|
||||||
@@ -171,14 +206,12 @@ async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None:
|
|||||||
if "://" not in auth_host:
|
if "://" not in auth_host:
|
||||||
auth_host = f"https://{auth_host}"
|
auth_host = f"https://{auth_host}"
|
||||||
caddy_origins.append(auth_host)
|
caddy_origins.append(auth_host)
|
||||||
caddy_origins.append(f"https://{args.rp_id}")
|
for rp_id in rp_ids:
|
||||||
if args.origins:
|
caddy_origins.append(f"https://{rp_id}")
|
||||||
for origin in args.origins:
|
for origin in _split_multi(args.origins):
|
||||||
if "://" not in origin:
|
if "://" not in origin:
|
||||||
origin = f"https://{origin}"
|
origin = f"https://{origin}"
|
||||||
caddy_origins.append(origin)
|
caddy_origins.append(origin)
|
||||||
if not caddy_origins:
|
|
||||||
caddy_origins.append(f"https://{args.rp_id}")
|
|
||||||
seen: set = set()
|
seen: set = set()
|
||||||
caddy_origins = [x for x in caddy_origins if not (x in seen or seen.add(x))]
|
caddy_origins = [x for x in caddy_origins if not (x in seen or seen.add(x))]
|
||||||
caddy_proc = await run_caddy(caddy_origins, viteurl, backurl)
|
caddy_proc = await run_caddy(caddy_origins, viteurl, backurl)
|
||||||
@@ -209,11 +242,22 @@ def main():
|
|||||||
help=f"FastAPI (default: localhost:{DEFAULT_DEV_PORT})",
|
help=f"FastAPI (default: localhost:{DEFAULT_DEV_PORT})",
|
||||||
)
|
)
|
||||||
parser.add_argument("--caddy", action="store_true", help="Run Caddy as HTTPS proxy")
|
parser.add_argument("--caddy", action="store_true", help="Run Caddy as HTTPS proxy")
|
||||||
parser.add_argument("--rp-id", default="localhost", help="Relying Party ID")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--origin", action="append", dest="origins", help="Allowed origin(s)"
|
"--rp-id",
|
||||||
|
action="append",
|
||||||
|
help="Relying Party ID(s) for first-run bootstrap (default: localhost). "
|
||||||
|
"Repeatable and comma-separated; the first is the default realm.",
|
||||||
)
|
)
|
||||||
parser.add_argument("--auth-host", help="Dedicated auth host")
|
parser.add_argument(
|
||||||
|
"--rp-name", help="Relying Party name of the default realm (bootstrap only)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--origin",
|
||||||
|
action="append",
|
||||||
|
dest="origins",
|
||||||
|
help="Allowed origin(s), bootstrap only",
|
||||||
|
)
|
||||||
|
parser.add_argument("--auth-host", help="Dedicated auth host for the default realm")
|
||||||
args, remaining = parser.parse_known_args()
|
args, remaining = parser.parse_known_args()
|
||||||
|
|
||||||
with suppress(KeyboardInterrupt):
|
with suppress(KeyboardInterrupt):
|
||||||
|
|||||||
Reference in New Issue
Block a user