X-Frame-Options, Content-Security-Policy

This commit is contained in:
Bill Burke 2014-08-11 17:45:01 -04:00
parent e73bdf9d8c
commit 7c97e02715
25 changed files with 246 additions and 31 deletions

View file

@ -52,6 +52,7 @@ public class RealmRepresentation {
protected Map<String, List<ScopeMappingRepresentation>> applicationScopeMappings; protected Map<String, List<ScopeMappingRepresentation>> applicationScopeMappings;
protected List<ApplicationRepresentation> applications; protected List<ApplicationRepresentation> applications;
protected List<OAuthClientRepresentation> oauthClients; protected List<OAuthClientRepresentation> oauthClients;
protected Map<String, String> browserSecurityHeaders;
protected Map<String, String> socialProviders; protected Map<String, String> socialProviders;
protected Map<String, String> smtpServer; protected Map<String, String> smtpServer;
protected List<UserFederationProviderRepresentation> userFederationProviders; protected List<UserFederationProviderRepresentation> userFederationProviders;
@ -291,6 +292,14 @@ public class RealmRepresentation {
this.updateProfileOnInitialSocialLogin = updateProfileOnInitialSocialLogin; this.updateProfileOnInitialSocialLogin = updateProfileOnInitialSocialLogin;
} }
public Map<String, String> getBrowserSecurityHeaders() {
return browserSecurityHeaders;
}
public void setBrowserSecurityHeaders(Map<String, String> browserSecurityHeaders) {
this.browserSecurityHeaders = browserSecurityHeaders;
}
public Map<String, String> getSocialProviders() { public Map<String, String> getSocialProviders() {
return socialProviders; return socialProviders;
} }

View file

@ -13,6 +13,7 @@ import org.keycloak.account.freemarker.model.SessionsBean;
import org.keycloak.account.freemarker.model.TotpBean; import org.keycloak.account.freemarker.model.TotpBean;
import org.keycloak.account.freemarker.model.UrlBean; import org.keycloak.account.freemarker.model.UrlBean;
import org.keycloak.audit.Event; import org.keycloak.audit.Event;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.FreeMarkerException; import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil; import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
@ -136,7 +137,9 @@ 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);
return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build(); Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result);
BrowserSecurityHeaderSetup.headers(builder, realm);
return builder.build();
} catch (FreeMarkerException e) { } catch (FreeMarkerException e) {
logger.error("Failed to process template", e); logger.error("Failed to process template", e);
return Response.serverError().build(); return Response.serverError().build();

View file

@ -0,0 +1,23 @@
package org.keycloak.freemarker;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.RealmModel;
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 Response.ResponseBuilder headers(Response.ResponseBuilder builder, RealmModel realm) {
for (Map.Entry<String, String> entry : realm.getBrowserSecurityHeaders().entrySet()) {
String headerName = BrowserSecurityHeaders.headerAttributeMap.get(entry.getKey());
if (headerName == null) continue;
builder.header(headerName, entry.getValue());
}
return builder;
}
}

View file

