Skip to content

Commit

Permalink
Merge pull request quarkusio#36935 from sberyozkin/oidc_verification_…
Browse files Browse the repository at this point in the history
…key_resolver_provider

Support for dynamic OIDC JWK set resolution
  • Loading branch information
sberyozkin authored Nov 14, 2023
2 parents 78584cf + af65f8a commit 5066de2
Show file tree
Hide file tree
Showing 16 changed files with 668 additions and 233 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@

public class OidcRequestContextProperties {

public static String TOKEN = "token";
public static String TOKEN_CREDENTIAL = "token_credential";

private final Map<String, Object> properties;

public OidcRequestContextProperties(Map<String, Object> properties) {
this.properties = properties;
}

public Object getProperty(String name) {
public Object get(String name) {
return properties.get(name);
}

public String getString(String name) {
return (String) get(name);
}

public <T> T get(String name, Class<T> type) {
return type.cast(get(name));
}

}
5 changes: 5 additions & 0 deletions extensions/oidc/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,81 @@ public void setCleanUpTimerInterval(Duration cleanUpTimerInterval) {
}
}

/**
* Configuration for controlling how JsonWebKeySet containing verification keys should be acquired and managed.
*/
@ConfigItem
public Jwks jwks = new Jwks();

@ConfigGroup
public static class Jwks {

/**
* If JWK verification keys should be fetched at the moment a connection to the OIDC provider
* is initialized.
* <p/>
* Disabling this property will delay the key acquisition until the moment the current token
* has to be verified. Typically it can only be necessary if the token or other telated request properties
* provide an additional context which is required to resolve the keys correctly.
*/
@ConfigItem(defaultValue = "true")
public boolean resolveEarly = true;

/**
* Maximum number of JWK keys that can be cached.
* This property will be ignored if the {@link #resolveEarly} property is set to true.
*/
@ConfigItem(defaultValue = "10")
public int cacheSize = 10;

/**
* Number of minutes a JWK key can be cached for.
* This property will be ignored if the {@link #resolveEarly} property is set to true.
*/
@ConfigItem(defaultValue = "10M")
public Duration cacheTimeToLive = Duration.ofMinutes(10);

/**
* Cache timer interval.
* If this property is set then a timer will check and remove the stale entries periodically.
* This property will be ignored if the {@link #resolveEarly} property is set to true.
*/
@ConfigItem
public Optional<Duration> cleanUpTimerInterval = Optional.empty();

public int getCacheSize() {
return cacheSize;
}

public void setCacheSize(int cacheSize) {
this.cacheSize = cacheSize;
}

public Duration getCacheTimeToLive() {
return cacheTimeToLive;
}

public void setCacheTimeToLive(Duration cacheTimeToLive) {
this.cacheTimeToLive = cacheTimeToLive;
}

public Optional<Duration> getCleanUpTimerInterval() {
return cleanUpTimerInterval;
}

public void setCleanUpTimerInterval(Duration cleanUpTimerInterval) {
this.cleanUpTimerInterval = Optional.of(cleanUpTimerInterval);
}

public boolean isResolveEarly() {
return resolveEarly;
}

public void setResolveEarly(boolean resolveEarly) {
this.resolveEarly = resolveEarly;
}
}

@ConfigGroup
public static class Frontchannel {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,103 +1,33 @@
package io.quarkus.oidc.runtime;

import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.enterprise.event.Observes;

import io.quarkus.oidc.OidcTenantConfig;
import io.vertx.core.Handler;
import io.quarkus.runtime.ShutdownEvent;
import io.vertx.core.Vertx;

public class BackChannelLogoutTokenCache {
private OidcTenantConfig oidcConfig;

private Map<String, CacheEntry> cacheMap = new ConcurrentHashMap<>();;
private AtomicInteger size = new AtomicInteger();
final MemoryCache<TokenVerificationResult> cache;

public BackChannelLogoutTokenCache(OidcTenantConfig oidcTenantConfig, Vertx vertx) {
this.oidcConfig = oidcTenantConfig;
init(vertx);
}

private void init(Vertx vertx) {
cacheMap = new ConcurrentHashMap<>();
if (oidcConfig.logout.backchannel.cleanUpTimerInterval.isPresent()) {
vertx.setPeriodic(oidcConfig.logout.backchannel.cleanUpTimerInterval.get().toMillis(), new Handler<Long>() {
@Override
public void handle(Long event) {
// Remove all the entries which have expired
removeInvalidEntries();
}
});
}
cache = new MemoryCache<TokenVerificationResult>(vertx, oidcTenantConfig.logout.backchannel.cleanUpTimerInterval,
oidcTenantConfig.logout.backchannel.tokenCacheTimeToLive, oidcTenantConfig.logout.backchannel.tokenCacheSize);
}

public void addTokenVerification(String token, TokenVerificationResult result) {
if (!prepareSpaceForNewCacheEntry()) {
clearCache();
}
cacheMap.put(token, new CacheEntry(result));
cache.add(token, result);
}

public TokenVerificationResult removeTokenVerification(String token) {
CacheEntry entry = removeCacheEntry(token);
return entry == null ? null : entry.result;
return cache.remove(token);
}

public boolean containsTokenVerification(String token) {
return cacheMap.containsKey(token);
}

public void clearCache() {
cacheMap.clear();
size.set(0);
}

private void removeInvalidEntries() {
long now = now();
for (Iterator<Map.Entry<String, CacheEntry>> it = cacheMap.entrySet().iterator(); it.hasNext();) {
Map.Entry<String, CacheEntry> next = it.next();
if (isEntryExpired(next.getValue(), now)) {
it.remove();
size.decrementAndGet();
}
}
}

private boolean prepareSpaceForNewCacheEntry() {
int currentSize;
do {
currentSize = size.get();
if (currentSize == oidcConfig.logout.backchannel.tokenCacheSize) {
return false;
}
} while (!size.compareAndSet(currentSize, currentSize + 1));
return true;
return cache.containsKey(token);
}

private CacheEntry removeCacheEntry(String token) {
CacheEntry entry = cacheMap.remove(token);
if (entry != null) {
size.decrementAndGet();
}
return entry;
}

private boolean isEntryExpired(CacheEntry entry, long now) {
return entry.createdTime + oidcConfig.logout.backchannel.tokenCacheTimeToLive.toMillis() < now;
}

private static long now() {
return System.currentTimeMillis();
}

private static class CacheEntry {
volatile TokenVerificationResult result;
long createdTime = System.currentTimeMillis();

public CacheEntry(TokenVerificationResult result) {
this.result = result;
}
void shutdown(@Observes ShutdownEvent event, Vertx vertx) {
cache.stopTimer(vertx);
}
}
Loading

0 comments on commit 5066de2

Please sign in to comment.