Skip to content

feat(smart): add SMART App Launch 2.0.0 and US Core Backend Services support - #1388

Open
vincentvanhees wants to merge 16 commits into
masterfrom
feat/uscore-fhir-ig-compliance
Open

vincentvanhees wants to merge 16 commits into
masterfrom
feat/uscore-fhir-ig-compliance

Conversation

@vincentvanhees

Copy link
Copy Markdown

Link to JIRA ticket if there is one:

Adds server-side SMART App Launch 2.0.0 support to fence so that
gen3-fhir-proxy can pass the Inferno US Core v6.1.0 test suite. All tests
pass except 1.3.2.01 and 1.3.2.02 (TLS-only checks; will pass on QA/prod).
Existing OAuth2 flows are unaffected.

The SMART App Launch discovery document (/.well-known/smart-configuration)
is served by gen3-fhir-proxy, not fence.

New Features

  • Token introspection (POST /oauth2/introspect, RFC 7662 / CONF-0123):
    validates a token using fence's JWT validation and returns an RFC 7662
    response including SMART patient context (patient, username) when present.
    Requires HTTP Basic auth with a registered client_id + client_secret.
  • Private key JWT client authentication (RFC 7523 / CONF-0120): clients
    register their JWKS in client_metadata["jwks"] and authenticate
    asymmetrically at the token endpoint — no shared secret required. Used by
    SMART Backend Services. Note: JTI replay prevention is not implemented; the
    exp claim limits the replay window to the token's lifetime.
  • PKCE (RFC 7636 / CONF-0117): code_challenge and
    code_challenge_method are now persisted on the authorization code and
    verified on token exchange. Supports S256 and plain. Backwards compatible —
    flows without PKCE continue to work.

Breaking Changes

None.

Bug Fixes

  • scope is now included in token responses per RFC 6749 §5.1. SMART
    clients depend on this field to confirm which scopes were granted.

Improvements

  • SMART scope validation: validate_scopes now accepts SMART scope
    prefixes (patient/, user/, system/, launch/) and literals
    (fhirUser, openid, etc.) without requiring them in
    CLIENT_ALLOWED_SCOPES. Backend Services flows (system/ scopes only)
    no longer require openid.
  • Config-driven extra scopes and claims (see Deployment changes).

Dependency updates

None.

Deployment changes

Two new optional config keys (both default to empty — no behaviour change
for non-FHIR deployments):

  • EXTRA_ALLOWED_SCOPE_PREFIXES / EXTRA_ALLOWED_SCOPES: allow additional
    scope patterns on client registration beyond CLIENT_ALLOWED_SCOPES.
  • SCOPE_EXTRA_CLAIMS: maps a scope name to an ID-token claim built from a
    user tag and a base URL read from an environment variable. Example for the
    SMART fhirUser claim:
    SCOPE_EXTRA_CLAIMS:
      fhirUser:
        base_url_env: MY_FHIR_BASE_URL
        user_tag: fhir_user_id

No new database migration is required — the PKCE columns (code_challenge,
code_challenge_method) are already added by the existing migration
9b3a5a7145d7.

Avantol13 and others added 4 commits September 30, 2026 14:42
… 0121-0123)

Add server-side support for SMART App Launch 2.0 and US Core Backend Services
requirements, with minimalistic changes to the existing OAuth2 infrastructure.

- Add token introspection endpoint (RFC 7662) at /oauth2/introspect (CONF-0123)
- Add private_key_jwt client authentication via FencePrivateKeyJWT (CONF-0120);
  clients register their JWKS in client_metadata["jwks"] — no schema migration needed
- Extend ClientCredentialsGrant to accept private_key_jwt authentication
- Fix validate_scopes to recognise system/ scopes as SMART scopes and drop the
  openid requirement for Backend Services flows (CONF-0121/0122)
