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:
parent
b4f791b632
commit
2b769e5129
8 changed files with 305 additions and 126 deletions
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -106,24 +106,26 @@ 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());
|
||||||
|
|
||||||
|
if (csp.isDefaultFrameAncestors()) {
|
||||||
|
// only remove frame ancestors if defined to default 'self'
|
||||||
csp.frameAncestors(null);
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void addHeader(BrowserSecurityHeaders header, MultivaluedMap<String, Object> headers) {
|
private void addHeader(BrowserSecurityHeaders header, MultivaluedMap<String, Object> headers) {
|
||||||
String value = headerValues.getOrDefault(header.getKey(), header.getDefaultValue());
|
String value = headerValues.getOrDefault(header.getKey(), header.getDefaultValue());
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue