Repository: quarkusio/quarkus
Issue: Query parameters lost on redirect to OIDC provider

**Describe the bug**
When using OpenID Connect (OIDC) to protect web applications, query parameters are lost if the user is redirected to login first.

**Expected behavior**
After the user has successfully authenticated with the OIDC provider, he/she should be redirected to the same URL that was initially requested.

**Actual behavior**
After the user has successfully authenticated with the OIDC provider, he/she is redirected to a different URL. The URL has the same scheme, host and path but different query parameters. The original query parameters are missing. Instead the query paramters `session`, `state` and `code` are present.

**To Reproduce**
Steps to reproduce the behavior:
1. `git clone https://github.com/quarkusio/quarkus-quickstarts.git`
2. Open the project `security-openid-connect-web-authentication-quickstart `
3. Configure a valid OIDC provider in `application.properties`
4. Extend the class`TokenResource` as shown below
5. Open the URL `http://localhost:8080/tokens?myparam=abc`

After authenticating, the URL is something like `http://localhost:8080/tokens?state=8b...3e&session_state=3f...57&code=bc...5c (instead of `http://localhost:8080/tokens?myparam=abc`).

The page starts with `myparam: null` (instead of `myparam: abc`).

**Configuration**
Same as example project except for:
- `quarkus.oidc.auth-server-url`
- `quarkus.oidc.client-id=abcfund`
- `quarkus.oidc.credentials.client_secret.value`

**Modified code for `TokenResource`**

Add query parameters in signature of method `getTokens` and output them as HTML:

```java
    @GET
    public String getTokens(@QueryParam("myparam") String myparam) {
        StringBuilder response = new StringBuilder().append("<html>")
                .append("<body>")
                .append("<ul>");

        response.append("<li>myparam: ").append(myparam).append("</li>");
        ...
```

**Environment (please complete the following information):**
 - Output of `uname -a` or `ver`: Microsoft Windows [Version 10.0.18363.836]
 - Output of `java -version`: openjdk version "13.0.1" 2019-10-15
 - GraalVM version (if different from Java): 
 - Quarkus version or git rev: 1.4.2.Final
 - Build tool (ie. output of `mvnw --version` or `gradlew --version`): Apache Maven 3.6.2 (40f52333136460af0dc0d7232c0dc0bcf0d9e117; 2019-08-27T17:06:16+02:00)

**Additional context**
The OIDC provider used was a Keycloak instance configured for authentication code flow and requiring a client secret for the flow.

**Additional thoughts**
I'm asking myself what would happend with a POST request. Would the request body be saved and replayed after the redirect?


