Skip to main content

Connecting the Client

This page walks through the three steps needed to make ajfhir-smart-client talk to this auth server instead of Epic's sandbox.


Step 1 โ€” Create a client profileโ€‹

In the ajfhir-smart-client project, create src/main/resources/application-server.yml:

# application-server.yml โ€” points the SMART client at our own auth server
smart:
epic:
# Must match the client_id seeded by DataInitializer
client-id: ajfhir-smart-client

# Must match the redirectUri in RegisteredApp
redirect-uri: http://localhost:8080/callback

scopes:
- launch
- openid
- fhirUser
- patient/Patient.rs
- patient/Condition.rs
- patient/MedicationRequest.rs
discovery-cache-minutes: 1 # short TTL โ€” useful when iterating locally

hapi:
fhir:
log-requests-and-responses: true

logging:
level:
org.ajfhir.smartfhir.client: DEBUG
org.springframework.security: DEBUG

Run the client with this profile:

cd ajfhir-smart-client
mvn spring-boot:run -Dspring-boot.run.profiles=server

Step 2 โ€” Register the discovery proxy on HAPIโ€‹

The SMART client sends /.well-known/smart-configuration to port 8080 (the FHIR base URL). This must be forwarded to the auth server on port 9000.

Copy SmartDiscoveryProxyFilter.java from the server project into your HAPI server, then register it:

// In your HAPI FhirServerConfig or @Configuration class
@Bean
public FilterRegistrationBean<SmartDiscoveryProxyFilter> discoveryProxy() {
FilterRegistrationBean<SmartDiscoveryProxyFilter> reg =
new FilterRegistrationBean<>();
reg.setFilter(new SmartDiscoveryProxyFilter("http://localhost:9000"));
reg.addUrlPatterns("/.well-known/*");
reg.setOrder(1); // run before HAPI's own filters
return reg;
}

Verify it works:

curl http://localhost:8080/fhir/.well-known/smart-configuration
# Should return the same JSON as:
curl http://localhost:9000/.well-known/smart-configuration

Step 3 โ€” Register the scope interceptor on HAPIโ€‹

SmartScopeAuthorizationInterceptor validates that every incoming FHIR request has a valid bearer token with the right scopes. Register it in your HAPI RestfulServer:

// In your HAPI RestfulServer setup
@Autowired
private SmartScopeAuthorizationInterceptor scopeInterceptor;

@Override
protected void initialize() {
registerInterceptor(scopeInterceptor);
}

If the RSA key is not available as a Spring bean in the HAPI server, construct it from the JWKS endpoint:

@Bean
public SmartScopeAuthorizationInterceptor scopeInterceptor()
throws Exception {
// Fetch the public key from the auth server's JWKS endpoint
String jwksUrl = "http://localhost:9000/oauth2/jwks";
com.nimbusds.jose.jwk.JWKSet jwkSet =
com.nimbusds.jose.jwk.JWKSet.load(new URL(jwksUrl));
com.nimbusds.jose.jwk.RSAKey rsaKey =
(com.nimbusds.jose.jwk.RSAKey) jwkSet.getKeys().get(0);
return new SmartScopeAuthorizationInterceptor(rsaKey);
}

Step 4 โ€” Run the full stackโ€‹

Start all three services:

# Terminal 1 โ€” HAPI FHIR JPA server (your existing server)
# (assumes it runs on port 8080 with the proxy filter registered)

# Terminal 2 โ€” Auth server
cd ajfhir-smart-auth-server
mvn spring-boot:run -Dspring-boot.run.profiles=dev

# Terminal 3 โ€” SMART client
cd ajfhir-smart-client
mvn spring-boot:run -Dspring-boot.run.profiles=server

Step 5 โ€” Launch from the portalโ€‹

  1. Open http://localhost:9000/portal
  2. Log in as dr.smith / password
  3. Search for a patient from your HAPI server
  4. Click ๐Ÿš€ Launch App
  5. Browser redirects through the SMART handshake
  6. Lands on http://localhost:8080/ โ€” the client dashboard with real FHIR data

What the logs show during a successful launchโ€‹

Auth server logs:

INFO  LaunchContextService    - Launch token created โ€” patient=eXXX, client=ajfhir-smart-client
INFO SmartTokenCustomizer - SMART extras added to access token โ€” patient=eXXX, encounter=eYYY
DEBUG SmartTokenResponseConverter - SMART token response written โ€” patient=eXXX

Client logs:

INFO  SmartLaunchController   - EHR launch received โ€” iss=http://localhost:8080/fhir
INFO SmartDiscoveryService - Fetching SMART configuration from ISS: http://localhost:8080/fhir
INFO SmartDiscoveryService - SMART configuration cached
INFO SmartCallbackController - SMART EHR launch complete โ€” patient=eXXX, scope=launch openid...
INFO IdTokenValidator - id_token validated โ€” subject=dr.smith

Troubleshootingโ€‹

??? question "Discovery fails โ€” connection refused at :8080/fhir/.well-known" The proxy filter is not registered on the HAPI server, or HAPI is not running. Verify: curl http://localhost:8080/fhir/.well-known/smart-configuration Should return JSON, not a 404.

??? question "invalid_grant on token exchange" The authorization code expired (> 5 minutes) or the PKCE verifier doesn't match. Check that OAuth2AuthorizationService bean is present in AuthorizationServerConfig. Restart the auth server and try again immediately.

??? question "Patient is null after launch" The launch token was not resolved โ€” either it expired or was already used. Check the auth server logs for LaunchContextService warnings. Ensure the launch token is resolved within 5 minutes of being created.

??? question "403 Forbidden on FHIR requests" The scope interceptor rejected the request. Check:

  1. The granted scopes include the required resource scope (e.g. patient/Condition.rs)
  2. The registered app in DataInitializer includes all needed scopes
  3. The scope interceptor is correctly registered on the HAPI server

??? question "id_token validation warning in client logs"

    WARN IdTokenValidator - JWKS fetch failed โ€” skipping signature check

Non-fatal โ€” FHIR access works normally. The JWKS endpoint at http://localhost:9000/oauth2/jwks may be unreachable from the client. Check that the auth server is running and accessible.