Skip to content

Commit

Permalink
Support for TokenIntrospection and UserInfo cache
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Sep 16, 2021
1 parent 377c56c commit b94641b
Show file tree
Hide file tree
Showing 25 changed files with 812 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,13 @@ Please see link:security-openid-connect#token-verification-introspection for det

Note that in case of `web-app` applications only `IdToken` is verified by default since the access token is not used by default to access the current Quarkus `web-app` endpoint and instead meant to be propagated to the services expecting this access token, for example, to the OpenId Connect Provider's UserInfo endpoint, etc. However if you expect the access token to contain the roles required to access the current Quarkus endpoint (`quarkus.oidc.roles.source=accesstoken`) then it will also be verified.

[[token-introspection-userinfo-cache]]
== Token Introspection and UserInfo Cache

Code flow access tokens are not introspected unless they are expected to be the source of roles but will be used to get `UserInfo`. So there will be one oe two remote calls with the code flow access token, if the token introspection and/or `UserInfo` are required.

Please see link:security-openid-connect#token-introspection-userinfo-cache for more information about using a default token cache or registering a custom cache implementation.

[[session-management]]
== Session Management

Expand Down
40 changes: 40 additions & 0 deletions docs/src/main/asciidoc/security-openid-connect.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,46 @@ quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect

Note that `io.quarkus.oidc.TokenIntrospection` (a simple `javax.json.JsonObject` wrapper) object will be created and can be either injected or accessed as a SecurityIdentity `introspection` attribute if either JWT or opaque token has been successfully introspected.

[[token-introspection-userinfo-cache]]
== Token Introspection and UserInfo Cache

All opaque and sometimes JWT Bearer access tokens have to be remotely introspected. If `UserInfo` is also required then the same access token will be used to do a remote call to OpenId Connect Provider again. So, if `UserInfo` is required and the current access token is opaque then for every such token there will be 2 remote calls done - one to introspect it and one to get UserInfo with it, and if the token is JWT then usually only a single remote call will be needed - to get UserInfo with it.

The cost of making up to 2 remote calls per every incoming bearer or code flow access token can sometimes be problematic.

If it is the case in your production then it can be recommended that the token introspection and `UserInfo` data are cached for a short period of time, for example, for 3 or 5 minutes.

`quarkus-oidc` provides `quarkus.oidc.TokenIntrospectionCache` and `quarkus.oidc.UserInfoCache` interfaces which can be used to implement `@ApplicationScoped` cache implementation which can be used to store and retrieve `quarkus.oidc.TokenIntrospection` and/or `quarkus.oidc.UserInfo` objects, for example:

[source, java]
----
@ApplicationScoped
@AlternativePriority(1)
public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache, UserInfoCache {
...
}
----

Each OIDC tenant can either permit or deny storing its `quarkus.oidc.TokenIntrospection` and/or `quarkus.oidc.UserInfo` data with boolean `quarkus.oidc."tenant".allow-token-introspection-cache` and `quarkus.oidc."tenant".allow-user-info-cache` properties.

Additionally, `quarkus-oidc` provides a simple default memory based token cache which implements both `quarkus.oidc.TokenIntrospectionCache` and `quarkus.oidc.UserInfoCache` interfaces.

It can be activated and configured as follows:

[source, properties]
----
# 'max-size' is 0 by default so the cache can be activated by setting 'max-size' to a positive value.
quarkus.oidc.token-cache.max-size=1000
# 'time-to-live' specifies how long a cache entry can be valid for and will be used by a clean up timer.
quarkus.oidc.token-cache.time-to-live=3M
# 'clean-up-timer-interval' is not set by default so the clean up timer can be activated by setting 'clean-up-timer-interval'.
quarkus.oidc.token-cache.clean-up-timer-interval=1M
----

The default cache uses a token as a key and each entry can have `TokenIntrospection` and/or `UserInfo`. It will only keep up to a `max-size` number of entries. If the cache is full when a new entry is to be added then an attempt will be made to find a space to remove a single expired entry. Additionally, the clean up timer, if activated, will periodically check for the expired entries and remove them.

Please experiment with the default cache implementation or register a custom one.

