diff --git a/API.md b/API.md index ace9c63..c23afdb 100644 --- a/API.md +++ b/API.md @@ -18,8 +18,7 @@ GET /auth/api/forward - Authentication validation for Caddy/Nginx (was /auth/for - `Remote-Role`: role UUID - `Remote-Role-Name`: role display name - `Remote-Session-Expires`: session expiry timestamp (ISO 8601) - - `Remote-Session-Type` (optional): session type metadata when available - - `Remote-Credential` (optional): credential UUID backing the session + - `Remote-Credential`: credential UUID backing the session POST /auth/validate - Token validation endpoint POST /auth/user-info - Get authenticated user information POST /auth/logout - Logout current user diff --git a/CADDY.md b/CADDY.md new file mode 100644 index 0000000..aae3095 --- /dev/null +++ b/CADDY.md @@ -0,0 +1,78 @@ +## Caddy configuration + +We provide a few Caddy snippets that make the configuration easier, although the `forward_auth` directive of Caddy can be used directly as well. Place the auth folder with the snippets where your Caddyfile is. + +What these snippets do +- Mount the auth UI at `/auth/` proxying to `:4401` (auth backend) +- Use the forward-auth interface `/auth/api/forward` to verify the required credentials +- Render a login page or a permission denied page if needed (without changing URL) + +### 1) Your site has no auth yet — protect the whole thing + +Use this when you want “login required everywhere” which is useful to protect some service that doesn't have any authentication of its own: + +```caddyfile +localhost { + import auth/all "" { + reverse_proxy :3000 # your app + } +} +``` + +The auth/all protects the entire site with a simple directive. Put your normal setup inside the block. In this example we don't require any permissions, only that the user is logged in. Instead of `""` you may specify `perm=myapp:login` or other permissions. + +### 2) Different areas, different permissions + +When you need a more fine-grained control, use the auth/setup and auth/require snippets: + +```caddyfile +localhost { + import auth/setup + + @public path /.well-known/* /favicon.ico + handle @public { + root * /var/www/ + file_server + } + + @reports path /reports + handle @reports { + import auth/require perm=myapp:reports + reverse_proxy :3000 + } + + # Anywhere else, require login only + handle { + import auth/require "" + reverse_proxy :3000 + } +} +``` + +Note: We use the `handle @name` approach rather than `handle_path` to prevent the matched path being removed out of upstream URL. Unlike bare directives, these blocks will be tried in sequence and each can contain what you'd typically put in your site definition. + +--- + +## Override the auth backend URL (AUTH_UPSTREAM) + +By default, the auth service is contacted at localhost port 4401 ("for authentication required"). You can point Caddy to a different by setting the `AUTH_UPSTREAM` environment variable for Caddy. + +If unset, the snippets use `:4401` by default. + +## Headers your app receives + +When a request is allowed, the auth service adds these headers before proxying to your app (e.g., the service at `:3000`). Your app can use them for user context and authorization. + +| Header | Meaning | Example | +|---|---|---| +| `Remote-User` | Authenticated user UUID | `3f1a2b3c-4d5e-6789-abcd-ef0123456789` | +| `Remote-Name` | User display name | `Jane Doe` | +| `Remote-Org` | Organization UUID | `a1b2c3d4-1111-2222-3333-444455556666` | +| `Remote-Org-Name` | Organization display name | `Acme Inc` | +| `Remote-Role` | Role UUID | `b2c3d4e5-2222-3333-4444-555566667777` | +| `Remote-Role-Name` | Role display name | `Administrators` | +| `Remote-Groups` | Comma‑separated permissions the user has | `myapp:reports,auth:admin` | +| `Remote-Session-Expires` | Session expiry timestamp (ISO 8601) | `2025-09-25T14:30:00Z` | +| `Remote-Credential` | Credential UUID backing the session | `c3d4e5f6-3333-4444-5555-666677778888` | + +Note: Incoming `Remote-*` headers from clients are stripped by `auth/setup` or `auth/all`, so apps can trust these values. diff --git a/README.md b/README.md index 8c9fcff..212f469 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ A minimal FastAPI WebAuthn server with WebSocket support for passkey registratio - 🛠️ Development tools: `ruff` for linting and formatting - 🧹 Clean architecture with local challenge management +## Docs + +- Caddy integration: see `CADDY.md` for short, copy-paste snippets to secure your site with Caddy. + ## Requirements - Python 3.9+ diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 0ca9217..f1ec8f1 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -8,7 +8,8 @@ localhost { # Public paths (no auth) @public path /favicon.ico /.well-known/* handle @public { - reverse_proxy :3000 + root * /var/www/ + file_server } # Respond with user's display name handle_path /hello { diff --git a/caddy/auth/require b/caddy/auth/require index ef293cd..917df34 100644 --- a/caddy/auth/require +++ b/caddy/auth/require @@ -11,7 +11,6 @@ forward_auth {$AUTH_UPSTREAM:localhost:4401} { Remote-Role Remote-Role-Name Remote-Session-Expires - Remote-Session-Type Remote-Credential } } diff --git a/caddy/auth/setup b/caddy/auth/setup index 51b0945..7e15c6c 100644 --- a/caddy/auth/setup +++ b/caddy/auth/setup @@ -2,5 +2,5 @@ header -Remote-* @auth_api path /auth /auth/* handle @auth_api { - reverse_proxy {$AUTH_UPSTREAM:localhost:4401} + reverse_proxy {$AUTH_UPSTREAM::4401} } diff --git a/passkey/fastapi/api.py b/passkey/fastapi/api.py index 26c92ee..b4031c6 100644 --- a/passkey/fastapi/api.py +++ b/passkey/fastapi/api.py @@ -65,7 +65,6 @@ async def forward_authentication(perm: list[str] = Query([]), auth=Cookie(None)) if ctx.permissions: role_permissions.update(permission.id for permission in ctx.permissions) - session_info = ctx.session.info or {} remote_headers: dict[str, str] = { "Remote-User": str(ctx.user.uuid), "Remote-Name": ctx.user.display_name, @@ -75,15 +74,8 @@ async def forward_authentication(perm: list[str] = Query([]), auth=Cookie(None)) "Remote-Role": str(ctx.role.uuid), "Remote-Role-Name": ctx.role.display_name, "Remote-Session-Expires": ctx.session.expires.isoformat(), + "Remote-Credential": str(ctx.session.credential_uuid), } - - session_type = session_info.get("type") - if session_type: - remote_headers["Remote-Session-Type"] = str(session_type) - - if ctx.session.credential_uuid: - remote_headers["Remote-Credential"] = str(ctx.session.credential_uuid) - return Response(status_code=204, headers=remote_headers) except HTTPException as e: return FileResponse(frontend.file("index.html"), status_code=e.status_code)