Skip to content

Commit

Permalink
Merge pull request quarkusio#44546 from michalvavrik/feature/quarkus-…
Browse files Browse the repository at this point in the history
…oidc-redis-token-state-manager

Add OIDC Redis Token State Manager extension
  • Loading branch information
sberyozkin authored Nov 18, 2024
2 parents 05b92aa + c3440a4 commit 2b2dbe4
Show file tree
Hide file tree
Showing 20 changed files with 680 additions and 1 deletion.
10 changes: 10 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,16 @@
<artifactId>quarkus-oidc-db-token-state-manager-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-redis-token-state-manager</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-redis-token-state-manager-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-oidc-token-propagation</artifactId>
Expand Down
13 changes: 13 additions & 0 deletions devtools/bom-descriptor-json/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1838,6 +1838,19 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-redis-token-state-manager</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-openshift</artifactId>
Expand Down
13 changes: 13 additions & 0 deletions docs/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,19 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-redis-token-state-manager-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-openshift-deployment</artifactId>
Expand Down
32 changes: 32 additions & 0 deletions docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,38 @@ For more information, refer to the xref:hibernate-orm.adoc[Hibernate ORM] guide.
<2> You can choose a column length depending on the length of your tokens.
endif::no-quarkus-oidc-db-token-state-manager[]

[[redis-token-state-manager]]
==== Redis TokenStateManager

Another approach for a stateful token storage strategy is a custom `TokenStateManager` provided by Quarkus to have your application store tokens in a Redis cache.
If you decided to use the OIDC Redis Token State Manager, you must add the following dependency:

[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-redis-token-state-manager</artifactId>
</dependency>
----

[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"]
.build.gradle
----
implementation("io.quarkus:quarkus-oidc-redis-token-state-manager")
----

Quarkus stores tokens in the default Redis client.
If you prefer to use different Redis client, you can configure it like in the example below:

[source, properties]
----
quarkus.oidc.redis-token-state-manager.redis-client-name=my-redis-client <1>
----
<1> The `my-redis-client` name must correspond to the Redis client config key specified with `quarkus.redis.my-redis-client.*` configuration properties.

Please refer to the xref:redis-reference.adoc[Quarkus Redis Client reference] for information how to configure the Redis client.

=== Logout and expiration

There are two main ways for the authentication information to expire: the tokens expired and were not renewed or an explicit logout operation was triggered.
Expand Down
98 changes: 98 additions & 0 deletions extensions/oidc-redis-token-state-manager/deployment/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>quarkus-oidc-redis-token-state-manager-parent</artifactId>
<groupId>io.quarkus</groupId>
<version>999-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>quarkus-oidc-redis-token-state-manager-deployment</artifactId>
<name>Quarkus - OpenID Connect Redis Token State Manager - Deployment</name>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-redis-token-state-manager</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-redis-client-deployment</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${project.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>test-containers</id>
<activation>
<property>
<name>test-containers</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.oidc.redis.token.state.manager.deployment;

import io.quarkus.redis.runtime.client.config.RedisConfig;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;

/**
* OIDC Redis Token State Manager build-time configuration.
*/
@ConfigRoot
@ConfigMapping(prefix = "quarkus.oidc.redis-token-state-manager")
public interface OidcRedisTokenStateManagerBuildConfig {

/**
* Enables this extension.
* Set to 'false' if this extension should be disabled.
*/
@WithDefault("true")
boolean enabled();

/**
* Selects Redis client used to store the OIDC token state.
* The default Redis client is used if this property is not configured.
* Used Redis datasource must only be accessible by trusted parties,
* because Quarkus will not encrypt tokens before storing them.
*/
@WithDefault(RedisConfig.DEFAULT_CLIENT_NAME)
String redisClientName();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.quarkus.oidc.redis.token.state.manager.deployment;

import java.util.function.BooleanSupplier;

import jakarta.enterprise.context.ApplicationScoped;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.Type;

import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.oidc.TokenStateManager;
import io.quarkus.oidc.redis.token.state.manager.runtime.OidcRedisTokenStateManagerRecorder;
import io.quarkus.redis.client.RedisClientName;
import io.quarkus.redis.datasource.ReactiveRedisDataSource;
import io.quarkus.redis.deployment.client.RequestedRedisClientBuildItem;
import io.quarkus.redis.runtime.client.config.RedisConfig;

@BuildSteps(onlyIf = OidcRedisTokenStateManagerProcessor.IsEnabled.class)
public class OidcRedisTokenStateManagerProcessor {

@BuildStep
RequestedRedisClientBuildItem requestRedisClient(OidcRedisTokenStateManagerBuildConfig buildConfig) {
return new RequestedRedisClientBuildItem(buildConfig.redisClientName());
}

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
SyntheticBeanBuildItem createTokenStateManager(OidcRedisTokenStateManagerRecorder recorder,
OidcRedisTokenStateManagerBuildConfig buildConfig) {
var redisClientName = buildConfig.redisClientName();
var beanConfigurator = SyntheticBeanBuildItem.configure(TokenStateManager.class)
.priority(1)
.alternative(true)
.unremovable()
.scope(ApplicationScoped.class);
if (RedisConfig.isDefaultClient(redisClientName)) {
beanConfigurator
.createWith(recorder.createTokenStateManager(null))
.addInjectionPoint(Type.create(ReactiveRedisDataSource.class));
} else {
beanConfigurator
.createWith(recorder.createTokenStateManager(redisClientName))
.addInjectionPoint(Type.create(ReactiveRedisDataSource.class),
AnnotationInstance.builder(RedisClientName.class).value(redisClientName).build());
}
return beanConfigurator.done();
}

static final class IsEnabled implements BooleanSupplier {

private final boolean enabled;

IsEnabled(OidcRedisTokenStateManagerBuildConfig buildConfig) {
this.enabled = buildConfig.enabled();
}

@Override
public boolean getAsBoolean() {
return enabled;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package io.quarkus.oidc.redis.token.state.manager.deployment;

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

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

import org.hamcrest.Matchers;
import org.htmlunit.SilentCssErrorHandler;
import org.htmlunit.TextPage;
import org.htmlunit.WebClient;
import org.htmlunit.WebRequest;
import org.htmlunit.WebResponse;
import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlPage;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.restassured.RestAssured;

public abstract class AbstractRedisTokenStateManagerTest {

protected static QuarkusUnitTest createQuarkusUnitTest(String... extraProps) {
return new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(ProtectedResource.class, UnprotectedResource.class, PublicResource.class)
.addAsResource(new StringAsset("""
quarkus.oidc.client-id=quarkus-web-app
quarkus.oidc.application-type=web-app
quarkus.oidc.logout.path=/protected/logout
quarkus.log.category."org.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL
quarkus.log.category."org.htmlunit.css".level=FATAL
""" + String.join(System.lineSeparator(), extraProps)), "application.properties"));
}

@TestHTTPResource
URL url;

@Test
public void testCodeFlow() throws IOException {
try (final WebClient webClient = createWebClient()) {

TextPage textPage = webClient.getPage(url.toString() + "unprotected");
assertEquals("unprotected", textPage.getContent());

HtmlPage page;
page = webClient.getPage(url.toString() + "protected");

assertEquals("Sign in to quarkus", page.getTitleText());

HtmlForm loginForm = page.getForms().get(0);

loginForm.getInputByName("username").setValueAttribute("alice");
loginForm.getInputByName("password").setValueAttribute("alice");

textPage = loginForm.getInputByName("login").click();

assertEquals("alice", textPage.getContent());

assertTokenStateCount(1);

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

webClient.getCookieManager().clearCookies();

assertTokenStateCount(0);
}
}

protected static void assertTokenStateCount(Integer tokenStateCount) {
RestAssured
.given()
.get("public/oidc-token-states-count")
.then()
.statusCode(200)
.body(Matchers.is(tokenStateCount.toString()));
}

protected static WebClient createWebClient() {
WebClient webClient = new WebClient();
webClient.setCssErrorHandler(new SilentCssErrorHandler());
return webClient;
}

}
Loading

0 comments on commit 2b2dbe4

Please sign in to comment.