KEYCLOAK-13128 Security Headers SPI and response filter

This commit is contained in:
stianst 2020-04-22 11:20:14 +02:00 committed by Stian Thorgersen
parent b40c12c712
commit 5b017e930d
42 changed files with 680 additions and 159 deletions

View file

@ -137,7 +137,7 @@ public class RealmEntity {
@Column(name="EMAIL_THEME") @Column(name="EMAIL_THEME")
protected String emailTheme; protected String emailTheme;
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm", fetch = FetchType.EAGER)
Collection<RealmAttributeEntity> attributes = new ArrayList<>(); Collection<RealmAttributeEntity> attributes = new ArrayList<>();
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")

View file

@ -0,0 +1,29 @@
/*
* Copyright 2020 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.headers;
public interface SecurityHeadersOptions {
SecurityHeadersOptions allowFrameSrc(String source);
SecurityHeadersOptions allowAnyFrameAncestor();
SecurityHeadersOptions skipHeaders();
SecurityHeadersOptions allowEmptyContentType();
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2020 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.headers;
import org.keycloak.provider.Provider;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
public interface SecurityHeadersProvider extends Provider {
SecurityHeadersOptions options();
void addHeaders(ContainerRequestContext requestContext, ContainerResponseContext responseContext);
@Override
default void close() {
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2020 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.headers;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderFactory;
public interface SecurityHeadersProviderFactory extends ProviderFactory<SecurityHeadersProvider> {
@Override
default void init(Config.Scope config) {
}
@Override
default void postInit(KeycloakSessionFactory factory) {
}
@Override
default void close() {
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright 2020 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.headers;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class SecurityHeadersSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "security-headers";
}
@Override
public Class<? extends Provider> getProviderClass() {
return SecurityHeadersProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return SecurityHeadersProviderFactory.class;
}
}

View file

@ -35,7 +35,7 @@ public class BrowserSecurityHeaders {
public static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy"; public static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
public static final String CONTENT_SECURITY_POLICY_DEFAULT = "frame-src 'self'; frame-ancestors 'self'; object-src 'none';"; public static final String CONTENT_SECURITY_POLICY_DEFAULT = ContentSecurityPolicyBuilder.create().build();
public static final String CONTENT_SECURITY_POLICY_KEY = "contentSecurityPolicy"; public static final String CONTENT_SECURITY_POLICY_KEY = "contentSecurityPolicy";
@ -94,4 +94,52 @@ public class BrowserSecurityHeaders {
defaultHeaders = Collections.unmodifiableMap(dh); defaultHeaders = Collections.unmodifiableMap(dh);
headerAttributeMap = Collections.unmodifiableMap(headerMap); headerAttributeMap = Collections.unmodifiableMap(headerMap);
} }
public static class ContentSecurityPolicyBuilder {
private String frameSrc = "self";
private String frameAncestors = "self";
private String objectSrc = "none";
private boolean first;
private StringBuilder sb;
public static ContentSecurityPolicyBuilder create() {
return new ContentSecurityPolicyBuilder();
}
public ContentSecurityPolicyBuilder frameSrc(String frameSrc) {
this.frameSrc = frameSrc;
return this;
}
public ContentSecurityPolicyBuilder frameAncestors(String frameancestors) {
this.frameAncestors = frameancestors;
return this;
}
public String build() {
sb = new StringBuilder();
first = true;
build("frame-src", frameSrc);
build("frame-ancestors", frameAncestors);
build("object-src", objectSrc);
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("';");
}
}
}
} }

View file

@ -79,3 +79,4 @@ org.keycloak.vault.VaultSpi
org.keycloak.crypto.CekManagementSpi org.keycloak.crypto.CekManagementSpi
org.keycloak.crypto.ContentEncryptionSpi org.keycloak.crypto.ContentEncryptionSpi
org.keycloak.validation.ClientValidationSPI org.keycloak.validation.ClientValidationSPI
org.keycloak.headers.SecurityHeadersSpi

View file

@ -0,0 +1,17 @@
package org.keycloak.models;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class BrowserSecurityHeadersTest {
@Test
public void contentSecurityPolicyBuilderTest() {
assertEquals("frame-src 'self'; frame-ancestors 'self'; object-src 'none';", BrowserSecurityHeaders.ContentSecurityPolicyBuilder.create().build());
assertEquals("frame-ancestors 'self'; object-src 'none';", BrowserSecurityHeaders.ContentSecurityPolicyBuilder.create().frameSrc(null).build());
assertEquals("frame-src 'self'; object-src 'none';", BrowserSecurityHeaders.ContentSecurityPolicyBuilder.create().frameAncestors(null).build());
assertEquals("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none';", BrowserSecurityHeaders.ContentSecurityPolicyBuilder.create().frameSrc("custom-frame-src").frameAncestors("custom-frame-ancestors").build());
}
}

View file

@ -26,6 +26,7 @@ import java.util.stream.Collectors;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
@ -160,7 +161,7 @@ public class PolicyService {
Policy model = storeFactory.getPolicyStore().findByName(name, this.resourceServer.getId()); Policy model = storeFactory.getPolicyStore().findByName(name, this.resourceServer.getId());
if (model == null) { if (model == null) {
return Response.status(Status.OK).build(); throw new NotFoundException();
} }
return Response.ok(toRepresentation(model, fields, authorization)).build(); return Response.ok(toRepresentation(model, fields, authorization)).build();
@ -219,7 +220,7 @@ public class PolicyService {
Set<String> resources = resourceStore.findByResourceServer(resourceFilters, resourceServer.getId(), -1, 1).stream().map(Resource::getId).collect(Collectors.toSet()); Set<String> resources = resourceStore.findByResourceServer(resourceFilters, resourceServer.getId(), -1, 1).stream().map(Resource::getId).collect(Collectors.toSet());
if (resources.isEmpty()) { if (resources.isEmpty()) {
return Response.ok().build(); return Response.noContent().build();
} }
search.put("resource", resources.toArray(new String[resources.size()])); search.put("resource", resources.toArray(new String[resources.size()]));
@ -240,7 +241,7 @@ public class PolicyService {
Set<String> scopes = scopeStore.findByResourceServer(scopeFilters, resourceServer.getId(), -1, 1).stream().map(Scope::getId).collect(Collectors.toSet()); Set<String> scopes = scopeStore.findByResourceServer(scopeFilters, resourceServer.getId(), -1, 1).stream().map(Scope::getId).collect(Collectors.toSet());
if (scopes.isEmpty()) { if (scopes.isEmpty()) {
return Response.ok().build(); return Response.noContent().build();
} }
search.put("scope", scopes.toArray(new String[scopes.size()])); search.put("scope", scopes.toArray(new String[scopes.size()]));

View file

@ -25,6 +25,7 @@ import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException; import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -40,6 +41,7 @@ import org.keycloak.util.JsonSerialization;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
@ -109,6 +111,9 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
} }
} }
// TODO Empty content with ok makes no sense. Should it display a page? Or use noContent?
session.getProvider(SecurityHeadersProvider.class).options().allowEmptyContentType();
return Response.ok().build(); return Response.ok().build();
} }

View file

@ -38,7 +38,6 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.theme.BrowserSecurityHeaderSetup;
import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.FreeMarkerUtil; import org.keycloak.theme.FreeMarkerUtil;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
@ -50,7 +49,6 @@ import org.keycloak.theme.beans.MessageType;
import org.keycloak.theme.beans.MessagesPerFieldBean; import org.keycloak.theme.beans.MessagesPerFieldBean;
import org.keycloak.utils.MediaType; import org.keycloak.utils.MediaType;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -271,7 +269,6 @@ public class FreeMarkerAccountProvider implements AccountProvider {
try { try {
String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme); String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme);
Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML_UTF_8_TYPE).language(locale).entity(result); Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML_UTF_8_TYPE).language(locale).entity(result);
BrowserSecurityHeaderSetup.headers(builder, realm);
builder.cacheControl(CacheControlUtil.noCache()); builder.cacheControl(CacheControlUtil.noCache());
return builder.build(); return builder.build();
} catch (FreeMarkerException e) { } catch (FreeMarkerException e) {

View file

@ -53,7 +53,6 @@ import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.theme.BrowserSecurityHeaderSetup;
import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.FreeMarkerUtil; import org.keycloak.theme.FreeMarkerUtil;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
@ -462,7 +461,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
String result = freeMarker.processTemplate(attributes, templateName, theme); String result = freeMarker.processTemplate(attributes, templateName, theme);
javax.ws.rs.core.MediaType mediaType = contentType == null ? MediaType.TEXT_HTML_UTF_8_TYPE : contentType; javax.ws.rs.core.MediaType mediaType = contentType == null ? MediaType.TEXT_HTML_UTF_8_TYPE : contentType;
Response.ResponseBuilder builder = Response.status(status == null ? Response.Status.OK : status).type(mediaType).language(locale).entity(result); Response.ResponseBuilder builder = Response.status(status == null ? Response.Status.OK : status).type(mediaType).language(locale).entity(result);
BrowserSecurityHeaderSetup.headers(builder, realm);
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) { for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
builder.header(entry.getKey(), entry.getValue()); builder.header(entry.getKey(), entry.getValue());
} }

View file

@ -0,0 +1,64 @@
/*
* Copyright 2020 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.headers;
public class DefaultSecurityHeadersOptions implements SecurityHeadersOptions {
private boolean skipHeaders;
private boolean allowAnyFrameAncestor;
private boolean allowEmptyContentType;
private String allowedFrameSrc;
public SecurityHeadersOptions allowFrameSrc(String source) {
allowedFrameSrc = source;
return this;
}
@Override
public SecurityHeadersOptions allowAnyFrameAncestor() {
allowAnyFrameAncestor = true;
return this;
}
public SecurityHeadersOptions skipHeaders() {
skipHeaders = true;
return this;
}
@Override
public SecurityHeadersOptions allowEmptyContentType() {
allowEmptyContentType = true;
return this;
}
String getAllowedFrameSrc() {
return allowedFrameSrc;
}
boolean isAllowAnyFrameAncestor() {
return allowAnyFrameAncestor;
}
boolean isSkipHeaders() {
return skipHeaders;
}
public boolean isAllowEmptyContentType() {
return allowEmptyContentType;
}
}

View file

@ -0,0 +1,168 @@
/*
* Copyright 2020 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.headers;
import org.jboss.logging.Logger;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import java.util.Map;
public class DefaultSecurityHeadersProvider implements SecurityHeadersProvider {
private static final Logger LOGGER = Logger.getLogger(DefaultSecurityHeadersProvider.class);
private final Map<String, String> headerValues;
private final KeycloakSession session;
private DefaultSecurityHeadersOptions options;
public DefaultSecurityHeadersProvider(KeycloakSession session) {
this.session = session;
RealmModel realm = session.getContext().getRealm();
if (realm != null) {
headerValues = realm.getBrowserSecurityHeaders();
} else {
headerValues = BrowserSecurityHeaders.defaultHeaders;
}
}
@Override
public SecurityHeadersOptions options() {
if (options == null) {
options = new DefaultSecurityHeadersOptions();
}
return options;
}
@Override
public void addHeaders(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
if (options != null && options.isSkipHeaders()) {
return;
}
MediaType requestType = requestContext.getMediaType();
MediaType responseType = responseContext.getMediaType();
MultivaluedMap<String, Object> headers = responseContext.getHeaders();
if (responseType == null && !isEmptyMediaTypeAllowed(requestContext, responseContext)) {
LOGGER.errorv("MediaType not set on path {0}, with response status {1}", session.getContext().getUri().getRequestUri().getPath(), responseContext.getStatus());
throw new InternalServerErrorException();
}
if (isRest(requestType, responseType)) {
addRestHeaders(headers);
} else if (isHtml(requestType, responseType)) {
addHtmlHeaders(headers);
} else {
addGenericHeaders(headers);
}
}
private void addGenericHeaders(MultivaluedMap<String, Object> headers) {
addHeader(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY_KEY, headers);
addHeader(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS_KEY, headers);
addHeader(BrowserSecurityHeaders.X_XSS_PROTECTION_KEY, headers);
}
private void addRestHeaders(MultivaluedMap<String, Object> headers) {
addHeader(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY_KEY, headers);
addHeader(BrowserSecurityHeaders.X_FRAME_OPTIONS_KEY, headers);
addHeader(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS_KEY, headers);
addHeader(BrowserSecurityHeaders.X_XSS_PROTECTION_KEY, headers);
}
private void addHtmlHeaders(MultivaluedMap<String, Object> headers) {
BrowserSecurityHeaders.headerAttributeMap.keySet().forEach(k -> addHeader(k, headers));
// TODO This will be refactored as part of introducing a more strict CSP header
if (options != null) {
BrowserSecurityHeaders.ContentSecurityPolicyBuilder csp = BrowserSecurityHeaders.ContentSecurityPolicyBuilder.create();
if (options.isAllowAnyFrameAncestor()) {
headers.remove(BrowserSecurityHeaders.X_FRAME_OPTIONS);
csp.frameAncestors(null);
}
String allowedFrameSrc = options.getAllowedFrameSrc();
if (allowedFrameSrc != null) {
csp.frameSrc(allowedFrameSrc);
}
if (BrowserSecurityHeaders.CONTENT_SECURITY_POLICY_DEFAULT.equals(headers.getFirst(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY))) {
headers.putSingle(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY, csp.build());
}
}
}
private void addHeader(String key, MultivaluedMap<String, Object> headers) {
String header = BrowserSecurityHeaders.headerAttributeMap.get(key);
String value = headerValues.get(key);
if (value != null && !value.isEmpty()) {
headers.putSingle(header, value);
}
}
/**
* Prevent responses without content-type unless explicitly safe to do so
*/
private boolean isEmptyMediaTypeAllowed(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
if (!responseContext.hasEntity()) {
if (options != null && options.isAllowEmptyContentType()) {
return true;
}
int status = responseContext.getStatus();
if (status == 201 || status == 204 || status == 301 || status == 302 || status == 303 || status == 400 || status == 401 || status == 403 || status == 404) {
return true;
}
if (requestContext.getMethod().equalsIgnoreCase("OPTIONS")) {
return true;
}
}
return false;
}
private boolean isRest(MediaType requestType, MediaType responseType) {
MediaType mediaType = responseType != null ? responseType : requestType;
return matches(mediaType, MediaType.APPLICATION_JSON_TYPE) || matches(mediaType, MediaType.APPLICATION_XML_TYPE);
}
private boolean isHtml(MediaType requestType, MediaType responseType) {
if (matches(responseType, MediaType.TEXT_HTML_TYPE)) {
return true;
} else if (matches(requestType, MediaType.APPLICATION_FORM_URLENCODED_TYPE)) {
return true;
}
return false;
}
private boolean matches(MediaType a, MediaType b) {
if (a == null) {
return b == null;
}
return a.getType().equalsIgnoreCase(b.getType()) && a.getSubtype().equalsIgnoreCase(b.getSubtype());
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright 2020 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.headers;
import org.keycloak.models.KeycloakSession;
public class DefaultSecurityHeadersProviderFactory implements SecurityHeadersProviderFactory {
@Override
public SecurityHeadersProvider create(KeycloakSession session) {
return new DefaultSecurityHeadersProvider(session);
}
@Override
public String getId() {
return "default";
}
}

View file

@ -26,7 +26,9 @@ import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext; import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
@ -47,6 +49,7 @@ import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
import org.keycloak.utils.MediaType;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@ -335,6 +338,8 @@ public class OIDCLoginProtocol implements LoginProtocol {
uriBuilder.queryParam(STATE_PARAM, state); uriBuilder.queryParam(STATE_PARAM, state);
return Response.status(302).location(uriBuilder.build()).build(); return Response.status(302).location(uriBuilder.build()).build();
} else { } else {
// TODO Empty content with ok makes no sense. Should it display a page? Or use noContent?
session.getProvider(SecurityHeadersProvider.class).options().allowEmptyContentType();
return Response.ok().build(); return Response.ok().build();
} }
} }

