Repository: quarkusio/quarkus
Issue: OIDC authentication loop if Cookie Policy sameSite=strict

### Description

Using the OIDC authentication code workflow may lead to a loop of authentication requests, if not properly configured and currently there is no hint to that in the Documentation.

If the user visits the app for the first time, the current default setting of `quarkus.oidc.authentication.cookie-same-site=strict` leads to the following chain:
1. user is not authenticated, goes to https://example.org/login, response is
  ```
  302 Found
  location: https://provider.org/oauth/authorize?response_type=code&client_id=XXXX&state=YYYY&...
set-cookie: q_auth=YYYY|/login?code=XYZ&state=YYYY; Max-Age=1800; Path=/; HTTPOnly; SameSite=Strict
  ```
2. User is redirected to provider.org and has to grant the new App permissions for authentication
3. Provider redirects the user to example.org with code & state  (Not 302 calls, as user action required!)
4. Because of the sameSite=Strict policy, the q_auth cookie is not send to example.org. So it retriggers an authentication with similar request as in 1
5. User is redirected to provider.org
6. provider.org sends 302 redirect to example.org with code & state, as already logged in
7. 302s were originally started from example.org, so cookie is sent and user is successfully authenticated (could not test that, just my understanding)

So after an additional roundtrip, the user is authenticated and everything is ok (if it works as I understand it).

However, if provider.org does not send a 302 for the redirection but an html page with a javascript redirect (e.g. gitlab, the only provider I currently test with) this leads to a loop, since the request is originated always from provider.org and with sameSite=strict the cookie will never be send.

It's not a bug as it can be configured and many providers probably do the redirect with a 302, but I think it is worth noting in the documentation.

The following setting solved the issue for me:
```
quarkus.oidc.authentication.cookie-same-site=lax
```


### Implementation ideas

_No response_

Changed files:
integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java

diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
index e25c963ca59..ebe214e2d7e 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
@@ -603,7 +603,7 @@ public static enum Source {
     public static class Authentication {
 
         /**
-         * SameSite attribute values for the session, state and post logout cookies.
+         * SameSite attribute values for the session cookie.
          */
         public enum CookieSameSite {
             STRICT,
@@ -767,7 +767,7 @@ public enum ResponseMode {
         public Optional<String> cookieDomain = Optional.empty();
 
         /**
-         * SameSite attribute for the session, state and post logout cookies.
+         * SameSite attribute for the session cookie.
          */
         @ConfigItem(defaultValue = "strict")
         public CookieSameSite cookieSameSite = CookieSameSite.STRICT;
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java
index 4dd98f19432..fdf6a4fb4df 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java
@@ -799,7 +799,7 @@ public Uni<? extends Void> apply(Void t) {
                                     public Void apply(String cookieValue) {
                                         String sessionCookie = createCookie(context, configContext.oidcConfig,
                                                 getSessionCookieName(configContext.oidcConfig),
-                                                cookieValue, sessionMaxAge).getValue();
+                                                cookieValue, sessionMaxAge, true).getValue();
                                         if (sessionCookie.length() >= MAX_COOKIE_VALUE_LENGTH) {
                                             LOG.warnf(
                                                     "Session cookie length for the tenant %s is equal or greater than %d bytes."
@@ -914,6 +914,11 @@ private String generatePostLogoutState(RoutingContext context, TenantConfigConte
 
     static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcConfig,
             String name, String value, long maxAge) {
+        return createCookie(context, oidcConfig, name, value, maxAge, false);
+    }
+
+    static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcConfig,
+            String name, String value, long maxAge, boolean sessionCookie) {
         ServerCookie cookie = new CookieImpl(name, value);
         cookie.setHttpOnly(true);
         cookie.setSecure(oidcConfig.authentication.cookieForceSecure || context.request().isSSL());
@@ -924,7 +929,9 @@ static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcCo
         if (auth.cookieDomain.isPresent()) {
             cookie.setDomain(auth.getCookieDomain().get());
         }
-        cookie.setSameSite(CookieSameSite.valueOf(auth.cookieSameSite.name()));
+        if (sessionCookie) {
+            cookie.setSameSite(CookieSameSite.valueOf(auth.cookieSameSite.name()));
+        }
         context.response().addCookie(cookie);
         return cookie;
     }
diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java
index e35ee29a41b..67f549bef42 100644
--- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java
+++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java
@@ -55,9 +55,9 @@ public void testCodeFlowNoConsent() throws IOException {
                     .loadWebResponse(new WebRequest(URI.create("http://localhost:8081/index.html").toURL()));
             verifyLocationHeader(webClient, webResponse.getResponseHeaderValue("location"), null, "web-app", false);
 
-            String stateCookieString = webResponse.getResponseHeaderValue("Set-Cookie");
-            assertTrue(stateCookieString.startsWith("q_auth_Default_test="));
-            assertTrue(stateCookieString.contains("SameSite=Strict"));
+            Cookie stateCookie = getStateCookie(webClient, null);
+            assertNotNull(stateCookie);
+            assertNull(stateCookie.getSameSite());
 
             webClient.getCookieManager().clearCookies();
 
@@ -95,6 +95,7 @@ public void testCodeFlowNoConsent() throws IOException {
 
             Cookie sessionCookie = getSessionCookie(webClient, null);
             assertNotNull(sessionCookie);
+            assertEquals("strict", sessionCookie.getSameSite());
 
             webClient.getCookieManager().clearCookies();
         }
@@ -176,10 +177,6 @@ public void testCodeFlowForceHttpsRedirectUriAndPkce() throws Exception {
             verifyLocationHeader(webClient, keycloakUrl, "tenant-https_test", "xforwarded%2Ftenant-https",
                     true);
 
-            String stateCookieString = webResponse.getResponseHeaderValue("Set-Cookie");
-            assertTrue(stateCookieString.startsWith("q_auth_tenant-https_test="));
-            assertTrue(stateCookieString.contains("SameSite=Lax"));
-
             HtmlPage page = webClient.getPage(keycloakUrl);
 
             assertEquals("Sign in to quarkus", page.getTitleText());
@@ -195,6 +192,7 @@ public void testCodeFlowForceHttpsRedirectUriAndPkce() throws Exception {
             String endpointLocation = webResponse.getResponseHeaderValue("location");
 
             Cookie stateCookie = getStateCookie(webClient, "tenant-https_test");
+            assertNull(stateCookie.getSameSite());
             verifyCodeVerifier(stateCookie, keycloakUrl);
 
             assertTrue(endpointLocation.startsWith("https"));
@@ -222,6 +220,7 @@ public void testCodeFlowForceHttpsRedirectUriAndPkce() throws Exception {
             assertEquals("tenant-https:reauthenticated", page.getBody().asNormalizedText());
             Cookie sessionCookie = getSessionCookie(webClient, "tenant-https_test");
             assertNotNull(sessionCookie);
+            assertEquals("lax", sessionCookie.getSameSite());
             webClient.getCookieManager().clearCookies();
         }
     }