Files
paskia/README.md
T
LeoVasanko b9e6f4bc27 Separate related domains (ROR) from the in-domain sign-in allow-list
RealmConfig.origins is again purely an allow-list of sign-in sites
within the realm's domain (unset = rp-id and all subdomains), restoring
the restriction semantics the realm rework had silently turned into an
always-open subtree. Cross-domain ROR origins move to their own
RealmConfig.related_origins field — always additive, capped, validated
to be outside the rp-id domain, and the sole source of the
/.well-known/webauthn document.

Admin API POST/PATCH accept related_origins; misfiled entries are
rejected (cross-domain in origins, in-domain in related_origins).

Admin UI: the realm dialog edits the two lists separately with
end-user-oriented explanations (allowed sign-in sites vs. related
domains + the well-known note); the Realms section intro explains the
multi-domain model, and the table shows sign-in site and related domain
counts.
2026-09-06 22:29:12 +00:00

9.8 KiB

Paskia

Login dialog screenshot

An easy to install passkey-based authentication service that protects any web application with strong passwordless login.

What is Paskia?

  • Easy to use fully featured auth&auth system (login and permissions)
  • Organization and role-based access control
    • Org admins control their users and roles
    • Multiple independent orgs
    • Master admin can do everything or delegate to org admins
  • User Profile and Admin by API and web interface
  • Implements login/reauth/forbidden flows for you
  • Single Sign-On (SSO): Users register once and authenticate across your services
  • Remote autentication by entering random keywords from another device (like 2fa)
  • No CORS, NodeJS or anything extra needed.

Authenticate to get to your app, or in your app

  • API fetch: auth checks and login without leaving your app
  • Forward-auth proxy: protect any unprotected site or service (Caddy, Nginx, and others)

The API mode is useful for applications that can be customized to run with Paskia. Forward auth can also protect your javascript and other assets. Each provides fine-grained permission control and reauthentication requests where needed, and both can be mixed where needed.

Authentication flows already done

Forbidden dialog, dark mode Automatic light/dark mode switching with overrides by user profile and protected app's theme.

Paskia includes set of login, reauthentication and forbidden dialogs that it can use to perform the needed flows. We never leave the URL, no redirections, and if you make use of API mode, we won't even interrupt whatever your app was doing but retry the blocked API fetch after login like nothing happened.

Quick Start

Install UV and run:

uvx paskia init --rp-id example.com
uvx paskia

The first command bootstraps the database and prints a registration link for the Admin. The second starts the server on 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 to serve HTTPS on your actual domain names and proxy requests to Paskia and your backend apps (see documentation below).

For a permanent install of paskia CLI command, not needing uvx:

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

paskia init [options]    # one-time bootstrap
paskia migrate [--rp-id] # convert a legacy {rp-id}.paskiadb database
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

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.

Tutorial: From Local Testing to Production

This section walks you through a complete example, from running Paskia locally to protecting a real site in production.

Step 1: Production Configuration

For a real deployment, bootstrap Paskia with your domain name (rp-id). This enables SSO setup for that domain and any subdomains.

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

Install Caddy and copy the auth folder to /etc/caddy/auth. Say your current unprotected Caddyfile looks like this:

app.example.com {
    reverse_proxy :3000
}

Add Paskia full site protection:

app.example.com {
    import auth/setup
    handle {
        import auth/require perm=myapp:login
        reverse_proxy :3000
    }
}

Run systemctl reload caddy. Now app.example.com requires the myapp:login permission. Try accessing it and you'll land on a login dialog.

Step 3: Assign Permissions via Admin Panel

Admin panel permissions

  1. Go to app.example.com/auth/admin/
  2. Create a permission, give it a name and scope myapp:login
  3. Assign it to Organization
  4. In that organization, assign it to the Administration role

Now you have granted yourself the new permission.

Permission scopes are text identifiers with colons as separators that we can use for permission checks. The myapp: prefix is a convention to namespace permissions per application—you but you can use other forms as you see fit (urlsafe characters, no spaces allowed).

Step 4: Add API Authentication to Your App

Your backend already receives Remote-* headers from Caddy's forward-auth. For frontend API calls, we provide a JS paskia module:

import { apiJson } from 'https://cdn.jsdelivr.net/npm/paskia@latest/dist/paskia.js'

const data = await apiJson('/api/sensitive', { method: 'POST' })

When a 401/403 occurs, the auth dialog appears automatically, and the request retries after authentication.

To protect the API path with a different permission, update your Caddyfile:

app.example.com {
    import auth/setup

    @api path /api/*
    handle @api {
        import auth/require perm=myapp:api
        reverse_proxy :3000
    }

    handle {
        import auth/require perm=myapp:login
        reverse_proxy :3000
    }
}

Create the myapp:api permission in the admin panel, that will be required for all API access. Link to /auth/ for the built-in profile page.

You may also remove the myapp:login protection from the rest of your site paths, unless you wish to keep all your assets behind a login page. Having this as the last entry in your config allows free access to everything not matched by other sections.

    handle {
        reverse_proxy :3000
    }

Step 5: Run Paskia as a Service

Create a system user paskia, install UV on the system, and create a systemd unit:

sudo useradd --system --home-dir /srv/paskia --create-home paskia

Install UV on the system (or arch btw pacman -S uv):

curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR=/usr/local/bin sh

Create a systemd unit:

sudo systemctl edit --force --full paskia.service

Paste the following and save:

[Unit]
Description=Paskia

[Service]
Type=simple
User=paskia
WorkingDirectory=/srv/paskia
ExecStart=uvx paskia@latest

[Install]
WantedBy=multi-user.target

Run the service and view log:

sudo systemctl enable --now paskia && sudo journalctl -n30 -ocat -fu paskia

Optional: Dedicated Authentication Site

Add a Caddy configuration for the authentication domain:

auth.example.com {
    reverse_proxy :4401
}

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.

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 let other domain names use its passkeys via WebAuthn Related Origin Requests — 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).

See Multi-Site documentation for details.

Further Documentation