0
# Key Rotation
1
2
Public key location and token verification utilities supporting key rotation for secure token validation. This module provides flexible key management strategies and comprehensive token verification capabilities.
3
4
## Capabilities
5
6
### PublicKeyLocator
7
8
Interface for locating public keys used in token verification with support for key rotation.
9
10
```java { .api }
11
/**
12
* Interface for locating public keys used in token verification with support for key rotation
13
*/
14
public interface PublicKeyLocator {
15
/**
16
* Get public key for token verification
17
* @param kid Key identifier from token header, may be null
18
* @param deployment Keycloak deployment configuration
19
* @return PublicKey for verification or null if not found
20
*/
21
PublicKey getPublicKey(String kid, KeycloakDeployment deployment);
22
23
/**
24
* Reset/clear cached keys, forcing fresh retrieval
25
* @param deployment Keycloak deployment configuration
26
*/
27
void reset(KeycloakDeployment deployment);
28
}
29
```
30
31
### HardcodedPublicKeyLocator
32
33
Public key locator implementation using a single hardcoded public key.
34
35
```java { .api }
36
/**
37
* Public key locator implementation using a single hardcoded public key
38
*/
39
public class HardcodedPublicKeyLocator implements PublicKeyLocator {
40
/**
41
* Constructor with hardcoded public key
42
* @param publicKey Public key to use for all token verification
43
*/
44
public HardcodedPublicKeyLocator(PublicKey publicKey);
45
46
/**
47
* Get the hardcoded public key
48
* @param kid Key identifier (ignored)
49
* @param deployment Keycloak deployment (ignored)
50
* @return The configured public key
51
*/
52
public PublicKey getPublicKey(String kid, KeycloakDeployment deployment);
53
54
/**
55
* Reset operation (no-op for hardcoded key)
56
* @param deployment Keycloak deployment (ignored)
57
*/
58
public void reset(KeycloakDeployment deployment);
59
}
60
```
61
62
**Usage Examples:**
63
64
```java
65
// Create hardcoded key locator from certificate
66
Certificate cert = loadCertificateFromFile("/path/to/realm-certificate.pem");
67
PublicKey publicKey = cert.getPublicKey();
68
PublicKeyLocator keyLocator = new HardcodedPublicKeyLocator(publicKey);
69
70
// Configure in deployment
71
KeycloakDeployment deployment = new KeycloakDeployment();
72
deployment.setPublicKeyLocator(keyLocator);
73
74
// Use for token verification
75
AccessToken token = AdapterTokenVerifier.verifyToken(tokenString, deployment);
76
```
77
78
### JWKPublicKeyLocator
79
80
Public key locator that downloads keys from Keycloak's JSON Web Key Set (JWKS) endpoint with caching and rotation support.
81
82
```java { .api }
83
/**
84
* Public key locator that downloads keys from Keycloak's JWKS endpoint with caching and rotation support
85
*/
86
public class JWKPublicKeyLocator implements PublicKeyLocator {
87
/**
88
* Default constructor
89
*/
90
public JWKPublicKeyLocator();
91
92
/**
93
* Get public key from JWKS endpoint, with caching
94
* @param kid Key identifier from token header
95
* @param deployment Keycloak deployment configuration
96
* @return PublicKey for verification or null if not found
97
*/
98
public PublicKey getPublicKey(String kid, KeycloakDeployment deployment);
99
100
/**
101
* Reset cached keys, forcing fresh download from JWKS endpoint
102
* @param deployment Keycloak deployment configuration
103
*/
104
public void reset(KeycloakDeployment deployment);
105
}
106
```
107
108
**Usage Examples:**
109
110
```java
111
// Create JWKS-based key locator (recommended for production)
112
PublicKeyLocator keyLocator = new JWKPublicKeyLocator();
113
114
// Configure deployment with JWKS support
115
KeycloakDeployment deployment = new KeycloakDeployment();
116
deployment.setPublicKeyLocator(keyLocator);
117
deployment.setMinTimeBetweenJwksRequests(10); // Minimum 10 seconds between JWKS requests
118
deployment.setPublicKeyCacheTtl(3600); // Cache keys for 1 hour
119
120
// The locator will automatically:
121
// 1. Download JWKS from deployment.getJwksUrl()
122
// 2. Cache keys with TTL
123
// 3. Handle key rotation automatically
124
// 4. Respect rate limiting for JWKS requests
125
126
// Token verification with automatic key resolution
127
try {
128
AccessToken token = AdapterTokenVerifier.verifyToken(tokenString, deployment);
129
// Token verified successfully with appropriate key
130
} catch (VerificationException e) {
131
// Verification failed - could be due to key rotation
132
// Reset keys and retry once
133
keyLocator.reset(deployment);
134
try {
135
AccessToken token = AdapterTokenVerifier.verifyToken(tokenString, deployment);
136
} catch (VerificationException retryEx) {
137
// Verification failed after key refresh
138
logger.warn("Token verification failed: {}", retryEx.getMessage());
139
}
140
}
141
```
142
143
### AdapterTokenVerifier
144
145
Utility class for token verification with integrated public key resolution.
146
147
```java { .api }
148
/**
149
* Utility class for token verification with integrated public key resolution
150
*/
151
public class AdapterTokenVerifier {
152
/**
153
* Container for verified tokens
154
*/
155
public static class VerifiedTokens {
156
// Implementation details - contains verified access and ID tokens
157
}
158
159
/**
160
* Verify access token using deployment configuration
161
* @param tokenString Raw JWT token string
162
* @param deployment Keycloak deployment with key locator
163
* @return Verified AccessToken instance
164
* @throws VerificationException If token verification fails
165
*/
166
public static AccessToken verifyToken(String tokenString, KeycloakDeployment deployment)
167
throws VerificationException;
168
169
/**
170
* Verify both access token and ID token
171
* @param accessTokenString Raw access token JWT string
172
* @param idTokenString Raw ID token JWT string
173
* @param deployment Keycloak deployment with key locator
174
* @return VerifiedTokens container with both verified tokens
175
* @throws VerificationException If either token verification fails
176
*/
177
public static VerifiedTokens verifyTokens(
178
String accessTokenString,
179
String idTokenString,
180
KeycloakDeployment deployment
181
) throws VerificationException;
182
183
/**
184
* Create token verifier with custom configuration
185
* @param tokenString Raw JWT token string
186
* @param deployment Keycloak deployment configuration
187
* @param withDefaultChecks Whether to include default verification checks
188
* @param tokenClass Class of token to verify (AccessToken, IDToken, etc.)
189
* @return Configured TokenVerifier instance
190
* @throws VerificationException If verifier creation fails
191
*/
192
public static <T extends JsonWebToken> TokenVerifier<T> createVerifier(
193
String tokenString,
194
KeycloakDeployment deployment,
195
boolean withDefaultChecks,
196
Class<T> tokenClass
197
) throws VerificationException;
198
}
199
```
200
201
**Usage Examples:**
202
203
```java
204
// Basic token verification
205
try {
206
AccessToken accessToken = AdapterTokenVerifier.verifyToken(tokenString, deployment);
207
208
// Token is valid, extract information
209
String username = accessToken.getPreferredUsername();
210
String email = accessToken.getEmail();
211
Set<String> roles = accessToken.getRealmAccess().getRoles();
212
long expiration = accessToken.getExpiration();
213
214
// Check if token is still valid (not expired)
215
if (expiration < System.currentTimeMillis() / 1000) {
216
logger.warn("Token has expired");
217
}
218
219
} catch (VerificationException e) {
220
logger.warn("Token verification failed: {}", e.getMessage());
221
// Handle invalid token (return 401, redirect to login, etc.)
222
}
223
224
// Verify both access and ID tokens
225
try {
226
AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(
227
accessTokenString,
228
idTokenString,
229
deployment
230
);
231
232
// Both tokens verified successfully
233
// Access token information and ID token information are available
234
235
} catch (VerificationException e) {
236
logger.warn("Token verification failed: {}", e.getMessage());
237
}
238
239
// Custom token verification with specific checks
240
try {
241
TokenVerifier<AccessToken> verifier = AdapterTokenVerifier.createVerifier(
242
tokenString,
243
deployment,
244
true, // Include default checks (signature, expiration, audience, etc.)
245
AccessToken.class
246
);
247
248
// Add custom verification checks
249
verifier.check(token -> {
250
// Custom validation logic
251
if (!token.getIssuer().equals(expectedIssuer)) {
252
throw new VerificationException("Invalid issuer");
253
}
254
255
// Check custom claims
256
Object customClaim = token.getOtherClaims().get("custom_claim");
257
if (customClaim == null) {
258
throw new VerificationException("Missing required custom claim");
259
}
260
});
261
262
AccessToken token = verifier.verify();
263
264
} catch (VerificationException e) {
265
logger.warn("Custom token verification failed: {}", e.getMessage());
266
}
267
```
268
269
## Key Management Patterns
270
271
### Custom Public Key Locator
272
273
```java
274
// Database-backed public key locator with caching
275
public class DatabasePublicKeyLocator implements PublicKeyLocator {
276
private final PublicKeyRepository keyRepository;
277
private final Map<String, PublicKey> keyCache = new ConcurrentHashMap<>();
278
private final Map<String, Long> cacheTimestamps = new ConcurrentHashMap<>();
279
private final long cacheTtlMs = TimeUnit.HOURS.toMillis(1);
280
281
public DatabasePublicKeyLocator(PublicKeyRepository keyRepository) {
282
this.keyRepository = keyRepository;
283
}
284
285
@Override
286
public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) {
287
String realm = deployment.getRealm();
288
String cacheKey = realm + ":" + (kid != null ? kid : "default");
289
290
// Check cache first
291
PublicKey cachedKey = keyCache.get(cacheKey);
292
Long cacheTime = cacheTimestamps.get(cacheKey);
293
294
if (cachedKey != null && cacheTime != null &&
295
(System.currentTimeMillis() - cacheTime) < cacheTtlMs) {
296
return cachedKey;
297
}
298
299
// Load from database
300
PublicKeyEntity keyEntity = keyRepository.findByRealmAndKid(realm, kid);
301
if (keyEntity != null) {
302
try {
303
PublicKey publicKey = parsePublicKey(keyEntity.getKeyData());
304
keyCache.put(cacheKey, publicKey);
305
cacheTimestamps.put(cacheKey, System.currentTimeMillis());
306
return publicKey;
307
} catch (Exception e) {
308
logger.warn("Failed to parse public key from database", e);
309
}
310
}
311
312
return null;
313
}
314
315
@Override
316
public void reset(KeycloakDeployment deployment) {
317
String realm = deployment.getRealm();
318
// Remove cached keys for this realm
319
keyCache.entrySet().removeIf(entry -> entry.getKey().startsWith(realm + ":"));
320
cacheTimestamps.entrySet().removeIf(entry -> entry.getKey().startsWith(realm + ":"));
321
}
322
323
private PublicKey parsePublicKey(String keyData) throws Exception {
324
// Parse PEM or other format
325
// Implementation depends on key format
326
}
327
}
328
```
329
330
### Composite Key Locator
331
332
```java
333
// Composite key locator with fallback strategy
334
public class CompositePublicKeyLocator implements PublicKeyLocator {
335
private final List<PublicKeyLocator> locators;
336
337
public CompositePublicKeyLocator(PublicKeyLocator... locators) {
338
this.locators = Arrays.asList(locators);
339
}
340
341
@Override
342
public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) {
343
for (PublicKeyLocator locator : locators) {
344
try {
345
PublicKey key = locator.getPublicKey(kid, deployment);
346
if (key != null) {
347
return key;
348
}
349
} catch (Exception e) {
350
logger.debug("Key locator {} failed: {}", locator.getClass().getSimpleName(), e.getMessage());
351
}
352
}
353
return null;
354
}
355
356
@Override
357
public void reset(KeycloakDeployment deployment) {
358
for (PublicKeyLocator locator : locators) {
359
try {
360
locator.reset(deployment);
361
} catch (Exception e) {
362
logger.warn("Failed to reset key locator {}", locator.getClass().getSimpleName(), e);
363
}
364
}
365
}
366
}
367
368
// Usage: JWKS first, then hardcoded fallback
369
PublicKeyLocator composite = new CompositePublicKeyLocator(
370
new JWKPublicKeyLocator(),
371
new HardcodedPublicKeyLocator(fallbackPublicKey)
372
);
373
```
374
375
### Key Rotation Monitoring
376
377
```java
378
// Monitor key rotation events
379
public class KeyRotationMonitor {
380
private final KeycloakDeployment deployment;
381
private final PublicKeyLocator keyLocator;
382
private volatile String currentKeyId;
383
384
public KeyRotationMonitor(KeycloakDeployment deployment) {
385
this.deployment = deployment;
386
this.keyLocator = deployment.getPublicKeyLocator();
387
}
388
389
public boolean handleTokenVerificationFailure(String tokenString, VerificationException e) {
390
try {
391
// Parse token header to get key ID
392
String kid = extractKeyIdFromToken(tokenString);
393
394
if (kid != null && !kid.equals(currentKeyId)) {
395
logger.info("Detected potential key rotation. Current: {}, Token: {}", currentKeyId, kid);
396
397
// Reset key cache and retry
398
keyLocator.reset(deployment);
399
400
// Attempt verification with fresh keys
401
AccessToken token = AdapterTokenVerifier.verifyToken(tokenString, deployment);
402
currentKeyId = kid;
403
404
logger.info("Token verification successful after key rotation");
405
return true;
406
}
407
} catch (Exception retryException) {
408
logger.warn("Token verification failed even after key rotation handling", retryException);
409
}
410
411
return false;
412
}
413
414
private String extractKeyIdFromToken(String tokenString) {
415
try {
416
String[] parts = tokenString.split("\\.");
417
if (parts.length >= 2) {
418
String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]));
419
// Parse JSON to extract "kid" field
420
// Implementation depends on JSON library
421
}
422
} catch (Exception e) {
423
logger.debug("Failed to extract key ID from token", e);
424
}
425
return null;
426
}
427
}
428
```
429
430
### Configuration Examples
431
432
```java
433
// Production configuration with JWKS and caching
434
KeycloakDeployment deployment = new KeycloakDeployment();
435
deployment.setRealm("production-realm");
436
deployment.setAuthServerBaseUrl("https://keycloak.company.com/auth");
437
deployment.setPublicKeyLocator(new JWKPublicKeyLocator());
438
deployment.setMinTimeBetweenJwksRequests(10); // Rate limit JWKS requests
439
deployment.setPublicKeyCacheTtl(3600); // Cache for 1 hour
440
441
// Development configuration with hardcoded key
442
PublicKey devKey = loadDevelopmentPublicKey();
443
KeycloakDeployment devDeployment = new KeycloakDeployment();
444
devDeployment.setPublicKeyLocator(new HardcodedPublicKeyLocator(devKey));
445
446
// High-availability configuration with multiple strategies
447
PublicKeyLocator haLocator = new CompositePublicKeyLocator(
448
new JWKPublicKeyLocator(), // Primary: JWKS
449
new DatabasePublicKeyLocator(keyRepository), // Fallback: Database
450
new HardcodedPublicKeyLocator(emergencyKey) // Emergency: Hardcoded
451
);
452
deployment.setPublicKeyLocator(haLocator);
453
```