Simplify paskia init to rp-id/rp-name only
Only rp-id and rp-name are essential bootstrap-time configuration; origins and auth hosts are set up afterwards via the admin interface. Removes --origin/--auth-host from init and from devserver, and the now-unused hostutil.validate_auth_host.
This commit is contained in:
@@ -52,7 +52,7 @@ uv tool install paskia
|
||||
|
||||
## Configuration
|
||||
|
||||
Bootstrapping is done once with `paskia init`; after that, `paskia` serves all configured realms from the database `paskia.kantadb` in the current directory. Realm configuration (rp-name, auth host, origins) is managed via the admin web interface, including adding further realms (rp-ids).
|
||||
Bootstrapping is done once with `paskia init`; after that, `paskia` serves all configured domains from the database `paskia.kantadb` in the current directory. Domain configuration (rp-name, auth host, origins) is managed via the admin web interface, including adding further domains (rp-ids).
|
||||
|
||||
```text
|
||||
paskia init [options] # one-time bootstrap
|
||||
@@ -63,10 +63,10 @@ paskia [-l endpoint] # serve
|
||||
| init option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| -l, --listen *endpoint* | Listen address: *host*:*port*, :*port* (all interfaces), or */path.sock* (stored in the database) | **localhost:4401** |
|
||||
| --rp-id *domain* | Main/top domain for passkeys. Repeatable and comma-separated for multiple realms; the first is the default realm | **localhost** |
|
||||
| --rp-name *"text"* | Branding name of the default realm (passkey auth, login dialog) | Same as rp-id |
|
||||
| --origin *url* | Only sites listed can login on the default realm (repeatable) | rp-id and all subdomains |
|
||||
| --auth-host *url* | Dedicated authentication site, e.g. **auth.example.com** | Use **/auth/** path on each site |
|
||||
| --rp-id *domain* | Main/top domain for passkeys. Repeatable and comma-separated for multiple domains | **localhost** |
|
||||
| --rp-name *"text"* | Branding name of the first domain (passkey auth, login dialog) | Same as rp-id |
|
||||
|
||||
Origins, auth hosts and related domains are configured afterwards in the admin panel's Domains section.
|
||||
|
||||
The `paskia` serve command accepts only `--listen` (overriding the stored value) and never converts databases: with no `paskia.kantadb` it tells you to run `paskia init`, or `paskia migrate` when a legacy `{rp-id}.paskiadb` database is present. `paskia migrate` converts the legacy database; with several candidates, `--rp-id` selects one by name and the rest are left in place.
|
||||
|
||||
@@ -217,13 +217,13 @@ auth.example.com {
|
||||
|
||||
Now all authentication happens at `auth.example.com` instead of `/auth/` paths on your apps. Your existing protected sites continue to work as before but they just forward to the dedicated site for user profile and other such functionality.
|
||||
|
||||
Set the auth host in the admin panel's Realms section, or pass `--auth-host=auth.example.com` to `paskia init` when bootstrapping.
|
||||
Set the auth host in the admin panel's Domains section.
|
||||
|
||||
## Multiple Realms and Related Origins
|
||||
## Multiple Domains and Related Origins
|
||||
|
||||
One Paskia instance can serve several realms (rp-ids) from the same database: users, orgs and permissions are shared, while passkeys are registered per realm. The master admin adds realms in the admin panel's Realms section; no restart is needed.
|
||||
One Paskia instance can serve several domains (rp-ids) from the same database: users, orgs and permissions are shared, while passkeys are registered per domain. The master admin adds domains in the admin panel's Domains section; no restart is needed.
|
||||
|
||||
A realm can also let *other* domain names use its passkeys via WebAuthn [Related Origin Requests](https://passkeys.dev/docs/advanced/related-origins/) — add them as related domains in the realm settings, and paskia serves the required `/.well-known/webauthn` declaration on the realm's domain. This is separate from the allowed-sign-in-sites list, which only restricts which subdomains of the realm's own domain may authenticate (empty = the domain and all subdomains).
|
||||
A domain can also let *other* domain names use its passkeys via WebAuthn [Related Origin Requests](https://passkeys.dev/docs/advanced/related-origins/) — add them as related domains in the domain settings, and paskia serves the required `/.well-known/webauthn` declaration on the domain's main site. This is separate from the allowed-sign-in-sites list, which only restricts which subdomains of the domain's own rp-id may authenticate (empty = the rp-id and all subdomains).
|
||||
|
||||
See [Multi-Site documentation](docs/MultiSite.md) for details.
|
||||
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ These are for internal use only, but are documented here because they are the co
|
||||
|
||||
### Auth host mode (dedicated auth site)
|
||||
|
||||
A domain may configure a dedicated authentication host (auth-host, a subdomain of the rp-id), either at bootstrap (`paskia init --auth-host`) or via the Domains admin panel.
|
||||
A domain may configure a dedicated authentication host (auth-host, a subdomain of the rp-id) via the Domains admin panel.
|
||||
|
||||
#### On the auth host:
|
||||
- The Web UI is served at site root instead of /auth/* (that redirects to root paths)
|
||||
|
||||
+7
-8
@@ -194,10 +194,10 @@ instance:
|
||||
- `--rp-id`: repeatable/comma-separated, default `["localhost"]`.
|
||||
Multiple values create multiple domains at once (useful for
|
||||
devserver/e2e).
|
||||
- `--rp-name`, `--auth-host`, `--origin`: apply to the **first**
|
||||
rp-id only. Their purpose is that the very first admin registration
|
||||
ceremony already shows the correct RP name; everything is editable
|
||||
via the admin interface afterwards.
|
||||
- `--rp-name`: applies to the **first** rp-id only. Its purpose is that
|
||||
the very first admin registration ceremony already shows the correct
|
||||
RP name; everything else (origins, auth hosts, related domains) is
|
||||
set up via the admin interface afterwards.
|
||||
- `--listen`: stored into `Config.listen` (process-global).
|
||||
- Seeds the admin user + registration reset link and prints the link.
|
||||
Refuses to run if `paskia.kantadb` already exists, or if an
|
||||
@@ -466,10 +466,9 @@ effective_auth_host(domain) = domain's own auth host or first configured one or
|
||||
## 12. Development
|
||||
|
||||
- `scripts/devserver.py`: bootstraps via one-shot `paskia init` when no
|
||||
database exists (multi `--rp-id`, and `--rp-name`/`--auth-host`/
|
||||
`--origin` for the first domain), then runs plain `paskia` serve.
|
||||
Caddy dev origins iterate all bootstrap rp-ids plus the auth host and
|
||||
explicit origins.
|
||||
database exists (multi `--rp-id`, `--rp-name` for the first domain),
|
||||
then runs plain `paskia` serve. Caddy dev origins iterate all bootstrap
|
||||
rp-ids.
|
||||
- `PASKIA_AUTH_HOST` (consumed by `frontend/vite.config.js`) is a
|
||||
comma-separated list of bare hostnames; the vite dev proxy forwards
|
||||
`/.well-known/openid-configuration` and `/.well-known/webauthn` to the
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ auth.example.com {
|
||||
}
|
||||
```
|
||||
|
||||
Remember to set the auth host for the domain — either `paskia init --auth-host auth.example.com` at bootstrap or in the admin panel's Domains section — to restrict the authentication services to this domain.
|
||||
Remember to set the auth host for the domain in the admin panel's Domains section to restrict the authentication services to this domain.
|
||||
|
||||
Note that we still reserve `/auth/` on each site for logout page and any APIs your application may require, while full user profile and global options are only available on the auth host.
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ authResponseHeaders:
|
||||
|
||||
The `/auth/` router above forwards all authentication UI, API, and WebSocket traffic to Paskia. Because this router does **not** use the `paskia-auth` middleware, users can reach the login page and profile UI without being authenticated first. Traefik handles WebSocket upgrades automatically when the client requests them.
|
||||
|
||||
If you are using a dedicated authentication host instead of `/auth/`, create a separate router for `auth.example.com` pointing to the Paskia service and set the domain's auth host (`paskia init --auth-host auth.example.com` at bootstrap, or the admin panel's Domains section).
|
||||
If you are using a dedicated authentication host instead of `/auth/`, create a separate router for `auth.example.com` pointing to the Paskia service and set the domain's auth host in the admin panel's Domains section.
|
||||
|
||||
## Adjusting requirements
|
||||
|
||||
|
||||
+10
-38
@@ -12,21 +12,17 @@ from kanta import Kanta
|
||||
from paskia.db import legacy
|
||||
from paskia.db.bootstrap import bootstrap, log_reset_link
|
||||
from paskia.db.paths import db_file_path
|
||||
from paskia.db.structs import DB, Config, DomainConfig, OriginEntry
|
||||
from paskia.db.structs import DB, Config, DomainConfig
|
||||
from paskia.domains import build as build_registry
|
||||
from paskia.domains import configure as configure_domains
|
||||
from paskia.domains import origin_key, validate_config
|
||||
from paskia.domains import validate_config
|
||||
from paskia.util import startupbox
|
||||
from paskia.util.constants import DEFAULT_PORT, DEVMODE
|
||||
from paskia.util.hostutil import (
|
||||
normalize_origin,
|
||||
validate_auth_host,
|
||||
)
|
||||
from paskia.util.runtime import ServeConfig
|
||||
|
||||
EPILOG = """\
|
||||
Examples:
|
||||
paskia init --rp-id example.com --rp-name "Example Corporation" --auth-host auth.example.com
|
||||
paskia init --rp-id example.com --rp-name "Example Corporation"
|
||||
paskia migrate --rp-id example.com
|
||||
paskia
|
||||
"""
|
||||
@@ -93,27 +89,16 @@ def cmd_init(args: argparse.Namespace) -> None:
|
||||
|
||||
rp_ids = _split_multi(args.rp_id) or ["localhost"]
|
||||
|
||||
# Only rp-id and rp-name are bootstrap-time configuration; everything
|
||||
# else (origins, auth host, related domains) is set up afterwards via
|
||||
# the admin interface.
|
||||
domains = {}
|
||||
for i, rp_id in enumerate(rp_ids):
|
||||
domain = DomainConfig()
|
||||
if i == 0:
|
||||
# Bootstrap-time naming and hosts apply to the first domain;
|
||||
# everything is editable via the admin interface afterwards.
|
||||
# The bootstrap rp-name exists so the very first admin
|
||||
# registration ceremony already shows the correct name.
|
||||
domain.rp_name = args.rp_name or None
|
||||
origins = {
|
||||
origin_key(normalize_origin(o)): True
|
||||
for o in _split_multi(args.origins)
|
||||
}
|
||||
auth_host = args.auth_host or None
|
||||
if auth_host:
|
||||
try:
|
||||
validate_auth_host(auth_host, rp_id)
|
||||
except ValueError as e:
|
||||
raise SystemExit(str(e)) from e
|
||||
if "://" not in auth_host:
|
||||
auth_host = f"https://{auth_host}"
|
||||
origins[origin_key(auth_host)] = OriginEntry(auth_host=True)
|
||||
domain.origins = origins
|
||||
domains[rp_id] = domain
|
||||
|
||||
config = Config(domains=domains, listen=_split_multi(args.listen) or None)
|
||||
@@ -228,27 +213,14 @@ def main():
|
||||
"--rp-id",
|
||||
action="append",
|
||||
help="Relying Party ID of the initial domain(s) (default: localhost). "
|
||||
"Repeatable and comma-separated; bootstrap options apply to the first. "
|
||||
"Further domains are added via the admin interface.",
|
||||
"Repeatable and comma-separated. Further domains, origins and auth "
|
||||
"hosts are added via the admin interface.",
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"--rp-name",
|
||||
help="Relying Party name of the first domain (default: same as rp-id). "
|
||||
"Used by the initial admin registration; editable later via admin UI.",
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"--origin",
|
||||
action="append",
|
||||
dest="origins",
|
||||
metavar="URL",
|
||||
help="Allowed origin URL(s) for the first domain. May be specified "
|
||||
"multiple times; comma-separated values accepted.",
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"--auth-host",
|
||||
help="Dedicated authentication site for the first domain "
|
||||
"(optionally with scheme/port)",
|
||||
)
|
||||
_add_listen_option(init_parser, help_extra=" (stored in the database)")
|
||||
|
||||
migrate_parser = argparse.ArgumentParser(
|
||||
|
||||
@@ -55,21 +55,6 @@ def is_subdomain(sub: str, domain: str) -> bool:
|
||||
return sub_parts[-len(domain_parts) :] == domain_parts
|
||||
|
||||
|
||||
def validate_auth_host(auth_host: str, rp_id: str) -> None:
|
||||
"""Validate that auth_host is a subdomain of rp_id.
|
||||
|
||||
Raises ValueError on invalid auth_host.
|
||||
"""
|
||||
parsed = urlparse(auth_host if "://" in auth_host else f"//{auth_host}")
|
||||
host = parsed.hostname or parsed.path
|
||||
if not host:
|
||||
raise ValueError(f"Invalid auth-host: '{auth_host}'")
|
||||
if not is_subdomain(host, rp_id):
|
||||
raise ValueError(
|
||||
f"auth-host '{auth_host}' is not a subdomain of rp-id '{rp_id}'"
|
||||
)
|
||||
|
||||
|
||||
def auth_host_netloc(auth_host: str) -> str | None:
|
||||
"""Return the host[:port] part of a configured auth host URL."""
|
||||
parsed = urlparse(auth_host if "://" in auth_host else f"//{auth_host}")
|
||||
|
||||
+1
-25
@@ -167,10 +167,6 @@ def ensure_database(rp_ids: list[str], args: argparse.Namespace, listen: str) ->
|
||||
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
|
||||
@@ -199,24 +195,13 @@ async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None:
|
||||
os.environ["PASKIA_VITE_URL"] = viteurl
|
||||
os.environ["PASKIA_BACKEND_URL"] = backurl
|
||||
os.environ["PASKIA_DEV"] = "1"
|
||||
if args.auth_host:
|
||||
os.environ["PASKIA_AUTH_HOST"] = args.auth_host
|
||||
|
||||
async with ProcessGroup() as pg:
|
||||
# Start Caddy first if requested (needs to bind ports)
|
||||
if args.caddy:
|
||||
caddy_origins = []
|
||||
if args.auth_host:
|
||||
auth_host = args.auth_host
|
||||
if "://" not in auth_host:
|
||||
auth_host = f"https://{auth_host}"
|
||||
caddy_origins.append(auth_host)
|
||||
for rp_id in rp_ids:
|
||||
caddy_origins.append(f"https://{rp_id}")
|
||||
for origin in _split_multi(args.origins):
|
||||
if "://" not in origin:
|
||||
origin = f"https://{origin}"
|
||||
caddy_origins.append(origin)
|
||||
seen: set = set()
|
||||
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)
|
||||
@@ -251,20 +236,11 @@ def main():
|
||||
"--rp-id",
|
||||
action="append",
|
||||
help="Relying Party ID(s) for first-run bootstrap (default: localhost). "
|
||||
"Repeatable and comma-separated; the bootstrap name/host options apply to the first.",
|
||||
"Repeatable and comma-separated.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rp-name", help="Relying Party name of the first domain (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 first domain (bootstrap only)"
|
||||
)
|
||||
args, remaining = parser.parse_known_args()
|
||||
|
||||
with suppress(KeyboardInterrupt):
|
||||
|
||||
+7
-11
@@ -20,7 +20,7 @@ from kanta import Kanta
|
||||
|
||||
from paskia.__main__ import _load_stored_config, main
|
||||
from paskia.db import legacy
|
||||
from paskia.db.structs import Config, OriginEntry
|
||||
from paskia.db.structs import Config
|
||||
from paskia.util.runtime import ServeConfig, clear_cache
|
||||
|
||||
|
||||
@@ -98,10 +98,6 @@ def test_init_full_options(run_cli, tmp_path):
|
||||
"example.com",
|
||||
"--rp-name",
|
||||
"Example Corp",
|
||||
"--auth-host",
|
||||
"auth.example.com",
|
||||
"--origin",
|
||||
"https://app.example.com",
|
||||
"--listen",
|
||||
"4402",
|
||||
)
|
||||
@@ -109,10 +105,7 @@ def test_init_full_options(run_cli, tmp_path):
|
||||
config = stored_config(tmp_path)
|
||||
domain = config.domains["example.com"]
|
||||
assert domain.rp_name == "Example Corp"
|
||||
assert domain.origins == {
|
||||
"app.example.com": True,
|
||||
"auth.example.com": OriginEntry(auth_host=True),
|
||||
}
|
||||
assert domain.origins == {}
|
||||
assert config.listen == ["4402"]
|
||||
|
||||
|
||||
@@ -135,9 +128,12 @@ def test_init_refuses_legacy_database(run_cli, tmp_path):
|
||||
run_cli("init")
|
||||
|
||||
|
||||
def test_init_invalid_auth_host(run_cli):
|
||||
def test_init_rejects_removed_options(run_cli):
|
||||
"""Origins and auth hosts are admin-interface configuration, not init's."""
|
||||
with pytest.raises(SystemExit):
|
||||
run_cli("init", "--rp-id", "example.com", "--auth-host", "notsub.example.org")
|
||||
run_cli("init", "--rp-id", "example.com", "--auth-host", "auth.example.com")
|
||||
with pytest.raises(SystemExit):
|
||||
run_cli("init", "--origin", "https://app.example.com")
|
||||
|
||||
|
||||
def test_serve_requires_database(run_cli):
|
||||
|
||||
Reference in New Issue
Block a user