Docs: multi-realm current state
README quick start and configuration updated for init/serve split and paskia.kantadb; API.md server-config endpoints replaced with admin realm CRUD and the well-known webauthn endpoint; proxy guides point at the realm auth-host setting; oidc.md documents per-realm issuers; MultiSite.md rewritten from the implementation plan into documentation of the shipped mechanics, policy model, and design rationale.
This commit is contained in:
@@ -36,10 +36,11 @@ Paskia includes set of login, reauthentication and forbidden dialogs that it can
|
||||
Install [UV](https://docs.astral.sh/uv/getting-started/installation/) and run:
|
||||
|
||||
```sh
|
||||
uvx paskia --rp-id example.com
|
||||
uvx paskia init --rp-id example.com
|
||||
uvx paskia
|
||||
```
|
||||
|
||||
On the first run it downloads the software and prints a registration link for the Admin. The server starts on [localhost:4401](http://localhost:4401), serving authentication for `*.example.com`. For local testing, leave out `--rp-id`.
|
||||
The first command bootstraps the database and prints a registration link for the Admin. The second starts the server on [localhost:4401](http://localhost:4401), serving authentication for `*.example.com`. For local testing, leave out `--rp-id` (defaults to `localhost`).
|
||||
|
||||
For production you need a web server such as [Caddy](https://caddyserver.com/) to serve HTTPS on your actual domain names and proxy requests to Paskia and your backend apps (see documentation below).
|
||||
|
||||
@@ -51,22 +52,22 @@ uv tool install paskia
|
||||
|
||||
## Configuration
|
||||
|
||||
You will need to specify your main domain to which all passkeys will be tied as rp-id. Use your main domain even if Paskia is not running there. All other options are optional.
|
||||
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).
|
||||
|
||||
```text
|
||||
paskia [options]
|
||||
paskia init [options] # one-time bootstrap
|
||||
paskia [-l endpoint] # serve
|
||||
```
|
||||
|
||||
| Option | Description | Default |
|
||||
| init option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| -l, --listen *endpoint* | Listen address: *host*:*port*, :*port* (all interfaces), or */path.sock* | **localhost:4401** |
|
||||
| --rp-id *domain* | Main/top domain for passkeys | **localhost** |
|
||||
| --rp-name *"text"* | Branding name for the entire system (passkey auth, login dialog). | Same as rp-id |
|
||||
| --origin *url* | Only sites listed can login (repeatable) | rp-id and all subdomains |
|
||||
| -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 |
|
||||
| --save | Save current options to database | (only --rp-id required on further invocations) |
|
||||
|
||||
To clear a stored setting, pass an empty value like `--auth-host=`. The database is stored in `{rp-id}.paskiadb` folder in current directory. This can be overridden by environment `PASKIA_DB` if needed.
|
||||
The `paskia` serve command accepts only `--listen` (overriding the stored value). An existing legacy `{rp-id}.paskiadb` database is converted to `paskia.kantadb` automatically on first serve.
|
||||
|
||||
## Tutorial: From Local Testing to Production
|
||||
|
||||
@@ -74,13 +75,14 @@ This section walks you through a complete example, from running Paskia locally t
|
||||
|
||||
### Step 1: Production Configuration
|
||||
|
||||
For a real deployment, configure Paskia with your domain name (rp-id). This enables SSO setup for that domain and any subdomains.
|
||||
For a real deployment, bootstrap Paskia with your domain name (rp-id). This enables SSO setup for that domain and any subdomains.
|
||||
|
||||
```sh
|
||||
uvx paskia --rp-id=example.com --rp-name="Example Corp"
|
||||
uvx paskia init --rp-id=example.com --rp-name="Example Corp"
|
||||
uvx paskia
|
||||
```
|
||||
|
||||
This binds passkeys to the rp-id, allowing them to be used there or on any subdomain of it. The `--rp-name` is the branding shown in UI and registered with passkeys for everything on your domain (rp id). On the first run, you'll see a registration link—use it to create your Admin account. You may enter your real name here for a more suitable account name.
|
||||
This binds passkeys to the rp-id, allowing them to be used there or on any subdomain of it. The `--rp-name` is the branding shown in UI and registered with passkeys for everything on your domain (rp id). Init prints a registration link—use it to create your Admin account. You may enter your real name here for a more suitable account name.
|
||||
|
||||
### Step 2: Set Up Caddy
|
||||
|
||||
@@ -177,20 +179,20 @@ curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR=/usr/local/
|
||||
Create a systemd unit:
|
||||
|
||||
```sh
|
||||
sudo systemctl edit --force --full paskia@.service
|
||||
sudo systemctl edit --force --full paskia.service
|
||||
```
|
||||
|
||||
Paste the following and save:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Paskia for %i
|
||||
Description=Paskia
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=paskia
|
||||
WorkingDirectory=/srv/paskia
|
||||
ExecStart=uvx paskia@latest --rp-id=%i
|
||||
ExecStart=uvx paskia@latest
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -199,7 +201,7 @@ WantedBy=multi-user.target
|
||||
Run the service and view log:
|
||||
|
||||
```sh
|
||||
sudo systemctl enable --now paskia@example.com && sudo journalctl -n30 -ocat -fu paskia@example.com
|
||||
sudo systemctl enable --now paskia && sudo journalctl -n30 -ocat -fu paskia
|
||||
```
|
||||
|
||||
### Optional: Dedicated Authentication Site
|
||||
@@ -214,7 +216,15 @@ 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.
|
||||
|
||||
Enter your auth site domain on Admin / Server Options panel or use `--auth-host=auth.example.com` when starting the server.
|
||||
Set the auth host in the admin panel's Realms section, or pass `--auth-host=auth.example.com` to `paskia init` when bootstrapping.
|
||||
|
||||
## Multiple Realms 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.
|
||||
|
||||
A realm can also allow passkey use on unrelated domains via WebAuthn [Related Origin Requests](https://passkeys.dev/docs/advanced/related-origins/) — add the origin to the realm and paskia serves the required `/.well-known/webauthn` declaration.
|
||||
|
||||
See [Multi-Site documentation](docs/MultiSite.md) for details.
|
||||
|
||||
|
||||
## Further Documentation
|
||||
|
||||
+18
-4
@@ -72,8 +72,12 @@ E.g. Org admin cannot see anything of the other orgs that he has no admin access
|
||||
| PATCH | /auth/api/admin/oidc-clients/{uuid} | Update OIDC client | 200/401/403 |
|
||||
| PATCH | /auth/api/admin/oidc-clients/{uuid}/reset-secret | Reset client secret | 200/401/403 |
|
||||
| DELETE | /auth/api/admin/oidc-clients/{uuid} | Delete OIDC client | 200/401/403 |
|
||||
| GET | /auth/api/admin/server-config/ | Get server config | 200/401/403 |
|
||||
| PATCH | /auth/api/admin/server-config/ | Update server config | 200/401/403 |
|
||||
| GET | /auth/api/admin/realms/ | List realms (rp-ids) with derived URLs | 200/401/403 |
|
||||
| POST | /auth/api/admin/realms/ | Create realm `{rp_id, rp_name?, auth_host?, origins?}` | 200/400/401/403 |
|
||||
| PATCH | /auth/api/admin/realms/{rp_id} | Update realm rp_name/auth_host/origins | 200/400/401/403 |
|
||||
| DELETE | /auth/api/admin/realms/{rp_id} | Delete realm (refused while credentials remain) | 200/400/401/403 |
|
||||
|
||||
Realm endpoints require the `auth:admin` permission; writes additionally require recent authentication (5 minutes). Changes are validated cross-realm and apply immediately.
|
||||
|
||||
### WebSockets: /auth/ws/*
|
||||
|
||||
@@ -86,7 +90,9 @@ E.g. Org admin cannot see anything of the other orgs that he has no admin access
|
||||
|
||||
These are for internal use only, but are documented here because they are the core piece in all passkey operations.
|
||||
|
||||
### Auth host mode (--auth-host)
|
||||
### Auth host mode (dedicated auth site)
|
||||
|
||||
A realm may configure a dedicated authentication host (auth-host, a subdomain of the rp-id), either at bootstrap (`paskia init --auth-host`) or via the Realms admin panel.
|
||||
|
||||
#### On the auth host:
|
||||
- The Web UI is served at site root instead of /auth/* (that redirects to root paths)
|
||||
@@ -98,4 +104,12 @@ These are for internal use only, but are documented here because they are the co
|
||||
- /auth/api/* is served normally.
|
||||
- /auth/api/user/*, /auth/api/admin/*, and /auth/ws/* don't exist.
|
||||
|
||||
The WebSocket connections are directed to auth host, and must have an allowed origin corresponding to the host where the user is logging in, that the session is tied with.
|
||||
The WebSocket connections are directed to the auth host, and must have an allowed origin corresponding to the host where the user is logging in, that the session is tied with.
|
||||
|
||||
#### Shared auth host across realms
|
||||
|
||||
When one realm has an auth host, other realms without their own use it as their *effective* auth host: their WebSocket and cross-device flows are directed there, but their own `/auth/` still serves the full profile (host mode is keyed off the realm's *own* auth host only). `/auth/api/settings` exposes both: `auth_host` (effective) and `own_auth_host` (this realm only, null when unset).
|
||||
|
||||
### Related Origin Requests: /.well-known/webauthn
|
||||
|
||||
`GET /.well-known/webauthn` returns `{"origins": [...]}` listing the realm's related origins (configured origins on domains unrelated to the rp-id), per WebAuthn Related Origin Requests. Browsers fetch this from the rp-id domain when an unrelated origin runs a ceremony with this realm's rp-id. Returns 404 when the realm has no related origins. If the rp-id's main site is hosted elsewhere, serve the JSON statically there (copy it from this instance).
|
||||
|
||||
+307
-553
File diff suppressed because it is too large
Load Diff
@@ -173,4 +173,4 @@ The auth check then always returns 204 (except reauth with `max_age`, which stil
|
||||
|
||||
- The auth request is `GET` by default. Since the `forward-auth` plugin does not forward the request body unless `request_method` is set to `POST`, the default `GET` is the right choice for Paskia.
|
||||
- Hop-by-hop headers are handled by APISIX when it builds the auth request, so no extra configuration is needed for `Connection`/`Upgrade`.
|
||||
- If Paskia is running on a different host, replace `localhost:4401` with the Paskia service address. For a dedicated authentication host (`--auth-host`), route `auth.example.com` to Paskia instead of `/auth/`.
|
||||
- If Paskia is running on a different host, replace `localhost:4401` with the Paskia service address. For a dedicated authentication host (the realm's auth-host setting), route `auth.example.com` to Paskia instead of `/auth/`.
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ auth.example.com {
|
||||
}
|
||||
```
|
||||
|
||||
Remember to specify `paskia serve --auth-host auth.example.com` to restrict the authentication services to this domain.
|
||||
Remember to set the auth host for the realm — either `paskia init --auth-host auth.example.com` at bootstrap or in the admin panel's Realms 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.
|
||||
|
||||
|
||||
+1
-1
@@ -161,7 +161,7 @@ See [perm argument](../api/perm.md) and [max_age argument](../api/max-age.md) fo
|
||||
|
||||
## WebSocket support for `/auth/`
|
||||
|
||||
If you use a dedicated authentication host (`--auth-host`), route `auth.example.com` to the Paskia cluster and you do not need the `/auth/` bypass above. Otherwise, make sure the `/auth/` route keeps the `Upgrade` and `Connection` headers so passkey WebSocket endpoints work. The default Envoy router handles `Upgrade` headers when the client requests them.
|
||||
If you use a dedicated authentication host (the realm's auth-host setting), route `auth.example.com` to the Paskia cluster and you do not need the `/auth/` bypass above. Otherwise, make sure the `/auth/` route keeps the `Upgrade` and `Connection` headers so passkey WebSocket endpoints work. The default Envoy router handles `Upgrade` headers when the client requests them.
|
||||
|
||||
## Public access
|
||||
|
||||
|
||||
@@ -123,4 +123,4 @@ The `Remote-*` success-headers glob already copies the `Remote-Public` header th
|
||||
- The Lua script strips the request body from the auth subrequest, so Paskia's `/auth/api/forward` will only see the headers.
|
||||
- HAProxy variables are limited to alphanumeric characters, dots, and underscores, but the script already normalizes header names for you (e.g. `Remote-User` becomes `req.auth_response_header.remote_user`). The `Remote-*` glob pattern in the success-headers argument handles this automatically.
|
||||
- The auth backend must be reachable without TLS. If you need TLS to Paskia, run a local TCP forwarder or use HAProxy's Lua HTTP support directly (not covered by this script).
|
||||
- If you use a dedicated authentication host (`--auth-host`), route `auth.example.com` to the Paskia backend and start Paskia with `--auth-host auth.example.com` instead of exposing `/auth/` on every site.
|
||||
- If you use a dedicated authentication host (the realm's auth-host setting), route `auth.example.com` to the Paskia backend instead of exposing `/auth/` on every site.
|
||||
|
||||
@@ -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 start Paskia with `--auth-host auth.example.com`.
|
||||
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 realm's auth host (`paskia init --auth-host auth.example.com` at bootstrap, or the admin panel's Realms section).
|
||||
|
||||
## Adjusting requirements
|
||||
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
OpenID Connect 1.0 provider enabling third-party apps to authenticate users via passkey. Also supports native cookie-based authentication.
|
||||
|
||||
## Realms (multi rp-id)
|
||||
|
||||
Each realm (rp-id) is an independent OIDC issuer with its own signing key and clients: `DB.oidc` is `dict[rp_id, OIDC]` and per-realm key files are `oidc.<rp-id>.key`. Discovery, keys, token and userinfo endpoints resolve the issuer from the request host (realm dispatch). `Session.issuer` records the realm that issued an OIDC session so refresh and back-channel logout select the right key, and auth codes (`OIDCCode`, `CookieCode`) are stamped with the realm's rp-id and verified against it at redemption.
|
||||
|
||||
## Data Models
|
||||
|
||||
**User** — Added: `email`, `preferred_username`
|
||||
|
||||
**Session** — Added: `client_uuid` (None = native, set = OIDC)
|
||||
**Session** — Added: `client_uuid` (None = native, set = OIDC), `rp_id`/`issuer` (realm that authenticated / issued the session)
|
||||
- `key: bytes` — hashed DB key, never stored raw
|
||||
- `secret` → `hash_secret("session", secret)` → DB lookup
|
||||
- OIDC `sid` → `base64url.encode(hash_secret("oidc", session.key))`
|
||||
@@ -89,4 +93,4 @@ Discovery: `backchannel_logout_supported: true`
|
||||
|
||||
**Created:** [paskia/authcode.py](paskia/authcode.py), [paskia/util/crypto.py](paskia/util/crypto.py), [paskia/fastapi/oid.py](paskia/fastapi/oid.py)
|
||||
|
||||
**Modified:** [paskia/db/structs.py](paskia/db/structs.py), [paskia/db/operations.py](paskia/db/operations.py), [paskia/fastapi/ws.py](paskia/fastapi/ws.py), [paskia/fastapi/api.py](paskia/fastapi/api.py), [paskia/globals.py](paskia/globals.py), [paskia/fastapi/mainapp.py](paskia/fastapi/mainapp.py)
|
||||
**Modified:** [paskia/db/structs.py](paskia/db/structs.py), [paskia/db/operations.py](paskia/db/operations.py), [paskia/fastapi/ws.py](paskia/fastapi/ws.py), [paskia/fastapi/api.py](paskia/fastapi/api.py), [paskia/realms.py](paskia/realms.py), [paskia/fastapi/mainapp.py](paskia/fastapi/mainapp.py)
|
||||
|
||||
Reference in New Issue
Block a user