Coverage for src/lumen/api/well_known.py: 100%
26 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
1"""
2This endpoint serves the well-known SMART-on-FHIR configuration discovery document.
3It publishes IdP OAuth endpoints and supported security capabilities to client applications.
4"""
6from __future__ import annotations
8import json
9from typing import Annotated, Any
11from fastapi import APIRouter, Depends
12from fastapi.responses import Response
14from ..auth.jwks import discover_oidc_config
15from ..config import Settings, get_settings
16from ..core.exceptions import OperationOutcomeError
18router = APIRouter()
20# SMART capabilities advertised for Stage 1-4 (compartment enforcement).
21# `launch-ehr`/`context-passthrough` are not advertised: the core enforces a
22# `patient` launch-context claim if one is present, but no launch endpoint
23# (launch parameter issuance/resolution) exists yet — that's on the IdP to
24# provide (see docs/architecture/smart-on-fhir.md §7).
25_SMART_CAPABILITIES = [
26 "permission-v1",
27 "permission-v2",
28 "permission-patient",
29 "client-confidential-symmetric",
30]
32_SCOPES_SUPPORTED = [
33 "openid",
34 "fhirUser",
35 "system/*.read",
36 "system/*.write",
37 "system/*.*",
38 "user/*.read",
39 "user/*.write",
40 "patient/*.read",
41 "patient/*.write",
42 "user/*.*",
43]
46@router.get("/.well-known/smart-configuration")
47async def smart_configuration(
48 settings: Annotated[Settings, Depends(get_settings)],
49) -> Response:
50 if not settings.smart.enabled or not settings.smart.tenant_map:
51 raise OperationOutcomeError(
52 status_code=404,
53 code="not-found",
54 diagnostics="SMART-on-FHIR is not enabled on this server.",
55 )
57 # Use the first configured issuer as the authoritative IdP for discovery.
58 issuer = next(iter(settings.smart.tenant_map))
60 fetch_base = settings.smart.internal_issuer_map.get(issuer)
61 try:
62 oidc: dict[str, Any] = await discover_oidc_config(issuer, fetch_base=fetch_base)
63 except Exception as exc:
64 raise OperationOutcomeError(
65 status_code=502,
66 code="exception",
67 diagnostics=f"Failed to fetch OIDC discovery from IdP: {exc}",
68 ) from exc
70 doc: dict[str, Any] = {
71 "issuer": issuer,
72 "authorization_endpoint": oidc.get("authorization_endpoint", ""),
73 "token_endpoint": oidc["token_endpoint"],
74 "jwks_uri": oidc["jwks_uri"],
75 "scopes_supported": _SCOPES_SUPPORTED,
76 "response_types_supported": ["code"],
77 "capabilities": _SMART_CAPABILITIES,
78 "code_challenge_methods_supported": ["S256"],
79 }
81 # Include optional endpoints when the IdP advertises them.
82 for key in ("userinfo_endpoint", "revocation_endpoint", "introspection_endpoint"):
83 if key in oidc:
84 doc[key] = oidc[key]
86 return Response(
87 content=json.dumps(doc),
88 media_type="application/json",
89 headers={"Cache-Control": "no-store"},
90 )