diff --git a/e2e/tests/20-api-auth.spec.ts b/e2e/tests/20-api-auth.spec.ts index 6f268b7..ce3292b 100644 --- a/e2e/tests/20-api-auth.spec.ts +++ b/e2e/tests/20-api-auth.spec.ts @@ -534,7 +534,7 @@ test.describe('API Mode - Direct API Response Format', () => { expect(data.auth).toBeDefined() expect(data.auth.iframe).toBeDefined() expect(data.auth.mode).toBe('login') - expect(data.auth.iframe).toContain('/auth/restricted/') + expect(data.auth.iframe).toContain('/auth/restricted/iframe') console.log(`✓ 401 response includes auth.iframe: ${data.auth.iframe}`) }) diff --git a/oidc.md b/oidc.md index acb25e6..b237b5c 100644 --- a/oidc.md +++ b/oidc.md @@ -53,7 +53,7 @@ oid_auth_codes: dict[str, OIDAuthCode] = {} - `GET /auth/oidc/userinfo` — UserInfo endpoint (bearer token) ### Authorization (via existing restricted app) -- `GET /auth/restricted/?client_id=...&redirect_uri=...&response_type=code&scope=openid...` +- `GET /auth/restricted/oidc?client_id=...&redirect_uri=...&response_type=code&scope=openid...` The restricted app detects OIDC params from URL and handles authentication via WebSocket. @@ -83,9 +83,9 @@ The restricted app detects OIDC params from URL and handles authentication via W ## Authorization Flow -The `/auth/restricted/` page handles OIDC authorization alongside normal iframe auth: +The `/auth/restricted/oidc` page handles OIDC authorization (same code as `/auth/restricted/iframe` for API auth): -1. Client redirects to `/auth/restricted/?client_id=...&redirect_uri=...&response_type=code&scope=openid&state=...` +1. Client redirects to `/auth/restricted/oidc?client_id=...&redirect_uri=...&response_type=code&scope=openid&state=...` 2. Frontend detects OIDC params from `window.location.search` 3. Frontend passes raw query string to `/auth/ws/authenticate?{query_string}` 4. User authenticates via passkey diff --git a/paskia-js/README.md b/paskia-js/README.md index 2f1441b..5c6b3e5 100644 --- a/paskia-js/README.md +++ b/paskia-js/README.md @@ -68,7 +68,7 @@ The JSON variants set headers automatically, with body and response in JSON. Normally you use apiJson/apiFetch and they handle this automatically. If you need to wire it yourself, on a 401/403 response that includes `auth.iframe`, call `showAuthIframe(...)` and then retry the original request. -The backend returns 401/403 responses with the correct URL for proper user feedback. Alternatively you may use `/auth/restricted/#mode=login`, `mode=reauth` or `mode=forbidden` to trigger the UX flow you need. +The backend returns 401/403 responses with the correct URL for proper user feedback. Alternatively you may use `/auth/restricted/iframe#mode=login`, `mode=reauth` or `mode=forbidden` to trigger the UX flow you need. ```js import { showAuthIframe, AuthCancelledError } from 'paskia' diff --git a/paskia/fastapi/api.py b/paskia/fastapi/api.py index 76f9d36..5e7b86c 100644 --- a/paskia/fastapi/api.py +++ b/paskia/fastapi/api.py @@ -128,7 +128,7 @@ async def forward_authentication( - If Accept header contains "text/html": HTML page for authentication with data attributes for mode and other metadata. - Otherwise: JSON response with error details and an `iframe` field - pointing to /auth/restricted/?mode=... for iframe-based authentication. + pointing to /auth/restricted/iframe#mode=... for iframe-based authentication. """ try: ctx = await authz.verify( diff --git a/paskia/fastapi/authz.py b/paskia/fastapi/authz.py index 3b192fb..c82b776 100644 --- a/paskia/fastapi/authz.py +++ b/paskia/fastapi/authz.py @@ -41,7 +41,7 @@ async def auth_error_content(exc: AuthException) -> dict: # Build hash fragment from mode and metadata params = {"mode": exc.mode, **exc.metadata} fragment = "&".join(f"{k}={v}" for k, v in params.items() if v is not None) - iframe_url = f"/auth/restricted/#{fragment}" + iframe_url = f"/auth/restricted/iframe#{fragment}" return { "detail": exc.detail, "auth": { diff --git a/paskia/fastapi/mainapp.py b/paskia/fastapi/mainapp.py index a8f04aa..1c82eea 100644 --- a/paskia/fastapi/mainapp.py +++ b/paskia/fastapi/mainapp.py @@ -102,7 +102,7 @@ async def openid_configuration(request: Request): return { "issuer": issuer, - "authorization_endpoint": f"{issuer}/auth/restricted/", + "authorization_endpoint": f"{issuer}/auth/restricted/oidc", "token_endpoint": f"{issuer}/auth/oidc/token", "userinfo_endpoint": f"{issuer}/auth/oidc/userinfo", "jwks_uri": f"{issuer}/.well-known/jwks.json", @@ -131,9 +131,10 @@ async def jwks(): return get_jwks() -@app.get("/auth/restricted/") +@app.get("/auth/restricted/iframe") +@app.get("/auth/restricted/oidc") async def restricted_view(): - """Serve the restricted/authentication UI for iframe embedding.""" + """Serve the restricted/authentication UI for iframe or OpenID Connect.""" return Response(*await vitedev.read("/auth/restricted/index.html")) diff --git a/paskia/fastapi/oid.py b/paskia/fastapi/oid.py index 79cc6ea..ab3e54e 100644 --- a/paskia/fastapi/oid.py +++ b/paskia/fastapi/oid.py @@ -5,7 +5,7 @@ Implements OpenID Connect 1.0 Authorization Code flow: - POST /token - Token endpoint (code exchange) - GET /userinfo - UserInfo endpoint (bearer token) -Authorization is handled by /auth/restricted/ which passes OIDC params to +Authorization is handled by /auth/restricted/oidc which passes OIDC params to the /auth/ws/authenticate WebSocket. """