View file

@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc.endpoints;
import org.keycloak.common.Version; import org.keycloak.common.Version;
import org.keycloak.common.util.UriUtils; import org.keycloak.common.util.UriUtils;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -29,7 +30,6 @@ import org.keycloak.utils.MediaType;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.CacheControl;
@ -63,6 +63,7 @@ public class LoginStatusIframeEndpoint {
InputStream resource = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html"); InputStream resource = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html");
if (resource != null) { if (resource != null) {
P3PHelper.addP3PHeader(); P3PHelper.addP3PHeader();
session.getProvider(SecurityHeadersProvider.class).options().allowAnyFrameAncestor();
return Response.ok(resource).cacheControl(cacheControl).build(); return Response.ok(resource).cacheControl(cacheControl).build();
} else { } else {
return Response.status(Response.Status.NOT_FOUND).build(); return Response.status(Response.Status.NOT_FOUND).build();

View file

@ -29,6 +29,7 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -157,6 +158,8 @@ public class LogoutEndpoint {
if (state != null) uriBuilder.queryParam(OIDCLoginProtocol.STATE_PARAM, state); if (state != null) uriBuilder.queryParam(OIDCLoginProtocol.STATE_PARAM, state);
return Response.status(302).location(uriBuilder.build()).build(); return Response.status(302).location(uriBuilder.build()).build();
} else { } else {
// TODO Empty content with ok makes no sense. Should it display a page? Or use noContent?
session.getProvider(SecurityHeadersProvider.class).options().allowEmptyContentType();
return Response.ok().build(); return Response.ok().build();
} }
} }

