Provide user info in Remote-* headers. Caddy configuration improved.
This commit is contained in:
11
API.md
11
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
|
||||
|
||||
35
Caddyfile
35
Caddyfile
@@ -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
|
||||
}
|
||||
}
|
||||
30
caddy/Caddyfile
Normal file
30
caddy/Caddyfile
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
6
caddy/auth/all
Normal file
6
caddy/auth/all
Normal file
@@ -0,0 +1,6 @@
|
||||
# Enable auth site at /auth (setup) and require authentication on all paths
|
||||
import setup
|
||||
handle {
|
||||
import require {args[0]}
|
||||
{block}
|
||||
}
|
||||
17
caddy/auth/require
Normal file
17
caddy/auth/require
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
6
caddy/auth/setup
Normal file
6
caddy/auth/setup
Normal file
@@ -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}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user