keycloak-uncached
Changes
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java 5(+4 -1)
forms/common-freemarker/src/main/java/org/keycloak/freemarker/BrowserSecurityHeaderSetup.java 23(+23 -0)
forms/common-themes/src/main/resources/theme/admin/base/resources/partials/brute-force.html 10(+2 -8)
forms/common-themes/src/main/resources/theme/admin/base/resources/partials/defense-headers.html 36(+36 -0)
forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-detail.html 3(+1 -2)
forms/common-themes/src/main/resources/theme/admin/base/resources/partials/session-realm.html 1(+0 -1)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java 5(+4 -1)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java 6(+6 -0)
Details
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index 3bb7e83..dad5eac 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -52,6 +52,7 @@ public class RealmRepresentation {
protected Map<String, List<ScopeMappingRepresentation>> applicationScopeMappings;
protected List<ApplicationRepresentation> applications;
protected List<OAuthClientRepresentation> oauthClients;
+ protected Map<String, String> browserSecurityHeaders;
protected Map<String, String> socialProviders;
protected Map<String, String> smtpServer;
protected List<UserFederationProviderRepresentation> userFederationProviders;
@@ -291,6 +292,14 @@ public class RealmRepresentation {
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() {
return socialProviders;
}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
index 3e89af8..6ec3416 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
@@ -13,6 +13,7 @@ import org.keycloak.account.freemarker.model.SessionsBean;
import org.keycloak.account.freemarker.model.TotpBean;
import org.keycloak.account.freemarker.model.UrlBean;
import org.keycloak.audit.Event;
+import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme;
@@ -136,7 +137,9 @@ public class FreeMarkerAccountProvider implements AccountProvider {
try {
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) {
logger.error("Failed to process template", e);
return Response.serverError().build();
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/BrowserSecurityHeaderSetup.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/BrowserSecurityHeaderSetup.java
new file mode 100755
index 0000000..da8fc5d
--- /dev/null
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/BrowserSecurityHeaderSetup.java
@@ -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;
+ }
+}
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
index 4f7b83a..4c434c2 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
@@ -675,16 +675,7 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'RealmRevocationCtrl'
})
- .when('/realms/:realm/sessions/brute-force', {
- templateUrl : 'partials/session-brute-force.html',
- resolve : {
- realm : function(RealmLoader) {
- return RealmLoader();
- }
- },
- controller : 'RealmBruteForceCtrl'
- })
- .when('/realms/:realm/sessions/realm', {
+ .when('/realms/:realm/sessions/realm', {
templateUrl : 'partials/session-realm.html',
resolve : {
realm : function(RealmLoader) {
@@ -761,6 +752,28 @@ module.config([ '$routeProvider', function($routeProvider) {
},
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', {
templateUrl : 'partials/home.html',
controller : 'LogoutCtrl'
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js
index e89401d..8ce2c34 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js
@@ -340,8 +340,6 @@ function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $l
var oldCopy = angular.copy($scope.realm);
-
-
$scope.changed = false;
$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) {
- 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) {
- 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) {
- 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) {
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/defense-headers.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/defense-headers.html
new file mode 100755
index 0000000..734efa9
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/defense-headers.html
@@ -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>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-detail.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-detail.html
index f9c1e3b..ac43458 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-detail.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-detail.html
@@ -15,17 +15,16 @@
<fieldset class="border-top">
<div class="form-group">
<label class="col-sm-2 control-label" for="name">Name <span class="required" data-ng-show="createRealm">*</span></label>
-
<div class="col-sm-4">
<input class="form-control" type="text" id="name" name="name" data-ng-model="realm.realm" autofocus required>
</div>
</div>
<div class="form-group">
<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">
<input ng-model="realm.enabled" name="enabled" id="enabled" onoffswitch />
</div>
- <span tooltip="Users and applications can only access a realm if it's enabled" class="pficon pficon-help"></span>
</div>
</fieldset>
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-menu.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-menu.html
index 1bc9c6b..b5dca73 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-menu.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-menu.html
@@ -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.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] == '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>
</ul>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/session-realm.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/session-realm.html
index 866b2f7..f5f164a 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/session-realm.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/session-realm.html
@@ -4,7 +4,6 @@
<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}}/sessions/revocation">Revocation</a></li>
- <li><a href="#/realms/{{realm.realm}}/sessions/brute-force">Brute Force Protection</a></li>
</ul>
<div id="content">
<ol class="breadcrumb">
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
index fcabeb1..34168cd 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
+import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme;
@@ -216,7 +217,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
try {
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) {
logger.error("Failed to process template", e);
return Response.serverError().build();
diff --git a/model/api/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java b/model/api/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java
new file mode 100755
index 0000000..e7d479f
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java
@@ -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);
+ }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
index 8d3779b..fdba16a 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
@@ -52,6 +52,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
private List<RequiredCredentialEntity> requiredCredentials = new ArrayList<RequiredCredentialEntity>();
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> socialConfig = new HashMap<String, String>();
@@ -317,6 +318,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
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() {
return smtpConfig;
}
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index 4ae280f..b863e32 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -151,6 +151,9 @@ public interface RealmModel extends RoleContainerModel {
List<OAuthClientModel> getOAuthClients();
+ Map<String, String> getBrowserSecurityHeaders();
+ void setBrowserSecurityHeaders(Map<String, String> headers);
+
Map<String, String> getSmtpConfig();
void setSmtpConfig(Map<String, String> smtpConfig);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 4499465..8e34e15 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -105,6 +105,7 @@ public class ModelToRepresentation {
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
rep.setSmtpServer(realm.getSmtpConfig());
rep.setSocialProviders(realm.getSocialConfig());
+ rep.setBrowserSecurityHeaders(realm.getBrowserSecurityHeaders());
rep.setAccountTheme(realm.getAccountTheme());
rep.setLoginTheme(realm.getLoginTheme());
rep.setAdminTheme(realm.getAdminTheme());
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index c7d7c26..b79d6c2 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -4,6 +4,7 @@ import net.iharder.Base64;
import org.jboss.logging.Logger;
import org.keycloak.enums.SslRequired;
import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.ClaimMask;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
@@ -199,6 +200,12 @@ public class RepresentationToModel {
newRealm.setSmtpConfig(new HashMap(rep.getSmtpServer()));
}
+ if (rep.getBrowserSecurityHeaders() != null) {
+ newRealm.setBrowserSecurityHeaders(rep.getBrowserSecurityHeaders());
+ } else {
+ newRealm.setBrowserSecurityHeaders(BrowserSecurityHeaders.defaultHeaders);
+ }
+
if (rep.getSocialProviders() != null) {
newRealm.setSocialConfig(new HashMap(rep.getSocialProviders()));
}
@@ -266,6 +273,10 @@ public class RepresentationToModel {
realm.setSocialConfig(new HashMap(rep.getSocialProviders()));
}
+ if (rep.getBrowserSecurityHeaders() != null) {
+ realm.setBrowserSecurityHeaders(rep.getBrowserSecurityHeaders());
+ }
+
if (rep.getUserFederationProviders() != null) {
List<UserFederationProviderModel> providerModels = convertFederationProviders(rep.getUserFederationProviders());
realm.setUserFederationProviders(providerModels);
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index cacc084..a6256b0 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
@@ -66,6 +66,7 @@ public class CachedRealm {
private List<RequiredCredentialModel> requiredCredentials = new ArrayList<RequiredCredentialModel>();
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> socialConfig = new HashMap<String, String>();
@@ -123,6 +124,7 @@ public class CachedRealm {
smtpConfig.putAll(model.getSmtpConfig());
socialConfig.putAll(model.getSocialConfig());
+ browserSecurityHeaders.putAll(model.getBrowserSecurityHeaders());
auditEnabled = model.isAuditEnabled();
auditExpiration = model.getAuditExpiration();
@@ -287,6 +289,10 @@ public class CachedRealm {
return socialConfig;
}
+ public Map<String, String> getBrowserSecurityHeaders() {
+ return browserSecurityHeaders;
+ }
+
public String getLoginTheme() {
return loginTheme;
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
index 9fb8a1e..fac8410 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
@@ -558,6 +558,19 @@ public class RealmAdapter implements RealmModel {
}
@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
public Map<String, String> getSmtpConfig() {
if (updated != null) return updated.getSmtpConfig();
return cached.getSmtpConfig();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 4b44180..612d3ec 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -196,6 +196,7 @@ public class RealmAdapter implements RealmModel {
}
public Map<String, String> getAttributes() {
+ // should always return a copy
Map<String, String> result = new HashMap<String, String>();
for (RealmAttributeEntity attr : realm.getAttributes()) {
result.put(attr.getName(), attr.getValue());
@@ -711,6 +712,27 @@ public class RealmAdapter implements RealmModel {
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
public Map<String, String> getSmtpConfig() {
return realm.getSmtpConfig();
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 893f52e..0c6eda2 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -735,6 +735,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
@Override
+ public Map<String, String> getBrowserSecurityHeaders() {
+ return realm.getBrowserSecurityHeaders();
+ }
+
+ @Override
+ public void setBrowserSecurityHeaders(Map<String, String> headers) {
+ realm.setBrowserSecurityHeaders(headers);
+ updateRealm();
+ }
+
+ @Override
public Map<String, String> getSmtpConfig() {
return realm.getSmtpConfig();
}
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index c8b613c..c6ccb76 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -7,6 +7,7 @@ import org.keycloak.exportimport.util.ImportUtils;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@@ -117,6 +118,8 @@ public class RealmManager {
protected void setupRealmDefaults(RealmModel realm) {
+ realm.setBrowserSecurityHeaders(BrowserSecurityHeaders.defaultHeaders);
+
// brute force
realm.setBruteForceProtected(false); // default settings off for now todo set it on
realm.setMaxFailureWaitSeconds(900);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
index 2d7c0ca..cce5a0b 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
@@ -8,6 +8,7 @@ import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.ClientConnection;
import org.keycloak.Config;
+import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.AdminRoles;
@@ -325,7 +326,9 @@ public class AdminConsole {
cacheControl.setNoTransform(false);
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 {
return Response.status(Response.Status.NOT_FOUND).build();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 0d0c170..9ebdaff 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -25,6 +25,7 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
@@ -158,12 +159,12 @@ public class AccountTest {
});
}
- /*
+ @Ignore
@Test
public void forever() throws Exception{
while (true) Thread.sleep(5000);
}
- */
+
@Test
public void returnToAppFromQueryParam() {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
index e22c9d1..4942411 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
@@ -278,6 +278,9 @@ public class AdminAPITest {
if (rep.getSocialProviders() != null) {
Assert.assertEquals(rep.getSocialProviders(), storedRealm.getSocialProviders());
}
+ if (rep.getBrowserSecurityHeaders() != null) {
+ Assert.assertEquals(rep.getBrowserSecurityHeaders(), storedRealm.getBrowserSecurityHeaders());
+ }
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
index c406bfb..4447019 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
@@ -27,6 +27,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
+import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
@@ -42,6 +43,11 @@ import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
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>
*/
@@ -86,6 +92,20 @@ public class LoginTest {
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
public void loginInvalidPassword() {
loginPage.open();
loginPage.login("login-test", "invalid");