Changed files:
integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java
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

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 2b813b876c0..5c2331a3d48 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
@@ -185,6 +185,7 @@ private Uni<SecurityIdentity> performCodeFlow(IdentityProviderManager identityPr
         TenantConfigContext configContext = resolver.resolve(context, true);
         Cookie stateCookie = context.getCookie(getStateCookieName(configContext));
 
+        String userQuery = null;
         if (stateCookie != null) {
             List<String> values = context.queryParam("state");
             // IDP must return a 'state' query parameter and the value of the state cookie must start with this parameter's value
@@ -200,22 +201,42 @@ private Uni<SecurityIdentity> performCodeFlow(IdentityProviderManager identityPr
                 if (pair.length == 2) {
                     // The extra path that needs to be added to the current request path
                     String extraPath = pair[1];
-                    // Adding a query marker that the state cookie has already been used to restore the path
-                    // as deleting it now would increase the risk of CSRF
-                    String extraQuery = "?pathChecked=true";
 
-                    // The query parameters returned from IDP need to be included
-                    if (context.request().query() != null) {
-                        extraQuery += ("&" + context.request().query());
-                    }
+                    // The original user query if any will be added to the final redirect URI
+                    // after the authentication has been complete
+
+                    int userQueryIndex = extraPath.indexOf("?");
+                    if (userQueryIndex != 0) {
+                        if (userQueryIndex > 0) {
+                            extraPath = extraPath.substring(0, userQueryIndex);
+                        }
+                        // Adding a query marker that the state cookie has already been used to restore the path
+                        // as deleting it now would increase the risk of CSRF
+                        String extraQuery = "?pathChecked=true";
 
-                    String localRedirectUri = buildUri(context, isForceHttps(configContext), extraPath + extraQuery);
-                    LOG.debugf("Local redirect URI: %s", localRedirectUri);
-                    return Uni.createFrom().failure(new AuthenticationRedirectException(localRedirectUri));
+                        // The query parameters returned from IDP need to be included
+                        if (context.request().query() != null) {
+                            extraQuery += ("&" + context.request().query());
+                        }
+
+                        String localRedirectUri = buildUri(context, isForceHttps(configContext), extraPath + extraQuery);
+                        LOG.debugf("Local redirect URI: %s", localRedirectUri);
+                        return Uni.createFrom().failure(new AuthenticationRedirectException(localRedirectUri));
+                    } else if (userQueryIndex + 1 < extraPath.length()) {
+                        // only the user query needs to be restored, no need to redirect
+                        userQuery = extraPath.substring(userQueryIndex + 1);
+                    }
                 }
                 // The original request path does not have to be restored, the state cookie is no longer needed
                 removeCookie(context, configContext, getStateCookieName(configContext));
             } else {
+                String[] pair = COOKIE_PATTERN.split(stateCookie.getValue());
+                if (pair.length == 2) {
+                    int userQueryIndex = pair[1].indexOf("?");
+                    if (userQueryIndex >= 0 && userQueryIndex + 1 < pair[1].length()) {
+                        userQuery = pair[1].substring(userQueryIndex + 1);
+                    }
+                }
                 // Local redirect restoring the original request path, the state cookie is no longer needed
                 removeCookie(context, configContext, getStateCookieName(configContext));
             }
@@ -244,6 +265,7 @@ private Uni<SecurityIdentity> performCodeFlow(IdentityProviderManager identityPr
             params.put("client_assertion", signJwtWithClientSecret(configContext.oidcConfig));
         }
 
+        final String finalUserQuery = userQuery;
         return Uni.createFrom().emitter(new Consumer<UniEmitter<? super SecurityIdentity>>() {
             @Override
             public void accept(UniEmitter<? super SecurityIdentity> uniEmitter) {
@@ -267,7 +289,10 @@ public void accept(SecurityIdentity identity) {
                                         processSuccessfulAuthentication(context, configContext, result, identity);
                                         if (configContext.oidcConfig.authentication.removeRedirectParameters
                                                 && context.request().query() != null) {
-                                            final String finalRedirectUri = buildUriWithoutQueryParams(context);
+                                            String finalRedirectUri = buildUriWithoutQueryParams(context);
+                                            if (finalUserQuery != null) {
+                                                finalRedirectUri += ("?" + finalUserQuery);
+                                            }
                                             LOG.debugf("Final redirect URI: %s", finalRedirectUri);
                                             uniEmitter.fail(new AuthenticationRedirectException(finalRedirectUri));
                                         } else {
@@ -332,10 +357,17 @@ private String generateCodeFlowState(RoutingContext context, TenantConfigContext
         String cookieValue = uuid;
 
         Authentication auth = configContext.oidcConfig.getAuthentication();
-        if (auth.isRestorePathAfterRedirect() && !redirectPath.equals(context.request().path())) {
-            cookieValue += (COOKIE_DELIM + context.request().path());
+        if (auth.isRestorePathAfterRedirect()) {
+            String requestPath = !redirectPath.equals(context.request().path()) ? context.request().path() : "";
+            if (context.request().query() != null) {
+                requestPath += ("?" + context.request().query());
+            }
+            if (!requestPath.isEmpty()) {
+                cookieValue += (COOKIE_DELIM + requestPath);
+            }
         }
-        return createCookie(context, configContext, getStateCookieName(configContext), cookieValue, 60 * 30).getValue();
+        createCookie(context, configContext, getStateCookieName(configContext), cookieValue, 60 * 30);
+        return uuid;
     }
 
     private String generatePostLogoutState(RoutingContext context, TenantConfigContext configContext) {
diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java
index 570d4f7f5c4..41574f0a9d1 100644
--- a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java
+++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java
@@ -4,6 +4,7 @@
 import javax.ws.rs.GET;
 import javax.ws.rs.InternalServerErrorException;
 import javax.ws.rs.Path;
+import javax.ws.rs.QueryParam;
 
 import org.eclipse.microprofile.jwt.JsonWebToken;
 
@@ -101,4 +102,10 @@ public String refresh() {
         }
         return refreshToken.getToken() != null && !refreshToken.getToken().isEmpty() ? "RT injected" : "no refresh";
     }
+
+    @GET
+    @Path("refresh-query")
+    public String refresh(@QueryParam("a") String aValue) {
+        return refresh() + ":" + aValue;
+    }
 }
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 2d67ba84fae..6f83c516172 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
@@ -480,6 +480,26 @@ public void testAccessAndRefreshTokenInjectionWithoutIndexHtml() throws IOExcept
         }
     }
 
+    @Test
+    public void testAccessAndRefreshTokenInjectionWithoutIndexHtmlWithQuery() throws Exception {
+        try (final WebClient webClient = createWebClient()) {
+            HtmlPage page = webClient.getPage("http://localhost:8081/web-app/refresh-query?a=aValue");
+            assertEquals("/web-app/refresh-query?a=aValue", getStateCookieSavedPath(webClient, null));
+
+            assertEquals("Log in to quarkus", page.getTitleText());
+
+            HtmlForm loginForm = page.getForms().get(0);
+
+            loginForm.getInputByName("username").setValueAttribute("alice");
+            loginForm.getInputByName("password").setValueAttribute("alice");
+
+            page = loginForm.getInputByName("login").click();
+
+            assertEquals("RT injected:aValue", page.getBody().asText());
+            webClient.getCookieManager().clearCookies();
+        }
+    }
+
     @Test
     public void testNoCodeFlowUnprotected() {
         RestAssured.when().get("/public-web-app/access")