View file

@ -26,7 +26,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakUriInfo; import org.keycloak.models.KeycloakUriInfo;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.urls.UrlType; import org.keycloak.urls.UrlType;

View file

@ -0,0 +1,39 @@
/*
* Copyright 2016 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.services.filters;
import org.keycloak.common.util.Resteasy;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.KeycloakSession;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class KeycloakSecurityHeadersFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
KeycloakSession session = Resteasy.getContextData(KeycloakSession.class);
SecurityHeadersProvider securityHeadersProvider = session.getProvider(SecurityHeadersProvider.class);
securityHeadersProvider.addHeaders(requestContext, responseContext);
}
}

View file

@ -42,6 +42,7 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.DefaultKeycloakSessionFactory; import org.keycloak.services.DefaultKeycloakSessionFactory;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.error.KeycloakErrorHandler; import org.keycloak.services.error.KeycloakErrorHandler;
import org.keycloak.services.filters.KeycloakSecurityHeadersFilter;
import org.keycloak.services.filters.KeycloakTransactionCommitter; import org.keycloak.services.filters.KeycloakTransactionCommitter;
import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
@ -114,6 +115,7 @@ public class KeycloakApplication extends Application {
classes.add(ThemeResource.class); classes.add(ThemeResource.class);
classes.add(JsResource.class); classes.add(JsResource.class);
classes.add(KeycloakSecurityHeadersFilter.class);
classes.add(KeycloakTransactionCommitter.class); classes.add(KeycloakTransactionCommitter.class);
classes.add(KeycloakErrorHandler.class); classes.add(KeycloakErrorHandler.class);

View file

@ -21,16 +21,13 @@ import org.keycloak.common.ClientConnection;
import org.keycloak.common.Version; import org.keycloak.common.Version;
import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.MimeTypeUtil; import org.keycloak.common.util.MimeTypeUtil;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.ForbiddenException; import org.keycloak.services.ForbiddenException;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.CookieHelper; import org.keycloak.services.util.CookieHelper;
import org.keycloak.theme.BrowserSecurityHeaderSetup;
import org.keycloak.theme.FreeMarkerUtil; import org.keycloak.theme.FreeMarkerUtil;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.urls.UrlType; import org.keycloak.urls.UrlType;
@ -210,7 +207,6 @@ public class WelcomeResource {
ResponseBuilder rb = Response.status(errorMessage == null ? Status.OK : Status.BAD_REQUEST) ResponseBuilder rb = Response.status(errorMessage == null ? Status.OK : Status.BAD_REQUEST)
.entity(result) .entity(result)
.cacheControl(CacheControlUtil.noCache()); .cacheControl(CacheControlUtil.noCache());
BrowserSecurityHeaderSetup.headers(rb);
return rb.build(); return rb.build();
} catch (Exception e) { } catch (Exception e) {
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);

View file

@ -2,7 +2,6 @@ package org.keycloak.services.resources.account;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.common.Profile;
import org.keycloak.common.Version; import org.keycloak.common.Version;
import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventStoreProvider;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -15,11 +14,8 @@ import org.keycloak.services.Urls;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.theme.BrowserSecurityHeaderSetup;
import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.FreeMarkerUtil; import org.keycloak.theme.FreeMarkerUtil;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
@ -31,12 +27,10 @@ import javax.json.Json;
import javax.json.JsonObjectBuilder; import javax.json.JsonObjectBuilder;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -124,7 +118,6 @@ public class AccountConsole {
FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil(); FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil();
String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme); String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme);
Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result); Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result);
BrowserSecurityHeaderSetup.headers(builder, realm);
return builder.build(); return builder.build();
} }
} }

View file

@ -395,7 +395,7 @@ public class AccountCredentialResource {
event.client(auth.getClient()).user(auth.getUser()).success(); event.client(auth.getClient()).user(auth.getUser()).success();
return Response.ok().build(); return Response.noContent().build();
} }
public static class PasswordDetails { public static class PasswordDetails {

View file

@ -217,7 +217,7 @@ public class AccountRestService {
event.success(); event.success();
return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build(); return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build();
} catch (ReadOnlyException e) { } catch (ReadOnlyException e) {
return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST); return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST);
} }
@ -364,7 +364,7 @@ public class AccountRestService {
session.users().revokeConsentForClient(realm, user.getId(), client.getId()); session.users().revokeConsentForClient(realm, user.getId(), client.getId());
event.success(); event.success();
return Cors.add(request, Response.accepted()).build(); return Cors.add(request, Response.noContent()).build();
} }
/** /**
@ -407,7 +407,7 @@ public class AccountRestService {
session.userCredentialManager().disableCredentialType(realm, user, CredentialModel.OTP); session.userCredentialManager().disableCredentialType(realm, user, CredentialModel.OTP);
event.event(EventType.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); event.event(EventType.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
return Cors.add(request, Response.accepted()).build(); return Cors.add(request, Response.noContent()).build();
} }
/** /**

View file

@ -233,7 +233,7 @@ public class LinkedAccountsResource {
.detail(Details.IDENTITY_PROVIDER_USERNAME, link.getUserName()) .detail(Details.IDENTITY_PROVIDER_USERNAME, link.getUserName())
.success(); .success();
return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build(); return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build();
} }
private String checkCommonPreconditions(String providerId) { private String checkCommonPreconditions(String providerId) {

View file

@ -25,7 +25,7 @@ import javax.ws.rs.NotFoundException;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.Version; import org.keycloak.common.Version;
import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.AdminRoles; import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
@ -34,13 +34,11 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.utils.WebOriginsUtils;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.theme.BrowserSecurityHeaderSetup;
import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.FreeMarkerUtil; import org.keycloak.theme.FreeMarkerUtil;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
@ -55,7 +53,6 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers; import javax.ws.rs.ext.Providers;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@ -309,15 +306,11 @@ public class AdminConsole {
String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme); String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme);
Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result); Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result);
BrowserSecurityHeaderSetup.Options headerOptions = null;
// Replace CSP if admin is hosted on different URL // Replace CSP if admin is hosted on different URL
if (!adminBaseUri.equals(authServerBaseUri)) { if (!adminBaseUri.equals(authServerBaseUri)) {
headerOptions = BrowserSecurityHeaderSetup.Options.create().allowFrameSrc(UriBuilder.fromUri(authServerBaseUri).replacePath("").build().toString()).build(); session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(UriBuilder.fromUri(authServerBaseUri).replacePath("").build().toString());
} }
BrowserSecurityHeaderSetup.headers(builder, realm, headerOptions);
return builder.build(); return builder.build();
} }
} }

View file

@ -830,7 +830,7 @@ public class UserResource {
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).success(); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).success();
return Response.ok().build(); return Response.noContent().build();
} catch (EmailException e) { } catch (EmailException e) {
ServicesLogger.LOGGER.failedToSendActionsEmail(e); ServicesLogger.LOGGER.failedToSendActionsEmail(e);
return ErrorResponse.error("Failed to send execute actions email", Status.INTERNAL_SERVER_ERROR); return ErrorResponse.error("Failed to send execute actions email", Status.INTERNAL_SERVER_ERROR);

View file

@ -27,7 +27,6 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.theme.BrowserSecurityHeaderSetup;
import org.keycloak.utils.MediaType; import org.keycloak.utils.MediaType;
/** /**
@ -181,7 +180,6 @@ public abstract class BrowserHistoryHelper {
} }
Response.ResponseBuilder builder = Response.status(200).type(MediaType.TEXT_HTML_UTF_8).entity(savedResponse); Response.ResponseBuilder builder = Response.status(200).type(MediaType.TEXT_HTML_UTF_8).entity(savedResponse);
BrowserSecurityHeaderSetup.headers(builder, session.getContext().getRealm()); // TODO rather all the headers from the saved response should be added here.
return builder.build(); return builder.build();
} }

View file

@ -1,82 +0,0 @@
/*
* Copyright 2016 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.theme;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.RealmModel;
import javax.swing.text.html.Option;
import javax.ws.rs.core.Response;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BrowserSecurityHeaderSetup {
public static class Options {
private String allowedFrameSrc;
public static Options create() {
return new Options();
}
public Options allowFrameSrc(String source) {
allowedFrameSrc = source;
return this;
}
public Options build() {
return this;
}
}
public static Response.ResponseBuilder headers(Response.ResponseBuilder builder, RealmModel realm) {
return headers(builder, realm.getBrowserSecurityHeaders(), null);
}
public static Response.ResponseBuilder headers(Response.ResponseBuilder builder, RealmModel realm, Options options) {
return headers(builder, realm.getBrowserSecurityHeaders(), options);
}
public static Response.ResponseBuilder headers(Response.ResponseBuilder builder) {
return headers(builder, BrowserSecurityHeaders.defaultHeaders, null);
}
private static Response.ResponseBuilder headers(Response.ResponseBuilder builder, Map<String, String> headers, Options options) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
String header = BrowserSecurityHeaders.headerAttributeMap.get(entry.getKey());
String value = entry.getValue();
if (options != null) {
if (header.equals(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY) && value.equals(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY_DEFAULT) && options.allowedFrameSrc != null) {
value = "frame-src " + options.allowedFrameSrc + "; frame-ancestors 'self'; object-src 'none';";
}
}
if (header != null && value != null && !value.isEmpty()) {
builder.header(header, value);
}
}
return builder;
}
}

View file

@ -0,0 +1 @@
org.keycloak.headers.DefaultSecurityHeadersProviderFactory

View file

@ -140,7 +140,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
} }
session.sessions().removeUserSession(realm, sessionModel); session.sessions().removeUserSession(realm, sessionModel);
return Response.ok().build(); return Response.noContent().build();
} }
@POST @POST
@ -150,7 +150,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
RealmModel realm = getRealmByName(realmName); RealmModel realm = getRealmByName(realmName);
session.sessions().removeUserSessions(realm); session.sessions().removeUserSessions(realm);
return Response.ok().build(); return Response.noContent().build();
} }
@GET @GET
@ -177,7 +177,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
session.authenticationSessions().removeExpired(realm); session.authenticationSessions().removeExpired(realm);
session.realms().removeExpiredClientInitialAccess(); session.realms().removeExpiredClientInitialAccess();
return Response.ok().build(); return Response.noContent().build();
} }
@GET @GET
@ -251,7 +251,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response clearEventQueue() { public Response clearEventQueue() {
EventsListenerProvider.clear(); EventsListenerProvider.clear();
return Response.ok().build(); return Response.noContent().build();
} }
@POST @POST
@ -259,7 +259,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response clearAdminEventQueue() { public Response clearAdminEventQueue() {
EventsListenerProvider.clearAdminEvents(); EventsListenerProvider.clearAdminEvents();
return Response.ok().build(); return Response.noContent().build();
} }
@GET @GET
@ -268,7 +268,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
public Response clearEventStore() { public Response clearEventStore() {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clear(); eventStore.clear();
return Response.ok().build(); return Response.noContent().build();
} }
@GET @GET
@ -277,7 +277,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
public Response clearEventStore(@QueryParam("realmId") String realmId) { public Response clearEventStore(@QueryParam("realmId") String realmId) {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clear(realmId); eventStore.clear(realmId);
return Response.ok().build(); return Response.noContent().build();
} }
@GET @GET
@ -286,7 +286,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
public Response clearEventStore(@QueryParam("realmId") String realmId, @QueryParam("olderThan") long olderThan) { public Response clearEventStore(@QueryParam("realmId") String realmId, @QueryParam("olderThan") long olderThan) {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clear(realmId, olderThan); eventStore.clear(realmId, olderThan);
return Response.ok().build(); return Response.noContent().build();
} }
/** /**
@ -398,7 +398,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
public Response clearAdminEventStore() { public Response clearAdminEventStore() {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clearAdmin(); eventStore.clearAdmin();
return Response.ok().build(); return Response.noContent().build();
} }
@GET @GET
@ -407,7 +407,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
public Response clearAdminEventStore(@QueryParam("realmId") String realmId) { public Response clearAdminEventStore(@QueryParam("realmId") String realmId) {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clearAdmin(realmId); eventStore.clearAdmin(realmId);
return Response.ok().build(); return Response.noContent().build();
} }
@GET @GET
@ -416,7 +416,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
public Response clearAdminEventStore(@QueryParam("realmId") String realmId, @QueryParam("olderThan") long olderThan) { public Response clearAdminEventStore(@QueryParam("realmId") String realmId, @QueryParam("olderThan") long olderThan) {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clearAdmin(realmId, olderThan); eventStore.clearAdmin(realmId, olderThan);
return Response.ok().build(); return Response.noContent().build();
} }
/** /**
@ -830,7 +830,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
@Path("/javascript") @Path("/javascript")
public TestJavascriptResource getJavascriptResource() { public TestJavascriptResource getJavascriptResource() {
return new TestJavascriptResource(); return new TestJavascriptResource(session);
} }
private void setFeatureInProfileFile(File file, Profile.Feature featureProfile, String newState) { private void setFeatureInProfileFile(File file, Profile.Feature featureProfile, String newState) {
@ -866,7 +866,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
} }
if (Profile.isFeatureEnabled(featureProfile)) if (Profile.isFeatureEnabled(featureProfile))
return Response.ok().build(); return Response.noContent().build();
FeatureDeployerUtil.initBeforeChangeFeature(featureProfile); FeatureDeployerUtil.initBeforeChangeFeature(featureProfile);
@ -883,7 +883,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
FeatureDeployerUtil.deployFactoriesAfterFeatureEnabled(featureProfile); FeatureDeployerUtil.deployFactoriesAfterFeatureEnabled(featureProfile);
if (Profile.isFeatureEnabled(featureProfile)) if (Profile.isFeatureEnabled(featureProfile))
return Response.ok().build(); return Response.noContent().build();
else else
return Response.status(Response.Status.NOT_FOUND).build(); return Response.status(Response.Status.NOT_FOUND).build();
} }
@ -902,7 +902,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
} }
if (!Profile.isFeatureEnabled(featureProfile)) if (!Profile.isFeatureEnabled(featureProfile))
return Response.ok().build(); return Response.noContent().build();
FeatureDeployerUtil.initBeforeChangeFeature(featureProfile); FeatureDeployerUtil.initBeforeChangeFeature(featureProfile);
@ -919,7 +919,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
FeatureDeployerUtil.undeployFactoriesAfterFeatureDisabled(featureProfile); FeatureDeployerUtil.undeployFactoriesAfterFeatureDisabled(featureProfile);
if (!Profile.isFeatureEnabled(featureProfile)) if (!Profile.isFeatureEnabled(featureProfile))
return Response.ok().build(); return Response.noContent().build();
else else
return Response.status(Response.Status.NOT_FOUND).build(); return Response.status(Response.Status.NOT_FOUND).build();
} }

View file

@ -1,5 +1,7 @@
package org.keycloak.testsuite.rest.resource; package org.keycloak.testsuite.rest.resource;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.testsuite.rest.TestingResourceProvider; import org.keycloak.testsuite.rest.TestingResourceProvider;
import javax.ws.rs.GET; import javax.ws.rs.GET;
@ -16,6 +18,12 @@ import java.io.InputStreamReader;
*/ */
public class TestJavascriptResource { public class TestJavascriptResource {
private KeycloakSession session;
public TestJavascriptResource(KeycloakSession session) {
this.session = session;
}
@GET @GET
@Path("/js/keycloak.js") @Path("/js/keycloak.js")
@Produces("application/javascript") @Produces("application/javascript")
@ -27,6 +35,7 @@ public class TestJavascriptResource {
@Path("/index.html") @Path("/index.html")
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
public String getJavascriptTestingEnvironment() throws IOException { public String getJavascriptTestingEnvironment() throws IOException {
session.getProvider(SecurityHeadersProvider.class).options().skipHeaders();
return resourceToString("/javascript/index.html"); return resourceToString("/javascript/index.html");
} }

View file

@ -56,7 +56,7 @@ public class TestingExportImportResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response runImport() { public Response runImport() {
new ExportImportManager(session).runImport(); new ExportImportManager(session).runImport();
return Response.ok().build(); return Response.noContent().build();
} }
@GET @GET
@ -64,7 +64,7 @@ public class TestingExportImportResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response runExport() { public Response runExport() {
new ExportImportManager(session).runExport(); new ExportImportManager(session).runExport();
return Response.ok().build(); return Response.noContent().build();
} }
@GET @GET
@ -157,6 +157,6 @@ public class TestingExportImportResource {
System.clearProperty(ACTION); System.clearProperty(ACTION);
System.clearProperty(FILE); System.clearProperty(FILE);
return Response.ok().build(); return Response.noContent().build();
} }
} }

