Skip to main content

RSA Key Management

What the key doesโ€‹

Every JWT issued by this server โ€” access tokens and id_tokens โ€” is signed with RSA-2048 (RS256). Clients verify the signature using the public key from the JWKS endpoint (GET /oauth2/jwks).

RsaKeyConfigโ€‹

On every startup, RsaKeyConfig generates a fresh RSA-2048 key pair using Nimbus JOSE:

@Bean
public RSAKey rsaKey() {
RSAKey rsaKey = new RSAKeyGenerator(2048)
.keyID(UUID.randomUUID().toString())
.generate();
log.info("RSA-2048 signing key generated โ€” kid={}", rsaKey.getKeyID());
return rsaKey;
}

@Bean
public JWKSource<SecurityContext> jwkSource(RSAKey rsaKey) {
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}

Spring Authorization Server consumes the JWKSource bean automatically to sign all issued JWTs.

Development behaviour (ephemeral key)โ€‹

In development the ephemeral key is fine. Each restart generates a new key, but since all tokens expire quickly (default 3600 s) this is not a problem. SMART client instances that cache the JWKS will fetch a new key on the next request after the kid changes.

Production: load a persistent keyโ€‹

For production you must replace the ephemeral key with one loaded from a keystore or secrets manager. Two reasons:

  1. Token continuity โ€” tokens issued before a restart will fail signature verification after the restart
  2. Multi-instance โ€” multiple server instances must share the same key or every instance rejects tokens issued by the others

Option A โ€” Environment variable (JWK JSON)โ€‹

Store the JWK JSON string in a secret (AWS Secrets Manager, HashiCorp Vault, Kubernetes Secret):

application.yml
smart:
server:
rsa-key-jwk: ${RSA_SIGNING_KEY_JWK:}
@Bean
public RSAKey rsaKey(SmartServerProperties props) {
String jwkJson = props.rsaKeyJwk();
if (jwkJson != null && !jwkJson.isBlank()) {
return RSAKey.parse(jwkJson);
}
// Fail fast in non-dev profiles
throw new IllegalStateException(
"RSA_SIGNING_KEY_JWK must be set in production");
}

Generate a key once and store it:

# Generate a JWK using the Nimbus CLI or any JWK generator
# Then store in your secrets manager
cat > /tmp/generate-key.sh << 'EOF'
java -cp nimbus-jose-jwt.jar com.nimbusds.jose.jwk.gen.RSAKeyGenerator \
--size 2048 --use sig --alg RS256 > signing-key.json
EOF

Option B โ€” Java KeyStore (JKS/PKCS12)โ€‹

application.yml
smart:
server:
keystore-path: ${KEYSTORE_PATH:classpath:keystore.p12}
keystore-password: ${KEYSTORE_PASSWORD}
key-alias: smart-fhir-signing

JWKS endpointโ€‹

The public key is served at GET /oauth2/jwks:

{
"keys": [{
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"kid": "a1b2c3d4-...",
"alg": "RS256",
"n": "0vx7agoebGcQ..."
}]
}

The kid (key ID) matches the kid header in every JWT. Clients use it to select the right key when the JWKS contains multiple keys during rotation.

Key rotationโ€‹

To rotate the key without downtime:

  1. Add the new key to the JWKS (publish both old and new)
  2. Start issuing new tokens with the new key
  3. Wait for all tokens issued with the old key to expire
  4. Remove the old key from the JWKS

With ImmutableJWKSet this requires a restart. For zero-downtime rotation use a mutable JWKSet implementation that can be updated via an admin endpoint.


Next: Registered Clients โ†’