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

Update OIDC TokenStateManager to return Uni #19807

Merged
merged 1 commit into from
Sep 1, 2021
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
34 changes: 21 additions & 13 deletions docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,9 @@ This interface allows you to dynamically create tenant configurations at runtime
package io.quarkus.it.keycloak;

import javax.enterprise.context.ApplicationScoped;
import java.util.function.Supplier;

import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TenantConfigResolver;
import io.vertx.ext.web.RoutingContext;
Expand All @@ -327,7 +329,7 @@ import io.vertx.ext.web.RoutingContext;
public class CustomTenantConfigResolver implements TenantConfigResolver {

@Override
public OidcTenantConfig resolve(RoutingContext context) {
public Uni<OidcTenantConfig> resolve(RoutingContext context, TenantConfigResolver.TenantConfigRequestContext requestContext) {
String path = context.request().path();
String[] parts = path.split("/");

Expand All @@ -337,24 +339,30 @@ public class CustomTenantConfigResolver implements TenantConfigResolver {
}

if ("tenant-c".equals(parts[1])) {
OidcTenantConfig config = new OidcTenantConfig();
// Do 'return requestContext.runBlocking(createTenantConfig());'
// if a blocking call is required to create a tenant config
return Uni.createFromItem(createTenantConfig());
}

// resolve to default tenant configuration
return null;
}

config.setTenantId("tenant-c");
config.setAuthServerUrl("http://localhost:8180/auth/realms/tenant-c");
config.setClientId("multi-tenant-client");
OidcTenantConfig.Credentials credentials = new OidcTenantConfig.Credentials();
private Supplier<OidcTenantConfig> createTenantConfig() {
final OidcTenantConfig config = new OidcTenantConfig();

credentials.setSecret("my-secret");
config.setTenantId("tenant-c");
config.setAuthServerUrl("http://localhost:8180/auth/realms/tenant-c");
config.setClientId("multi-tenant-client");
OidcTenantConfig.Credentials credentials = new OidcTenantConfig.Credentials();

config.setCredentials(credentials);
credentials.setSecret("my-secret");

// any other setting support by the quarkus-oidc extension
config.setCredentials(credentials);

return config;
}
// any other setting support by the quarkus-oidc extension

// resolve to default tenant configuration
return null;
return () -> config;
}
}
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,59 @@ In such cases, you can use `quarkus.oidc.token-state-manager.split-tokens=true`

