From 0b2be18b44f165c2a3eca97cab852cd51c2c6acb Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 6 Feb 2026 00:08:27 +0000 Subject: [PATCH] Docs --- README.md | 11 ++- docs/Integration.md | 221 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 197 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 2b6b6f6..efc882e 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,19 @@ An easy to install passkey-based authentication service that protects any web ap - Remote autentication by entering random keywords from another device (like 2fa) - No CORS, NodeJS or anything extra needed. -Two interfaces: +## 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) 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](https://git.zi.fi/leovasanko/paskia/raw/main/docs/screenshots/forbidden-dark.webp) -Automatic light/dark mode switching with overrides by user profile and protected app's theme. +**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 @@ -117,6 +122,8 @@ Run `systemctl reload caddy`. Now `app.example.com` requires the `myapp:login` p 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 5: 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](https://www.npmjs.com/package/paskia): diff --git a/docs/Integration.md b/docs/Integration.md index 56840d4..9be5810 100644 --- a/docs/Integration.md +++ b/docs/Integration.md @@ -1,43 +1,198 @@ # Integrating Paskia with your App -Protect API routes with forward-auth (see [Caddy configuration](Caddy.md)). Optionally protect your app assets and not just the API. +This guide covers frontend and backend integration with Paskia. For Caddy forward-auth setup, see [Caddy configuration](Caddy.md). -Catch response status 401/403 in fetch calls to protected endpoints and implement authentication flow in this case. The response is JSON and contains `detail` (an error message describing what is needed) and `auth.iframe` (a URL). Render that URL in an iframe and retry the request after authentication (see below). +## Frontend Integration -While the app is in (active) use, call `/auth/api/validate` occasionally to keep the session alive (session lifetime is 24h), otherwise the user will have to login every day. Max-age limits are unaffected by this and can be used on endpoints needing to reauthenticate with passkey more frequently. +### Using the paskia-js Module -Fetch `/auth/api/user-info` to display user/session details, or link to `/auth/` if you prefer using the built-in profile UI and not having to do anything more. +The [paskia](https://www.npmjs.com/package/paskia) JavaScript module provides utilities for API calls, session validation, and authentication overlays. Works with any framework or vanilla JS. -## Authentication Flow (iframe) - -```js -// Show an authentication dialog -const iframe = document.createElement('iframe') -iframe.src = auth.url // from 401/403 response JSON -iframe.style.cssText = ` - position: fixed; - inset: 0; - width: 100%; - height: 100%; - border: 0; - z-index: 9999; - background: transparent; - backdrop-filter: blur(0.1rem) brightness(0.7); -` -document.body.appendChild(iframe) - -// Wait until user is finished with the dialog -const handler = ev => { - if (ev.origin !== location.origin) return - iframe.remove() - removeEventListener('message', handler) - if (ev.data?.type === 'auth-success') retry_original_fetch() -} -addEventListener('message', handler) +```html + ``` -This describes the frontend flow for handling 401/403 responses from endpoints protected by Paskia forward-auth, without ever exiting your app. +Or install to your project: -When a protected request fails, the backend returns 401 (needs auth / reauth) or 403 (missing permission). For API requests, the response is JSON that includes an iframe URL. Your app should render that URL in a full-screen iframe overlay, and retry the request after the iframe reports success. If it reports `auth-cancel`, don't try again. The backdrop for the dialog is a stylistic choice, and you can style the background shown with the dialog any way you wish, and consider using CSS file with the iframe rather than inline styles as used in the example. +```sh +npm install paskia +``` -Following this flow the user gets authenticated properly and after that your app keeps running as if nothing ever happened. +### API Fetch with Automatic Auth + +Use `apiJson` or `apiFetch` for API calls. When a 401/403 response includes an auth URL, the authentication dialog appears automatically, then the request retries. The JSON variant is purely for convenience, doing JSON headers and conversions for you. + +```js +import { apiJson, apiFetch, AuthCancelledError } from 'paskia' + +// JSON API call (sets Content-Type, parses response) +try { + const data = await apiJson('/api/endpoint', { method: 'POST', body: { key: 'value' } }) +} catch (e) { + if (e instanceof AuthCancelledError) { + // User cancelled auth dialog + } +} + +// Raw fetch with auth handling (returns Response object) +const response = await apiFetch('/api/endpoint') +``` + +For requests that shouldn't trigger auth dialogs, use standard `fetch` or our `fetchJson`. + +### Session Validation Polling + +Keep sessions alive and detect when the user logs out or switches accounts: + +```js +import { SessionValidator } from 'paskia' + +const validator = new SessionValidator( + () => currentUser?.uuid, // getter for current user ID + (error) => handleSessionLost(error) // callback when session is lost or user changes +) + +validator.start() // start polling (pauses on idle) +validator.stop() // stop polling +``` + +The validator calls `/auth/api/validate` periodically to: +- Renew the session cookie (24h lifetime) +- Detect if the user logged out or switched accounts +- Pause polling when the page is idle, allowing sessions to expire when not used + +### Manual Auth Flow + +If you need custom control, handle 401/403 responses manually: + +```js +import { showAuthIframe, AuthCancelledError } from 'paskia' + +const response = await fetch('/api/protected') +if (response.status === 401 || response.status === 403) { + const data = await response.json() + if (data.auth?.iframe) { + try { + await showAuthIframe(data.auth.iframe) + // Retry the original request + } catch (e) { + if (e instanceof AuthCancelledError) { + // User clicked Back + } + } + } +} +``` + +### User Info and Profile + +Get current user details: + +```js +const user = await apiJson('/auth/api/user-info', { method: 'POST' }) +// Returns: { uuid, display_name, credentials, sessions, permissions, ... } +``` + +Or link to the built-in profile page: `/auth/` + +## Backend Integration + +### Using Forward-Auth Headers + +When using Caddy forward-auth, your backend receives `Remote-*` headers on authenticated requests. See [Headers](Headers.md) for the full list. + +```python +# Example: Python/FastAPI +@app.get("/api/data") +def get_data(request: Request): + user_id = request.headers.get("Remote-User") + org_id = request.headers.get("Remote-Org") + permissions = request.headers.get("Remote-Groups", "").split(",") + # ... +``` + +### Direct Validation from Backend + +Your backend can validate sessions directly by calling Paskia's validate endpoint: + +```python +import httpx + +async def validate_session(request) -> dict: + """Validate a session cookie and check permissions.""" + authcookie = request.get("__Host-paskia") + response = await httpx.post( + "http://localhost:4401/auth/api/validate?perm=myapp:login+myapp:api", + headers={ + "Host": request.headers["host"] + "X-Forwarded-For": request.client.host, + "Cookie": f"__Host-paskia={}", + }, + ) + if response.status_code != 200: + return response.json() # Return to client + # User authenticated... We are good to go! + ctx = response.json() # User and session information +``` + +This is useful for: +- WebSocket connections where headers aren't available after handshake +- Background jobs that need to verify a stored session +- APIs not behind forward-auth (auth/restrict) + +### Validate Endpoint Parameters + +`POST /auth/api/validate` accepts query parameters: + +| Parameter | Description | +|-----------|-------------| +| `perm=scope:name` | Require this permission (repeatable) | +| `max_age=5min` | Require recent passkey use | + +Returns 200 with user info on success, 401/403 on failure. + +## Proxying /auth/ to Paskia + +Your app server needs to proxy `/auth/` paths to Paskia. This can be done by your application but is much easier done by Caddy or Nginx. + +### Caddy + +This handles both HTTP and WebSocket connections. Caddy's `reverse_proxy` handles HTTP and WebSocket transparently. This is essentially what our Caddy [auth/setup](../caddy/auth/setup) snippet does: `reverse_proxy :4401`. + +```caddyfile +app.example.com { + import auth/setup + # ... your routes in handle blocks +} +``` + +### Nginx + +Certain headers need to be configured for correct host and WS support: + +```nginx +location /auth/ { + proxy_pass http://localhost:4401; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; +} +``` + +### Node.js / Express + +Using `http-proxy-middleware`: + +```js +import { createProxyMiddleware } from 'http-proxy-middleware' + +app.use('/auth', createProxyMiddleware({ target: 'http://localhost:4401', ws: true, changeOrigin: false })) +``` + +### Python / FastAPI + +You will need to process and handle `/auth/` for HTTP requests and `/auth/ws/` for WebSockets manually, which is beyond the scope of this documentation. + +We highly recommend Caddy instead as the simpler and more production-worthy solution that Just Works.