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;
|
||||
|
||||
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";
|
||||
|
||||
private boolean first;
|
||||
private StringBuilder sb;
|
||||
// constants for specific directive value keywords
|
||||
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() {
|
||||
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 ContentSecurityPolicyBuilder addFrameAncestors(String frameancestors) {
|
||||
return add(DIRECTIVE_NAME_FRAME_ANCESTORS, frameancestors);
|
||||
}
|
||||
|
||||
public String build() {
|
||||
sb = new StringBuilder();
|
||||
first = true;
|
||||
|
||||
build("frame-src", frameSrc);
|
||||
build("frame-ancestors", frameAncestors);
|
||||
build("object-src", objectSrc);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!directives.isEmpty()) {
|
||||
for (Map.Entry<String, String> 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;
|
||||
|
||||
sb.append(k).append(" ").append(v).append(";");
|
||||
private ContentSecurityPolicyBuilder put(String name, String value) {
|
||||
if (name != null && value != null) {
|
||||
directives.put(name, value);
|
||||
}
|
||||
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-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
|
||||
|
|
|
@ -106,24 +106,26 @@ public class DefaultSecurityHeadersProvider implements SecurityHeadersProvider {
|
|||
|
||||
// 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());
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addHeader(BrowserSecurityHeaders header, MultivaluedMap<String, Object> headers) {
|
||||
String value = headerValues.getOrDefault(header.getKey(), header.getDefaultValue());
|
||||
|
|
|
@ -67,7 +67,6 @@ public class FrontChannelLogoutHandler {
|
|||
allowFrameSrc.append(client.frontChannelLogoutUrl.getAuthority()).append(' ');
|
||||
}
|
||||
|
||||
session.getProvider(SecurityHeadersProvider.class).options().allowAnyFrameAncestor();
|
||||
session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(allowFrameSrc.toString());
|
||||
}
|
||||
|
||||
|
|
|
@ -68,6 +68,11 @@ public class ClientAttributeUpdater extends ServerResourceUpdater<ClientAttribut
|
|||
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)) {
|
||||
|
|
|
@ -174,4 +174,9 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
|
|||
rep.getSmtpServer().put(name, value);
|
||||
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.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 class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
|
|||
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();
|
||||
|
|
Loading…
Reference in a new issue