diff --git a/server-spi-private/src/main/java/org/keycloak/models/ContentSecurityPolicyBuilder.java b/server-spi-private/src/main/java/org/keycloak/models/ContentSecurityPolicyBuilder.java index b220c1f50f0f..080854e15b88 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/ContentSecurityPolicyBuilder.java +++ b/server-spi-private/src/main/java/org/keycloak/models/ContentSecurityPolicyBuilder.java @@ -1,48 +1,129 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.keycloak.models; +import java.util.LinkedHashMap; +import java.util.Map; + public class ContentSecurityPolicyBuilder { - private String frameSrc = "'self'"; - private String frameAncestors = "'self'"; - private String objectSrc = "'none'"; + // constants for directive names used in the class + public static final String DIRECTIVE_NAME_FRAME_SRC = "frame-src"; + public static final String DIRECTIVE_NAME_FRAME_ANCESTORS = "frame-ancestors"; + public static final String DIRECTIVE_NAME_OBJECT_SRC = "object-src"; + + // constants for specific directive value keywords + public static final String DIRECTIVE_VALUE_SELF = "'self'"; + public static final String DIRECTIVE_VALUE_NONE = "'none'"; - private boolean first; - private StringBuilder sb; + private final Map directives = new LinkedHashMap<>(); public static ContentSecurityPolicyBuilder create() { - return new ContentSecurityPolicyBuilder(); + return new ContentSecurityPolicyBuilder() + .add(DIRECTIVE_NAME_FRAME_SRC, DIRECTIVE_VALUE_SELF) + .add(DIRECTIVE_NAME_FRAME_ANCESTORS, DIRECTIVE_VALUE_SELF) + .add(DIRECTIVE_NAME_OBJECT_SRC, DIRECTIVE_VALUE_NONE); + } + + public static ContentSecurityPolicyBuilder create(String directives) { + return new ContentSecurityPolicyBuilder().parse(directives); } public ContentSecurityPolicyBuilder frameSrc(String frameSrc) { - this.frameSrc = frameSrc; + if (frameSrc == null) { + directives.remove(DIRECTIVE_NAME_FRAME_SRC); + } else { + put(DIRECTIVE_NAME_FRAME_SRC, frameSrc); + } return this; } + public ContentSecurityPolicyBuilder addFrameSrc(String frameSrc) { + return add(DIRECTIVE_NAME_FRAME_SRC, frameSrc); + } + + public boolean isDefaultFrameAncestors() { + return DIRECTIVE_VALUE_SELF.equals(directives.get(DIRECTIVE_NAME_FRAME_ANCESTORS)); + } + public ContentSecurityPolicyBuilder frameAncestors(String frameancestors) { - this.frameAncestors = frameancestors; + if (frameancestors == null) { + directives.remove(DIRECTIVE_NAME_FRAME_ANCESTORS); + } else { + put(DIRECTIVE_NAME_FRAME_ANCESTORS, frameancestors); + } return this; } - public String build() { - sb = new StringBuilder(); - first = true; - - build("frame-src", frameSrc); - build("frame-ancestors", frameAncestors); - build("object-src", objectSrc); + public ContentSecurityPolicyBuilder addFrameAncestors(String frameancestors) { + return add(DIRECTIVE_NAME_FRAME_ANCESTORS, frameancestors); + } + public String build() { + StringBuilder sb = new StringBuilder(); + if (!directives.isEmpty()) { + for (Map.Entry entry : directives.entrySet()) { + sb.append(entry.getKey()); + if (!entry.getValue().isEmpty()) { + sb.append(" ").append(entry.getValue()); + } + sb.append("; "); + } + sb.setLength(sb.length() - 1); + } return sb.toString(); } - private void build(String k, String v) { - if (v != null) { - if (!first) { - sb.append(" "); - } - first = false; + private ContentSecurityPolicyBuilder put(String name, String value) { + if (name != null && value != null) { + directives.put(name, value); + } + return this; + } - sb.append(k).append(" ").append(v).append(";"); + private ContentSecurityPolicyBuilder add(String name, String value) { + if (name != null && value != null) { + String current = directives.get(name); + if (current != null && !current.isEmpty()) { + value = current + " " + value; + } + directives.put(name, value); } + return this; } + // W3C Working Draft: https://www.w3.org/TR/CSP/ + // Only managing spaces not the other whitespaces defined in the spec + private ContentSecurityPolicyBuilder parse(String value) { + if (value == null) { + return this; + } + String[] values = value.split(";"); + if (values != null) { + for (String directive : values) { + directive = directive.trim(); + int idx = directive.indexOf(' '); + if (idx > 0) { + add(directive.substring(0, idx), directive.substring(idx + 1, directive.length()).trim()); + } else if (!directive.isEmpty()) { + add(directive, ""); + } + } + } + return this; + } } diff --git a/server-spi-private/src/test/java/org/keycloak/models/BrowserSecurityHeadersTest.java b/server-spi-private/src/test/java/org/keycloak/models/BrowserSecurityHeadersTest.java index e4b39afc4aee..cb88f12d0289 100644 --- a/server-spi-private/src/test/java/org/keycloak/models/BrowserSecurityHeadersTest.java +++ b/server-spi-private/src/test/java/org/keycloak/models/BrowserSecurityHeadersTest.java @@ -24,6 +24,24 @@ public void contentSecurityPolicyBuilderTest() { assertEquals("frame-ancestors 'self'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameSrc(null).build()); assertEquals("frame-src 'self'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameAncestors(null).build()); assertEquals("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameSrc("'custom-frame-src'").frameAncestors("'custom-frame-ancestors'").build()); + assertEquals("frame-src localhost; frame-ancestors 'self'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameSrc("localhost").build()); + assertEquals("frame-src 'self' localhost; frame-ancestors 'self'; object-src 'none';", + ContentSecurityPolicyBuilder.create().addFrameSrc("localhost").build()); + } + + private void assertParsedDirectives(String directives) { + assertEquals(directives, ContentSecurityPolicyBuilder.create(directives).build()); + } + + @Test + public void parseSecurityPolicyBuilderTest() { + assertParsedDirectives("frame-src 'self'; frame-ancestors 'self'; object-src 'none';"); + assertParsedDirectives("frame-ancestors 'self'; object-src 'none';"); + assertParsedDirectives("frame-src 'self'; object-src 'none';"); + assertParsedDirectives("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none';"); + assertParsedDirectives("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none'; style-src 'self';"); + assertParsedDirectives("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none'; sandbox;"); + assertEquals("frame-src 'custom-frame-src'; sandbox;", ContentSecurityPolicyBuilder.create("frame-src 'custom-frame-src' ; sandbox ; ").build()); } @Test diff --git a/services/src/main/java/org/keycloak/headers/DefaultSecurityHeadersProvider.java b/services/src/main/java/org/keycloak/headers/DefaultSecurityHeadersProvider.java index 58af8967d980..b10b2b28dfdf 100644 --- a/services/src/main/java/org/keycloak/headers/DefaultSecurityHeadersProvider.java +++ b/services/src/main/java/org/keycloak/headers/DefaultSecurityHeadersProvider.java @@ -106,22 +106,24 @@ private void addHtmlHeaders(MultivaluedMap headers) { // TODO This will be refactored as part of introducing a more strict CSP header if (options != null) { - ContentSecurityPolicyBuilder csp = ContentSecurityPolicyBuilder.create(); + ContentSecurityPolicyBuilder csp = ContentSecurityPolicyBuilder.create( + headers.getFirst(CONTENT_SECURITY_POLICY.getHeaderName()).toString()); if (options.isAllowAnyFrameAncestor()) { headers.remove(BrowserSecurityHeaders.X_FRAME_OPTIONS.getHeaderName()); - csp.frameAncestors(null); + if (csp.isDefaultFrameAncestors()) { + // only remove frame ancestors if defined to default 'self' + csp.frameAncestors(null); + } } String allowedFrameSrc = options.getAllowedFrameSrc(); if (allowedFrameSrc != null) { - csp.frameSrc(allowedFrameSrc); + csp.addFrameSrc(allowedFrameSrc); } - if (CONTENT_SECURITY_POLICY.getDefaultValue().equals(headers.getFirst(CONTENT_SECURITY_POLICY.getHeaderName()))) { - headers.putSingle(CONTENT_SECURITY_POLICY.getHeaderName(), csp.build()); - } + headers.putSingle(CONTENT_SECURITY_POLICY.getHeaderName(), csp.build()); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/FrontChannelLogoutHandler.java b/services/src/main/java/org/keycloak/protocol/oidc/FrontChannelLogoutHandler.java index bcac389cc67b..f292aa89c0eb 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/FrontChannelLogoutHandler.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/FrontChannelLogoutHandler.java @@ -67,7 +67,6 @@ private void configureCSP() { allowFrameSrc.append(client.frontChannelLogoutUrl.getAuthority()).append(' '); } - session.getProvider(SecurityHeadersProvider.class).options().allowAnyFrameAncestor(); session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(allowFrameSrc.toString()); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java index 7e04bb8cb84b..d3fd229b7010 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java @@ -68,6 +68,11 @@ public ClientAttributeUpdater setClientId(String clientId) { return this; } + public ClientAttributeUpdater setName(String name) { + this.rep.setName(name); + return this; + } + public ClientAttributeUpdater setAttribute(String name, String value) { this.rep.getAttributes().put(name, value); if (value != null && !this.origRep.getAttributes().containsKey(name)) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java index 0473cea9bbb3..1dca3561b5a2 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java @@ -174,4 +174,9 @@ public RealmAttributeUpdater setSmtpServer(String name, String value) { rep.getSmtpServer().put(name, value); return this; } + + public RealmAttributeUpdater setBrowserSecurityHeader(String name, String value) { + rep.getBrowserSecurityHeaders().put(name, value); + return this; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RPInitiatedFrontChannelLogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RPInitiatedFrontChannelLogoutTest.java new file mode 100644 index 000000000000..fa1f0598849b --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RPInitiatedFrontChannelLogoutTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.forms; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.models.BrowserSecurityHeaders; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; +import org.keycloak.representations.IDToken; +import org.keycloak.representations.LogoutToken; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.updaters.ClientAttributeUpdater; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; +import org.keycloak.testsuite.util.OAuthClient; + +/** + * + * @author rmartinc + */ +public class RPInitiatedFrontChannelLogoutTest extends AbstractTestRealmKeycloakTest { + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + // no-op + } + + @Test + public void testFrontChannelLogoutWithPostLogoutRedirectUri() throws Exception { + ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); + ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); + rep.setFrontchannelLogout(true); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, OAuthClient.APP_ROOT + "/admin/frontchannelLogout"); + clients.get(rep.getId()).update(rep); + try { + oauth.clientSessionState("client-session"); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String idTokenString = tokenResponse.getIdToken(); + String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString) + .postLogoutRedirectUri(OAuthClient.APP_AUTH_ROOT).build(); + driver.navigate().to(logoutUrl); + LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); + Assert.assertNotNull(logoutToken); + + IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class); + + Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer()); + Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId()); + } finally { + rep.setFrontchannelLogout(false); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); + clients.get(rep.getId()).update(rep); + } + } + + @Test + public void testFrontChannelLogoutWithoutSessionRequired() throws Exception { + ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); + ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); + rep.setFrontchannelLogout(true); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, OAuthClient.APP_ROOT + "/admin/frontchannelLogout"); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, "false"); + clients.get(rep.getId()).update(rep); + try { + oauth.clientSessionState("client-session"); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String idTokenString = tokenResponse.getIdToken(); + String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString) + .postLogoutRedirectUri(OAuthClient.APP_AUTH_ROOT).build(); + driver.navigate().to(logoutUrl); + LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); + Assert.assertNotNull(logoutToken); + + Assert.assertNull(logoutToken.getIssuer()); + Assert.assertNull(logoutToken.getSid()); + } finally { + rep.setFrontchannelLogout(false); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, "true"); + clients.get(rep.getId()).update(rep); + } + } + + @Test + public void testFrontChannelLogout() throws Exception { + ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); + ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); + rep.setName("My Testing App"); + rep.setFrontchannelLogout(true); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, OAuthClient.APP_ROOT + "/admin/frontchannelLogout"); + clients.get(rep.getId()).update(rep); + try { + oauth.clientSessionState("client-session"); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String idTokenString = tokenResponse.getIdToken(); + String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString).build(); + driver.navigate().to(logoutUrl); + LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); + org.keycloak.testsuite.Assert.assertNotNull(logoutToken); + IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class); + org.keycloak.testsuite.Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer()); + org.keycloak.testsuite.Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId()); + Assert.assertTrue(driver.getTitle().equals("Logging out")); + Assert.assertTrue(driver.getPageSource().contains("You are logging out from following apps")); + Assert.assertTrue(driver.getPageSource().contains("My Testing App")); + } finally { + rep.setFrontchannelLogout(false); + rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); + clients.get(rep.getId()).update(rep); + } + } + + @Test + public void testFrontChannelLogoutCustomCSP() throws Exception { + try (RealmAttributeUpdater realmUpdater = new RealmAttributeUpdater(adminClient.realm(oauth.getRealm())) + .setBrowserSecurityHeader(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY.getKey(), + "frame-src 'keycloak.org'; frame-ancestors 'self'; object-src 'none'; style-src 'self';") + .update(); + ClientAttributeUpdater clientUpdater = ClientAttributeUpdater.forClient(adminClient, oauth.getRealm(), oauth.getClientId()) + .setName("My Testing App") + .setFrontchannelLogout(true) + .setAttribute(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, OAuthClient.APP_ROOT + "/admin/frontchannelLogout") + .update()) { + oauth.clientSessionState("client-session"); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String idTokenString = tokenResponse.getIdToken(); + String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString).build(); + driver.navigate().to(logoutUrl); + LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); + Assert.assertNotNull(logoutToken); + IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class); + Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer()); + Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId()); + Assert.assertTrue(driver.getTitle().equals("Logging out")); + Assert.assertTrue(driver.getPageSource().contains("You are logging out from following apps")); + Assert.assertTrue(driver.getPageSource().contains("My Testing App")); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java index a56ebeee1ccc..e5b9bfe16c93 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java @@ -28,18 +28,14 @@ import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; -import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.util.UriUtils; import org.keycloak.events.Details; import org.keycloak.events.Errors; -import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.Constants; import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.representations.IDToken; -import org.keycloak.representations.LogoutToken; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.Assert; @@ -1032,98 +1028,6 @@ public void testIncorrectChangingParameters() throws IOException { events.expectLogoutError(Errors.LOGOUT_FAILED).assertEvent(); } - - @Test - public void testFrontChannelLogoutWithPostLogoutRedirectUri() throws Exception { - ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); - ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); - rep.setFrontchannelLogout(true); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout"); - clients.get(rep.getId()).update(rep); - try { - oauth.clientSessionState("client-session"); - oauth.doLogin("test-user@localhost", "password"); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); - String idTokenString = tokenResponse.getIdToken(); - String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString) - .postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build(); - driver.navigate().to(logoutUrl); - LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); - Assert.assertNotNull(logoutToken); - - IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class); - - Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer()); - Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId()); - } finally { - rep.setFrontchannelLogout(false); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); - clients.get(rep.getId()).update(rep); - } - } - - @Test - public void testFrontChannelLogoutWithoutSessionRequired() throws Exception { - ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); - ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); - rep.setFrontchannelLogout(true); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout"); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, "false"); - clients.get(rep.getId()).update(rep); - try { - oauth.clientSessionState("client-session"); - oauth.doLogin("test-user@localhost", "password"); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); - String idTokenString = tokenResponse.getIdToken(); - String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString) - .postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build(); - driver.navigate().to(logoutUrl); - LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); - Assert.assertNotNull(logoutToken); - - Assert.assertNull(logoutToken.getIssuer()); - Assert.assertNull(logoutToken.getSid()); - } finally { - rep.setFrontchannelLogout(false); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, "true"); - clients.get(rep.getId()).update(rep); - } - } - - @Test - public void testFrontChannelLogout() throws Exception { - ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); - ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); - rep.setName("My Testing App"); - rep.setFrontchannelLogout(true); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout"); - clients.get(rep.getId()).update(rep); - try { - oauth.clientSessionState("client-session"); - oauth.doLogin("test-user@localhost", "password"); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); - String idTokenString = tokenResponse.getIdToken(); - String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString).build(); - driver.navigate().to(logoutUrl); - LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken(); - Assert.assertNotNull(logoutToken); - IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class); - Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer()); - Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId()); - assertTrue(driver.getTitle().equals("Logging out")); - assertTrue(driver.getPageSource().contains("You are logging out from following apps")); - assertTrue(driver.getPageSource().contains("My Testing App")); - } finally { - rep.setFrontchannelLogout(false); - rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, ""); - clients.get(rep.getId()).update(rep); - } - } - @Test public void logoutWithIdTokenAndDisabledClientMustWork() throws Exception { OAuthClient.AccessTokenResponse tokenResponse = loginUser();