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โ
- Open
http://localhost:9000/portal - Log in as
dr.smith/password - Search for a patient from your HAPI server
- Click ๐ Launch App
- Browser redirects through the SMART handshake
- 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:
- The granted scopes include the required resource scope (e.g.
patient/Condition.rs) - The registered app in
DataInitializerincludes all needed scopes - 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.