Provider Access is not just a FHIR endpoint.
Four independent controls have to agree before any member data is assembled, and a production deployment has to trust an identity provider it does not run. Both are decisions with a wrong answer that keeps working right up until it matters.
CMS-0057-F is the federal rule that requires Medicare Advantage, Medicaid, CHIP, and some exchange plans to offer FHIR APIs for patient access, provider access, payer-to-payer exchange, and prior authorization by January 1, 2027.
Provider Access is a caller shape, not a route
There is no /provider-access endpoint. Provider Access is the ordinary
/fhir/r4/{Resource} read surface reached by a provider-shaped token —
one carrying user/… or system/… scopes, meaning a provider or backend
service reading someone else's record.
A patient-scoped token is Patient Access: the member reading their own record, governed by the SMART patient binding rather than by Provider Access consent — a member does not need to authorize a disclosure to themselves. The distinction is drawn from the token, never from a controller name or route string, so it cannot be lost by someone adding an endpoint.
Enforcement lives in a global action filter rather than middleware, for a reason worth stating: the decision needs the tenant, and tenant resolution runs after the SMART check in the pipeline. A filter runs after the whole middleware pipeline — so authentication, scope and tenant are established facts — and still before any action body, so an unauthorized request never assembles or retrieves member data. It is registered once, globally: a new member-scoped FHIR controller is governed the moment it exists, and there is nothing to remember to opt into.
Four independent controls, all mandatory
| Having this | Does not imply |
|---|---|
| a correct SMART scope | attribution, or consent |
| attribution | consent |
| consent | attribution |
| a Payer-to-Payer consent | Provider Access |
What the filter governs, and where the list comes from
Every member-scoped resource the FHIR surface serves: the administrative and claims types —
Patient, Coverage, ExplanationOfBenefit, Encounter,
Claim, Task, Communication, DocumentReference,
ClaimResponse — plus the twelve USCDI clinical types. The clinical list is not restated
in the filter: it is read from the same inventory the SMART layer, the routes and the
CapabilityStatement read, with a test pinning them together.
Protecting Patient alone would leave the claims history readable; protecting the claims
history alone would leave the clinical record readable. Taking both from one table removes the
possibility rather than relying on a reviewer to notice.
Resolving the member — and refusing when there is none
A resource id alone is not resolved to a member, because resolving it means reading the resource — which is the access being authorized. No member context therefore denies rather than guesses. That is why a provider-shaped search across the whole membership is refused, and why a provider reading a clinical resource by id must also name the member.
FHIR operations are deliberately not routed through this gate. Payer-to-Payer runs its own gate for its own purpose, and an operation name is not a member id — a control applied to the wrong surface is not a stronger control.
Consent is purpose-scoped, and the separation runs both ways
Provider Access requires the ProviderAccess purpose specifically. It is the same registry and
the same policy Payer-to-Payer uses; the only difference is the purpose asked for, so neither direction
can drift more permissive than the other. A member with only a Payer-to-Payer consent is denied here; a
member with only a Provider Access consent is denied there. A generic consent carrying no purpose
authorizes neither. The required purpose is a constant in code, not configuration.
Consent is evaluated at the authorization instant chosen by the plan — never a timestamp supplied in the request — and the effective period is applied by the policy, so a record persisted as Active past its expiry still denies.
What it takes to trust an identity provider you do not run
Cloud Health Office's FHIR service is a resource server. It does not issue production tokens and hosts no production authorization flow — the bundled authorization server is a development and acceptance-suite issuer only. Trusting a payer's real IdP is therefore a set of explicit decisions, and each of them is one that quietly goes wrong if left implicit.
Two modes, stated rather than inferred
| Mode | Trusts | Permitted where |
|---|---|---|
| Demo | the bundled development issuer | development hosts only |
| ExternalIssuer | the configured trusted issuers | anywhere |
A Demo deployment on a non-development host fails startup. The mode is not derived from "is any external issuer configured?", because that would turn a missing config file into a silent downgrade to demo trust — the one failure nobody notices, since everything keeps working and the service simply trusts the wrong authorization server. There is no production fallback to Demo: the host does not start, and the error names the setting. The legacy single-issuer settings still work in Demo and are ignored in ExternalIssuer mode, so a half-migrated configuration cannot keep trusting the demo issuer by accident.
The issuer is resolved first, and supplies everything else
A token validates only if its iss matches a configured issuer exactly. That entry then
supplies its own keys, its own audiences, its own permitted algorithms, its own claim mapping and its own
permitted tenants.
The alternative — a global key set and a global audience list — quietly makes trust the union of every configured issuer's, so a token from issuer A would be accepted bearing issuer B's audience and verified against B's keys. With one IdP that is invisible. With two it is the difference between per-customer trust and one shared trust blob. Trust is administrator-controlled: nothing in a token can add an issuer, change an audience or redirect key retrieval, and keys are never fetched for an issuer that was not configured. Trust-on-first-use is not a weaker form of trust; it is the absence of it.
Rotation, and a refresh signal that is attacker-controlled
An issuer rotates on its own schedule and does not announce it. The only signal is a token whose key id is unknown — and anyone can present a token with a random key id. So refreshes are rate-limited per issuer and single-flighted: a burst carrying one new key id produces exactly one fetch and the rest wait for its result. A forged key id costs at most one fetch per interval.
Keys already retrieved stay usable while the issuer is unreachable, up to a bounded staleness age. Within that window previously-seen key ids keep working and unknown ones fail closed, so an IdP outage degrades rotation rather than signature validation. Past the bound the keys are dropped, because indefinitely stale trust would keep honouring a revoked key. Failing to retrieve keys never disables authentication — no keys, no validated tokens.
Algorithms, audience, and why HMAC is absent
RS, PS and ES families are accepted; none is absent and
signed tokens are required. HMAC is absent deliberately. A symmetric verifier will accept
a token signed with the issuer's public key as the shared secret — the classic
algorithm-confusion attack. A resource server validating a third-party IdP only ever holds public keys, so
admitting HMAC has no legitimate use and one catastrophic misuse; symmetric keys are filtered out of a key
set at ingestion rather than relied on being unreachable later.
Audience is validated against the audiences of the issuer that actually signed the token, so a correctly signed token minted for another API is refused — issuer plus signature alone would have accepted it. Clock skew is configurable but capped at startup: skew large enough to meaningfully extend a token's life is lifetime validation switched off wearing a clock-drift costume.
Discovery and JWKS fetches cannot be redirected
A fetch target is refused unless configuration already named it: the issuer's own host or an explicitly listed additional host, HTTPS outside development, and never a loopback, link-local (cloud instance metadata) or private address — an allow-list entry does not override that bar. A discovery document pointing its key-set URI at an unapproved origin is refused before the request is made, because a refusal after the fetch has already performed the SSRF.
Only literal IP addresses are checked. A DNS name resolving into private space is a network-egress concern owned by the platform; re-resolving here would be a time-of-check/time-of-use check that reads as protection without being any.
Tenant binding: a header may fill a vacuum, never contradict a token
Tenant comes from the token's mapped claim first, and a service-to-service header second. A header that disagrees with the token is a 403 tenant conflict, not an override. Previously the header was consulted whenever the token carried no tenant claim, which meant any authenticated caller whose issuer did not map a tenant could name any tenant and be believed. Where an issuer declares its permitted tenants, the resolved tenant must be one of them — so customer A's IdP cannot authenticate into customer B's data however its claims are shaped. Tenant is never taken from a FHIR body or query parameter.
Provider identity is believed only when an issuer vouches for it
An NPI is public information, so a claim merely named npi proves nothing. It becomes
authoritative only when a trusted issuer was configured, by an administrator, to assert it. Only the
configured claim is read — never a conventionally named one — its shape is validated, and
absence is treated as "no issuer has vouched for this caller's identity" rather than as "the
caller has no NPI".
The default is unset. Where an issuer does assert provider identity, the CDex attachment submission compares the token's NPI against the provider the request was addressed to — closing the substitution a corroborating key cannot detect, namely a caller who knows another provider's public NPI and puts it in the payload. This only ever tightens: deployments without a provider identity claim behave exactly as before.
Configuration errors fail startup; network conditions fail readiness
Demo mode on a production host, no trusted issuer, an invalid issuer URI, a missing audience, an unsupported algorithm, a duplicate issuer or a disallowed key-set host all prevent the host from starting. An unreachable IdP does not: the service starts and readiness reports it, degraded or unhealthy according to whether any issuer has usable keys. A flood of 401s from expired tokens is a healthy resource server doing its job, and conflating that with a trust outage would make the signal fire during an attack and stay silent during an outage.
Every refusal looks the same from outside
"Not attributed", "no consent" and "no such member" are deliberately indistinguishable: one
403 with a FHIR OperationOutcome reading "Provider Access is not authorized
for this request." An acceptance test asserts the response bodies are byte-identical.
A differentiated refusal would confirm which members exist and let a caller enumerate the membership. The structured category is kept in the audit record instead, where an operator can act on it — and every decision is recorded with PHI-free identifiers only: tenant, member id, caller id, resource type, the authorizing consent id, the decision category and the evaluation instant. Grants log at information, refusals at warning. Never logged: demographics, clinical payloads, consent narrative, tokens, or credentials.
What proves the above
Implementation evidence
- Scenarios
- PROV-01 attributed member data pull · PROV-02 attribution enforcement · PROV-03 opt-out honored · CONSENT-01 one consent registry · SEC-01 SMART on FHIR / OAuth · PAT-02 USCDI clinical through the same gate
- Enforcement point
- A global MVC action filter, running after authentication, SMART scope and tenant resolution and before any action body
- Architecture docs
- provider-access.md · smart-oauth-trust.md · consent.md · idp-integration-contract.md
- Pull requests
- #1152 purpose-scoped consent · #1153 Provider Access consent and attribution enforcement · #1157 clinical resources through the same gate · #1158 production SMART/OAuth external issuer trust
- Test data
- Synthetic. The acceptance suite runs against a test issuer in Demo mode — a test configuration, never a bypass.
Provider Access, security and consent scenarios from the latest published acceptance evidence.
Loading the latest published CMS-0057-F acceptance evidence. It is generated in CI from the acceptance suite at a named source revision and committed to this repository, so the status shown here is the tested status rather than a claim typed into a page.
What is not proven here
- No customer identity provider has been onboarded. There is a real difference between the product implements production external-issuer trust and a particular payer's IdP is connected. Only the first is claimed. Connecting a specific IdP is deployment work against the documented integration contract.
- No live attribution feed. Provider panels come from a configured catalog. It enforces for real — an empty catalog attributes nobody, and unknown provider, unknown member or blank ids all return false — but a roster integration from a payer source system is engagement work behind the same interface, and no code claims otherwise.
- Attribution is membership-level, not date-scoped. A panel entry does not yet carry an effective period the way a consent record does.
- Audit is emitted to the log, not to a durable audit store. The FHIR service has no audit sink.
- Cloud Health Office does not host a production authorization flow. It is a resource server; it advertises only the configured authorization server's own endpoints, and never an authorization flow it does not host.
- Passing every security scenario is not a security certification, a penetration test result, or an assessment by any third party.
Bring your IdP's discovery document.
The trust model is configuration an administrator controls: issuers, audiences, algorithms, claim mapping and permitted tenants. The onboarding conversation starts there.