X-Frame-Options, Content-Security-Policy
This commit is contained in:
parent
e73bdf9d8c
commit
7c97e02715
25 changed files with 246 additions and 31 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -675,15 +675,6 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
controller : 'RealmRevocationCtrl'
|
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',
|
templateUrl : 'partials/session-realm.html',
|
||||||
resolve : {
|
resolve : {
|
||||||
|
@ -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'
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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">
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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">
|
||||||
|
|
|
@ -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();
|
||||||
|
|
27
model/api/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java
Executable file
27
model/api/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java
Executable 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue