diff --git a/.drone.yml b/.drone.yml
index dd4ec40420971423d42b3b1720c88a7362ff5ddf..8d3e3551a6e86cd098ffcfc269f79b280a3c4712 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -30,6 +30,7 @@ pipeline:
     image: nextcloudci/php7.0:php7.0-7
     commands:
       - ./occ app:check-code admin_audit
+      - ./occ app:check-code bruteforcesettings
       - ./occ app:check-code comments
       - ./occ app:check-code federation
       - ./occ app:check-code sharebymail
diff --git a/.gitignore b/.gitignore
index d8669fed074382ef61978020bef10922f5e94a7a..1e0f53a3c076fb04e1d6f1582e197208c8174f0b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,7 +38,7 @@
 /apps/files_external/3rdparty/irodsphp/prods/tutorials
 /apps/files_external/3rdparty/irodsphp/prods/test*
 /apps/files_external/tests/config.*.php
-
+!/apps/bruteforcesettings
 
 
 # ignore themes except the example and the README
diff --git a/apps/bruteforcesettings/appinfo/info.xml b/apps/bruteforcesettings/appinfo/info.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2c3e1256e8505bccbdd6528662e0ae83260c3553
--- /dev/null
+++ b/apps/bruteforcesettings/appinfo/info.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<info>
+	<id>bruteforcesettings</id>
+	<name>Brute force settings</name>
+	<description>
+		This applications allows admins to configure the brute force settings.
+	</description>
+	<licence>AGPL</licence>
+	<author>Roeland Jago Douma</author>
+	<default_enable/>
+	<version>1.0.0</version>
+	<dependencies>
+		<owncloud min-version="9.2" max-version="9.2" />
+	</dependencies>
+
+	<namespace>BruteForceSettings</namespace>
+
+	<settings>
+		<admin>OCA\BruteForceSettings\Settings\IPWhitelist</admin>
+	</settings>
+</info>
diff --git a/apps/bruteforcesettings/appinfo/routes.php b/apps/bruteforcesettings/appinfo/routes.php
new file mode 100644
index 0000000000000000000000000000000000000000..28ab6e9cee43f4364acf9d663da148ac297d22ec
--- /dev/null
+++ b/apps/bruteforcesettings/appinfo/routes.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+return [
+	'routes' => [
+		[ 'name' => 'IPWhitelist#getAll', 'url' => '/ipwhitelist', 'verb' => 'GET' ],
+		[ 'name' => 'IPWhitelist#add', 'url' => '/ipwhitelist', 'verb' => 'POST' ],
+		[ 'name' => 'IPWhitelist#remove', 'url' => '/ipwhitelist/{id}', 'verb' => 'DELETE' ],
+	]
+];
diff --git a/apps/bruteforcesettings/js/IPWhitelist.js b/apps/bruteforcesettings/js/IPWhitelist.js
new file mode 100644
index 0000000000000000000000000000000000000000..163d5d2852a62e2594e5d88d452026485439fc27
--- /dev/null
+++ b/apps/bruteforcesettings/js/IPWhitelist.js
@@ -0,0 +1,44 @@
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+(function() {
+
+	OCA.BruteForceSettings = OCA.BruteForceSettings || {};
+
+	OCA.BruteForceSettings.WhiteList = {
+
+		collection: null,
+		view: null,
+
+		init: function () {
+			this.collection = new OCA.BruteForceSettings.WhitelistCollection();
+			this.view = new OCA.BruteForceSettings.WhitelistView({
+				collection: this.collection
+			});
+			this.view.reload();
+		}
+	};
+})();
+
+$(document).ready(function() {
+	OCA.BruteForceSettings.WhiteList.init();
+});
diff --git a/apps/bruteforcesettings/js/IPWhitelistCollection.js b/apps/bruteforcesettings/js/IPWhitelistCollection.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf5b34d1ad0dc437ae571af1e3ff2b28b118a21a
--- /dev/null
+++ b/apps/bruteforcesettings/js/IPWhitelistCollection.js
@@ -0,0 +1,34 @@
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+(function() {
+
+	OCA.BruteForceSettings = OCA.BruteForceSettings || {};
+
+	OCA.BruteForceSettings.WhitelistCollection = OC.Backbone.Collection.extend({
+		model: OCA.BruteForceSettings.WhitelistModel,
+
+		url: function() {
+			return OC.generateUrl('/apps/bruteforcesettings/ipwhitelist');
+		}
+	});
+})();
diff --git a/apps/bruteforcesettings/js/IPWhitelistModel.js b/apps/bruteforcesettings/js/IPWhitelistModel.js
new file mode 100644
index 0000000000000000000000000000000000000000..5c309f5af74bad6499482e2117653f6aca4261dc
--- /dev/null
+++ b/apps/bruteforcesettings/js/IPWhitelistModel.js
@@ -0,0 +1,29 @@
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+(function() {
+
+	OCA.BruteForceSettings = OCA.BruteForceSettings || {};
+
+	OCA.BruteForceSettings.WhitelistModel = OC.Backbone.Model.extend({
+	});
+})();
diff --git a/apps/bruteforcesettings/js/IPWhitelistView.js b/apps/bruteforcesettings/js/IPWhitelistView.js
new file mode 100644
index 0000000000000000000000000000000000000000..da711ae12252e5e941785cd618e0fc3c350e869f
--- /dev/null
+++ b/apps/bruteforcesettings/js/IPWhitelistView.js
@@ -0,0 +1,128 @@
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+(function () {
+
+	OCA.BruteForceSettings = OCA.BruteForceSettings || {};
+
+	var TEMPLATE_WHITELIST =
+		'<tr data-id="{{id}}">'
+		+ '<td><span>{{ip}}/{{mask}}</span></td>'
+		+ '<td><a class="icon-delete has-tooltip" title="' + t('bruteforcesettings', 'Delete') + '">BAD CSS</a></td>'
+		+ '</tr>';
+
+	OCA.BruteForceSettings.WhitelistView = OC.Backbone.View.extend({
+		collection: null,
+
+		ipInput: undefined,
+		maskInput: undefined,
+		submit: undefined,
+
+		list: undefined,
+		listHeader: undefined,
+
+		initialize: function(options) {
+			this.collection = options.collection;
+
+			this.ipInput = $('#whitelist_ip');
+			this.maskInput = $('#whitelist_mask');
+			this.submit = $('#whitelist_submit');
+			this.submit.click(_.bind(this._addWhitelist, this));
+
+			this.list = $('#whitelist-list');
+			this.listHeader = $('#whitelist-list-header');
+
+			this.list.on('click', 'a.icon-delete', _.bind(this._onDeleteRetention, this));
+			this.listenTo(this.collection, 'sync', this.render);
+		},
+
+
+
+		reload: function() {
+			var _this = this;
+			var loadingWhitelists = this.collection.fetch();
+
+			$.when(loadingWhitelists).done(function () {
+				_this.render();
+			});
+			$.when(loadingWhitelists).fail(function () {
+				OC.Notification.showTemporary(t('bruteforcesettings', 'Error while whitelists.'));
+			});
+		},
+
+		template: function (data) {
+			if (_.isUndefined(this._template)) {
+				this._template = Handlebars.compile(TEMPLATE_WHITELIST);
+			}
+
+			return this._template(data);
+		},
+
+		render: function () {
+			var _this = this;
+			this.list.html('');
+
+			this.collection.forEach(function (model) {
+				var data = {
+					id: model.attributes.id,
+					ip: model.attributes.ip,
+					mask: model.attributes.mask
+				};
+				var html = _this.template(data);
+				var $html = $(html);
+				_this.list.append($html);
+			});
+		},
+
+		_onDeleteRetention: function(event) {
+			var $target = $(event.target);
+			var $row = $target.closest('tr');
+			var id = $row.data('id');
+
+			var whitelist = this.collection.get(id);
+
+			if (_.isUndefined(whitelist)) {
+				// Ignore event
+				return;
+			}
+
+			var destroyingRetention = whitelist.destroy();
+
+			$row.find('.icon-delete').tooltip('hide');
+
+			var _this = this;
+			$.when(destroyingRetention).fail(function () {
+				OC.Notification.showTemporary(t('bruteforcesettings', 'Error while deleting a whitelist'));
+			});
+			$.when(destroyingRetention).always(function () {
+				_this.render();
+			});
+		},
+
+		_addWhitelist: function() {
+			this.collection.create({
+				ip: this.ipInput.val(),
+				mask: this.maskInput.val()
+			});
+		}
+	});
+})();
diff --git a/apps/bruteforcesettings/lib/Controller/IPWhitelistController.php b/apps/bruteforcesettings/lib/Controller/IPWhitelistController.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b5183f7495fb68b550e626d9498b07d8d0fcfc8
--- /dev/null
+++ b/apps/bruteforcesettings/lib/Controller/IPWhitelistController.php
@@ -0,0 +1,123 @@
+<?php
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\BruteForceSettings\Controller;
+
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IConfig;
+use OCP\IRequest;
+
+class IPWhitelistController extends Controller {
+
+	/** @var IConfig */
+	private $config;
+
+	/**
+	 * IPWhitelistController constructor.
+	 *
+	 * @param string $appName
+	 * @param IRequest $request
+	 * @param IConfig $config
+	 */
+	public function __construct($appName,
+								IRequest $request,
+								IConfig $config) {
+		parent::__construct($appName, $request);
+
+		$this->config = $config;
+	}
+
+	/**
+	 * @return JSONResponse
+	 */
+	public function getAll() {
+		$keys = $this->config->getAppKeys('bruteForce');
+		$keys = array_filter($keys, function($key) {
+			$regex = '/^whitelist_/S';
+			return preg_match($regex, $key) === 1;
+		});
+
+		$result = [];
+
+		foreach ($keys as $key) {
+			$value = $this->config->getAppValue('bruteForce', $key);
+			$values = explode('/', $value);
+
+			$result[] = [
+				'id' => (int)substr($key, 10),
+				'ip' => $values[0],
+				'mask' => $values[1],
+			];
+		}
+
+		return new JSONResponse($result);
+	}
+
+	/**
+	 * @param string $ip
+	 * @param int $mask
+	 * @return JSONResponse
+	 */
+	public function add($ip, $mask) {
+		if (!filter_var($ip, FILTER_VALIDATE_IP) ||
+			(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && ($mask < 0 || $mask > 32)) ||
+			(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && ($mask < 0 || $mask > 128))) {
+			return new JSONResponse([], Http::STATUS_BAD_REQUEST);
+		}
+
+		$keys = $this->config->getAppKeys('bruteForce');
+		$keys = array_filter($keys, function($key) {
+			$regex = '/^whitelist_/S';
+			return preg_match($regex, $key) === 1;
+		});
+
+		$id = 0;
+		foreach ($keys as $key) {
+			$tmp = (int)substr($key, 10);
+			if ($tmp > $id) {
+				$id = $tmp;
+			}
+		}
+		$id++;
+
+		$value = $ip . '/' . $mask;
+		$this->config->setAppValue('bruteForce', 'whitelist_'.$id, $value);
+		return new JSONResponse([
+			'id' => $id,
+			'ip' => $ip,
+			'mask' => $mask,
+		]);
+	}
+
+	/**
+	 * @param int $id
+	 * @return JSONResponse
+	 */
+	public function remove($id) {
+		$this->config->deleteAppValue('bruteForce', 'whitelist_'.$id);
+
+		return new JSONResponse([]);
+	}
+}
diff --git a/apps/bruteforcesettings/lib/Settings/IPWhitelist.php b/apps/bruteforcesettings/lib/Settings/IPWhitelist.php
new file mode 100644
index 0000000000000000000000000000000000000000..7e4aab2f22f4f064aa6640554b4022159bba3a7a
--- /dev/null
+++ b/apps/bruteforcesettings/lib/Settings/IPWhitelist.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\BruteForceSettings\Settings;
+
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\Settings\ISettings;
+
+class IPWhitelist implements ISettings {
+
+	public function getForm() {
+		return new TemplateResponse('bruteforcesettings', 'ipwhitelist');
+	}
+
+	public function getSection() {
+		return 'security';
+	}
+
+	public function getPriority() {
+		return 50;
+	}
+}
diff --git a/apps/bruteforcesettings/templates/ipwhitelist.php b/apps/bruteforcesettings/templates/ipwhitelist.php
new file mode 100644
index 0000000000000000000000000000000000000000..69354956168d49908d6f137743898abc3422686b
--- /dev/null
+++ b/apps/bruteforcesettings/templates/ipwhitelist.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+script('core', [
+	'oc-backbone-webdav',
+]);
+script('bruteforcesettings', [
+	'IPWhitelist',
+	'IPWhitelistModel',
+	'IPWhitelistCollection',
+	'IPWhitelistView',
+]);
+
+/** @var \OCP\IL10N $l */
+?>
+<form id="IPWhiteList" class="section">
+	<h2><?php p($l->t('Brute force ip whitelist')); ?></h2>
+
+	<table>
+		<tbody id="whitelist-list">
+
+		</tbody>
+	</table>
+
+	<input type="text" name="whitelist_ip" id="whitelist_ip" placeholder="1.2.3.4" style="width: 200px;" />/
+	<input type="number" id="whitelist_mask" name="whitelist_mask" placeholder="24" style="width: 50px;">
+	<input type="button" id="whitelist_submit" value="<?php p($l->t('Add')); ?>">
+</form>
diff --git a/apps/bruteforcesettings/tests/Controller/IPWhitelistControllerTest.php b/apps/bruteforcesettings/tests/Controller/IPWhitelistControllerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c45fc6a9417b2f444849346a5a9512772447f34d
--- /dev/null
+++ b/apps/bruteforcesettings/tests/Controller/IPWhitelistControllerTest.php
@@ -0,0 +1,151 @@
+<?php
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\BruteForceSettings\Tests\Controller;
+
+use OCA\BruteForceSettings\Controller\IPWhitelistController;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IConfig;
+use OCP\IRequest;
+use Test\TestCase;
+
+class IPWhitelistControllerTest extends TestCase {
+
+	/** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
+	private $config;
+	/** @var IPWhitelistController */
+	private $controller;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->config = $this->createMock(IConfig::class);
+		$this->controller = new IPWhitelistController(
+			'bruteforce',
+			$this->createMock(IRequest::class),
+			$this->config
+		);
+	}
+
+	public function testGetAll() {
+		$this->config->method('getAppKeys')
+			->with($this->equalTo('bruteForce'))
+			->willReturn([
+				'foobar',
+				'whitelist_0',
+				'whitelist_99',
+			]);
+
+		$this->config->method('getAppValue')
+			->will($this->returnCallback(function($app, $key) {
+				if ($app !== 'bruteForce') {
+					$this->fail();
+				}
+				if ($key === 'whitelist_0') {
+					return '192.168.2.0/24';
+				} else if ($key === 'whitelist_99') {
+					return 'dead:beef:cafe::/92';
+				}
+				$this->fail();
+			}));
+
+		$expected = new JSONResponse([
+			[
+				'id' => 0,
+				'ip' => '192.168.2.0',
+				'mask' => '24',
+			],
+			[
+				'id' => 99,
+				'ip' => 'dead:beef:cafe::',
+				'mask' => '92',
+			]
+		]);
+
+		$this->assertEquals($expected, $this->controller->getAll());
+	}
+
+	public function dataAdd() {
+		return [
+			['8.500.2.3', 24, false],
+			['1.2.3.4', 24, true],
+			['1.2.3.4', -1, false],
+			['1.2.3.4', 33, false],
+
+			['dead:nope::8', 24, false],
+			['1234:567:abef::1a2b', 24, true],
+			['1234:567:abef::1a2b', -1, false],
+			['1234:567:abef::1a2b', 129, false],
+		];
+	}
+
+	/**
+	 * @dataProvider dataAdd
+	 *
+	 * @param string $ip
+	 * @param int $mask
+	 * @param bool $valid
+	 */
+	public function testAdd($ip, $mask, $valid) {
+		if (!$valid) {
+			$expected = new JSONResponse([], Http::STATUS_BAD_REQUEST);
+		} else {
+			$this->config->method('getAppKeys')
+				->with($this->equalTo('bruteForce'))
+				->willReturn([
+					'foobar',
+					'whitelist_0',
+					'whitelist_99',
+				]);
+
+			$this->config->expects($this->once())
+				->method('setAppValue')
+				->with(
+					$this->equalTo('bruteForce'),
+					$this->equalTo('whitelist_100'),
+					$this->equalTo($ip.'/'.$mask)
+				);
+
+			$expected = new JSONResponse([
+				'id' => 100,
+				'ip' => $ip,
+				'mask' => $mask,
+			]);
+		}
+
+		$this->assertEquals($expected, $this->controller->add($ip, $mask));
+	}
+
+	public function testRemove() {
+		$this->config->expects($this->once())
+			->method('deleteAppValue')
+			->with(
+				$this->equalTo('bruteForce'),
+				$this->equalTo('whitelist_42')
+			);
+
+		$expected = new JSONResponse([]);
+		$this->assertEquals($expected, $this->controller->remove(42));
+	}
+}
diff --git a/apps/bruteforcesettings/tests/Settings/IPWhitelistTest.php b/apps/bruteforcesettings/tests/Settings/IPWhitelistTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..37d13a86d562772009639546940794e1661bb1f8
--- /dev/null
+++ b/apps/bruteforcesettings/tests/Settings/IPWhitelistTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace OCA\BruteForceSettings\Tests\Settings;
+
+use OCA\BruteForceSettings\Settings\IPWhitelist;
+use OCP\AppFramework\Http\TemplateResponse;
+use Test\TestCase;
+
+class IPWhitelistTest extends TestCase {
+
+	/** @var IPWhitelist */
+	private $settings;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->settings = new IPWhitelist();
+	}
+
+	public function testGetForm() {
+		$expected = new TemplateResponse('bruteforcesettings', 'ipwhitelist');
+
+		$this->assertEquals($expected, $this->settings->getForm());
+	}
+
+	public function testGetSection() {
+		$this->assertSame('security', $this->settings->getSection());
+	}
+
+	public function testGetPriority() {
+		$this->assertSame(50, $this->settings->getPriority());
+	}
+}
diff --git a/apps/bruteforcesettings/tests/js/IPWhitelistSpec.js b/apps/bruteforcesettings/tests/js/IPWhitelistSpec.js
new file mode 100644
index 0000000000000000000000000000000000000000..21ba32faa3620e48e212e1aa744b631d1be90e7b
--- /dev/null
+++ b/apps/bruteforcesettings/tests/js/IPWhitelistSpec.js
@@ -0,0 +1,174 @@
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+describe('OCA.BruteForceSettings.IPWhiteList tests', function() {
+	beforeEach(function() {
+		// init parameters and test table elements
+		$('#testArea').append(
+			'<table>'+
+			'<tbody id="whitelist-list">' +
+			'</tbody>' +
+			'</table>' +
+			'<input type="text" name="whitelist_ip" id="whitelist_ip" placeholder="1.2.3.4" style="width: 200px;" />/' +
+			'<input type="number" id="whitelist_mask" name="whitelist_mask" placeholder="24" style="width: 50px;">' +
+			'<input type="button" id="whitelist_submit" value="Add">'
+		);
+	});
+
+	it('get intial empty', function() {
+		OCA.BruteForceSettings.WhiteList.init();
+
+		expect(fakeServer.requests.length).toEqual(1);
+		expect(fakeServer.requests[0].method).toEqual('GET');
+		expect(fakeServer.requests[0].url).toEqual(
+			OC.generateUrl('/apps/bruteforcesettings/ipwhitelist')
+		);
+		fakeServer.requests[0].respond(
+			200,
+			{ 'Content-Type': 'application/json' },
+			'[]'
+		);
+
+		expect($('#whitelist-list > tr').length).toEqual(0);
+	});
+	it('get intial filled', function() {
+		OCA.BruteForceSettings.WhiteList.init();
+
+		expect(fakeServer.requests.length).toEqual(1);
+		expect(fakeServer.requests[0].method).toEqual('GET');
+		expect(fakeServer.requests[0].url).toEqual(
+			OC.generateUrl('/apps/bruteforcesettings/ipwhitelist')
+		);
+		fakeServer.requests[0].respond(
+			200,
+			{ 'Content-Type': 'application/json' },
+			JSON.stringify([
+				{
+					id: 1,
+					ip: '11.22.0.0',
+					mask: 16
+				},
+				{
+					id: 12,
+					ip: 'cafe:cafe::',
+					mask: 80
+				}
+			])
+		);
+
+		expect($('#whitelist-list > tr').length).toEqual(2);
+
+		var el1 = $($('#whitelist-list > tr').get(0));
+		expect(el1.data('id')).toEqual(1);
+		expect($(el1.find('td > span')[0]).html()).toEqual('11.22.0.0/16');
+
+		var el2 = $($('#whitelist-list > tr').get(1));
+		expect(el2.data('id')).toEqual(12);
+		expect($(el2.find('td > span')[0]).html()).toEqual('cafe:cafe::/80');
+	});
+	it('add whitelist', function() {
+		OCA.BruteForceSettings.WhiteList.init();
+
+		expect(fakeServer.requests.length).toEqual(1);
+		expect(fakeServer.requests[0].method).toEqual('GET');
+		expect(fakeServer.requests[0].url).toEqual(
+			OC.generateUrl('/apps/bruteforcesettings/ipwhitelist')
+		);
+		fakeServer.requests[0].respond(
+			200,
+			{ 'Content-Type': 'application/json' },
+			'[]'
+		);
+
+		expect($('#whitelist-list > tr').length).toEqual(0);
+
+		$('#whitelist_ip').val('2.4.8.16');
+		$('#whitelist_mask').val('8');
+		$('#whitelist_submit').click();
+
+		expect(fakeServer.requests.length).toEqual(2);
+		expect(fakeServer.requests[1].method).toEqual('POST');
+		expect(JSON.parse(fakeServer.requests[1].requestBody)).toEqual({
+			ip: '2.4.8.16',
+			mask: '8'
+		});
+		expect(fakeServer.requests[1].url).toEqual(
+			OC.generateUrl('/apps/bruteforcesettings/ipwhitelist')
+		);
+		fakeServer.requests[1].respond(
+			200,
+			{ 'Content-Type': 'application/json' },
+			JSON.stringify({
+				id: 99,
+				ip: '2.4.8.16',
+				mask: 8
+			})
+		);
+
+		expect($('#whitelist-list > tr').length).toEqual(1);
+
+		var el1 = $($('#whitelist-list > tr').get(0));
+		expect(el1.data('id')).toEqual(99);
+		expect($(el1.find('td > span')[0]).html()).toEqual('2.4.8.16/8');
+	});
+	it('delete whitelist', function() {
+		OCA.BruteForceSettings.WhiteList.init();
+
+		expect(fakeServer.requests.length).toEqual(1);
+		expect(fakeServer.requests[0].method).toEqual('GET');
+		expect(fakeServer.requests[0].url).toEqual(
+			OC.generateUrl('/apps/bruteforcesettings/ipwhitelist')
+		);
+		fakeServer.requests[0].respond(
+			200,
+			{ 'Content-Type': 'application/json' },
+			JSON.stringify([
+				{
+					id: 1,
+					ip: '1.2.3.4',
+					mask: 8
+				}
+			])
+		);
+
+		expect($('#whitelist-list > tr').length).toEqual(1);
+
+		var el1 = $($('#whitelist-list > tr').get(0));
+		expect(el1.data('id')).toEqual(1);
+		expect($(el1.find('td > span')[0]).html()).toEqual('1.2.3.4/8');
+		el1.find('.icon-delete').click();
+
+		expect(fakeServer.requests.length).toEqual(2);
+		expect(fakeServer.requests[1].method).toEqual('DELETE');
+		expect(fakeServer.requests[1].url).toEqual(
+			OC.generateUrl('/apps/bruteforcesettings/ipwhitelist/1')
+		);
+
+		fakeServer.requests[1].respond(
+			200,
+			{ 'Content-Type': 'application/json' },
+			'[]'
+		);
+
+		expect($('#whitelist-list > tr').length).toEqual(0);
+	});
+});
diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature
index ad9d901d0515cf0ae40f824ec4094a4a30e56614..856d17e5c106c4a0046af58f98b8f48d46847864 100644
--- a/build/integration/features/provisioning-v1.feature
+++ b/build/integration/features/provisioning-v1.feature
@@ -282,6 +282,7 @@ Feature: provisioning
 		Then the OCS status code should be "100"
 		And the HTTP status code should be "200"
 		And apps returned are