@ -675,16 +675,7 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'RealmRevocationCtrl' controller : 'RealmRevocationCtrl'
}) })
.when('/realms/:realm/sessions/brute-force', { .when('/realms/:realm/sessions/realm', {
templateUrl : 'partials/session-brute-force.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
}
},
controller : 'RealmBruteForceCtrl'
})
.when('/realms/:realm/sessions/realm', {
templateUrl : 'partials/session-realm.html', templateUrl : 'partials/session-realm.html',
resolve : { resolve : {
realm : function(RealmLoader) { realm : function(RealmLoader) {
@ -761,6 +752,28 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'GenericUserFederationCtrl' controller : 'GenericUserFederationCtrl'
}) })
.when('/realms/:realm/defense/headers', {
templateUrl : 'partials/defense-headers.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
}
},
controller : 'DefenseHeadersCtrl'
})
.when('/realms/:realm/defense/brute-force', {
templateUrl : 'partials/brute-force.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
}
},
controller : 'RealmBruteForceCtrl'
})
.when('/logout', { .when('/logout', {
templateUrl : 'partials/home.html', templateUrl : 'partials/home.html',
controller : 'LogoutCtrl' controller : 'LogoutCtrl'

View file

@ -340,8 +340,6 @@ function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $l
var oldCopy = angular.copy($scope.realm); var oldCopy = angular.copy($scope.realm);
$scope.changed = false; $scope.changed = false;
$scope.$watch('realm', function() { $scope.$watch('realm', function() {
@ -384,16 +382,20 @@ function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $l
} }
module.controller('DefenseHeadersCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/defense/headers");
});
module.controller('RealmLoginSettingsCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) { module.controller('RealmLoginSettingsCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/login-settings") genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/login-settings");
}); });
module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) { module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings") genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
}); });
module.controller('RealmCacheCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) { module.controller('RealmCacheCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/cache-settings") genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/cache-settings");
}); });
module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications, PasswordPolicy) { module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications, PasswordPolicy) {

View file

@ -1,16 +1,10 @@
<div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div> <div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div>
<div id="content-area" class="col-sm-9" role="main"> <div id="content-area" class="col-sm-9" role="main">
<ul class="nav nav-tabs nav-tabs-pf" data-ng-show="!create"> <ul class="nav nav-tabs nav-tabs-pf" data-ng-show="!create">
<li><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li> <li><a href="#/realms/{{realm.realm}}/defense/headers">Headers</a></li>
<li><a href="#/realms/{{realm.realm}}/token-settings">Timeout Settings</a></li> <li class="active"><a href="#/realms/{{realm.realm}}/defense/brute-force">Brute Force Protection</a></li>
<li><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li>
<li class="active"><a href="#/realms/{{realm.realm}}/sessions/brute-force">Brute Force Protection</a></li>
</ul> </ul>
<div id="content"> <div id="content">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}">{{realm.realm}}</a></li>
<li class="active">Brute Force</li>
</ol>
<h2><span>{{realm.realm}}</span> Brute Force Protection Settings</h2> <h2><span>{{realm.realm}}</span> Brute Force Protection Settings</h2>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm"> <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top"> <fieldset class="border-top">

View file

@ -0,0 +1,36 @@
<div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div>
<div id="content-area" class="col-sm-9" role="main">
<ul class="nav nav-tabs nav-tabs-pf">
<li class="active"><a href="#/realms/{{realm.realm}}/defense/headers">Headers</a></li>
<li><a href="#/realms/{{realm.realm}}/defense/brute-force">Brute Force Detection</a></li>
</ul>
<div id="content">
<div data-ng-show="access.viewRealm">
<h2><span>{{realm.realm}}</span> Browser Security Headers</h2>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top">
<div class="form-group">
<label class="col-sm-2 control-label" for="xFrameOptions"><a href="http://tools.ietf.org/html/rfc7034">X-Frame-Options</a></label>
<div class="col-sm-6">
<input class="form-control" id="xFrameOptions" type="text" ng-model="realm.browserSecurityHeaders.xFrameOptions">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="contentSecurityPolicy"><a href="http://www.w3.org/TR/CSP/">Content-Security-Policy</a></label>
<div class="col-sm-6">
<input class="form-control" id="contentSecurityPolicy" type="text" ng-model="realm.browserSecurityHeaders.contentSecurityPolicy">
</div>
</div>
</fieldset>
<div class="pull-right form-actions" data-ng-show="access.manageRealm">
<button kc-reset data-ng-show="changed">Clear changes</button>
<button kc-save data-ng-show="changed">Save</button>
</div>
</form>
</div>
<div data-ng-hide="access.viewRealm">
<h2 ><span>{{realm.realm}}</span></h2>
</div>
</div>
</div>

View file

