Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4.x] OIDC logout functionality fixed #6131

Merged
merged 1 commit into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> idTokenCookie = req.headers()
.cookies()
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);

Expand Down Expand Up @@ -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));
Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ public CompletionStage<OutboundSecurityResponse> outboundSecurity(ProviderReques
provides = {AuthenticationProvider.class, SecurityProvider.class})
public static final class Builder implements io.helidon.common.Builder<Builder, OidcProvider> {

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<TenantConfigProvider> tenantConfigProviders = HelidonServiceLoader
.builder(ServiceLoader.load(TenantConfigProvider.class))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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!";
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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> 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");
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/oidc/src/test/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down