[[single-page-applications]]
== Single Page Applications

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.runtime.DefaultTenantConfigResolver;
import io.quarkus.oidc.runtime.DefaultTokenIntrospectionUserInfoCache;
import io.quarkus.oidc.runtime.DefaultTokenStateManager;
import io.quarkus.oidc.runtime.OidcAuthenticationMechanism;
import io.quarkus.oidc.runtime.OidcConfig;
Expand Down Expand Up @@ -85,7 +86,8 @@ public void additionalBeans(BuildProducer<AdditionalBeanBuildItem> additionalBea
.addBeanClass(OidcConfigurationMetadataProducer.class)
.addBeanClass(OidcIdentityProvider.class)
.addBeanClass(DefaultTenantConfigResolver.class)
.addBeanClass(DefaultTokenStateManager.class);
.addBeanClass(DefaultTokenStateManager.class)
.addBeanClass(DefaultTokenIntrospectionUserInfoCache.class);
additionalBeans.produce(builder.build());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,22 @@ public class OidcTenantConfig extends OidcCommonConfig {
@ConfigItem
public TokenStateManager tokenStateManager = new TokenStateManager();

/**
* Allow caching the token introspection data.
* Note enabling this property does not enable the cache itself but only permits to cache the token introspection
* for a given tenant. See {@link TokenCache.TokenIntrospectionCache} how to enable the default cache.
*/
@ConfigItem(defaultValue = "true")
public boolean allowTokenIntrospectionCache = true;

/**
* Allow caching the user info data.
* Note enabling this property does not enable the cache itself but only permits to cache the user info data
* for a given tenant. See {@link TokenCache.TokenIntrospectionCache} how to enable the default cache.
*/
@ConfigItem(defaultValue = "true")
public boolean allowUserInfoCache = true;

@ConfigGroup
public static class Logout {

Expand Down Expand Up @@ -870,4 +886,20 @@ public ApplicationType getApplicationType() {
public void setApplicationType(ApplicationType type) {
this.applicationType = type;
}

public boolean isAllowTokenIntrospectionCache() {
return allowTokenIntrospectionCache;
}

public void setAllowTokenIntrospectionCache(boolean allowTokenIntrospectionCache) {
this.allowTokenIntrospectionCache = allowTokenIntrospectionCache;
}

public boolean isAllowUserInfoCache() {
return allowUserInfoCache;
}

public void setAllowUserInfoCache(boolean allowUserInfoCache) {
this.allowUserInfoCache = allowUserInfoCache;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ public TokenIntrospection(String introspectionJson) {
public TokenIntrospection(JsonObject json) {
super(json);
}

public String getIntrospectionString() {
return getNonNullJsonString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.quarkus.oidc;

import java.util.function.Supplier;

import io.smallrye.mutiny.Uni;

/**
* Token introspection cache.
*/
public interface TokenIntrospectionCache {

/**
* Add a new {@link TokenIntrospection} result to the cache.
*
* @param token the token which has been introspected
* @param introspection the token introspection result
* @param oidcConfig the tenant configuration
* @param requestContext the request context which can be used to run the blocking tasks
*/
Uni<Void> addIntrospection(String token, TokenIntrospection introspection, OidcTenantConfig oidcConfig,
AddIntrospectionRequestContext requestContext);

/**
* Get the cached {@link TokenIntrospection} result.
*
* @param token the token which has to be introspected
* @param oidcConfig the tenant configuration
* @param requestContext the request context which can be used to run the blocking tasks
*/
Uni<TokenIntrospection> getIntrospection(String token, OidcTenantConfig oidcConfig,
GetIntrospectionRequestContext requestContext);

/**
* A context object that can be used to add a new token introspection result by running a blocking task.
* <p>
* Blocking {@code TokenIntrospectionCache} providers should use this context object to run blocking tasks, to prevent
* excessive and unnecessary delegation to thread pools.
*/
interface AddIntrospectionRequestContext {

Uni<Void> runBlocking(Supplier<Void> function);

}

/**
* A context object that can be used to get the existing token introspection result by running a blocking task.
* <p>
* Blocking {@code TokenIntrospectionCache} providers should use this context object to run blocking tasks, to prevent
* excessive and unnecessary delegation to thread pools.
*/
interface GetIntrospectionRequestContext {

Uni<TokenIntrospection> runBlocking(Supplier<TokenIntrospection> function);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ public UserInfo(String userInfoJson) {
public UserInfo(JsonObject json) {
super(json);
}

public String getUserInfoString() {
return getNonNullJsonString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.quarkus.oidc;

import java.util.function.Supplier;

import io.smallrye.mutiny.Uni;

/**
* UserInfo cache.
*/
public interface UserInfoCache {

/**
* Add a new {@link UserInfo} to the cache.
*
* @param token the token which was used to get {@link UserInfo}
* @param userInfo {@link UserInfo}
* @param oidcConfig the tenant configuration
* @param requestContext the request context which can be used to run the blocking tasks
*/
Uni<Void> addUserInfo(String token, UserInfo userInfo, OidcTenantConfig oidcConfig,
AddUserInfoRequestContext requestContext);

/**
* Get the cached {@link UserInfo}.
*
* @param token the token which will be used to get new {@link UserInfo} if no {@link UserInfo} is cached.
* Effectively this token is a cache key which has to be stored when
* {@link #addUserInfo(String, UserInfo, OidcTenantConfig, AddUserInfoRequestContext)}
* is called.
* @param oidcConfig the tenant configuration
* @param requestContext the request context which can be used to run the blocking tasks
*/
Uni<UserInfo> getUserInfo(String token, OidcTenantConfig oidcConfig,
GetUserInfoRequestContext requestContext);

/**
* A context object that can be used to add a new @{link UserInfo} by running a blocking task.
* <p>
* Blocking {@code UserInfoCache} providers should use this context object to run blocking tasks, to prevent
* excessive and unnecessary delegation to thread pools.
*/
interface AddUserInfoRequestContext {

Uni<Void> runBlocking(Supplier<Void> function);

}

/**
* A context object that can be used to get the existing token introspection result by running a blocking task.
* <p>
* Blocking {@code TokenIntrospectionCache} providers should use this context object to run blocking tasks, to prevent
* excessive and unnecessary delegation to thread pools.
*/
interface GetUserInfoRequestContext {

Uni<UserInfo> runBlocking(Supplier<UserInfo> function);

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.quarkus.oidc.runtime;

import java.util.function.Consumer;
import java.util.function.Supplier;

import io.quarkus.runtime.BlockingOperationControl;
import io.quarkus.runtime.ExecutorRecorder;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.UniEmitter;

public abstract class AbstractBlockingTaskRunner<T> {
public Uni<T> runBlocking(Supplier<T> function) {
return Uni.createFrom().deferred(new Supplier<Uni<? extends T>>() {
@Override
public Uni<T> get() {
if (BlockingOperationControl.isBlockingAllowed()) {
try {
return Uni.createFrom().item(function.get());
} catch (Throwable t) {
return Uni.createFrom().failure(t);
}
} else {
return Uni.createFrom().emitter(new Consumer<UniEmitter<? super T>>() {
@Override
public void accept(UniEmitter<? super T> uniEmitter) {
ExecutorRecorder.getCurrent().execute(new Runnable() {
@Override
public void run() {
try {
uniEmitter.complete(function.get());
} catch (Throwable t) {
uniEmitter.fail(t);
}
}
});
}
});
}
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
import javax.json.JsonValue;

public class AbstractJsonObjectResponse {
private String jsonString;
private JsonObject json;

public AbstractJsonObjectResponse() {
}

public AbstractJsonObjectResponse(String introspectionJson) {
this(toJsonObject(introspectionJson));
public AbstractJsonObjectResponse(String jsonString) {
this(toJsonObject(jsonString));
this.jsonString = jsonString;
}

public AbstractJsonObjectResponse(JsonObject json) {
Expand Down Expand Up @@ -47,6 +49,10 @@ public JsonObject getObject(String name) {
return json.getJsonObject(name);
}

public JsonObject getJsonObject() {
return json;
}

public Object get(String name) {
return json.get(name);
}
Expand All @@ -63,6 +69,10 @@ public Set<Map.Entry<String, JsonValue>> getAllProperties() {
return Collections.unmodifiableSet(json.entrySet());
}

protected String getNonNullJsonString() {
return jsonString == null ? json.toString() : jsonString;
}

private static JsonObject toJsonObject(String userInfoJson) {
try (JsonReader jsonReader = Json.createReader(new StringReader(userInfoJson))) {
return jsonReader.readObject();
Expand Down
Loading

0 comments on commit b94641b

Please sign in to comment.