@ -15,17 +15,16 @@
<fieldset class="border-top"> <fieldset class="border-top">
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="name">Name <span class="required" data-ng-show="createRealm">*</span></label> <label class="col-sm-2 control-label" for="name">Name <span class="required" data-ng-show="createRealm">*</span></label>
<div class="col-sm-4"> <div class="col-sm-4">
<input class="form-control" type="text" id="name" name="name" data-ng-model="realm.realm" autofocus required> <input class="form-control" type="text" id="name" name="name" data-ng-model="realm.realm" autofocus required>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="enabled">Enabled</label> <label class="col-sm-2 control-label" for="enabled">Enabled</label>
<span tooltip="Users and applications can only access a realm if it's enabled" class="pficon pficon-help"></span>
<div class="col-sm-4"> <div class="col-sm-4">
<input ng-model="realm.enabled" name="enabled" id="enabled" onoffswitch /> <input ng-model="realm.enabled" name="enabled" id="enabled" onoffswitch />
</div> </div>
<span tooltip="Users and applications can only access a realm if it's enabled" class="pficon pficon-help"></span>
</div> </div>
</fieldset> </fieldset>

View file

@ -9,5 +9,6 @@
<li data-ng-show="access.viewApplications" data-ng-class="(path[2] == 'applications' || path[1] == 'application' || path[3] == 'applications') && 'active'"><a href="#/realms/{{realm.realm}}/applications">Applications</a></li> <li data-ng-show="access.viewApplications" data-ng-class="(path[2] == 'applications' || path[1] == 'application' || path[3] == 'applications') && 'active'"><a href="#/realms/{{realm.realm}}/applications">Applications</a></li>
<li data-ng-show="access.viewClients" data-ng-class="(path[2] == 'oauth-clients' || path[1] == 'oauth-client') && 'active'"><a href="#/realms/{{realm.realm}}/oauth-clients">OAuth Clients</a></li> <li data-ng-show="access.viewClients" data-ng-class="(path[2] == 'oauth-clients' || path[1] == 'oauth-client') && 'active'"><a href="#/realms/{{realm.realm}}/oauth-clients">OAuth Clients</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'sessions' || path[2] == 'token-settings') && 'active'"><a href="#/realms/{{realm.realm}}/sessions/realm">Sessions and Tokens</a></li> <li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'sessions' || path[2] == 'token-settings') && 'active'"><a href="#/realms/{{realm.realm}}/sessions/realm">Sessions and Tokens</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'defense') && 'active'"><a href="#/realms/{{realm.realm}}/defense/headers">Security Defenses</a></li>
<li data-ng-show="access.viewAudit" data-ng-class="(path[2] == 'audit' || path[2] == 'audit-settings') && 'active'"><a href="#/realms/{{realm.realm}}/audit">Audit</a></li> <li data-ng-show="access.viewAudit" data-ng-class="(path[2] == 'audit' || path[2] == 'audit-settings') && 'active'"><a href="#/realms/{{realm.realm}}/audit">Audit</a></li>
</ul> </ul>

View file

@ -4,7 +4,6 @@
<li class="active"><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li> <li class="active"><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li>
<li><a href="#/realms/{{realm.realm}}/token-settings">Timeout Settings</a></li> <li><a href="#/realms/{{realm.realm}}/token-settings">Timeout Settings</a></li>
<li><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li> <li><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li>
<li><a href="#/realms/{{realm.realm}}/sessions/brute-force">Brute Force Protection</a></li>
</ul> </ul>
<div id="content"> <div id="content">
<ol class="breadcrumb"> <ol class="breadcrumb">

View file

@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; import org.keycloak.email.EmailProvider;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.FreeMarkerException; import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil; import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
@ -216,7 +217,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
try { try {
String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme); String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme);
return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build(); Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result);
BrowserSecurityHeaderSetup.headers(builder, realm);
return builder.build();
} catch (FreeMarkerException e) { } catch (FreeMarkerException e) {
logger.error("Failed to process template", e); logger.error("Failed to process template", e);
return Response.serverError().build(); return Response.serverError().build();

View file

@ -0,0 +1,27 @@
package org.keycloak.models;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BrowserSecurityHeaders {
public static final Map<String, String> headerAttributeMap;
public static final Map<String, String> defaultHeaders;
static {
Map<String, String> headerMap = new HashMap<String, String>();
headerMap.put("xFrameOptions", "X-Frame-Options");
headerMap.put("contentSecurityPolicy", "Content-Security-Policy");
Map<String, String> dh = new HashMap<String, String>();
dh.put("xFrameOptions", "SAMEORIGIN");
dh.put("contentSecurityPolicy", "frame-src 'self'");
defaultHeaders = Collections.unmodifiableMap(dh);
headerAttributeMap = Collections.unmodifiableMap(headerMap);
}
}

View file

@ -151,6 +151,9 @@ public interface RealmModel extends RoleContainerModel {
List<OAuthClientModel> getOAuthClients(); List<OAuthClientModel> getOAuthClients();
Map<String, String> getBrowserSecurityHeaders();
void setBrowserSecurityHeaders(Map<String, String> headers);
Map<String, String> getSmtpConfig(); Map<String, String> getSmtpConfig();
void setSmtpConfig(Map<String, String> smtpConfig); void setSmtpConfig(Map<String, String> smtpConfig);

View file

@ -52,6 +52,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
private List<RequiredCredentialEntity> requiredCredentials = new ArrayList<RequiredCredentialEntity>(); private List<RequiredCredentialEntity> requiredCredentials = new ArrayList<RequiredCredentialEntity>();
private List<UserFederationProviderEntity> userFederationProviders = new ArrayList<UserFederationProviderEntity>(); private List<UserFederationProviderEntity> userFederationProviders = new ArrayList<UserFederationProviderEntity>();
private Map<String, String> browserSecurityHeaders = new HashMap<String, String>();
private Map<String, String> smtpConfig = new HashMap<String, String>(); private Map<String, String> smtpConfig = new HashMap<String, String>();
private Map<String, String> socialConfig = new HashMap<String, String>(); private Map<String, String> socialConfig = new HashMap<String, String>();
@ -317,6 +318,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
this.requiredCredentials = requiredCredentials; this.requiredCredentials = requiredCredentials;
} }
public Map<String, String> getBrowserSecurityHeaders() {
return browserSecurityHeaders;
}
public void setBrowserSecurityHeaders(Map<String, String> browserSecurityHeaders) {
this.browserSecurityHeaders = browserSecurityHeaders;
}
public Map<String, String> getSmtpConfig() { public Map<String, String> getSmtpConfig() {
return smtpConfig; return smtpConfig;
} }

View file

@ -105,6 +105,7 @@ public class ModelToRepresentation {
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction()); rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
rep.setSmtpServer(realm.getSmtpConfig()); rep.setSmtpServer(realm.getSmtpConfig());
rep.setSocialProviders(realm.getSocialConfig()); rep.setSocialProviders(realm.getSocialConfig());
rep.setBrowserSecurityHeaders(realm.getBrowserSecurityHeaders());
rep.setAccountTheme(realm.getAccountTheme()); rep.setAccountTheme(realm.getAccountTheme());
rep.setLoginTheme(realm.getLoginTheme()); rep.setLoginTheme(realm.getLoginTheme());
rep.setAdminTheme(realm.getAdminTheme()); rep.setAdminTheme(realm.getAdminTheme());

View file

@ -4,6 +4,7 @@ import net.iharder.Base64;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.enums.SslRequired; import org.keycloak.enums.SslRequired;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.ClaimMask; import org.keycloak.models.ClaimMask;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -199,6 +200,12 @@ public class RepresentationToModel {
newRealm.setSmtpConfig(new HashMap(rep.getSmtpServer())); newRealm.setSmtpConfig(new HashMap(rep.getSmtpServer()));
} }
if (rep.getBrowserSecurityHeaders() != null) {
newRealm.setBrowserSecurityHeaders(rep.getBrowserSecurityHeaders());
} else {
newRealm.setBrowserSecurityHeaders(BrowserSecurityHeaders.defaultHeaders);
}
if (rep.getSocialProviders() != null) { if (rep.getSocialProviders() != null) {
newRealm.setSocialConfig(new HashMap(rep.getSocialProviders())); newRealm.setSocialConfig(new HashMap(rep.getSocialProviders()));
} }
@ -266,6 +273,10 @@ public class RepresentationToModel {
realm.setSocialConfig(new HashMap(rep.getSocialProviders())); realm.setSocialConfig(new HashMap(rep.getSocialProviders()));
} }
if (rep.getBrowserSecurityHeaders() != null) {
realm.setBrowserSecurityHeaders(rep.getBrowserSecurityHeaders());
}
if (rep.getUserFederationProviders() != null) { if (rep.getUserFederationProviders() != null) {
List<UserFederationProviderModel> providerModels = convertFederationProviders(rep.getUserFederationProviders()); List<UserFederationProviderModel> providerModels = convertFederationProviders(rep.getUserFederationProviders());
realm.setUserFederationProviders(providerModels); realm.setUserFederationProviders(providerModels);

View file

@ -557,6 +557,19 @@ public class RealmAdapter implements RealmModel {
return clients; return clients;
} }
@Override
public Map<String, String> getBrowserSecurityHeaders() {
if (updated != null) return updated.getBrowserSecurityHeaders();
return cached.getBrowserSecurityHeaders();
}
@Override
public void setBrowserSecurityHeaders(Map<String, String> headers) {
getDelegateForUpdate();
updated.setBrowserSecurityHeaders(headers);
}
@Override @Override
public Map<String, String> getSmtpConfig() { public Map<String, String> getSmtpConfig() {
if (updated != null) return updated.getSmtpConfig(); if (updated != null) return updated.getSmtpConfig();

View file

@ -66,6 +66,7 @@ public class CachedRealm {
private List<RequiredCredentialModel> requiredCredentials = new ArrayList<RequiredCredentialModel>(); private List<RequiredCredentialModel> requiredCredentials = new ArrayList<RequiredCredentialModel>();
private List<UserFederationProviderModel> userFederationProviders = new ArrayList<UserFederationProviderModel>(); private List<UserFederationProviderModel> userFederationProviders = new ArrayList<UserFederationProviderModel>();
private Map<String, String> browserSecurityHeaders = new HashMap<String, String>();
private Map<String, String> smtpConfig = new HashMap<String, String>(); private Map<String, String> smtpConfig = new HashMap<String, String>();
private Map<String, String> socialConfig = new HashMap<String, String>(); private Map<String, String> socialConfig = new HashMap<String, String>();
@ -123,6 +124,7 @@ public class CachedRealm {
smtpConfig.putAll(model.getSmtpConfig()); smtpConfig.putAll(model.getSmtpConfig());
socialConfig.putAll(model.getSocialConfig()); socialConfig.putAll(model.getSocialConfig());
browserSecurityHeaders.putAll(model.getBrowserSecurityHeaders());
auditEnabled = model.isAuditEnabled(); auditEnabled = model.isAuditEnabled();
auditExpiration = model.getAuditExpiration(); auditExpiration = model.getAuditExpiration();
@ -287,6 +289,10 @@ public class CachedRealm {
return socialConfig; return socialConfig;
} }
public Map<String, String> getBrowserSecurityHeaders() {
return browserSecurityHeaders;
}
public String getLoginTheme() { public String getLoginTheme() {
return loginTheme; return loginTheme;
} }

View file

@ -196,6 +196,7 @@ public class RealmAdapter implements RealmModel {
} }
public Map<String, String> getAttributes() { public Map<String, String> getAttributes() {
// should always return a copy
Map<String, String> result = new HashMap<String, String>(); Map<String, String> result = new HashMap<String, String>();
for (RealmAttributeEntity attr : realm.getAttributes()) { for (RealmAttributeEntity attr : realm.getAttributes()) {
result.put(attr.getName(), attr.getValue()); result.put(attr.getName(), attr.getValue());
@ -711,6 +712,27 @@ public class RealmAdapter implements RealmModel {
return list; return list;
} }
private static final String BROWSER_HEADER_PREFIX = "_browser_header.";
@Override
public Map<String, String> getBrowserSecurityHeaders() {
Map<String, String> attributes = getAttributes();
Map<String, String> headers = new HashMap<String, String>();
for (Map.Entry<String, String> entry : attributes.entrySet()) {
if (entry.getKey().startsWith(BROWSER_HEADER_PREFIX)) {
headers.put(entry.getKey().substring(BROWSER_HEADER_PREFIX.length()), entry.getValue());
}
}
return headers;
}
@Override
public void setBrowserSecurityHeaders(Map<String, String> headers) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
setAttribute(BROWSER_HEADER_PREFIX + entry.getKey(), entry.getValue());
}
}
@Override @Override
public Map<String, String> getSmtpConfig() { public Map<String, String> getSmtpConfig() {
return realm.getSmtpConfig(); return realm.getSmtpConfig();

View file

@ -734,6 +734,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
return model; return model;
} }
@Override
public Map<String, String> getBrowserSecurityHeaders() {
return realm.getBrowserSecurityHeaders();
}
@Override
public void setBrowserSecurityHeaders(Map<String, String> headers) {
realm.setBrowserSecurityHeaders(headers);
updateRealm();
}
@Override @Override
public Map<String, String> getSmtpConfig() { public Map<String, String> getSmtpConfig() {
return realm.getSmtpConfig(); return realm.getSmtpConfig();

View file

@ -7,6 +7,7 @@ import org.keycloak.exportimport.util.ImportUtils;
import org.keycloak.models.AccountRoles; import org.keycloak.models.AccountRoles;
import org.keycloak.models.AdminRoles; import org.keycloak.models.AdminRoles;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -117,6 +118,8 @@ public class RealmManager {
protected void setupRealmDefaults(RealmModel realm) { protected void setupRealmDefaults(RealmModel realm) {
realm.setBrowserSecurityHeaders(BrowserSecurityHeaders.defaultHeaders);
// brute force // brute force
realm.setBruteForceProtected(false); // default settings off for now todo set it on realm.setBruteForceProtected(false); // default settings off for now todo set it on
realm.setMaxFailureWaitSeconds(900); realm.setMaxFailureWaitSeconds(900);

View file

@ -8,6 +8,7 @@ import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider; import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.AdminRoles; import org.keycloak.models.AdminRoles;
@ -325,7 +326,9 @@ public class AdminConsole {
cacheControl.setNoTransform(false); cacheControl.setNoTransform(false);
cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1)); cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1));
return Response.ok(resource).type(contentType).cacheControl(cacheControl).build(); Response.ResponseBuilder builder = Response.ok(resource).type(contentType).cacheControl(cacheControl);
BrowserSecurityHeaderSetup.headers(builder, realm);
return builder.build();
} else { } else {
return Response.status(Response.Status.NOT_FOUND).build(); return Response.status(Response.Status.NOT_FOUND).build();
} }

View file

@ -25,6 +25,7 @@ import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.audit.Details; import org.keycloak.audit.Details;
@ -158,12 +159,12 @@ public class AccountTest {
}); });
} }
/* @Ignore
@Test @Test
public void forever() throws Exception{ public void forever() throws Exception{
while (true) Thread.sleep(5000); while (true) Thread.sleep(5000);
} }
*/
@Test @Test
public void returnToAppFromQueryParam() { public void returnToAppFromQueryParam() {

View file

@ -278,6 +278,9 @@ public class AdminAPITest {
if (rep.getSocialProviders() != null) { if (rep.getSocialProviders() != null) {
Assert.assertEquals(rep.getSocialProviders(), storedRealm.getSocialProviders()); Assert.assertEquals(rep.getSocialProviders(), storedRealm.getSocialProviders());
} }
if (rep.getBrowserSecurityHeaders() != null) {
Assert.assertEquals(rep.getBrowserSecurityHeaders(), storedRealm.getBrowserSecurityHeaders());
}
} }

View file

@ -27,6 +27,7 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details; import org.keycloak.audit.Details;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -42,6 +43,11 @@ import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule; import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;
import java.util.Map;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -85,6 +91,20 @@ public class LoginTest {
private static String userId; private static String userId;
@Test
public void testBrowserSecurityHeaders() {
Client client = ClientBuilder.newClient();
Response response = client.target(oauth.getLoginFormUrl()).request().get();
Assert.assertEquals(200, response.getStatus());
for (Map.Entry<String, String> entry : BrowserSecurityHeaders.defaultHeaders.entrySet()) {
String headerName = BrowserSecurityHeaders.headerAttributeMap.get(entry.getKey());
String headerValue = response.getHeaderString(headerName);
Assert.assertNotNull(headerValue);
Assert.assertEquals(headerValue, entry.getValue());
}
response.close();
}
@Test @Test
public void loginInvalidPassword() { public void loginInvalidPassword() {
loginPage.open(); loginPage.open();