- Add PKCE support (RFC 7636) to the authorization code grant (CONF-0117)
- Advertise client-confidential-asymmetric and private_key_jwt in well-known
  metadata; move the SMART discovery document to gen3-fhir-proxy (FHIR layer)
- Filter null values from introspection responses per RFC 7662
…iven

Fence previously hardcoded SMART on FHIR scope prefixes (patient/, user/,
system/, launch/) and literals (fhirUser, openid, etc.) in create_client(),
and hardcoded fhirUser ID-token claim generation in generate_id_token().
This couples fence to FHIR semantics it should not know about.

Replace with two generic config mechanisms:

EXTRA_ALLOWED_SCOPE_PREFIXES / EXTRA_ALLOWED_SCOPES
  Lists of scope prefixes and exact literals that bypass the
  CLIENT_ALLOWED_SCOPES check when registering OAuth clients. FHIR
  deployments set these in their fence-config.yaml; a stock fence
  deployment leaves them empty (defaults: []).

SCOPE_EXTRA_CLAIMS
  Map of scope-name → {base_url_env, user_tag}. When a matching scope is
  requested and the authenticated user has the named tag, the claim is
  added to the ID token as "{env_var_value}/{tag_value}". FHIR deployments
  configure this for the fhirUser claim; fence itself carries no FHIR
  knowledge.

Also include "scope" in token responses per RFC 6749 §5.1.
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Integration Tests

Test summary after running integration tests

filepath passed failed skipped SUBTOTAL
tests/test_oauth2.py 15 0 0 15
tests/test_centralized_auth.py 16 0 0 16
tests/test_audit_service.py 3 0 3 6
tests/test_data_upload.py 8 0 1 9
tests/test_drs_endpoint.py 25 0 0 25
tests/test_presigned_url.py 8 0 0 8
tests/test_dbgap.py 2 2 1 5
tests/test_user_token.py 5 0 0 5
tests/test_fence_admin.py 1 1 0 2
tests/test_register_user.py 2 0 0 2
tests/test_oidc_client.py 2 0 0 2
tests/test_user_login_activation.py 2 0 1 3
tests/test_client_credentials.py 1 0 0 1
tests/test_ras_authn.py 0 0 3 3
tests/test_ras_passport.py 0 0 3 3
tests/test_google_data_access.py 0 1 0 1
TOTAL 90 4 12 106

Test summary after rerunning failed integration tests

filepath passed failed SUBTOTAL
tests/test_fence_admin.py 1 0 1
tests/test_dbgap.py 0 2 2
tests/test_google_data_access.py 0 1 1
TOTAL 1 3 4

Please find the detailed integration test report here

Please find the detailed integration test report after rerunning failed tests here

Please find the Github Action logs here

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Failure Analysis

Please find the detailed test analysis report here

@pieterlukasse
pieterlukasse self-requested a review October 12, 2026 13:11
validate_scopes() returned True early for any request containing a SMART
scope (including openid), bypassing client.check_requested_scopes() for
the entire request. This allowed arbitrary unknown scopes to pass through
unchecked whenever openid was also present.

Non-SMART scopes are now explicitly validated against the client's allowed
scopes even when the request is identified as a SMART flow.
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 38256453707

Coverage decreased (-0.4%) to 74.268%