+			| bruteforcesettings |
 			| comments |
 			| dav |
 			| federatedfilesharing |
diff --git a/tests/karma.config.js b/tests/karma.config.js
index 91052f62cd2fcffd643390a8bb638af0c198725e..f965738872f3da7a022b117de1969794fe930c7a 100644
--- a/tests/karma.config.js
+++ b/tests/karma.config.js
@@ -118,6 +118,19 @@ module.exports = function(config) {
 					'settings/tests/js/appsSpec.js',
 					'settings/tests/js/users/deleteHandlerSpec.js'
 				]
+			},
+			{
+				name: 'bruteforcesettings',
+				srcFiles: [
+					// need to enforce loading order...
+					'apps/bruteforcesettings/js/IPWhitelistModel.js',
+					'apps/bruteforcesettings/js/IPWhitelistCollection.js',
+					'apps/bruteforcesettings/js/IPWhitelistView.js',
+					'apps/bruteforcesettings/js/IPWhitelist.js',
+				],
+				testFiles: [
+					'apps/bruteforcesettings/tests/js/IPWhitelistSpec.js'
+				]
 			}
 		];
 	}
diff --git a/tests/phpunit-autotest.xml b/tests/phpunit-autotest.xml
index 9a9c9c957e374dd8924080295391f2009cd1cb5a..40633aff5aed053d61c134e8d863958235bdb92d 100644
--- a/tests/phpunit-autotest.xml
+++ b/tests/phpunit-autotest.xml
@@ -21,6 +21,7 @@
 			<directory suffix=".php">..</directory>
 			<exclude>
 				<directory suffix=".php">../3rdparty</directory>
+				<directory suffix=".php">../apps/bruteforcesettings/tests</directory>
 				<directory suffix=".php">../apps/dav/tests</directory>
 				<directory suffix=".php">../apps/encryption/tests</directory>
 				<directory suffix=".php">../apps/federatedfilesharing/tests</directory>