From 3cef9b3aac2b8813ad43bb0fa6cb848fd9298311 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 9 Feb 2023 13:38:07 +0100 Subject: [PATCH] OIDC logout functionality fixed Signed-off-by: David Kral --- .../providers/oidc/reactive/OidcSupport.java | 4 +- .../security/providers/oidc/OidcFeature.java | 5 ++- .../security/providers/oidc/OidcProvider.java | 4 +- .../tests/integration/oidc/TestResource.java | 13 +++++++ .../integration/oidc/CookieBasedLoginIT.java | 38 +++++++++++++++++++ .../oidc/src/test/resources/application.yaml | 2 + 6 files changed, 62 insertions(+), 4 deletions(-) diff --git a/security/providers/oidc-reactive/src/main/java/io/helidon/security/providers/oidc/reactive/OidcSupport.java b/security/providers/oidc-reactive/src/main/java/io/helidon/security/providers/oidc/reactive/OidcSupport.java index 2be0b7d8682..fff41ced92b 100644 --- a/security/providers/oidc-reactive/src/main/java/io/helidon/security/providers/oidc/reactive/OidcSupport.java +++ b/security/providers/oidc-reactive/src/main/java/io/helidon/security/providers/oidc/reactive/OidcSupport.java @@ -242,6 +242,7 @@ private void processTenantLogout(ServerRequest req, ServerResponse res, String t private void logoutWithTenant(ServerRequest req, ServerResponse res, Tenant tenant) { OidcCookieHandler idTokenCookieHandler = oidcConfig.idTokenCookieHandler(); OidcCookieHandler tokenCookieHandler = oidcConfig.tokenCookieHandler(); + OidcCookieHandler tenantCookieHandler = oidcConfig.tenantCookieHandler(); Optional idTokenCookie = req.headers() .cookies() @@ -269,6 +270,7 @@ private void logoutWithTenant(ServerRequest req, ServerResponse res, Tenant tena ResponseHeaders headers = res.headers(); headers.addCookie(tokenCookieHandler.removeCookie().build()); headers.addCookie(idTokenCookieHandler.removeCookie().build()); + headers.addCookie(tenantCookieHandler.removeCookie().build()); res.status(Http.Status.TEMPORARY_REDIRECT_307) .addHeader(Http.Header.LOCATION, sb.toString()) @@ -457,7 +459,7 @@ private String processJsonResponse(ServerRequest req, .forSingle(builder -> { headers.addCookie(builder.build()); if (idToken != null && oidcConfig.logoutEnabled()) { - tokenCookieHandler.createCookie(idToken) + oidcConfig.idTokenCookieHandler().createCookie(idToken) .forSingle(it -> { headers.addCookie(it.build()); res.send(); diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java index 3175462b87c..0fd409822d0 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java @@ -148,6 +148,7 @@ public final class OidcFeature implements HttpFeature { private final OidcConfig oidcConfig; private final OidcCookieHandler tokenCookieHandler; private final OidcCookieHandler idTokenCookieHandler; + private final OidcCookieHandler tenantCookieHandler; private final boolean enabled; private final CorsSupport corsSupport; @@ -156,6 +157,7 @@ private OidcFeature(Builder builder) { this.enabled = builder.enabled; this.tokenCookieHandler = oidcConfig.tokenCookieHandler(); this.idTokenCookieHandler = oidcConfig.idTokenCookieHandler(); + this.tenantCookieHandler = oidcConfig.tenantCookieHandler(); this.corsSupport = prepareCrossOriginSupport(oidcConfig.redirectUri(), oidcConfig.crossOriginConfig()); this.oidcConfigFinders = List.copyOf(builder.tenantConfigFinders); @@ -304,7 +306,7 @@ private void logoutWithTenant(ServerRequest req, ServerResponse res, Tenant tena idTokenCookieHandler.decrypt(encryptedIdToken) .forSingle(idToken -> { - StringBuilder sb = new StringBuilder(oidcConfig.logoutEndpointUri() + StringBuilder sb = new StringBuilder(tenant.logoutEndpointUri() + "?id_token_hint=" + idToken + "&post_logout_redirect_uri=" + postLogoutUri(req)); @@ -315,6 +317,7 @@ private void logoutWithTenant(ServerRequest req, ServerResponse res, Tenant tena ServerResponseHeaders headers = res.headers(); headers.addCookie(tokenCookieHandler.removeCookie().build()); headers.addCookie(idTokenCookieHandler.removeCookie().build()); + headers.addCookie(tenantCookieHandler.removeCookie().build()); res.status(Http.Status.TEMPORARY_REDIRECT_307) .header(Http.Header.LOCATION, sb.toString()) diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java index 602f5a91b78..cbbe3d64f3d 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java @@ -248,8 +248,8 @@ public CompletionStage outboundSecurity(ProviderReques provides = {AuthenticationProvider.class, SecurityProvider.class}) public static final class Builder implements io.helidon.common.Builder { - private static final int BUILDER_WEIGHT = 50000; - private static final int DEFAULT_WEIGHT = 100000; + private static final int BUILDER_WEIGHT = 300; + private static final int DEFAULT_WEIGHT = 100; private final HelidonServiceLoader.Builder tenantConfigProviders = HelidonServiceLoader .builder(ServiceLoader.load(TenantConfigProvider.class)) diff --git a/tests/integration/oidc/src/main/java/io/helidon/tests/integration/oidc/TestResource.java b/tests/integration/oidc/src/main/java/io/helidon/tests/integration/oidc/TestResource.java index 1a690d42f0f..7b350f5791e 100644 --- a/tests/integration/oidc/src/main/java/io/helidon/tests/integration/oidc/TestResource.java +++ b/tests/integration/oidc/src/main/java/io/helidon/tests/integration/oidc/TestResource.java @@ -20,6 +20,8 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import io.helidon.security.annotations.Authenticated; @@ -32,6 +34,7 @@ public class TestResource { public static final String EXPECTED_TEST_MESSAGE = "Hello world"; + public static final String EXPECTED_POST_LOGOUT_TEST_MESSAGE = "Post logout endpoint reached with no cookies"; /** * Return hello world message. @@ -45,5 +48,15 @@ public String getDefaultMessage() { return EXPECTED_TEST_MESSAGE; } + @Path("/postLogout") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String postLogout(@Context HttpHeaders httpHeaders) { + if (httpHeaders.getCookies().isEmpty()) { + return EXPECTED_POST_LOGOUT_TEST_MESSAGE; + } + return "Cookies are not cleared!"; + } + } diff --git a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CookieBasedLoginIT.java b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CookieBasedLoginIT.java index 010ce546084..181ee4a2eda 100644 --- a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CookieBasedLoginIT.java +++ b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CookieBasedLoginIT.java @@ -24,6 +24,7 @@ import org.jsoup.nodes.Document; import org.junit.jupiter.api.Test; +import static io.helidon.tests.integration.oidc.TestResource.EXPECTED_POST_LOGOUT_TEST_MESSAGE; import static io.helidon.tests.integration.oidc.TestResource.EXPECTED_TEST_MESSAGE; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; @@ -124,6 +125,43 @@ public void testDefaultTenantUsage(WebTarget webTarget) { } } + @Test + public void testLogoutFunctionality(WebTarget webTarget) { + String formUri; + + //greet endpoint is protected, and we need to get JWT token out of the Keycloak. We will get redirected to the Keycloak. + try (Response response = client.target(webTarget.getUri()).path("/test") + .request() + .get()) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + //We need to get form URI out of the HTML + formUri = getRequestUri(response.readEntity(String.class)); + } + + //Sending authentication to the Keycloak and getting redirected back to the running Helidon app. + Entity
form = Entity.form(new Form().param("username", "userone") + .param("password", "12345") + .param("credentialId", "")); + try (Response response = client.target(formUri).request().post(form)) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + assertThat(response.readEntity(String.class), is(EXPECTED_TEST_MESSAGE)); + } + + try (Response response = client.target(webTarget.getUri()).path("/oidc/logout") + .request() + .get()) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + assertThat(response.readEntity(String.class), is(EXPECTED_POST_LOGOUT_TEST_MESSAGE)); + } + + try (Response response = client.target(webTarget.getUri()).path("/oidc/logout") + .request() + .get()) { + //There should be not token present among the cookies since it was cleared by the previous call + assertThat(response.getStatus(), is(Response.Status.FORBIDDEN.getStatusCode())); + } + } + private String getRequestUri(String html) { Document document = Jsoup.parse(html); return document.getElementById("kc-form-login").attr("action"); diff --git a/tests/integration/oidc/src/test/resources/application.yaml b/tests/integration/oidc/src/test/resources/application.yaml index acedf65eb5c..d5286506cb1 100644 --- a/tests/integration/oidc/src/test/resources/application.yaml +++ b/tests/integration/oidc/src/test/resources/application.yaml @@ -31,6 +31,8 @@ security: redirect-uri: "/oidc/redirect" audience: "account" header-use: true + logout-enabled: true + post-logout-uri: "/test/postLogout" client-id: "clientOne" client-secret: "F5s4VBtMJF3SMdiIRkLEXioM9UPf34OR" identity-uri: "http://localhost:8080/realms/test/"