Token Response Converter
The problemโ
Spring Authorization Server's default token response returns only standard OAuth2 fields:
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "launch openid patient/Patient.rs",
"refresh_token": "eyJ..."
}
SMART App Launch v2.2 requires patient, encounter, and need_patient_banner as top-level fields in this same JSON object. The SMART client's SmartTokenResponse reads them with Jackson's @JsonProperty("patient") โ if they are only inside the JWT payload (not in the response body), they will be null.
The solution: SmartTokenResponseConverterโ
This class implements Spring Security's AuthenticationSuccessHandler and is registered as the token endpoint success handler in AuthorizationServerConfig:
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.tokenEndpoint(token -> token
.accessTokenResponseHandler(tokenResponseConverter)
);
On every successful token exchange it:
- Builds the standard OAuth2 response fields (
access_token,token_type,expires_in,scope,refresh_token) - Decodes the JWT and extracts the SMART extras that
SmartTokenCustomizeradded to the claims - Promotes them to top-level fields in the JSON response body
- Writes the complete JSON response
Full token responseโ
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "launch openid fhirUser patient/Patient.rs patient/Condition.rs",
"refresh_token": "eyJhbGciOiJSUzI1NiJ9...",
"patient": "ePatient-8675309",
"encounter": "eEncounter-001",
"need_patient_banner": true,
"id_token": "eyJhbGciOiJSUzI1NiJ9..."
}
Standalone launchโ
For standalone launch (no launch token in the authorize request), SmartTokenCustomizer does not add patient or encounter to the JWT. SmartTokenResponseConverter handles this gracefully โ extractSmartExtras() tries to read the claims and skips them if absent. The response only contains the fields that were set.
The extraction logicโ
private void extractSmartExtras(String jwtValue, Map<String, Object> target) {
SignedJWT jwt = SignedJWT.parse(jwtValue);
JWTClaimsSet claims = jwt.getJWTClaimsSet();
String patient = claims.getStringClaim("patient");
if (patient != null) target.put("patient", patient);
String encounter = claims.getStringClaim("encounter");
if (encounter != null) target.put("encounter", encounter);
Object banner = claims.getClaim("need_patient_banner");
if (banner != null) target.put("need_patient_banner", banner);
}
This decodes the JWT without verifying the signature โ it is safe here because this converter is called only after Spring Authorization Server has already issued and signed the token. We are reading a token we just created.
Next: Discovery Endpoint โ