1010import java .security .cert .CertificateException ;
1111import java .security .cert .CertificateFactory ;
1212import java .security .cert .X509Certificate ;
13+ import java .util .Arrays ;
1314import java .util .Base64 ;
1415import java .util .List ;
16+ import java .util .concurrent .ConcurrentHashMap ;
17+ import java .util .concurrent .atomic .AtomicLong ;
18+ import java .util .concurrent .atomic .AtomicReference ;
1519
1620import org .bouncycastle .asn1 .x500 .RDN ;
1721import org .bouncycastle .asn1 .x500 .X500Name ;
@@ -40,6 +44,12 @@ public class JpaCertificateTruststoreProvider implements CertificateTruststorePr
4044 private final KeycloakSession session ;
4145 protected static final Logger logger = Logger .getLogger (JpaCertificateTruststoreProvider .class );
4246
47+ // Cache for certificates to fallback when database is unavailable
48+ private static final AtomicReference <CertificateRepresentation []> cachedAllCertificates = new AtomicReference <>(null );
49+ private static final ConcurrentHashMap <Boolean , AtomicReference <CertificateRepresentation []>> cachedFilteredCertificates = new ConcurrentHashMap <>();
50+ private static final AtomicLong cacheTimestamp = new AtomicLong (0 );
51+ private static final long CACHE_MAX_AGE_MS = 5 * 60 * 1000 ; // 5 minutes
52+
4353 public JpaCertificateTruststoreProvider (KeycloakSession session ) {
4454 this .session = session ;
4555 }
@@ -117,6 +127,45 @@ private boolean isHibernateFlushError(Throwable e) {
117127 return isHibernateFlushError (e .getCause ());
118128 }
119129
130+ /**
131+ * Updates the cache with the provided certificates.
132+ */
133+ private void updateCache (CertificateRepresentation [] certificates ) {
134+ if (certificates != null ) {
135+ cachedAllCertificates .set (certificates );
136+ cacheTimestamp .set (System .currentTimeMillis ());
137+ logger .debugf ("Updated certificate cache with %d certificates" , certificates .length );
138+ }
139+ }
140+
141+ /**
142+ * Updates the cache for filtered certificates (by isRootCA).
143+ */
144+ private void updateFilteredCache (boolean isRootCA , CertificateRepresentation [] certificates ) {
145+ if (certificates != null ) {
146+ cachedFilteredCertificates .computeIfAbsent (isRootCA , k -> new AtomicReference <>()).set (certificates );
147+ logger .debugf ("Updated filtered certificate cache (isRootCA=%s) with %d certificates" , isRootCA , certificates .length );
148+ }
149+ }
150+
151+ /**
152+ * Invalidates the cache. Should be called when certificates are added, updated, or removed.
153+ */
154+ private void invalidateCache () {
155+ cachedAllCertificates .set (null );
156+ cachedFilteredCertificates .clear ();
157+ cacheTimestamp .set (0 );
158+ logger .debug ("Invalidated certificate cache" );
159+ }
160+
161+ /**
162+ * Checks if the cache is still valid (not too old).
163+ */
164+ private boolean isCacheValid () {
165+ long age = System .currentTimeMillis () - cacheTimestamp .get ();
166+ return age < CACHE_MAX_AGE_MS && cachedAllCertificates .get () != null ;
167+ }
168+
120169 @ Override
121170 public void close () {
122171 // nothing to close
@@ -254,6 +303,9 @@ public CertificateRepresentation addCertificate(String alias, String certificate
254303 getEntityManager ().flush ();
255304 getEntityManager ().clear ();
256305
306+ // Invalidate cache since we added a new certificate
307+ invalidateCache ();
308+
257309 return toCertificateRepresentation (entity );
258310 }
259311
@@ -274,6 +326,9 @@ public CertificateRepresentation updateCertificate(String alias, String certific
274326 getEntityManager ().flush ();
275327 getEntityManager ().clear ();
276328
329+ // Invalidate cache since we updated a certificate
330+ invalidateCache ();
331+
277332 return toCertificateRepresentation (entity );
278333 }
279334
@@ -286,12 +341,15 @@ public void removeCertificate(String alias) {
286341 getEntityManager ().remove (entity );
287342 getEntityManager ().flush ();
288343 getEntityManager ().clear ();
344+
345+ // Invalidate cache since we removed a certificate
346+ invalidateCache ();
289347 }
290348
291349 @ Override
292350 public CertificateRepresentation [] getCertificates () {
293351 try {
294- return Retry .call ((iteration ) -> {
352+ CertificateRepresentation [] result = Retry .call ((iteration ) -> {
295353 try {
296354 EntityManager em = getEntityManager ();
297355 // Set flush mode to COMMIT to prevent automatic flushing before query execution
@@ -307,9 +365,12 @@ public CertificateRepresentation[] getCertificates() {
307365 .setHint (AvailableHints .HINT_CACHEABLE , false )
308366 .setHint (AvailableHints .HINT_CACHE_MODE , CacheMode .IGNORE )
309367 .getResultList ();
310- return list .stream ()
368+ CertificateRepresentation [] certificates = list .stream ()
311369 .map (this ::toCertificateRepresentation )
312370 .toArray (CertificateRepresentation []::new );
371+ // Update cache on successful retrieval
372+ updateCache (certificates );
373+ return certificates ;
313374 } finally {
314375 // Restore original flush mode
315376 em .setFlushMode (originalFlushMode );
@@ -325,16 +386,30 @@ public CertificateRepresentation[] getCertificates() {
325386 throw e ;
326387 }
327388 }, 3 , 50 ); // 3 attempts with 50ms delay
389+ return result ;
328390 } catch (Exception e ) {
329- logger .error ("Failed to get certificates after retries" , e );
330- throw new RuntimeException ("Failed to get certificates" , e );
391+ logger .error ("Failed to get certificates after retries, attempting cache fallback" , e );
392+ // Fallback to cache if available and valid
393+ CertificateRepresentation [] cached = cachedAllCertificates .get ();
394+ if (cached != null && isCacheValid ()) {
395+ logger .warnf ("Using cached certificates (count: %d) due to database failure. Cache age: %d ms" ,
396+ cached .length , System .currentTimeMillis () - cacheTimestamp .get ());
397+ return cached ;
398+ } else if (cached != null ) {
399+ logger .warnf ("Cache exists but is expired (age: %d ms). Using expired cache as last resort." ,
400+ System .currentTimeMillis () - cacheTimestamp .get ());
401+ return cached ;
402+ } else {
403+ logger .error ("No cached certificates available, cannot fallback" );
404+ throw new RuntimeException ("Failed to get certificates and no cache available" , e );
405+ }
331406 }
332407 }
333408
334409 @ Override
335410 public CertificateRepresentation [] getCertificates (boolean isRootCA ) {
336411 try {
337- return Retry .call ((iteration ) -> {
412+ CertificateRepresentation [] result = Retry .call ((iteration ) -> {
338413 try {
339414 EntityManager em = getEntityManager ();
340415 // Set flush mode to COMMIT to prevent automatic flushing before query execution
@@ -343,13 +418,16 @@ public CertificateRepresentation[] getCertificates(boolean isRootCA) {
343418 FlushModeType originalFlushMode = em .getFlushMode ();
344419 try {
345420 em .setFlushMode (FlushModeType .COMMIT );
346- return em
421+ CertificateRepresentation [] certificates = em
347422 .createNamedQuery ("findByIsRootCA" , TruststoreEntity .class )
348423 .setParameter ("isRootCA" , isRootCA )
349424 .getResultList ()
350425 .stream ()
351426 .map (this ::toCertificateRepresentation )
352427 .toArray (CertificateRepresentation []::new );
428+ // Update filtered cache on successful retrieval
429+ updateFilteredCache (isRootCA , certificates );
430+ return certificates ;
353431 } finally {
354432 // Restore original flush mode
355433 em .setFlushMode (originalFlushMode );
@@ -365,9 +443,52 @@ public CertificateRepresentation[] getCertificates(boolean isRootCA) {
365443 throw e ;
366444 }
367445 }, 3 , 50 ); // 3 attempts with 50ms delay
446+ return result ;
368447 } catch (Exception e ) {
369- logger .errorf ("Failed to get certificates (isRootCA=%s) after retries" , isRootCA , e );
370- throw new RuntimeException ("Failed to get certificates" , e );
448+ logger .errorf ("Failed to get certificates (isRootCA=%s) after retries, attempting cache fallback" , isRootCA , e );
449+ // First try filtered cache
450+ AtomicReference <CertificateRepresentation []> filteredCache = cachedFilteredCertificates .get (isRootCA );
451+ if (filteredCache != null && filteredCache .get () != null ) {
452+ logger .warnf ("Using cached filtered certificates (isRootCA=%s, count: %d) due to database failure" ,
453+ isRootCA , filteredCache .get ().length );
454+ return filteredCache .get ();
455+ }
456+ // Fallback to full cache and filter it
457+ CertificateRepresentation [] cached = cachedAllCertificates .get ();
458+ if (cached != null && isCacheValid ()) {
459+ logger .warnf ("Using cached certificates and filtering (isRootCA=%s) due to database failure. Cache age: %d ms" ,
460+ isRootCA , System .currentTimeMillis () - cacheTimestamp .get ());
461+ // Filter the cached certificates
462+ return Arrays .stream (cached )
463+ .filter (cert -> {
464+ try {
465+ X509Certificate x509Cert = toX509Certificate (cert .certificate ());
466+ return isSelfSigned (x509Cert ) == isRootCA ;
467+ } catch (Exception ex ) {
468+ logger .warnf ("Error filtering cached certificate %s: %s" , cert .alias (), ex .getMessage ());
469+ return false ;
470+ }
471+ })
472+ .toArray (CertificateRepresentation []::new );
473+ } else if (cached != null ) {
474+ logger .warnf ("Cache exists but is expired (age: %d ms). Using expired cache as last resort." ,
475+ System .currentTimeMillis () - cacheTimestamp .get ());
476+ // Filter the expired cached certificates
477+ return Arrays .stream (cached )
478+ .filter (cert -> {
479+ try {
480+ X509Certificate x509Cert = toX509Certificate (cert .certificate ());
481+ return isSelfSigned (x509Cert ) == isRootCA ;
482+ } catch (Exception ex ) {
483+ logger .warnf ("Error filtering cached certificate %s: %s" , cert .alias (), ex .getMessage ());
484+ return false ;
485+ }
486+ })
487+ .toArray (CertificateRepresentation []::new );
488+ } else {
489+ logger .errorf ("No cached certificates available for isRootCA=%s, cannot fallback" , isRootCA );
490+ throw new RuntimeException ("Failed to get certificates and no cache available" , e );
491+ }
371492 }
372493 }
373494
0 commit comments