Refactor user-profile, restricted access and reset token registration as separate apps so the frontend does not need to guess which context it is running in.
Support user-navigable URLs at / as well as /auth/, allowing for a dedicated authentication site with pretty URLs.
This commit is contained in:
@@ -94,6 +94,13 @@ def add_common_options(p: argparse.ArgumentParser) -> None:
|
||||
)
|
||||
p.add_argument("--rp-name", help="Relying Party name (default: same as rp-id)")
|
||||
p.add_argument("--origin", help="Origin URL (default: https://<rp-id>)")
|
||||
p.add_argument(
|
||||
"--auth-host",
|
||||
help=(
|
||||
"Dedicated host (optionally with scheme/port) to serve the auth UI at the root,"
|
||||
" e.g. auth.example.com or https://auth.example.com"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -168,6 +175,16 @@ def main():
|
||||
os.environ["PASSKEY_RP_NAME"] = args.rp_name
|
||||
if origin:
|
||||
os.environ["PASSKEY_ORIGIN"] = origin
|
||||
if getattr(args, "auth_host", None):
|
||||
os.environ["PASSKEY_AUTH_HOST"] = args.auth_host
|
||||
else:
|
||||
# Preserve pre-set env variable if CLI option omitted
|
||||
args.auth_host = os.environ.get("PASSKEY_AUTH_HOST")
|
||||
|
||||
if getattr(args, "auth_host", None):
|
||||
from passkey.util import hostutil as _hostutil # local import
|
||||
|
||||
_hostutil.reload_config()
|
||||
|
||||
# One-time initialization + bootstrap before starting any server processes.
|
||||
# Lifespan in worker processes will call globals.init with bootstrap disabled.
|
||||
|
||||
@@ -6,7 +6,6 @@ from fastapi.responses import FileResponse, JSONResponse
|
||||
|
||||
from ..authsession import expires
|
||||
from ..globals import db
|
||||
from ..globals import passkey as global_passkey
|
||||
from ..util import frontend, hostutil, passphrase, permutil, querysafe, tokens
|
||||
from . import authz
|
||||
|
||||
@@ -358,10 +357,8 @@ async def admin_create_user_registration_link(
|
||||
expires=expires(),
|
||||
info={"type": "device addition", "created_by_admin": True},
|
||||
)
|
||||
origin = hostutil.effective_origin(
|
||||
request.url.scheme, request.headers.get("host"), global_passkey.instance.rp_id
|
||||
)
|
||||
url = f"{origin}/auth/{token}"
|
||||
base = hostutil.auth_site_base_url(request.url.scheme, request.headers.get("host"))
|
||||
url = f"{base}{token}"
|
||||
return {"url": url, "expires": expires().isoformat()}
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from fastapi import (
|
||||
Request,
|
||||
Response,
|
||||
)
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.security import HTTPBearer
|
||||
|
||||
from passkey.util import frontend
|
||||
@@ -112,13 +112,20 @@ async def forward_authentication(perm: list[str] = Query([]), auth=Cookie(None))
|
||||
}
|
||||
return Response(status_code=204, headers=remote_headers)
|
||||
except HTTPException as e:
|
||||
return FileResponse(frontend.file("index.html"), status_code=e.status_code)
|
||||
html = frontend.file("restricted", "index.html").read_bytes()
|
||||
return Response(html, status_code=e.status_code, media_type="text/html")
|
||||
|
||||
|
||||
@app.get("/settings")
|
||||
async def get_settings():
|
||||
pk = global_passkey.instance
|
||||
return {"rp_id": pk.rp_id, "rp_name": pk.rp_name}
|
||||
base_path = hostutil.ui_base_path()
|
||||
return {
|
||||
"rp_id": pk.rp_id,
|
||||
"rp_name": pk.rp_name,
|
||||
"ui_base_path": base_path,
|
||||
"auth_host": hostutil.configured_auth_host(),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/user-info")
|
||||
@@ -267,10 +274,8 @@ async def api_create_link(request: Request, auth=Cookie(None)):
|
||||
expires=expires(),
|
||||
info=session.infodict(request, "device addition"),
|
||||
)
|
||||
origin = hostutil.effective_origin(
|
||||
request.url.scheme, request.headers.get("host"), global_passkey.instance.rp_id
|
||||
)
|
||||
url = f"{origin}/auth/{token}"
|
||||
base = hostutil.auth_site_base_url(request.url.scheme, request.headers.get("host"))
|
||||
url = f"{base}{token}"
|
||||
return {
|
||||
"message": "Registration link generated successfully",
|
||||
"url": url,
|
||||
|
||||
@@ -2,11 +2,11 @@ import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi import Cookie, FastAPI, HTTPException
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from passkey.util import frontend, passphrase
|
||||
from passkey.util import frontend, hostutil, passphrase
|
||||
|
||||
from . import admin, api, ws
|
||||
|
||||
@@ -53,26 +53,37 @@ app.mount(
|
||||
"/auth/assets/", StaticFiles(directory=frontend.file("assets")), name="assets"
|
||||
)
|
||||
|
||||
# Navigable URLs are defined here. We support both / and /auth/ as the base path
|
||||
# / is used on a dedicated auth site, /auth/ on app domains with auth
|
||||
|
||||
|
||||
@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."""
|
||||
return FileResponse(frontend.file("index.html"))
|
||||
|
||||
|
||||
@app.get("/admin", include_in_schema=False)
|
||||
@app.get("/auth/admin", include_in_schema=False)
|
||||
async def admin_root_redirect():
|
||||
return RedirectResponse(f"{hostutil.ui_base_path()}admin/", status_code=307)
|
||||
|
||||
|
||||
@app.get("/admin/", include_in_schema=False)
|
||||
async def admin_root(auth=Cookie(None)):
|
||||
return await admin.adminapp(auth) # Delegate to handler of /auth/admin/
|
||||
|
||||
|
||||
@app.get("/{reset}")
|
||||
@app.get("/auth/{reset}")
|
||||
async def reset_link(request: Request, reset: str):
|
||||
"""Pretty URL for reset links."""
|
||||
if reset == "admin":
|
||||
# Admin app missing trailing slash lands here, be friendly to user
|
||||
return RedirectResponse(request.url_for("adminapp"), status_code=303)
|
||||
async def reset_link(reset: str):
|
||||
"""Serve the SPA directly with an injected reset token."""
|
||||
if not passphrase.is_well_formed(reset):
|
||||
raise HTTPException(status_code=404)
|
||||
url = request.url_for("frontapp").include_query_params(reset=reset)
|
||||
return RedirectResponse(url, status_code=303)
|
||||
return FileResponse(frontend.file("reset", "index.html"))
|
||||
|
||||
|
||||
@app.get("/restricted", include_in_schema=False)
|
||||
@app.get("/auth/restricted", include_in_schema=False)
|
||||
async def restricted_view():
|
||||
return FileResponse(frontend.file("restricted", "index.html"))
|
||||
|
||||
@@ -17,7 +17,7 @@ from uuid import UUID
|
||||
|
||||
from passkey import authsession as _authsession
|
||||
from passkey import globals as _g
|
||||
from passkey.util import passphrase
|
||||
from passkey.util import hostutil, passphrase
|
||||
from passkey.util import tokens as _tokens
|
||||
|
||||
|
||||
@@ -69,7 +69,8 @@ async def _create_reset(user, role_name: str):
|
||||
expires=_authsession.expires(),
|
||||
info={"type": "manual reset", "role": role_name},
|
||||
)
|
||||
return f"{_g.passkey.instance.origin}/auth/{token}", token
|
||||
base = hostutil.auth_site_base_url()
|
||||
return f"{base}{token}", token
|
||||
|
||||
|
||||
async def _main(query: str | None) -> int:
|
||||
|
||||
Reference in New Issue
Block a user