View file

@ -77,13 +77,13 @@ public class KeycloakTestingClient implements AutoCloseable {
public void enableFeature(Profile.Feature feature) { public void enableFeature(Profile.Feature feature) {
try (Response response = testing().enableFeature(feature.toString())) { try (Response response = testing().enableFeature(feature.toString())) {
assertEquals(200, response.getStatus()); assertEquals(204, response.getStatus());
} }
} }
public void disableFeature(Profile.Feature feature) { public void disableFeature(Profile.Feature feature) {
try (Response response = testing().disableFeature(feature.toString())) { try (Response response = testing().disableFeature(feature.toString())) {
assertEquals(200, response.getStatus()); assertEquals(204, response.getStatus());
} }
} }

View file

@ -138,7 +138,7 @@ public class AccountRestServiceCorsTest extends AbstractTestRealmKeycloakTest {
error = t; error = t;
} }
if (result == null || result.getStatus() != 200 || error != null) { if (result == null || (result.getStatus() != 200 && result.getStatus() != 204) || error != null) {
if (expectAllowed) { if (expectAllowed) {
throw new AssertionError("Cors request failed: " + WebDriverLogDumper.dumpBrowserLogs(driver)); throw new AssertionError("Cors request failed: " + WebDriverLogDumper.dumpBrowserLogs(driver));
} else { } else {

View file

@ -174,7 +174,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
user.setAttributes(originalAttributes); user.setAttributes(originalAttributes);
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse(); SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
System.out.println(response.asString()); System.out.println(response.asString());
assertEquals(200, response.getStatus()); assertEquals(204, response.getStatus());
} }
} }
@ -198,13 +198,13 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
} finally { } finally {
user.setFirstName(originalFirstname); user.setFirstName(originalFirstname);
int status = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asStatus(); int status = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asStatus();
assertEquals(200, status); assertEquals(204, status);
} }
} }
private UserRepresentation updateAndGet(UserRepresentation user) throws IOException { private UserRepresentation updateAndGet(UserRepresentation user) throws IOException {
int status = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asStatus(); int status = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asStatus();
assertEquals(200, status); assertEquals(204, status);
return SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class); return SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
} }
@ -270,7 +270,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
events.poll(); events.poll();
//Change the password //Change the password
updatePassword("password", "Str0ng3rP4ssw0rd", 200); updatePassword("password", "Str0ng3rP4ssw0rd", 204);
//Get the new value for lastUpdate //Get the new value for lastUpdate
AccountCredentialResource.PasswordDetails updatedDetails = getPasswordDetails(); AccountCredentialResource.PasswordDetails updatedDetails = getPasswordDetails();
@ -285,17 +285,17 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
assertEquals(updatedDetails.getLastUpdate(), finalDetails.getLastUpdate()); assertEquals(updatedDetails.getLastUpdate(), finalDetails.getLastUpdate());
//Change the password back //Change the password back
updatePassword("Str0ng3rP4ssw0rd", "password", 200); updatePassword("Str0ng3rP4ssw0rd", "password", 204);
} }
@Test @Test
public void testPasswordConfirmation() throws IOException { public void testPasswordConfirmation() throws IOException {
updatePassword("password", "Str0ng3rP4ssw0rd", "confirmationDoesNotMatch", 400); updatePassword("password", "Str0ng3rP4ssw0rd", "confirmationDoesNotMatch", 400);
updatePassword("password", "Str0ng3rP4ssw0rd", "Str0ng3rP4ssw0rd", 200); updatePassword("password", "Str0ng3rP4ssw0rd", "Str0ng3rP4ssw0rd", 204);
//Change the password back //Change the password back
updatePassword("Str0ng3rP4ssw0rd", "password", 200); updatePassword("Str0ng3rP4ssw0rd", "password", 204);
} }
private AccountCredentialResource.PasswordDetails getPasswordDetails() throws IOException { private AccountCredentialResource.PasswordDetails getPasswordDetails() throws IOException {
@ -1090,14 +1090,14 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
.header("Accept", "application/json") .header("Accept", "application/json")
.auth(token.getToken()) .auth(token.getToken())
.asResponse(); .asResponse();
assertEquals(202, response.getStatus()); assertEquals(204, response.getStatus());
response = SimpleHttp response = SimpleHttp
.doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) .doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient)
.header("Accept", "application/json") .header("Accept", "application/json")
.auth(token.getToken()) .auth(token.getToken())
.asResponse(); .asResponse();
assertEquals(202, response.getStatus()); assertEquals(204, response.getStatus());
} }
@Test @Test

