Repository: quarkusio/quarkus
Issue: OIDC refresh token lost if not resent by provider

**Describe the bug**
When using Google OpenID Connect as Quarkus OIDC provider, the application receives both an access and refresh token, when `access_type=offline` is sent to Google.

It work that Quarkus uses this refresh token under the hood to refresh the expired access token, however, Google sends the refresh token only for the very first time (and never again), when a token is requested.

`CodeAuthenticationMechanism` then causes the refresh token to be lost in the `q_session` cookie, since it overrides the value with the result from the refresh, and there the refresh token is non-existent.

**Expected behavior**
To save and reuse the refresh token, since it might not be resent -- this is the case for Google, not sure how other OIDC providers do this.

**Potential fix**

I've fixed this issue in my project by manually building `quarkus-oidc` with the [following patch](https://github.com/sdaschner/quarkus/commit/9d7aac49d1176252ef4efefa9a7a4698c878c73d)

Happy to provide a PR, however, I'm not quite sure whether this is just a dirty hack or solves it in all solutions; in general, it would be good if the OIDC extension wouldn't loose refresh tokens that are only issues once, like for Google.


Changed files:
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java

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 ffa6358f851..ba91c1dd8e4 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
@@ -303,7 +303,8 @@ public void accept(SecurityIdentity identity) {
                                             LOG.debug("ID Token is required to contain 'exp' and 'iat' claims");
                                             uniEmitter.fail(new AuthenticationCompletionException());
                                         }
-                                        processSuccessfulAuthentication(context, configContext, result, identity);
+                                        processSuccessfulAuthentication(context, configContext, result,
+                                                result.opaqueRefreshToken(), identity);
 
                                         if (configContext.oidcConfig.authentication.isRemoveRedirectParameters()
                                                 && context.request().query() != null) {
@@ -348,15 +349,12 @@ private String signJwtWithClientSecret(OidcTenantConfig cfg) {
     }
 
     private void processSuccessfulAuthentication(RoutingContext context, TenantConfigContext configContext,
-            AccessToken result, SecurityIdentity securityIdentity) {
-
+            AccessToken result, String refreshToken, SecurityIdentity securityIdentity) {
         removeCookie(context, configContext, getSessionCookieName(configContext));
 
-        String cookieValue = new StringBuilder(result.opaqueIdToken())
-                .append(COOKIE_DELIM)
-                .append(result.opaqueAccessToken())
-                .append(COOKIE_DELIM)
-                .append(result.opaqueRefreshToken()).toString();
+        String cookieValue = result.opaqueIdToken() + COOKIE_DELIM
+                + result.opaqueAccessToken() + COOKIE_DELIM
+                + refreshToken;
 
         long maxAge = result.idToken().getLong("exp") - result.idToken().getLong("iat");
         if (configContext.oidcConfig.token.lifespanGrace.isPresent()) {
@@ -470,8 +468,12 @@ public void handle(AsyncResult<Void> result) {
                                             .subscribe().with(new Consumer<SecurityIdentity>() {
                                                 @Override
                                                 public void accept(SecurityIdentity identity) {
-                                                    // after a successful refresh, rebuild the identity and update the cookie 
-                                                    processSuccessfulAuthentication(context, configContext, token,
+                                                    // the refresh token might not have been send in the response again
+                                                    String refresh = token.opaqueRefreshToken() != null
+                                                            ? token.opaqueRefreshToken()
+                                                            : refreshToken;
+                                                    // after a successful refresh, rebuild the identity and update the cookie
+                                                    processSuccessfulAuthentication(context, configContext, token, refresh,
                                                             identity);
                                                     // update the token so that blocking threads get the latest one
                                                     emitter.complete(