Register your own `io.quarkus.oidc.TokenStateManager' implementation as an `@ApplicationScoped` CDI bean if you need to customize the way the tokens are associated with the session cookie. For example, you may want to keep the tokens in a database and have only a database pointer stored in a session cookie. Note though that it may present some challenges in making the tokens available across multiple microservices nodes.

Here is a simple example:

[source, java]
----
package io.quarkus.oidc.test;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import io.quarkus.arc.AlternativePriority;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenStateManager;
import io.quarkus.oidc.runtime.DefaultTokenStateManager;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
@AlternativePriority(1)
public class CustomTokenStateManager implements TokenStateManager {

@Inject
DefaultTokenStateManager tokenStateManager;

@Override
public Uni<String> createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig,
AuthorizationCodeTokens sessionContent, TokenStateManager.CreateTokenStateRequestContext requestContext) {
return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent, requestContext)
.map(t -> (t + "|custom"));
}

@Override
public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig,
String tokenState, TokenStateManager.GetTokensRequestContext requestContext) {
if (!tokenState.endsWith("|custom")) {
throw new IllegalStateException();
}
String defaultState = tokenState.substring(0, tokenState.length() - 7);
return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState, requestContext);
}

@Override
public Uni<Void> deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState,
TokenStateManager.DeleteTokensRequestContext requestContext) {
if (!tokenState.endsWith("|custom")) {
throw new IllegalStateException();
}
String defaultState = tokenState.substring(0, tokenState.length() - 7);
return tokenStateManager.deleteTokens(routingContext, oidcConfig, defaultState, requestContext);
}
}
----

== Listening to important authentication events

One can register `@ApplicationScoped` bean which will observe important OIDC authentication events. The listener will be updated when a user has logged in for the first time or re-authenticated, as well as when the session has been refreshed. More events may be reported in the future. For example:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package io.quarkus.oidc.test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.net.URI;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
Expand All @@ -14,6 +16,8 @@
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;

Expand Down Expand Up @@ -87,6 +91,12 @@ public void testAccessAndRefreshTokenInjectionDevMode() throws IOException, Inte

assertEquals("custom", page.getWebClient().getCookieManager().getCookie("q_session").getValue().split("\\|")[3]);

webClient.getOptions().setRedirectEnabled(false);
WebResponse webResponse = webClient
.loadWebResponse(new WebRequest(URI.create("http://localhost:8080/protected/logout").toURL()));
assertEquals(302, webResponse.getStatusCode());
assertNull(webClient.getCookieManager().getCookie("q_session"));

webClient.getCookieManager().clearCookies();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenStateManager;
import io.quarkus.oidc.runtime.DefaultTokenStateManager;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
Expand All @@ -18,25 +19,29 @@ public class CustomTokenStateManager implements TokenStateManager {
DefaultTokenStateManager tokenStateManager;

@Override
public String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig,
AuthorizationCodeTokens sessionContent) {
return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent) + "|custom";
public Uni<String> createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig,
AuthorizationCodeTokens sessionContent, TokenStateManager.CreateTokenStateRequestContext requestContext) {
return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent, requestContext)
.map(t -> (t + "|custom"));
}

@Override
public AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig,
String tokenState) {
public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig,
String tokenState, TokenStateManager.GetTokensRequestContext requestContext) {
if (!tokenState.endsWith("|custom")) {
throw new IllegalStateException();
}
String defaultState = tokenState.substring(0, tokenState.length() - 7);
return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState);
return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState, requestContext);
}

@Override
public void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) {
public Uni<Void> deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState,
TokenStateManager.DeleteTokensRequestContext requestContext) {
if (!tokenState.endsWith("|custom")) {
throw new IllegalStateException();
}
String defaultState = tokenState.substring(0, tokenState.length() - 7);
return tokenStateManager.deleteTokens(routingContext, oidcConfig, defaultState, requestContext);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ public String getName() {
public String getTenantName(@PathParam("id") String tenantId) {
return tenantId + ":" + idToken.getName();
}

@GET
@Path("logout")
public void logout() {
throw new RuntimeException("Logout must be handled by CodeAuthenticationMechanism");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ quarkus.oidc.client-id=client-dev
quarkus.oidc.credentials.client-secret.provider.name=vault-secret-provider
quarkus.oidc.credentials.client-secret.provider.key=secret-from-vault
quarkus.oidc.application-type=web-app
quarkus.oidc.logout.path=/protected/logout

quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL

Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ public interface TenantConfigResolver {
*
* @param context the routing context
* @return the tenant configuration. If {@code null}, indicates that the default configuration/tenant should be chosen
*
* @deprecated Use {@link #resolve(RoutingContext, TenantConfigRequestContext))} instead.
*/
@Deprecated
default OidcTenantConfig resolve(RoutingContext context) {
throw new UnsupportedOperationException("resolve not implemented");
throw new UnsupportedOperationException("resolve is not implemented");
}

/**
Expand All @@ -39,10 +41,10 @@ default Uni<OidcTenantConfig> resolve(RoutingContext routingContext, TenantConfi
}

/**
* A context object that can be used to run blocking tasks
* A context object that can be used to run blocking tasks.
* <p>
* Blocking config providers should used this context object to run blocking tasks, to prevent excessive and
* unnecessary delegation to thread pools
* Blocking {@code TenantConfigResolver} providers should use this context object to run blocking tasks, to prevent
* excessive and unnecessary delegation to thread pools.
*/
interface TenantConfigRequestContext {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.quarkus.oidc;

import java.util.function.Supplier;

import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

/**
Expand All @@ -13,9 +16,125 @@
*/
public interface TokenStateManager {

String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, AuthorizationCodeTokens tokens);
/**
* Convert the authorization code flow tokens into a token state.
*
* @param routingContext the request context
* @param oidcConfig the tenant configuration
* @param tokens the authorization code flow tokens
*
* @return the token state
*
* @deprecated Use
* {@link #createTokenState(RoutingContext, OidcTenantConfig, AuthorizationCodeTokens, CreateTokenStateRequestContext)}
*
*/
@Deprecated
default String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig,
AuthorizationCodeTokens tokens) {
throw new UnsupportedOperationException("createTokenState is not implemented");
}

/**
* Convert the authorization code flow tokens into a token state.
*
* @param routingContext the request context
* @param oidcConfig the tenant configuration
* @param tokens the authorization code flow tokens
* @param requestContext the request context
*
* @return the token state
*/
default Uni<String> createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig,
AuthorizationCodeTokens tokens, CreateTokenStateRequestContext requestContext) {
return Uni.createFrom().item(createTokenState(routingContext, oidcConfig, tokens));
}

/**
* Convert the token state into the authorization code flow tokens.
*
* @param routingContext the request context
* @param oidcConfig the tenant configuration
* @param tokens the token state
*
* @return the authorization code flow tokens
*
* @deprecated Use {@link #getTokens(RoutingContext, OidcTenantConfig, String, GetTokensRequestContext)} instead.
*/
@Deprecated
default AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) {
throw new UnsupportedOperationException("getTokens is not implemented");
}

/**
* Convert the token state into the authorization code flow tokens.
*
* @param routingContext the request context
* @param oidcConfig the tenant configuration
* @param tokens the token state
* @param requestContext the request context
*
* @return the authorization code flow tokens
*/
default Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig,
String tokenState, GetTokensRequestContext requestContext) {
return Uni.createFrom().item(getTokens(routingContext, oidcConfig, tokenState));
}

/**
* Delete the token state.
*
* @param routingContext the request context
* @param oidcConfig the tenant configuration
* @param tokens the token state
*
* @deprecated Use {@link #deleteTokens(RoutingContext, OidcTenantConfig, String, DeleteTokensRequestContext)} instead
*/
@Deprecated
default void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) {
throw new UnsupportedOperationException("deleteTokens is not implemented");
}

/**
* Delete the token state.
*
* @param routingContext the request context
* @param oidcConfig the tenant configuration
* @param tokens the token state
*/
default Uni<Void> deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState,
DeleteTokensRequestContext requestContext) {
deleteTokens(routingContext, oidcConfig, tokenState);
return Uni.createFrom().voidItem();
}

/**
* A context object that can be used to create a token state by running a blocking task.
* <p>
* Blocking providers should use this context to prevent excessive and unnecessary delegation to thread pools.
*/
interface CreateTokenStateRequestContext {

Uni<String> runBlocking(Supplier<String> function);
}

/**
* A context object that can be used to convert the token state to the tokens by running a blocking task.
* <p>
* Blocking providers should use this context to prevent excessive and unnecessary delegation to thread pools.
*/
interface GetTokensRequestContext {

Uni<AuthorizationCodeTokens> runBlocking(Supplier<AuthorizationCodeTokens> function);
}

AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState);
/**
* A context object that can be used to delete the token state by running a blocking task.
* <p>
* Blocking providers should use this context to prevent excessive and unnecessary delegation to thread pools.
*/
interface DeleteTokensRequestContext {

void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState);
Uni<Void> runBlocking(Supplier<Void> function);
}
}
Loading