diff --git a/API.md b/API.md index 78c48d9..ace9c63 100644 --- a/API.md +++ b/API.md @@ -9,6 +9,17 @@ This document describes all API endpoints available in the PassKey Auth FastAPI ### HTTP Endpoints GET /auth/ - Main authentication app GET /auth/api/forward - Authentication validation for Caddy/Nginx (was /auth/forward-auth) + - On success returns `204 No Content` with the following headers: + - `Remote-User`: authenticated user UUID + - `Remote-Name`: display name + - `Remote-Groups`: comma-separated permission IDs (no spaces) + - `Remote-Org`: organization UUID + - `Remote-Org-Name`: organization display name + - `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 POST /auth/validate - Token validation endpoint POST /auth/user-info - Get authenticated user information POST /auth/logout - Logout current user diff --git a/Caddyfile b/Caddyfile deleted file mode 100644 index 323dd09..0000000 --- a/Caddyfile +++ /dev/null @@ -1,35 +0,0 @@ -(auth) { - # Permission check (named arg: perm=...) - forward_auth localhost:4401 { - uri /auth/api/forward?{args.0} - copy_headers x-auth-* - } -} - -localhost { - # Single definition for auth service endpoints (avoid duplicate matcher names) - @auth_api path /auth/* - handle @auth_api { - reverse_proxy localhost:4401 - } - - # Admin-protected paths - handle_path /admin/* { - import auth perm=auth:admin - # Respond with a message for the admin area - respond "Admin area (protected)" 200 - } - - # Reports-protected paths - handle_path /reports/* { - import auth perm=reports:view - # Respond with a message for the reports area - respond "Reports area (protected)" 200 - } - - # Unprotected (fallback) - handle { - # Respond with a public content message - respond "Public content" 200 - } -} diff --git a/caddy/Caddyfile b/caddy/Caddyfile new file mode 100644 index 0000000..0ca9217 --- /dev/null +++ b/caddy/Caddyfile @@ -0,0 +1,30 @@ +localhost { + import auth/setup + # Only users with myapp:reports and auth admin permissions + handle_path /reports { + import auth/require perm=myapp:reports&perm=auth:admin + respond "Reports area (protected) for {http.request.header.remote-org-name}" 200 + } + # Public paths (no auth) + @public path /favicon.ico /.well-known/* + handle @public { + reverse_proxy :3000 + } + # Respond with user's display name + handle_path /hello { + import auth/require "" + respond "Hello, {http.request.header.remote-name}! Your permissions: {http.request.header.remote-groups}" 200 + } + # Default route, requires authentication but no authorization + handle { + import auth/require "" + reverse_proxy :3000 + } +} + +localhost:4404 { + # Full site protected, /auth/ reserved for auth service + import auth/all perm=auth:admin { + reverse_proxy :3000 + } +} \ No newline at end of file diff --git a/caddy/auth/all b/caddy/auth/all new file mode 100644 index 0000000..15bc202 --- /dev/null +++ b/caddy/auth/all @@ -0,0 +1,6 @@ +# Enable auth site at /auth (setup) and require authentication on all paths +import setup +handle { + import require {args[0]} + {block} +} diff --git a/caddy/auth/require b/caddy/auth/require new file mode 100644 index 0000000..ef293cd --- /dev/null +++ b/caddy/auth/require @@ -0,0 +1,17 @@ +# Permission to use within your endpoints that need authentication/authorization, that +# is different depending on the route (otherwise use auth/all). +forward_auth {$AUTH_UPSTREAM:localhost:4401} { + uri /auth/api/forward?{args[0]} + copy_headers { + Remote-User + Remote-Name + Remote-Groups + Remote-Org + Remote-Org-Name + Remote-Role + Remote-Role-Name + Remote-Session-Expires + Remote-Session-Type + Remote-Credential + } +} diff --git a/caddy/auth/setup b/caddy/auth/setup new file mode 100644 index 0000000..51b0945 --- /dev/null +++ b/caddy/auth/setup @@ -0,0 +1,6 @@ +# Setup auth service at /auth/ and remove any Remote-* headers sent by client (for security) +header -Remote-* +@auth_api path /auth /auth/* +handle @auth_api { + reverse_proxy {$AUTH_UPSTREAM:localhost:4401} +} diff --git a/passkey/fastapi/admin.py b/passkey/fastapi/admin.py index 666082e..afaad99 100644 --- a/passkey/fastapi/admin.py +++ b/passkey/fastapi/admin.py @@ -1,8 +1,8 @@ import logging from uuid import UUID, uuid4 -from fastapi import Body, Cookie, FastAPI, HTTPException, Request -from fastapi.responses import FileResponse, JSONResponse, RedirectResponse +from fastapi import Body, Cookie, FastAPI, HTTPException +from fastapi.responses import FileResponse, JSONResponse from ..authsession import expires from ..globals import db @@ -24,12 +24,6 @@ async def general_exception_handler(_request, exc: Exception): return JSONResponse(status_code=500, content={"detail": "Internal server error"}) -@app.get("") -def adminapp_slashmissing(request: Request): - print("HERE") - return RedirectResponse(url=request.url_for("adminapp")) - - @app.get("/") async def adminapp(auth=Cookie(None)): try: diff --git a/passkey/fastapi/api.py b/passkey/fastapi/api.py index c1e4168..26c92ee 100644 --- a/passkey/fastapi/api.py +++ b/passkey/fastapi/api.py @@ -12,9 +12,11 @@ from fastapi import ( Request, Response, ) -from fastapi.responses import JSONResponse +from fastapi.responses import FileResponse, JSONResponse from fastapi.security import HTTPBearer +from passkey.util import frontend + from .. import aaguid from ..authsession import delete_credential, expires, get_reset, get_session from ..globals import db @@ -49,21 +51,42 @@ async def validate_token(perm: list[str] = Query([]), auth=Cookie(None)): @app.get("/forward") async def forward_authentication(perm: list[str] = Query([]), auth=Cookie(None)): - """Forward auth validation for Caddy/Nginx (moved from /auth/forward-auth). + """Forward auth validation for Caddy/Nginx. Query Params: - perm: repeated permission IDs the authenticated user must possess (ALL required). - Success: 204 No Content with x-auth-user-uuid header. + Success: 204 No Content with Remote-* headers describing the authenticated user. Failure (unauthenticated / unauthorized): 4xx JSON body with detail. """ try: ctx = await authz.verify(auth, perm) - return Response( - status_code=204, headers={"x-auth-user-uuid": str(ctx.session.user_uuid)} - ) - except HTTPException as e: # pass through explicitly - raise e + role_permissions = set(ctx.role.permissions or []) + 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, + "Remote-Groups": ",".join(sorted(role_permissions)), + "Remote-Org": str(ctx.org.uuid), + "Remote-Org-Name": ctx.org.display_name, + "Remote-Role": str(ctx.role.uuid), + "Remote-Role-Name": ctx.role.display_name, + "Remote-Session-Expires": ctx.session.expires.isoformat(), + } + + 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) @app.get("/settings") diff --git a/passkey/fastapi/mainapp.py b/passkey/fastapi/mainapp.py index d24db93..50866ba 100644 --- a/passkey/fastapi/mainapp.py +++ b/passkey/fastapi/mainapp.py @@ -54,6 +54,12 @@ app.mount( ) +@app.get("/") +async def frontapp_redirect(request: Request): + """Redirect root (in case accessed on backend) to the main authentication app.""" + return RedirectResponse(request.url_for("frontapp"), status_code=303) + + @app.get("/auth/") async def frontapp(): """Serve the main authentication app."""