Details

  • Coverage decreased (-0.4%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 195 coverage regressions across 6 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

195 previously-covered lines in 6 files lost coverage.

File Lines Losing Coverage Coverage
utils.py 59 64.39%
oidc/endpoints.py 45 39.13%
oidc/grants/oidc_code_grant.py 44 68.09%
models.py 35 85.67%
jwt/token.py 10 90.86%
blueprints/oauth2.py 2 77.84%

Coverage Stats

Coverage Status
Relevant Lines: 12028
Covered Lines: 8933
Line Coverage: 74.27%
Coverage Strength: 0.74 hits per line

💛 - Coveralls

@github-actions

Copy link
Copy Markdown

Integration Tests

filepath passed skipped SUBTOTAL
tests/test_oauth2.py 15 0 15
tests/test_centralized_auth.py 16 0 16
tests/test_audit_service.py 3 3 6
tests/test_drs_endpoint.py 25 0 25
tests/test_data_upload.py 8 1 9
tests/test_presigned_url.py 8 0 8
tests/test_dbgap.py 4 1 5
tests/test_user_token.py 5 0 5
tests/test_user_login_activation.py 2 1 3
tests/test_fence_admin.py 2 0 2
tests/test_register_user.py 2 0 2
tests/test_oidc_client.py 2 0 2
tests/test_google_data_access.py 1 0 1
tests/test_client_credentials.py 1 0 1
tests/test_ras_authn.py 0 3 3
tests/test_ras_passport.py 0 3 3
TOTAL 94 12 106

Please find the detailed integration test report here

Please find the Github Action logs here

@vincentvanhees
vincentvanhees marked this pull request as ready for review October 12, 2026 15:19
Fix resolve_client_public_key() in private_key_jwt.py for authlib 1.6.12
compatibility: list(key_set) is not supported (use key_set.keys), and
find_by_kid() raises ValueError on a miss rather than returning None.

Set PROMETHEUS_MULTIPROC_DIR in tests/conftest.py before fence is
imported so the test suite runs without requiring the env var to be
set manually.

Add 74 new tests across 5 files targeting the new SMART on FHIR /
US Core compliance code on this branch:
- tests/oidc/test_private_key_jwt.py: create_claims_options and all
  resolve_client_public_key branches (no JWKS, matching kid, unknown
  kid, no kid, empty key set)
- tests/oidc/test_endpoints.py: JWTIntrospectionToken, IntrospectionEndpoint
  payload building and query_token, revoke and introspect HTTP endpoints
- tests/oidc/test_jwt_generator.py: generate_client_response scope
  handling, client_credentials dispatch, implicit scope-as-string path
- tests/oidc/test_oidc_code_grant.py: PKCE S256/plain full flows and
  _validate_code_verifier branches, exists_nonce, validate_request_prompt
  (none/consent/select_account prompts), token request error paths
- tests/oidc/test_oauth2_blueprint.py: auto_approve, consent denial,
  prompt=none error cases, unauthenticated authorize paths, revoke with
  invalid token, display_error endpoint
… 0121-0123)

Add server-side support for SMART App Launch 2.0 and US Core Backend Services
requirements, with minimalistic changes to the existing OAuth2 infrastructure.

- Add token introspection endpoint (RFC 7662) at /oauth2/introspect (CONF-0123)
- Add private_key_jwt client authentication via FencePrivateKeyJWT (CONF-0120);
  clients register their JWKS in client_metadata["jwks"] — no schema migration needed
- Extend ClientCredentialsGrant to accept private_key_jwt authentication
- Fix validate_scopes to recognise system/ scopes as SMART scopes and drop the
  openid requirement for Backend Services flows (CONF-0121/0122)
- Add PKCE support (RFC 7636) to the authorization code grant (CONF-0117)
- Advertise client-confidential-asymmetric and private_key_jwt in well-known
  metadata; move the SMART discovery document to gen3-fhir-proxy (FHIR layer)
- Filter null values from introspection responses per RFC 7662
…iven

Fence previously hardcoded SMART on FHIR scope prefixes (patient/, user/,
system/, launch/) and literals (fhirUser, openid, etc.) in create_client(),
and hardcoded fhirUser ID-token claim generation in generate_id_token().
This couples fence to FHIR semantics it should not know about.

Replace with two generic config mechanisms:

EXTRA_ALLOWED_SCOPE_PREFIXES / EXTRA_ALLOWED_SCOPES
  Lists of scope prefixes and exact literals that bypass the
  CLIENT_ALLOWED_SCOPES check when registering OAuth clients. FHIR
  deployments set these in their fence-config.yaml; a stock fence
  deployment leaves them empty (defaults: []).