View file

@ -0,0 +1,57 @@
package org.keycloak.testsuite.admin;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.services.resources.Cors;
import org.keycloak.testsuite.util.UserBuilder;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class AdminHeadersTest extends AbstractAdminTest {
private CloseableHttpClient client;
@Before
public void before() {
client = HttpClientBuilder.create().build();
}
@After
public void after() {
try {
client.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test
public void testHeaders() {
Response response = realm.users().create(UserBuilder.create().username("headers-user").build());
MultivaluedMap<String, Object> h = response.getHeaders();
assertEquals(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY_DEFAULT, h.getFirst(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY));
assertEquals(BrowserSecurityHeaders.X_FRAME_OPTIONS_DEFAULT, h.getFirst(BrowserSecurityHeaders.X_FRAME_OPTIONS));
assertEquals(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS_DEFAULT, h.getFirst(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS));
assertEquals(BrowserSecurityHeaders.X_XSS_PROTECTION_DEFAULT, h.getFirst(BrowserSecurityHeaders.X_XSS_PROTECTION));
response.close();
}
private String getAdminUrl(String resource) {
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/admin/" + resource;
}
}

View file

@ -232,9 +232,10 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
groups.group(group.getId()).remove(); groups.group(group.getId()).remove();
GroupPolicyRepresentation policy = getClient().authorization().policies().group().findByName(representation.getName()); try {
getClient().authorization().policies().group().findByName(representation.getName());
assertNull(policy); } catch (NotFoundException e) {
}
representation.getGroups().clear(); representation.getGroups().clear();
representation.addGroupPath("/Group H/Group I/Group K"); representation.addGroupPath("/Group H/Group I/Group K");
@ -246,7 +247,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
groups.group(group.getId()).remove(); groups.group(group.getId()).remove();
policy = getClient().authorization().policies().group().findByName(representation.getName()); GroupPolicyRepresentation policy = getClient().authorization().policies().group().findByName(representation.getName());
assertNotNull(policy); assertNotNull(policy);
assertEquals(1, policy.getGroups().size()); assertEquals(1, policy.getGroups().size());

View file

@ -13,6 +13,7 @@ import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.util.StreamUtil; import org.keycloak.common.util.StreamUtil;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -20,6 +21,7 @@ import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
@ -29,6 +31,7 @@ import java.net.URI;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -115,6 +118,27 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest {
assertEquals("An internal server error has occurred", errorPage.getError()); assertEquals("An internal server error has occurred", errorPage.getError());
} }
@Test
@UncaughtServerErrorExpected
public void uncaughtErrorHeaders() throws IOException {
URI uri = suiteContext.getAuthServerInfo().getUriBuilder().path("/auth/realms/master/testing/uncaught-error").build();
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
SimpleHttp.Response response = SimpleHttp.doGet(uri.toString(), client).header("Accept", MediaType.TEXT_HTML_UTF_8).asResponse();
for (Map.Entry<String, String> e : BrowserSecurityHeaders.headerAttributeMap.entrySet()) {
String header = e.getValue();
String expectedValue = BrowserSecurityHeaders.defaultHeaders.get(e.getKey());
if (expectedValue == null || expectedValue.isEmpty()) {
assertNull(response.getFirstHeader(header));
} else {
assertEquals(expectedValue, response.getFirstHeader(header));
}
}
}
}
@Test @Test
public void errorPageException() { public void errorPageException() {
oauth.realm("master"); oauth.realm("master");

View file

@ -32,6 +32,7 @@ import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -50,6 +51,7 @@ import java.util.List;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
@ -128,6 +130,9 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest {
assertTrue(s.contains("function getCookie()")); assertTrue(s.contains("function getCookie()"));
assertEquals("CP=\"This is not a P3P policy!\"", response.getFirstHeader("P3P").getValue()); assertEquals("CP=\"This is not a P3P policy!\"", response.getFirstHeader("P3P").getValue());
assertNull(response.getFirstHeader(BrowserSecurityHeaders.X_FRAME_OPTIONS));
assertEquals("frame-src 'self'; object-src 'none';", response.getFirstHeader(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY).getValue());
assertEquals("none", response.getFirstHeader(BrowserSecurityHeaders.X_ROBOTS_TAG).getValue());
response.close(); response.close();