Better management of the CSP header

Closes https://github.com/keycloak/keycloak/issues/24568

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2023-11-02 17:11:20 +01:00 committed by Marek Posolda
parent b4f791b632
commit 2b769e5129
8 changed files with 305 additions and 126 deletions

View file

@ -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; package org.keycloak.models;
import java.util.LinkedHashMap;
import java.util.Map;
public class ContentSecurityPolicyBuilder { public class ContentSecurityPolicyBuilder {
private String frameSrc = "'self'"; // constants for directive names used in the class
private String frameAncestors = "'self'"; public static final String DIRECTIVE_NAME_FRAME_SRC = "frame-src";
private String objectSrc = "'none'"; public static final String DIRECTIVE_NAME_FRAME_ANCESTORS = "frame-ancestors";
public static final String DIRECTIVE_NAME_OBJECT_SRC = "object-src";
private boolean first; // constants for specific directive value keywords
private StringBuilder sb; public static final String DIRECTIVE_VALUE_SELF = "'self'";
public static final String DIRECTIVE_VALUE_NONE = "'none'";
private final Map<String, String> directives = new LinkedHashMap<>();
public static ContentSecurityPolicyBuilder create() { 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) { 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; 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) { 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; return this;
} }
public ContentSecurityPolicyBuilder addFrameAncestors(String frameancestors) {
return add(DIRECTIVE_NAME_FRAME_ANCESTORS, frameancestors);
}
public String build() { public String build() {
sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
first = true; if (!directives.isEmpty()) {
for (Map.Entry<String, String> entry : directives.entrySet()) {
build("frame-src", frameSrc); sb.append(entry.getKey());
build("frame-ancestors", frameAncestors); if (!entry.getValue().isEmpty()) {
build("object-src", objectSrc); sb.append(" ").append(entry.getValue());
}
sb.append("; ");
}
sb.setLength(sb.length() - 1);
}
return sb.toString(); return sb.toString();
} }
private void build(String k, String v) { private ContentSecurityPolicyBuilder put(String name, String value) {
if (v != null) { if (name != null && value != null) {
if (!first) { directives.put(name, value);
sb.append(" ");
}
first = false;
sb.append(k).append(" ").append(v).append(";");
} }
return this;
} }
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;
}
} }

View file

@ -24,6 +24,24 @@ public class BrowserSecurityHeadersTest {
assertEquals("frame-ancestors 'self'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameSrc(null).build()); 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 '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 '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 @Test

View file

@ -106,22 +106,24 @@ public class DefaultSecurityHeadersProvider implements SecurityHeadersProvider {
// TODO This will be refactored as part of introducing a more strict CSP header // TODO This will be refactored as part of introducing a more strict CSP header
if (options != null) { if (options != null) {
ContentSecurityPolicyBuilder csp = ContentSecurityPolicyBuilder.create(); ContentSecurityPolicyBuilder csp = ContentSecurityPolicyBuilder.create(
headers.getFirst(CONTENT_SECURITY_POLICY.getHeaderName()).toString());
if (options.isAllowAnyFrameAncestor()) { if (options.isAllowAnyFrameAncestor()) {
headers.remove(BrowserSecurityHeaders.X_FRAME_OPTIONS.getHeaderName()); 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(); String allowedFrameSrc = options.getAllowedFrameSrc();
if (allowedFrameSrc != null) { 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());
}
} }
} }

View file

@ -67,7 +67,6 @@ public class FrontChannelLogoutHandler {
allowFrameSrc.append(client.frontChannelLogoutUrl.getAuthority()).append(' '); allowFrameSrc.append(client.frontChannelLogoutUrl.getAuthority()).append(' ');
} }
session.getProvider(SecurityHeadersProvider.class).options().allowAnyFrameAncestor();
session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(allowFrameSrc.toString()); session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(allowFrameSrc.toString());
} }

View file

@ -68,6 +68,11 @@ public class ClientAttributeUpdater extends ServerResourceUpdater<ClientAttribut
return this; return this;
} }
public ClientAttributeUpdater setName(String name) {
this.rep.setName(name);
return this;
}
public ClientAttributeUpdater setAttribute(String name, String value) { public ClientAttributeUpdater setAttribute(String name, String value) {
this.rep.getAttributes().put(name, value); this.rep.getAttributes().put(name, value);
if (value != null && !this.origRep.getAttributes().containsKey(name)) { if (value != null && !this.origRep.getAttributes().containsKey(name)) {

View file

@ -174,4 +174,9 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
rep.getSmtpServer().put(name, value); rep.getSmtpServer().put(name, value);
return this; return this;
} }
public RealmAttributeUpdater setBrowserSecurityHeader(String name, String value) {
rep.getBrowserSecurityHeaders().put(name, value);
return this;
}
} }

View file

@ -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"));
}
}
}

View file

@ -28,18 +28,14 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.util.UriUtils; import org.keycloak.common.util.UriUtils;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; 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.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
@ -1032,98 +1028,6 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
events.expectLogoutError(Errors.LOGOUT_FAILED).assertEvent(); 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 @Test
public void logoutWithIdTokenAndDisabledClientMustWork() throws Exception { public void logoutWithIdTokenAndDisabledClientMustWork() throws Exception {
OAuthClient.AccessTokenResponse tokenResponse = loginUser(); OAuthClient.AccessTokenResponse tokenResponse = loginUser();