SCOPE_EXTRA_CLAIMS
  Map of scope-name → {base_url_env, user_tag}. When a matching scope is
  requested and the authenticated user has the named tag, the claim is
  added to the ID token as "{env_var_value}/{tag_value}". FHIR deployments
  configure this for the fhirUser claim; fence itself carries no FHIR
  knowledge.

Also include "scope" in token responses per RFC 6749 §5.1.
validate_scopes() returned True early for any request containing a SMART
scope (including openid), bypassing client.check_requested_scopes() for
the entire request. This allowed arbitrary unknown scopes to pass through
unchecked whenever openid was also present.

Non-SMART scopes are now explicitly validated against the client's allowed
scopes even when the request is identified as a SMART flow.
Fix resolve_client_public_key() in private_key_jwt.py for authlib 1.6.12
compatibility: list(key_set) is not supported (use key_set.keys), and
find_by_kid() raises ValueError on a miss rather than returning None.

Set PROMETHEUS_MULTIPROC_DIR in tests/conftest.py before fence is
imported so the test suite runs without requiring the env var to be
set manually.

Add 74 new tests across 5 files targeting the new SMART on FHIR /
US Core compliance code on this branch:
- tests/oidc/test_private_key_jwt.py: create_claims_options and all
  resolve_client_public_key branches (no JWKS, matching kid, unknown
  kid, no kid, empty key set)
- tests/oidc/test_endpoints.py: JWTIntrospectionToken, IntrospectionEndpoint
  payload building and query_token, revoke and introspect HTTP endpoints
- tests/oidc/test_jwt_generator.py: generate_client_response scope
  handling, client_credentials dispatch, implicit scope-as-string path
- tests/oidc/test_oidc_code_grant.py: PKCE S256/plain full flows and
  _validate_code_verifier branches, exists_nonce, validate_request_prompt
  (none/consent/select_account prompts), token request error paths
- tests/oidc/test_oauth2_blueprint.py: auto_approve, consent denial,
  prompt=none error cases, unauthenticated authorize paths, revoke with
  invalid token, display_error endpoint
@github-actions

Copy link
Copy Markdown

Integration Tests

filepath passed skipped SUBTOTAL
tests/test_oauth2.py 15 0 15
tests/test_centralized_auth.py 16 0 16
tests/test_audit_service.py 3 3 6
tests/test_drs_endpoint.py 25 0 25
tests/test_data_upload.py 8 1 9
tests/test_presigned_url.py 8 0 8
tests/test_dbgap.py 4 1 5
tests/test_user_token.py 5 0 5
tests/test_user_login_activation.py 2 1 3
tests/test_fence_admin.py 2 0 2
tests/test_register_user.py 2 0 2
tests/test_google_data_access.py 1 0 1
tests/test_client_credentials.py 1 0 1
tests/test_oidc_client.py 2 0 2
tests/test_ras_authn.py 0 3 3
tests/test_ras_passport.py 0 3 3
TOTAL 94 12 106

Please find the detailed integration test report here

Please find the Github Action logs here

… OIDC discovery

Add introspection_endpoint (RFC 7662), revocation_endpoint (RFC 7009),
and private_key_jwt to token_endpoint_auth_methods_supported in the
/.well-known/openid-configuration response.

These were missing because the SMART/US Core compliance work added the
underlying endpoints but did not update the discovery document. Clients
that auto-discover fence capabilities via OIDC discovery (rather than
via /.well-known/smart-configuration) would not know these endpoints
exist. This is particularly relevant for Backend Services clients
checking for private_key_jwt support (CONF-0120).
@github-actions

Copy link
Copy Markdown

Integration Tests

Please find the detailed integration test report here

Please find the Github Action logs here

@github-actions

Copy link
Copy Markdown

Failure Analysis

Please find the detailed test analysis report here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants