This commit is contained in:
2026-02-06 00:08:27 +00:00
parent 1c8be68811
commit 0b2be18b44
2 changed files with 197 additions and 35 deletions
+9 -2
View File
@@ -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) - Remote autentication by entering random keywords from another device (like 2fa)
- No CORS, NodeJS or anything extra needed. - 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 - API fetch: auth checks and login without leaving your app
- Forward-auth proxy: protect any unprotected site or service (Caddy, Nginx) - 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. 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) ![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 ## 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. 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 ### 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): 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):
+188 -33
View File
@@ -1,43 +1,198 @@
# Integrating Paskia with your App # 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) ```html
<script type="module">
```js import { apiJson, apiFetch, SessionValidator } from 'https://cdn.jsdelivr.net/npm/paskia@latest/dist/paskia.js'
// Show an authentication dialog </script>
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)
``` ```
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.