KEYCLOAK-13128 Security Headers SPI and response filter
This commit is contained in:
parent
b40c12c712
commit
5b017e930d
42 changed files with 680 additions and 159 deletions
|
@ -137,7 +137,7 @@ public class RealmEntity {
|
|||
@Column(name="EMAIL_THEME")
|
||||
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<>();
|
||||
|
||||
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -35,7 +35,7 @@ public class BrowserSecurityHeaders {
|
|||
|
||||
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";
|
||||
|
||||
|
@ -94,4 +94,52 @@ public class BrowserSecurityHeaders {
|
|||
defaultHeaders = Collections.unmodifiableMap(dh);
|
||||
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("';");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -78,4 +78,5 @@ org.keycloak.crypto.HashSpi
|
|||
org.keycloak.vault.VaultSpi
|
||||
org.keycloak.crypto.CekManagementSpi
|
||||
org.keycloak.crypto.ContentEncryptionSpi
|
||||
org.keycloak.validation.ClientValidationSPI
|
||||
org.keycloak.validation.ClientValidationSPI
|
||||
org.keycloak.headers.SecurityHeadersSpi
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -26,6 +26,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
|
@ -160,7 +161,7 @@ public class PolicyService {
|
|||
Policy model = storeFactory.getPolicyStore().findByName(name, this.resourceServer.getId());
|
||||
|
||||
if (model == null) {
|
||||
return Response.status(Status.OK).build();
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
if (resources.isEmpty()) {
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
if (scopes.isEmpty()) {
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
search.put("scope", scopes.toArray(new String[scopes.size()]));
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.constants.AdapterConstants;
|
|||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.headers.SecurityHeadersProvider;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -40,6 +41,7 @@ import org.keycloak.util.JsonSerialization;
|
|||
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.theme.BrowserSecurityHeaderSetup;
|
||||
import org.keycloak.theme.FreeMarkerException;
|
||||
import org.keycloak.theme.FreeMarkerUtil;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
@ -50,7 +49,6 @@ import org.keycloak.theme.beans.MessageType;
|
|||
import org.keycloak.theme.beans.MessagesPerFieldBean;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import javax.ws.rs.core.CacheControl;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
@ -271,7 +269,6 @@ public class FreeMarkerAccountProvider implements AccountProvider {
|
|||
try {
|
||||
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);
|
||||
BrowserSecurityHeaderSetup.headers(builder, realm);
|
||||
builder.cacheControl(CacheControlUtil.noCache());
|
||||
return builder.build();
|
||||
} catch (FreeMarkerException e) {
|
||||
|
|
|
@ -53,7 +53,6 @@ import org.keycloak.services.Urls;
|
|||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.theme.BrowserSecurityHeaderSetup;
|
||||
import org.keycloak.theme.FreeMarkerException;
|
||||
import org.keycloak.theme.FreeMarkerUtil;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
@ -462,7 +461,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
String result = freeMarker.processTemplate(attributes, templateName, theme);
|
||||
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);
|
||||
BrowserSecurityHeaderSetup.headers(builder, realm);
|
||||
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
|
||||
builder.header(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -26,7 +26,9 @@ import org.keycloak.constants.AdapterConstants;
|
|||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.headers.SecurityHeadersProvider;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.BrowserSecurityHeaders;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
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.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
@ -335,6 +338,8 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
uriBuilder.queryParam(STATE_PARAM, state);
|
||||
return Response.status(302).location(uriBuilder.build()).build();
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc.endpoints;
|
|||
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.common.util.UriUtils;
|
||||
import org.keycloak.headers.SecurityHeadersProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -29,7 +30,6 @@ import org.keycloak.utils.MediaType;
|
|||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.CacheControl;
|
||||
|
@ -63,6 +63,7 @@ public class LoginStatusIframeEndpoint {
|
|||
InputStream resource = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html");
|
||||
if (resource != null) {
|
||||
P3PHelper.addP3PHeader();
|
||||
session.getProvider(SecurityHeadersProvider.class).options().allowAnyFrameAncestor();
|
||||
return Response.ok(resource).cacheControl(cacheControl).build();
|
||||
} else {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.keycloak.events.Details;
|
|||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.headers.SecurityHeadersProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -157,6 +158,8 @@ public class LogoutEndpoint {
|
|||
if (state != null) uriBuilder.queryParam(OIDCLoginProtocol.STATE_PARAM, state);
|
||||
return Response.status(302).location(uriBuilder.build()).build();
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.KeycloakUriInfo;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.resources.KeycloakApplication;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.urls.UrlType;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -42,6 +42,7 @@ import org.keycloak.representations.idm.UserRepresentation;
|
|||
import org.keycloak.services.DefaultKeycloakSessionFactory;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.error.KeycloakErrorHandler;
|
||||
import org.keycloak.services.filters.KeycloakSecurityHeadersFilter;
|
||||
import org.keycloak.services.filters.KeycloakTransactionCommitter;
|
||||
import org.keycloak.services.managers.ApplianceBootstrap;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
|
@ -114,6 +115,7 @@ public class KeycloakApplication extends Application {
|
|||
classes.add(ThemeResource.class);
|
||||
classes.add(JsResource.class);
|
||||
|
||||
classes.add(KeycloakSecurityHeadersFilter.class);
|
||||
classes.add(KeycloakTransactionCommitter.class);
|
||||
classes.add(KeycloakErrorHandler.class);
|
||||
|
||||
|
|
|
@ -21,16 +21,13 @@ import org.keycloak.common.ClientConnection;
|
|||
import org.keycloak.common.Version;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.MimeTypeUtil;
|
||||
import org.keycloak.models.BrowserSecurityHeaders;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.services.ForbiddenException;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.ApplianceBootstrap;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.services.util.CookieHelper;
|
||||
import org.keycloak.theme.BrowserSecurityHeaderSetup;
|
||||
import org.keycloak.theme.FreeMarkerUtil;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.urls.UrlType;
|
||||
|
@ -210,7 +207,6 @@ public class WelcomeResource {
|
|||
ResponseBuilder rb = Response.status(errorMessage == null ? Status.OK : Status.BAD_REQUEST)
|
||||
.entity(result)
|
||||
.cacheControl(CacheControlUtil.noCache());
|
||||
BrowserSecurityHeaderSetup.headers(rb);
|
||||
return rb.build();
|
||||
} catch (Exception e) {
|
||||
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.keycloak.services.resources.account;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
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.Auth;
|
||||
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.validation.Validation;
|
||||
import org.keycloak.theme.BrowserSecurityHeaderSetup;
|
||||
import org.keycloak.theme.FreeMarkerException;
|
||||
import org.keycloak.theme.FreeMarkerUtil;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
@ -31,12 +27,10 @@ import javax.json.Json;
|
|||
import javax.json.JsonObjectBuilder;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
@ -124,7 +118,6 @@ public class AccountConsole {
|
|||
FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil();
|
||||
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);
|
||||
BrowserSecurityHeaderSetup.headers(builder, realm);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -395,7 +395,7 @@ public class AccountCredentialResource {
|
|||
|
||||
event.client(auth.getClient()).user(auth.getUser()).success();
|
||||
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
public static class PasswordDetails {
|
||||
|
|
|
@ -217,7 +217,7 @@ public class AccountRestService {
|
|||
|
||||
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) {
|
||||
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());
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -233,7 +233,7 @@ public class LinkedAccountsResource {
|
|||
.detail(Details.IDENTITY_PROVIDER_USERNAME, link.getUserName())
|
||||
.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) {
|
||||
|
|
|
@ -25,7 +25,7 @@ import javax.ws.rs.NotFoundException;
|
|||
import org.keycloak.Config;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
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.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
|
@ -34,13 +34,11 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.protocol.oidc.utils.WebOriginsUtils;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.ClientManager;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.theme.BrowserSecurityHeaderSetup;
|
||||
import org.keycloak.theme.FreeMarkerException;
|
||||
import org.keycloak.theme.FreeMarkerUtil;
|
||||
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.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import javax.ws.rs.ext.Providers;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
@ -309,15 +306,11 @@ public class AdminConsole {
|
|||
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);
|
||||
|
||||
BrowserSecurityHeaderSetup.Options headerOptions = null;
|
||||
|
||||
// Replace CSP if admin is hosted on different URL
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -830,7 +830,7 @@ public class UserResource {
|
|||
|
||||
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).success();
|
||||
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
} catch (EmailException e) {
|
||||
ServicesLogger.LOGGER.failedToSendActionsEmail(e);
|
||||
return ErrorResponse.error("Failed to send execute actions email", Status.INTERNAL_SERVER_ERROR);
|
||||
|
|
|
@ -27,7 +27,6 @@ import org.jboss.logging.Logger;
|
|||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.theme.BrowserSecurityHeaderSetup;
|
||||
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);
|
||||
BrowserSecurityHeaderSetup.headers(builder, session.getContext().getRealm()); // TODO rather all the headers from the saved response should be added here.
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.headers.DefaultSecurityHeadersProviderFactory
|
|
@ -140,7 +140,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
}
|
||||
|
||||
session.sessions().removeUserSession(realm, sessionModel);
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
|
@ -150,7 +150,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
RealmModel realm = getRealmByName(realmName);
|
||||
|
||||
session.sessions().removeUserSessions(realm);
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -177,7 +177,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
session.authenticationSessions().removeExpired(realm);
|
||||
session.realms().removeExpiredClientInitialAccess();
|
||||
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -251,7 +251,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response clearEventQueue() {
|
||||
EventsListenerProvider.clear();
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
|
@ -259,7 +259,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response clearAdminEventQueue() {
|
||||
EventsListenerProvider.clearAdminEvents();
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -268,7 +268,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
public Response clearEventStore() {
|
||||
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
|
||||
eventStore.clear();
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -277,7 +277,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
public Response clearEventStore(@QueryParam("realmId") String realmId) {
|
||||
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
|
||||
eventStore.clear(realmId);
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -286,7 +286,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
public Response clearEventStore(@QueryParam("realmId") String realmId, @QueryParam("olderThan") long olderThan) {
|
||||
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
|
||||
eventStore.clear(realmId, olderThan);
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -398,7 +398,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
public Response clearAdminEventStore() {
|
||||
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
|
||||
eventStore.clearAdmin();
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -407,7 +407,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
public Response clearAdminEventStore(@QueryParam("realmId") String realmId) {
|
||||
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
|
||||
eventStore.clearAdmin(realmId);
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -416,7 +416,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
public Response clearAdminEventStore(@QueryParam("realmId") String realmId, @QueryParam("olderThan") long olderThan) {
|
||||
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
|
||||
eventStore.clearAdmin(realmId, olderThan);
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -830,7 +830,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
|
||||
@Path("/javascript")
|
||||
public TestJavascriptResource getJavascriptResource() {
|
||||
return new TestJavascriptResource();
|
||||
return new TestJavascriptResource(session);
|
||||
}
|
||||
|
||||
private void setFeatureInProfileFile(File file, Profile.Feature featureProfile, String newState) {
|
||||
|
@ -866,7 +866,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
}
|
||||
|
||||
if (Profile.isFeatureEnabled(featureProfile))
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
|
||||
FeatureDeployerUtil.initBeforeChangeFeature(featureProfile);
|
||||
|
||||
|
@ -883,7 +883,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
FeatureDeployerUtil.deployFactoriesAfterFeatureEnabled(featureProfile);
|
||||
|
||||
if (Profile.isFeatureEnabled(featureProfile))
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
else
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
@ -902,7 +902,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
}
|
||||
|
||||
if (!Profile.isFeatureEnabled(featureProfile))
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
|
||||
FeatureDeployerUtil.initBeforeChangeFeature(featureProfile);
|
||||
|
||||
|
@ -919,7 +919,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
FeatureDeployerUtil.undeployFactoriesAfterFeatureDisabled(featureProfile);
|
||||
|
||||
if (!Profile.isFeatureEnabled(featureProfile))
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
else
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.keycloak.testsuite.rest.resource;
|
||||
|
||||
import org.keycloak.headers.SecurityHeadersProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.testsuite.rest.TestingResourceProvider;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
|
@ -16,6 +18,12 @@ import java.io.InputStreamReader;
|
|||
*/
|
||||
public class TestJavascriptResource {
|
||||
|
||||
private KeycloakSession session;
|
||||
|
||||
public TestJavascriptResource(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/js/keycloak.js")
|
||||
@Produces("application/javascript")
|
||||
|
@ -27,6 +35,7 @@ public class TestJavascriptResource {
|
|||
@Path("/index.html")
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public String getJavascriptTestingEnvironment() throws IOException {
|
||||
session.getProvider(SecurityHeadersProvider.class).options().skipHeaders();
|
||||
return resourceToString("/javascript/index.html");
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ public class TestingExportImportResource {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response runImport() {
|
||||
new ExportImportManager(session).runImport();
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -64,7 +64,7 @@ public class TestingExportImportResource {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response runExport() {
|
||||
new ExportImportManager(session).runExport();
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -157,6 +157,6 @@ public class TestingExportImportResource {
|
|||
System.clearProperty(ACTION);
|
||||
System.clearProperty(FILE);
|
||||
|
||||
return Response.ok().build();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,13 +77,13 @@ public class KeycloakTestingClient implements AutoCloseable {
|
|||
|
||||
public void enableFeature(Profile.Feature feature) {
|
||||
try (Response response = testing().enableFeature(feature.toString())) {
|
||||
assertEquals(200, response.getStatus());
|
||||
assertEquals(204, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
public void disableFeature(Profile.Feature feature) {
|
||||
try (Response response = testing().disableFeature(feature.toString())) {
|
||||
assertEquals(200, response.getStatus());
|
||||
assertEquals(204, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,7 @@ public class AccountRestServiceCorsTest extends AbstractTestRealmKeycloakTest {
|
|||
error = t;
|
||||
}
|
||||
|
||||
if (result == null || result.getStatus() != 200 || error != null) {
|
||||
if (result == null || (result.getStatus() != 200 && result.getStatus() != 204) || error != null) {
|
||||
if (expectAllowed) {
|
||||
throw new AssertionError("Cors request failed: " + WebDriverLogDumper.dumpBrowserLogs(driver));
|
||||
} else {
|
||||
|
|
|
@ -174,7 +174,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
user.setAttributes(originalAttributes);
|
||||
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
||||
System.out.println(response.asString());
|
||||
assertEquals(200, response.getStatus());
|
||||
assertEquals(204, response.getStatus());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -198,13 +198,13 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
} finally {
|
||||
user.setFirstName(originalFirstname);
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -270,7 +270,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
events.poll();
|
||||
|
||||
//Change the password
|
||||
updatePassword("password", "Str0ng3rP4ssw0rd", 200);
|
||||
updatePassword("password", "Str0ng3rP4ssw0rd", 204);
|
||||
|
||||
//Get the new value for lastUpdate
|
||||
AccountCredentialResource.PasswordDetails updatedDetails = getPasswordDetails();
|
||||
|
@ -285,17 +285,17 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
assertEquals(updatedDetails.getLastUpdate(), finalDetails.getLastUpdate());
|
||||
|
||||
//Change the password back
|
||||
updatePassword("Str0ng3rP4ssw0rd", "password", 200);
|
||||
updatePassword("Str0ng3rP4ssw0rd", "password", 204);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordConfirmation() throws IOException {
|
||||
updatePassword("password", "Str0ng3rP4ssw0rd", "confirmationDoesNotMatch", 400);
|
||||
|
||||
updatePassword("password", "Str0ng3rP4ssw0rd", "Str0ng3rP4ssw0rd", 200);
|
||||
updatePassword("password", "Str0ng3rP4ssw0rd", "Str0ng3rP4ssw0rd", 204);
|
||||
|
||||
//Change the password back
|
||||
updatePassword("Str0ng3rP4ssw0rd", "password", 200);
|
||||
updatePassword("Str0ng3rP4ssw0rd", "password", 204);
|
||||
}
|
||||
|
||||
private AccountCredentialResource.PasswordDetails getPasswordDetails() throws IOException {
|
||||
|
@ -1090,14 +1090,14 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
.header("Accept", "application/json")
|
||||
.auth(token.getToken())
|
||||
.asResponse();
|
||||
assertEquals(202, response.getStatus());
|
||||
assertEquals(204, response.getStatus());
|
||||
|
||||
response = SimpleHttp
|
||||
.doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient)
|
||||
.header("Accept", "application/json")
|
||||
.auth(token.getToken())
|
||||
.asResponse();
|
||||
assertEquals(202, response.getStatus());
|
||||
assertEquals(204, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -232,9 +232,10 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
|
|||
|
||||
groups.group(group.getId()).remove();
|
||||
|
||||
GroupPolicyRepresentation policy = getClient().authorization().policies().group().findByName(representation.getName());
|
||||
|
||||
assertNull(policy);
|
||||
try {
|
||||
getClient().authorization().policies().group().findByName(representation.getName());
|
||||
} catch (NotFoundException e) {
|
||||
}
|
||||
|
||||
representation.getGroups().clear();
|
||||
representation.addGroupPath("/Group H/Group I/Group K");
|
||||
|
@ -246,7 +247,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
|
|||
|
||||
groups.group(group.getId()).remove();
|
||||
|
||||
policy = getClient().authorization().policies().group().findByName(representation.getName());
|
||||
GroupPolicyRepresentation policy = getClient().authorization().policies().group().findByName(representation.getName());
|
||||
|
||||
assertNotNull(policy);
|
||||
assertEquals(1, policy.getGroups().size());
|
||||
|
|
|
@ -157,7 +157,7 @@ public class RealmTest extends AbstractAdminTest {
|
|||
|
||||
Assert.assertNames(adminClient.realms().findAll(), "master", AuthRealm.TEST, REALM_NAME);
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void createRealmRejectReservedChar() {
|
||||
RealmRepresentation rep = new RealmRepresentation();
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.junit.Test;
|
|||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
import org.keycloak.models.BrowserSecurityHeaders;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
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.pages.ErrorPage;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
|
@ -29,6 +31,7 @@ import java.net.URI;
|
|||
import java.nio.charset.Charset;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
@ -115,6 +118,27 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest {
|
|||
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
|
||||
public void errorPageException() {
|
||||
oauth.realm("master");
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.apache.http.impl.client.HttpClients;
|
|||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.models.BrowserSecurityHeaders;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
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.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
|
||||
|
@ -128,6 +130,9 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest {
|
|||
assertTrue(s.contains("function getCookie()"));
|
||||
|
||||
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();
|
||||
|
||||
|
|
Loading…
Reference in a new issue