From f92fc5675b4d644605d9f3f8d1dcb08ff9839eb4 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 4 Sep 2024 07:50:16 +0200 Subject: [PATCH 1/2] Fix registration of backchannel logout route Currently the backchannel logout route is registered with the root-path, resulting in a path like this: ``/backend/backend/back-channel-logout`` instead of ``/backend/back-channel-logout`` See also https://github.com/quarkusio/quarkus/issues/42990 --- .../java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java index f66c5899e834f..b34cd66bc3f9d 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java @@ -46,7 +46,7 @@ public void setup(@Observes Router router) { private void addRoute(Router router, OidcTenantConfig oidcTenantConfig) { if (oidcTenantConfig.isTenantEnabled() && oidcTenantConfig.logout.backchannel.path.isPresent()) { - router.route(getRootPath() + oidcTenantConfig.logout.backchannel.path.get()) + router.route(oidcTenantConfig.logout.backchannel.path.get()) .handler(new RouteHandler(oidcTenantConfig)); } } From 228883027f96ba285a1ff9e75e7bacc3369dc041 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Wed, 4 Sep 2024 18:17:15 +0100 Subject: [PATCH 2/2] Add Back channel logout test with quarkus.http.root-path --- .../runtime/BackChannelLogoutHandler.java | 11 +- .../oidc-wiremock-logout/pom.xml | 106 ++++++++++++++++++ .../it/keycloak/CodeFlowFormPostResource.java | 21 ++++ .../src/main/resources/application.properties | 20 ++++ .../src/main/resources/privateKey.jwk | 12 ++ .../CodeFlowAuthorizationInGraalITCase.java | 7 ++ .../keycloak/CodeFlowAuthorizationTest.java | 96 ++++++++++++++++ integration-tests/pom.xml | 1 + 8 files changed, 269 insertions(+), 5 deletions(-) create mode 100644 integration-tests/oidc-wiremock-logout/pom.xml create mode 100644 integration-tests/oidc-wiremock-logout/src/main/java/io/quarkus/it/keycloak/CodeFlowFormPostResource.java create mode 100644 integration-tests/oidc-wiremock-logout/src/main/resources/application.properties create mode 100644 integration-tests/oidc-wiremock-logout/src/main/resources/privateKey.jwk create mode 100644 integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationInGraalITCase.java create mode 100644 integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java index b34cd66bc3f9d..9c204af34a12a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java @@ -61,11 +61,13 @@ class RouteHandler implements Handler { @Override public void handle(RoutingContext context) { LOG.debugf("Back channel logout request for the tenant %s received", oidcTenantConfig.getTenantId().get()); - final TenantConfigContext tenantContext = getTenantConfigContext(context); + final String requestPath = context.request().path(); + final TenantConfigContext tenantContext = getTenantConfigContext(requestPath); if (tenantContext == null) { LOG.errorf( - "Tenant configuration for the tenant %s is not available or does not match the backchannel logout path", - oidcTenantConfig.getTenantId().get()); + "Tenant configuration for the tenant %s is not available " + + "or does not match the backchannel logout path %s", + oidcTenantConfig.getTenantId().get(), requestPath); context.response().setStatusCode(400); context.response().end(); return; @@ -147,8 +149,7 @@ private boolean verifyLogoutTokenClaims(TokenVerificationResult result) { return true; } - private TenantConfigContext getTenantConfigContext(RoutingContext context) { - String requestPath = context.request().path(); + private TenantConfigContext getTenantConfigContext(final String requestPath) { if (isMatchingTenant(requestPath, resolver.getTenantConfigBean().getDefaultTenant())) { return resolver.getTenantConfigBean().getDefaultTenant(); } diff --git a/integration-tests/oidc-wiremock-logout/pom.xml b/integration-tests/oidc-wiremock-logout/pom.xml new file mode 100644 index 0000000000000..151ad1ec97221 --- /dev/null +++ b/integration-tests/oidc-wiremock-logout/pom.xml @@ -0,0 +1,106 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-integration-test-oidc-wiremock-logout + Quarkus - Integration Tests - OpenID Connect Adapter WireMock Logout + Module that contains OpenID Connect logout related tests using WireMock + + + + io.quarkus + quarkus-oidc + + + io.quarkus + quarkus-rest-jackson + + + + + io.quarkus + quarkus-test-oidc-server + test + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.htmlunit + htmlunit + test + + + org.eclipse.jetty + * + + + + + + io.quarkus + quarkus-oidc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + maven-surefire-plugin + + + maven-failsafe-plugin + + + io.quarkus + quarkus-maven-plugin + + + + generate-code + build + + + + + + + + + diff --git a/integration-tests/oidc-wiremock-logout/src/main/java/io/quarkus/it/keycloak/CodeFlowFormPostResource.java b/integration-tests/oidc-wiremock-logout/src/main/java/io/quarkus/it/keycloak/CodeFlowFormPostResource.java new file mode 100644 index 0000000000000..e91d93c7919f2 --- /dev/null +++ b/integration-tests/oidc-wiremock-logout/src/main/java/io/quarkus/it/keycloak/CodeFlowFormPostResource.java @@ -0,0 +1,21 @@ +package io.quarkus.it.keycloak; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/code-flow-form-post") +public class CodeFlowFormPostResource { + + @Inject + SecurityIdentity identity; + + @GET + @Authenticated + public String access() { + return identity.getPrincipal().getName(); + } +} diff --git a/integration-tests/oidc-wiremock-logout/src/main/resources/application.properties b/integration-tests/oidc-wiremock-logout/src/main/resources/application.properties new file mode 100644 index 0000000000000..beaa3fa7f635e --- /dev/null +++ b/integration-tests/oidc-wiremock-logout/src/main/resources/application.properties @@ -0,0 +1,20 @@ +quarkus.keycloak.devservices.enabled=false + +quarkus.oidc.code-flow-form-post.auth-server-url=${keycloak.url}/realms/quarkus-form-post/ +quarkus.oidc.code-flow-form-post.client-id=quarkus-web-app +quarkus.oidc.code-flow-form-post.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.oidc.code-flow-form-post.application-type=web-app +quarkus.oidc.code-flow-form-post.authentication.response-mode=form_post +quarkus.oidc.code-flow-form-post.discovery-enabled=false +# redirect the user to ${keycloak.url}/realms/quarkus-form-post/ which will respond with form post data +quarkus.oidc.code-flow-form-post.authorization-path=/ +# reuse the wiremock access token stub for the `quarkus` realm - it is the same for the query and form post response mode +quarkus.oidc.code-flow-form-post.token-path=${keycloak.url}/realms/quarkus/token +# reuse the wiremock JWK endpoint stub for the `quarkus` realm - it is the same for the query and form post response mode +quarkus.oidc.code-flow-form-post.jwks-path=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs +quarkus.oidc.code-flow-form-post.logout.backchannel.path=/back-channel-logout +quarkus.oidc.code-flow-form-post.token.audience=https://server.example.com,https://id.server.example.com + +quarkus.native.additional-build-args=-H:IncludeResources=private.*\\.*,-H:IncludeResources=.*\\.p12 + +quarkus.http.root-path=/service \ No newline at end of file diff --git a/integration-tests/oidc-wiremock-logout/src/main/resources/privateKey.jwk b/integration-tests/oidc-wiremock-logout/src/main/resources/privateKey.jwk new file mode 100644 index 0000000000000..a5c70b8e55f88 --- /dev/null +++ b/integration-tests/oidc-wiremock-logout/src/main/resources/privateKey.jwk @@ -0,0 +1,12 @@ +{ + "kty":"RSA", + "kid":"1", + "n":"iJw33l1eVAsGoRlSyo-FCimeOc-AaZbzQ2iESA3Nkuo3TFb1zIkmt0kzlnWVGt48dkaIl13Vdefh9hqw_r9yNF8xZqX1fp0PnCWc5M_TX_ht5fm9y0TpbiVmsjeRMWZn4jr3DsFouxQ9aBXUJiu26V0vd2vrECeeAreFT4mtoHY13D2WVeJvboc5mEJcp50JNhxRCJ5UkY8jR_wfUk2Tzz4-fAj5xQaBccXnqJMu_1C6MjoCEiB7G1d13bVPReIeAGRKVJIF6ogoCN8JbrOhc_48lT4uyjbgnd24beatuKWodmWYhactFobRGYo5551cgMe8BoxpVQ4to30cGA0qjQ", + "e":"AQAB", + "d":"AvIDTlsK_priQLTwEQf5IVf2Xl638Q7dHdXyDC-oAAPmv1GcqRVH7Wm5oAPW_CZQfWhV55WRVaJzP8AhksyD5NcslH79hQZT4NT6xgApGYecrvmseuZ4dfR-e1cxXTRNBxaoXvwSiv4LuOPHmC8XGX712AhOoCGKiZp1WFqqkKwTpkgJEApJFVb-XRIKQa0YaRKpJsJ534pLMwTh7LoPLM4BCaBVbRfHzH2H5L3TSJP718kyCuxg3z2p9Y7zIOLTmgFdeR0_kd_xKUFZ2ByN3SKlC0IWlLUSiMPsGYExRpZTMZHKyD939gv-2_Z-bOYfKlYNIvAmQH_8CcX2I039LQ", + "p":"104AjPaxZoi_BiMBODlChnZOvRJT071PdkeZ283uyrdW8qqKD9q8FTMgUXzKoboHtUiHbJbLOobPmPDh93839rq7dTdCNzNVOuLmE-V3_bmaShdzvxEIazwPf6AvjbEZAc-zu2RS4SNkp1LbzgSl9nINSlF7t6Lkl6T28PYULys", + "q":"om5ooyzxa4ZJ-dU0ODsEb-Bmz6xwb27xF9aEhBYJprHeoNs2QM1D64_A39weD9MYwBux4-ivshCJ0dVKEbDujJRLnzf-ssrasA6CFyaaCT4DKtq1oWb9rcG-2LQd5Bm9PttrUrSUNqitr085IYikaLEz7UU6gtXPoC8UOcJ4cSc", + "dp":"DeWE95Q8oweUfMrpmz1m49LjBiUWsAX6CQJaFevWy9LFk-gZ_Sf7F8sy_M93LLUbJkJGK2YYO_DTmWWC0Dyv2gb3bntglLuFdsWKYCJhekjugnW9DMoGpxU7Utt99kFGAe3sBd5V0x47sukQMt3t8FgwL2nO-G1VH8yP-8GGT_0", + "dq":"TGBeE1wuqMCcSD1YMJiPnYuGzF_o_nzMIMldxj4Wi6tXY4uwFwhtx3Xw21JFUGuSV8KuAtyGwNPF-kSwb2Eiyjdw140c1jVMXzxzLy-XfoEKPDxa62niHrHba0pGQ9tWgRfrfxgqGQl3odc-peX6aL_qCsdim-KtnkSE3iPzPkE", + "qi":"Jzp5KnT24y0wOoPUn_11S3ZcYl0i03dkaH4c5zR02G1MJG9K017juurx2aXVTctOzrj7O226EUiL1Qbq3QtnWFDDGY6vNZuqzJM7AMXsvp1djq_6fEVhxCIOgfJbmhb3mkG82rxn4et9o_TNr6mvEmHzG15sHbvZbAnn4GeqToY" +} diff --git a/integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationInGraalITCase.java b/integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationInGraalITCase.java new file mode 100644 index 0000000000000..010d5dd89846c --- /dev/null +++ b/integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationInGraalITCase.java @@ -0,0 +1,7 @@ +package io.quarkus.it.keycloak; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class CodeFlowAuthorizationInGraalITCase extends CodeFlowAuthorizationTest { +} diff --git a/integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java new file mode 100644 index 0000000000000..57201376da7b7 --- /dev/null +++ b/integration-tests/oidc-wiremock-logout/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -0,0 +1,96 @@ +package io.quarkus.it.keycloak; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; +import java.net.URI; + +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.htmlunit.util.Cookie; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.oidc.server.OidcWiremockTestResource; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; + +@QuarkusTest +@QuarkusTestResource(OidcWiremockTestResource.class) +public class CodeFlowAuthorizationTest { + + @Test + public void testCodeFlowFormPostAndBackChannelLogout() throws IOException { + try (final WebClient webClient = createWebClient()) { + webClient.getOptions().setRedirectEnabled(true); + HtmlPage page = webClient.getPage("http://localhost:8081/service/code-flow-form-post"); + + HtmlForm form = page.getFormByName("form"); + form.getInputByName("username").type("alice"); + form.getInputByName("password").type("alice"); + + TextPage textPage = form.getInputByValue("login").click(); + + assertEquals("alice", textPage.getContent()); + + assertNotNull(getSessionCookie(webClient, "code-flow-form-post")); + + textPage = webClient.getPage("http://localhost:8081/service/code-flow-form-post"); + assertEquals("alice", textPage.getContent()); + + // Session is still active + assertNotNull(getSessionCookie(webClient, "code-flow-form-post")); + + // ID token subject is `123456` + // request a back channel logout for some other subject + RestAssured.given() + .when().contentType(ContentType.URLENC) + .body("logout_token=" + OidcWiremockTestResource.getLogoutToken("789")) + .post("http://localhost:8081/service/back-channel-logout") + .then() + .statusCode(200); + + // No logout: + textPage = webClient.getPage("http://localhost:8081/service/code-flow-form-post"); + assertEquals("alice", textPage.getContent()); + // Session is still active + assertNotNull(getSessionCookie(webClient, "code-flow-form-post")); + + // request a back channel logout for the same subject + RestAssured.given() + .when().contentType(ContentType.URLENC).body("logout_token=" + + OidcWiremockTestResource.getLogoutToken("123456")) + .post("http://localhost:8081/service/back-channel-logout") + .then() + .statusCode(200); + + // Confirm 302 is returned and the session cookie is null + webClient.getOptions().setRedirectEnabled(false); + WebResponse webResponse = webClient + .loadWebResponse(new WebRequest(URI.create("http://localhost:8081/service/code-flow-form-post").toURL())); + assertEquals(302, webResponse.getStatusCode()); + + assertNull(getSessionCookie(webClient, "code-flow-form-post")); + + webClient.getCookieManager().clearCookies(); + } + } + + private WebClient createWebClient() { + WebClient webClient = new WebClient(); + webClient.setCssErrorHandler(new SilentCssErrorHandler()); + return webClient; + } + + private Cookie getSessionCookie(WebClient webClient, String tenantId) { + return webClient.getCookieManager().getCookie("q_session" + (tenantId == null ? "" : "_" + tenantId)); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index f77304827c2ca..74394754e7eb9 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -264,6 +264,7 @@ oidc-code-flow oidc-tenancy oidc-wiremock + oidc-wiremock-logout keycloak-authorization rest-csrf reactive-db2-client