Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for dynamic OIDC JWK set resolution #36935

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading