Skip to content

Commit

Permalink
Introduce OIDC Databse Token State Manager
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Oct 3, 2023
1 parent 1760b3d commit 0c1f591
Show file tree
Hide file tree
Showing 42 changed files with 1,507 additions and 17 deletions.
10 changes: 10 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,16 @@
<artifactId>quarkus-oidc-token-propagation-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-db-token-state-manager</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-db-token-state-manager-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-token-propagation-reactive</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,9 @@ public interface Capability {

String CACHE = QUARKUS_PREFIX + ".cache";
String JDBC_ORACLE = QUARKUS_PREFIX + ".jdbc.oracle";
String REACTIVE_PG_CLIENT = QUARKUS_PREFIX + ".reactive-pg-client";
String REACTIVE_ORACLE_CLIENT = QUARKUS_PREFIX + ".reactive-oracle-client";
String REACTIVE_MYSQL_CLIENT = QUARKUS_PREFIX + ".reactive-mysql-client";
String REACTIVE_MSSQL_CLIENT = QUARKUS_PREFIX + ".reactive-mssql-client";
String REACTIVE_DB2_CLIENT = QUARKUS_PREFIX + ".reactive-db2-client";
}
13 changes: 13 additions & 0 deletions devtools/bom-descriptor-json/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1591,6 +1591,19 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-db-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-oidc-token-propagation</artifactId>
Expand Down
13 changes: 13 additions & 0 deletions docs/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1607,6 +1607,19 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-db-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-oidc-token-propagation-deployment</artifactId>
Expand Down
126 changes: 114 additions & 12 deletions docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -518,10 +518,15 @@ For example, if you have Quarkus services deployed on the following two domains,


[[token-state-manager]]
===== Customizing the cookie with TokenStateManager
==== Session cookie and default TokenStateManager

OIDC `CodeAuthenticationMechanism` uses the default `io.quarkus.oidc.TokenStateManager` interface implementation to keep the ID, access, and refresh tokens returned in the authorization code or to refresh grant responses in a session cookie.
This makes Quarkus OIDC endpoints completely stateless.
OIDC `CodeAuthenticationMechanism` uses the default `io.quarkus.oidc.TokenStateManager` interface implementation to keep the ID, access, and refresh tokens returned in the authorization code or refresh grant responses in an encrypted session cookie.

It makes Quarkus OIDC endpoints completely stateless and it is recommended to follow this strategy in order to achieve the best scalability results.

See <<db-token-state-manager>> and <<custom-token-state-manager>> sections of this guide for alternative approaches where tokens can be stored in the database or other server-side storage, if you prefer and have good reasons for storing the token state on the server.

You can configure the default `TokenStateManager` to avoid saving an access token in the session cookie and only keep ID and refresh tokens or ID token only.

An access token is only required if the endpoint needs to:

Expand All @@ -542,13 +547,12 @@ In such cases, use the `quarkus.oidc.token-state-manager.strategy` property to c

|===


If your chosen cookie strategy combines tokens and generates a large session cookie value that is greater than 4KB, some browsers might not be able to handle such cookie sizes.
If your chosen session cookie strategy combines tokens and generates a large session cookie value that is greater than 4KB, some browsers might not be able to handle such cookie sizes.
This can occur when the ID, access, and refresh tokens are JWT tokens and the selected strategy is `keep-all-tokens` or with ID and refresh tokens when the strategy is `id-refresh-token`.
To workaround this issue, you can set `quarkus.oidc.token-state-manager.split-tokens=true` to create a unique session token for each token.
To workaround this issue, you can set `quarkus.oidc.token-state-manager.split-tokens=true` to create a unique session token for each token. An alternative solution is to have the tokens saved in the database, see <<db-token-state-manager>> for more information.

`TokenStateManager` encrypts the tokens before storing them in the session cookie.
The following example shows how you configure `TokenStateManager` to split the tokens and encrypt them:
Default `TokenStateManager` encrypts the tokens before storing them in the session cookie.
The following example shows how you configure it to split the tokens and encrypt them:

[source, properties]
----
Expand All @@ -563,7 +567,6 @@ quarkus.oidc.token-state-manager.encryption-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34Y
The token encryption secret must be at least 32 characters long.
If this key is not configured then either `quarkus.oidc.credentials.secret` or `quarkus.oidc.credentials.jwt.secret` will be hashed to create an encryption key.


Configure the `quarkus.oidc.token-state-manager.encryption-secret` property if Quarkus authenticates to the OpenId Connect Provider by using one of the following authentication methods:

* mTLS
Expand All @@ -573,8 +576,12 @@ Otherwise, a random key is generated, which can be problematic if the Quarkus ap

You can disable token encryption in the session cookie by setting `quarkus.oidc.token-state-manager.encryption-required=false`.

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.
[[custom-token-state-manager]]
==== Session cookie and custom TokenStateManager

Register a custom `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 cache cluster and have only a key stored in a session cookie.
Note that this approach might introduce some challenges if you need to make the tokens available across multiple microservices nodes.

Here is a simple example:
Expand Down Expand Up @@ -631,7 +638,102 @@ public class CustomTokenStateManager implements TokenStateManager {
}
}
----
//SJ: In next iteration, propose to recompose Logout information into a new concept topic

See <<token-state-manager>> for the information about the default `TokenStateManager` storing the tokens in an encrypted session cookie.

See <<db-token-state-manager>> for the information about the custom `TokenStatemanager` implementation provided by Quarkus.

[[db-token-state-manager]]
==== Database TokenStateManager

If you prefer to follow a stateful token storage strategy, then you can use a custom `TokenStateManager` provided by Quarkus to have your application storing tokens in a database, instead of storing them in an encrypted session cookie which is done by default, as described in the <<token-state-manager>> section.

To use this feature, add the following extension to your project:

:add-extension-extensions: oidc-db-token-state-manager
include::{includes}/devtools/extension-add.adoc[]

This extension will replace the default `io.quarkus.oidc.TokenStateManager' with a database-based one.

OIDC Database Token State Manager is using a Reactive SQL client under the hood to avoid blocking since the authentication is likely to happen on IO thread.

Depending on your database, please include and configure exactly one xref:reactive-sql-clients.adoc[Reactive SQL client].
Following Reactive SQL clients are supported:

* Reactive MS SQL client
* Reactive MySQL client
* Reactive PostgreSQL client
* Reactive Oracle client
* Reactive DB2 client

IMPORTANT: Your application is not required to switch to using the Reactive SQL client if it already uses Hibernate ORM with one of the JDBC driver extensions.

Let's say you already have application that is using the Hibernate ORM extension together with a PostgreSQL JDBC Driver and your datasource is configured like this:

[source, properties]
----
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus_test
quarkus.datasource.password=quarkus_test
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/quarkus_test
----

Now, if you decided to use OIDC Database Token State Manager, you need to add following dependencies and set a reactive driver URL.

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

[source, properties]
----
quarkus.datasource.reactive.url=postgresql://localhost:5432/quarkus_test
----

And you are ready to go.

By default, a database table used for storing tokens is created for you, however you can disable this option with the `quarkus.oidc.db-token-state-manager.create-database-table-if-not-exists` configuration property.
Should you want the Hibernate ORM extension to create this table instead, you simply need to include an Entity like this one:

[source, java]
----
package org.acme.manager;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Table(name = "oidc_db_token_state_manager") <1>
@Entity
public class OidcDbTokenStateManagerEntity {
@Id
String id;
@Column(name = "id_token", length = 4000) <2>
String idToken;
@Column(name = "refresh_token", length = 4000)
String refreshToken;
@Column(name = "access_token", length = 4000)
String accessToken;
@Column(name = "expires_in")
Long expiresIn;
}
----
<1> The Hibernate ORM extension will only create this table for you when database schema is generated. Please refer to the xref:hibernate-orm.adoc[Hibernate ORM] guide for more information.
<2> You can choose column length depending on the length of your tokens.

==== Logout and expiration

Expand Down
99 changes: 99 additions & 0 deletions extensions/oidc-db-token-state-manager/deployment/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?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-db-token-state-manager-parent</artifactId>
<groupId>io.quarkus</groupId>
<version>999-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

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

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-db-token-state-manager</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-deployment</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-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>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${project.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>test-keycloak</id>
<activation>
<property>
<name>test-containers</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>false</skip>
<systemPropertyVariables>
<mssql.image>${mssql.image}</mssql.image>
<db2.image>${db2.image}</db2.image>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.oidc.db.token.state.manager;

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

@ConfigMapping(prefix = "quarkus.oidc.db-token-state-manager")
@ConfigRoot
public interface OidcDbTokenStateManagerBuildTimeConfig {

/**
* Whether token state should be stored in the database.
*/
@WithDefault("true")
boolean enabled();

}
Loading

0 comments on commit 0c1f591

Please sign in to comment.