Added user authentication failure log
authorMarcel Werk <burntime@woltlab.com>
Tue, 24 Jun 2014 13:55:29 +0000 (15:55 +0200)
committerMarcel Werk <burntime@woltlab.com>
Tue, 24 Jun 2014 13:55:29 +0000 (15:55 +0200)
17 files changed:
com.woltlab.wcf/acpMenu.xml
com.woltlab.wcf/option.xml
com.woltlab.wcf/templates/login.tpl
wcfsetup/install/files/acp/templates/captcha.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/captchaQuestion.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/login.tpl
wcfsetup/install/files/acp/templates/recaptcha.tpl
wcfsetup/install/files/acp/templates/userAuthenticationFailureList.tpl [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/LoginForm.class.php
wcfsetup/install/files/lib/acp/page/UserAuthenticationFailureListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailure.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailureAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailureEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailureList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/cronjob/DailyCleanUpCronjob.class.php
wcfsetup/install/lang/de.xml
wcfsetup/setup/db/install.sql

index 29df7909ae7eace4447bf84613faa7da6ae43825..42f72dfc57dc51c694fc0d42214f37dfcd1ad08d 100644 (file)
                        <parent>wcf.acp.menu.link.log</parent>
                        <permissions>admin.system.canViewLog</permissions>
                </acpmenuitem>
+               
+               <acpmenuitem name="wcf.acp.menu.link.log.authentication.failure">
+                       <controller><![CDATA[wcf\acp\page\UserAuthenticationFailureListPage]]></controller>
+                       <parent>wcf.acp.menu.link.log</parent>
+                       <permissions>admin.system.canViewLog</permissions>
+                       <options>enable_user_authentication_failure</options>
+               </acpmenuitem>
                <!-- /log -->
                
                <acpmenuitem name="wcf.acp.menu.link.user">
index d83bc75e537343ed39c4a1d69ee67c9a4d5d4e2d..28ce1007115960065202910a93bbc910f44a32f7 100644 (file)
                                        <category name="security.general.session">
                                                <parent>security.general</parent>
                                        </category>
+                                       <category name="security.general.authentication">
+                                               <parent>security.general</parent>
+                                       </category>
                                <category name="security.blacklist">
                                        <parent>security</parent>
                                </category>
@@ -481,7 +484,7 @@ imagick:wcf.acp.option.image_adapter_type.imagick]]>
                        </option>
                        <!-- /general.system.proxy -->
                        
-                       <!-- general.session -->
+                       <!-- security.general.session -->
                        <option name="session_timeout">
                                <categoryname>security.general.session</categoryname>
                                <optiontype>integer</optiontype>
@@ -510,7 +513,48 @@ imagick:wcf.acp.option.image_adapter_type.imagick]]>
                                <optiontype>boolean</optiontype>
                                <defaultvalue>1</defaultvalue>
                        </option>
-                       <!-- /general.session -->
+                       <!-- /security.general.session -->
+                       
+                       <!-- security.general.authentication -->
+                       <option name="enable_user_authentication_failure">
+                               <categoryname>security.general.authentication</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                               <enableoptions>user_authentication_failure_timeout,user_authentication_failure_ip_captcha,user_authentication_failure_ip_block,user_authentication_failure_user_captcha,user_authentication_failure_expiration</enableoptions>
+                       </option>
+                       <option name="user_authentication_failure_timeout">
+                               <categoryname>security.general.authentication</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>7200</defaultvalue>
+                               <minvalue>300</minvalue>
+                               <maxvalue>86400</maxvalue>
+                       </option>
+                       <option name="user_authentication_failure_ip_captcha">
+                               <categoryname>security.general.authentication</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>3</defaultvalue>
+                               <minvalue>0</minvalue>
+                       </option>
+                       <option name="user_authentication_failure_ip_block">
+                               <categoryname>security.general.authentication</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>10</defaultvalue>
+                               <minvalue>0</minvalue>
+                       </option>
+                       <option name="user_authentication_failure_user_captcha">
+                               <categoryname>security.general.authentication</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>3</defaultvalue>
+                               <minvalue>0</minvalue>
+                       </option>
+                       <option name="user_authentication_failure_expiration">
+                               <categoryname>security.general.authentication</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>30</defaultvalue>
+                               <minvalue>1</minvalue>
+                               <maxvalue>365</maxvalue>
+                       </option>
+                       <!-- /security.general.authentication -->
                        
                        <!-- security.blacklist -->
                        <option name="blacklist_ip_addresses">
index 7fd7c171c69dd221e1050b7a99ab7e7a7bff2c2c..cc68f2bf82114f9bb8d7347a67260dd71799b841 100644 (file)
                {/hascontent}
                
                {event name='fieldsets'}
+               
+               {include file='captcha'}
        </div>
        
        <div class="formSubmit">
diff --git a/wcfsetup/install/files/acp/templates/captcha.tpl b/wcfsetup/install/files/acp/templates/captcha.tpl
new file mode 100644 (file)
index 0000000..9ad50b2
--- /dev/null
@@ -0,0 +1,3 @@
+{if $captchaObjectType}
+       {@$captchaObjectType->getProcessor()->getFormElement()}
+{/if}
diff --git a/wcfsetup/install/files/acp/templates/captchaQuestion.tpl b/wcfsetup/install/files/acp/templates/captchaQuestion.tpl
new file mode 100644 (file)
index 0000000..06e0a3f
--- /dev/null
@@ -0,0 +1,43 @@
+<input type="hidden" name="captchaQuestion" value="{$captchaQuestion}" />
+
+{if !$captchaQuestionAnswered}
+       <fieldset>
+               <legend>{lang}wcf.captcha.question.captcha{/lang}</legend>
+               <small>{lang}wcf.captcha.question.captcha.description{/lang}</small>
+               
+               <dl{if (($errorType|isset && $errorType|is_array && $errorType[captchaAnswer]|isset) || ($errorField|isset && $errorField == 'captchaAnswer'))} class="formError"{/if}>
+                       <dt><label for="captchaAnswer">{lang}{$captchaQuestionObject->question}{/lang}</label></dt>
+                       <dd>
+                               <input type="text" id="captchaAnswer" name="captchaAnswer" class="medium" />
+                               {if (($errorType|isset && $errorType|is_array && $errorType[captchaAnswer]|isset) || ($errorField|isset && $errorField == 'captchaAnswer'))}
+                                       {if $errorType|is_array && $errorType[captchaAnswer]|isset}
+                                               {assign var='__errorType' value=$errorType[captchaAnswer]}
+                                       {else}
+                                               {assign var='__errorType' value=$errorType}
+                                       {/if}
+                                       
+                                       {if $__errorType == 'empty'}
+                                               <small class="innerError">{lang}wcf.global.form.error.empty{/lang}</small>
+                                       {else}
+                                               <small class="innerError">{lang}wcf.captcha.question.answer.error.{$__errorType}{/lang}</small>
+                                       {/if}
+                               {/if}
+                       </dd>
+               </dl>
+       </fieldset>
+       
+       {if !$ajaxCaptcha|empty}
+               <script data-relocate="true">
+                       //<![CDATA[
+                       $(function() {
+                               WCF.System.Captcha.addCallback('{$captchaID}', function() {
+                                       return {
+                                               captchaAnswer: $('#captchaAnswer').val(),
+                                               captchaQuestion: '{$captchaQuestion}'
+                                       };
+                               });
+                       });
+                       //]]>
+               </script>
+       {/if}
+{/if}
index 7ceeacac068ddb727de08c17b4bb6e1238b84804..be65192cfbe6bd7cc879f3a104ef8ec601aa30d0 100644 (file)
@@ -58,6 +58,8 @@
                </fieldset>
                
                {event name='fieldsets'}
+               
+               {include file='captcha'}
        </div>
        
        <div class="formSubmit">
index 22ef413416163aa9f2ca77646254d5472755a71d..c538a1157fe5ebb546c8a0659f5c42f6edb8dbde 100644 (file)
@@ -1,55 +1,95 @@
-<fieldset>
-       <legend><label for="recaptcha_response_field">{lang}wcf.recaptcha.title{/lang}</label></legend>
-       <small>{lang}wcf.recaptcha.description{/lang}</small>
-       
-       <dl class="wide reCaptcha{if $errorField == 'recaptchaString'} formError{/if}">
-               <script data-relocate="true">
-                       //<![CDATA[
-                       var RecaptchaOptions = {
-                               lang: '{@$recaptchaLanguageCode}',
-                               theme : 'custom'
-                       }
-                       //]]>
-               </script>
-               <dt class="jsOnly">
-                       <label for="recaptcha_response_field">reCAPTCHA</label>
-               </dt>
-               <dd class="jsOnly">
-                       <div id="recaptcha_image" class="framed"></div>
-                       <input type="text" id="recaptcha_response_field" name="recaptcha_response_field" class="medium marginTop" />
-                       {if $errorField == 'recaptchaString'}
-                               <small class="innerError">
-                                       {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if}
-                                       {if $errorType == 'false'}{lang}wcf.recaptcha.error.recaptchaString.false{/lang}{/if}
-                               </small>
-                       {/if}
-               </dd>
-               
-               {event name='fields'}
-               
-               <dd class="jsOnly">
-                       <ul class="buttonList smallButtons">
-                               <li><a href="javascript:Recaptcha.reload()" class="button small"><span class="icon icon16 icon-repeat"></span> <span>{lang}wcf.recaptcha.reload{/lang}</span></a></li>
-                               <li class="recaptcha_only_if_image"><a href="javascript:Recaptcha.switch_type('audio')" class="button small"><span class="icon icon16 icon-volume-up"></span> <span>{lang}wcf.recaptcha.audio{/lang}</span></a></li>
-                               <li class="recaptcha_only_if_audio"><a href="javascript:Recaptcha.switch_type('image')" class="button small"><span class="icon icon16 icon-eye-open"></span> <span>{lang}wcf.recaptcha.image{/lang}</span></a></li>
-                               <li><a href="javascript:Recaptcha.showhelp()" class="button small"><span class="icon icon16 icon-question-sign"></span> <span>{lang}wcf.recaptcha.help{/lang}</span></a></li>
-                               {event name='buttons'}
-                       </ul>
-               </dd>
+{if $recaptchaLegacyMode|empty}
+       {include file='captcha'}
+{else}
+       <fieldset>
+               <legend><label for="recaptcha_response_field">{lang}wcf.recaptcha.title{/lang}</label></legend>
+               <small>{lang}wcf.recaptcha.description{/lang}</small>
                
-               <script data-relocate="true" src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/challenge?k={$recaptchaPublicKey}"></script>
-               <noscript>
-                       <dd>
-                               <iframe src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/noscript?k={$recaptchaPublicKey}" height="300" width="500" seamless="seamless"></iframe><br />
-                               <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
-                               <input type="hidden" name="recaptcha_response_field" value="manual_challenge" />
+               <dl class="wide reCaptcha{if $errorField|isset && $errorField == 'recaptchaString'} formError{/if}">
+                       {if !$ajaxCaptcha|isset || !$ajaxCaptcha}
+                               <script data-relocate="true">
+                                       //<![CDATA[
+                                       var RecaptchaOptions = {
+                                               lang: '{@$recaptchaLanguageCode}',
+                                               theme : 'custom'
+                                       }
+                                       //]]>
+                               </script>
+                       {/if}
+                       <dt class="jsOnly">
+                               <label for="recaptcha_response_field">reCAPTCHA</label>
+                       </dt>
+                       <dd class="jsOnly">
+                               <div id="recaptcha_image" class="framed"></div>
+                               <input type="text" id="recaptcha_response_field" name="recaptcha_response_field" class="medium marginTop" />
+                               {if (($errorType|isset && $errorType|is_array && $errorType[recaptchaString]|isset) || ($errorField|isset && $errorField == 'recaptchaString'))}
+                                       {if $errorType|is_array && $errorType[recaptchaString]|isset}
+                                               {assign var='__errorType' value=$errorType[recaptchaString]}
+                                       {else}
+                                               {assign var='__errorType' value=$errorType}
+                                       {/if}
+                                       <small class="innerError">
+                                               {if $__errorType == 'empty'}
+                                                       {lang}wcf.global.form.error.empty{/lang}
+                                               {else}
+                                                       {lang}wcf.recaptcha.error.recaptchaString.{$__errorType}{/lang}
+                                               {/if}
+                                       </small>
+                               {/if}
+                       </dd>
+                       
+                       {event name='fields'}
+                       
+                       <dd class="jsOnly">
+                               <ul class="buttonList smallButtons">
+                                       <li><a href="javascript:Recaptcha.reload()" class="button small"><span class="icon icon16 icon-repeat"></span> <span>{lang}wcf.recaptcha.reload{/lang}</span></a></li>
+                                       <li class="recaptcha_only_if_image"><a href="javascript:Recaptcha.switch_type('audio')" class="button small"><span class="icon icon16 icon-volume-up"></span> <span>{lang}wcf.recaptcha.audio{/lang}</span></a></li>
+                                       <li class="recaptcha_only_if_audio"><a href="javascript:Recaptcha.switch_type('image')" class="button small"><span class="icon icon16 icon-eye-open"></span> <span>{lang}wcf.recaptcha.image{/lang}</span></a></li>
+                                       <li><a href="javascript:Recaptcha.showhelp()" class="button small"><span class="icon icon16 icon-question-sign"></span> <span>{lang}wcf.recaptcha.help{/lang}</span></a></li>
+                                       {event name='buttons'}
+                               </ul>
                        </dd>
-                       {if $errorField == 'recaptchaString'}
-                               <small class="innerError">
-                                       {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if}
-                                       {if $errorType == 'false'}{lang}wcf.recaptcha.error.recaptchaString.false{/lang}{/if}
-                               </small>
+                       
+                       {if !$ajaxCaptcha|isset || !$ajaxCaptcha}
+                               <script data-relocate="true" src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/challenge?k={$recaptchaPublicKey}"></script>
+                               <noscript>
+                                       <dd>
+                                               <iframe src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/noscript?k={$recaptchaPublicKey}" height="300" width="500" seamless="seamless"></iframe><br />
+                                               <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
+                                               <input type="hidden" name="recaptcha_response_field" value="manual_challenge" />
+                                       </dd>
+                                       {if (($errorType|isset && $errorType|is_array && $errorType[recaptchaString]|isset) || ($errorField|isset && $errorField == 'recaptchaString'))}
+                                               {if $errorType|is_array && $errorType[recaptchaString]|isset}
+                                                       {assign var='__errorType' value=$errorType[recaptchaString]}
+                                               {else}
+                                                       {assign var='__errorType' value=$errorType}
+                                               {/if}
+                                               <small class="innerError">
+                                                       {if $errorType == 'empty'}
+                                                               {lang}wcf.global.form.error.empty{/lang}
+                                                       {else}
+                                                               {lang}wcf.recaptcha.error.recaptchaString.{$__errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                               </noscript>
+                       {else}
+                               <script data-relocate="true">
+                                       //<![CDATA[
+                                       Recaptcha.create("{$recaptchaPublicKey}", "recaptcha_image", {
+                                               lang: '{@$recaptchaLanguageCode}',
+                                               theme : 'custom'
+                                       });
+                                       
+                                       WCF.System.Captcha.addCallback('{$captchaID}', function() {
+                                               return {
+                                                       recaptcha_challenge_field: Recaptcha.get_challenge(),
+                                                       recaptcha_response_field: Recaptcha.get_response()
+                                               };
+                                       });
+                                       //]]>
+                               </script>
                        {/if}
-               </noscript>
-       </dl>
-</fieldset>
+               </dl>
+       </fieldset>
+{/if}
diff --git a/wcfsetup/install/files/acp/templates/userAuthenticationFailureList.tpl b/wcfsetup/install/files/acp/templates/userAuthenticationFailureList.tpl
new file mode 100644 (file)
index 0000000..61b9346
--- /dev/null
@@ -0,0 +1,75 @@
+{include file='header' pageTitle='wcf.acp.user.authentication.failure.list'}
+
+<header class="boxHeadline">
+       <h1>{lang}wcf.acp.user.authentication.failure.list{/lang}</h1>
+</header>
+
+<div class="contentNavigation">
+       {pages print=true assign=pagesLinks controller='UserAuthenticationFailureList' link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}
+       
+       {hascontent}
+               <nav>
+                       <ul>
+                               {content}
+                                       {event name='contentNavigationButtonsTop'}
+                               {/content}
+                       </ul>
+               </nav>
+       {/hascontent}
+</div>
+
+{if $objects|count}
+       <div class="tabularBox tabularBoxTitle marginTop">
+               <header>
+                       <h2>{lang}wcf.acp.user.authentication.failure.list{/lang} <span class="badge badgeInverse">{#$items}</span></h2>
+               </header>
+               
+               <table class="table">
+                       <thead>
+                               <tr>
+                                       <th class="columnID columnFailureID{if $sortField == 'failureID'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=failureID&sortOrder={if $sortField == 'failureID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+                                       <th class="columnText columnEnvironment{if $sortField == 'environment'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=environment&sortOrder={if $sortField == 'environment' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.acp.user.authentication.failure.environment{/lang}</a></th>
+                                       <th class="columnTitle columnUsername{if $sortField == 'username'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=username&sortOrder={if $sortField == 'username' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.user.username{/lang}</a></th>
+                                       <th class="columnDate columnTime{if $sortField == 'time'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=time&sortOrder={if $sortField == 'time' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.acp.user.authentication.failure.time{/lang}</a></th>
+                                       <th class="columnURL columnIpAddress{if $sortField == 'ipAddress'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=ipAddress&sortOrder={if $sortField == 'ipAddress' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.user.ipAddress{/lang}</a></th>
+                                       <th class="columnText columnUserAgent{if $sortField == 'userAgent'} active {@$sortOrder}{/if}"><a href="{link controller='UserAuthenticationFailureList'}pageNo={@$pageNo}&sortField=userAgent&sortOrder={if $sortField == 'userAgent' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.user.userAgent{/lang}</a></th>
+                                       
+                                       {event name='columnHeads'}
+                               </tr>
+                       </thead>
+                       
+                       <tbody>
+                               {foreach from=$objects item='authenticationFailure'}
+                                       <tr>
+                                               <td class="columnID columnFailureID">{@$authenticationFailure->failureID}</td>
+                                               <td class="columnText columnEnvironment">{lang}wcf.acp.user.authentication.failure.environment.{@$authenticationFailure->environment}{/lang}</td>
+                                               <td class="columnTitle columnUsername">{if $authenticationFailure->userID}<a href="{link controller='UserEdit' id=$authenticationFailure->userID}{/link}">{$authenticationFailure->username}</a>{else}{$authenticationFailure->username}{/if}</td>
+                                               <td class="columnDate columnTime">{@$authenticationFailure->time|time}</td>
+                                               <td class="columnSmallText columnIpAddress">{$authenticationFailure->getIpAddress()}</td>
+                                               <td class="columnSmallText columnUserAgent" title="{$authenticationFailure->userAgent}">{$authenticationFailure->userAgent|truncate:75|tableWordwrap}</td>
+                                               
+                                               {event name='columns'}
+                                       </tr>
+                               {/foreach}
+                       </tbody>
+               </table>
+       </div>
+       
+       <div class="contentNavigation">
+               {@$pagesLinks}
+               
+               {hascontent}
+                       <nav>
+                               <ul>
+                                       {content}
+                                               {event name='contentNavigationButtonsBottom'}
+                                       {/content}
+                               </ul>
+                       </nav>
+               {/hascontent}
+       </div>
+{else}
+       <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+{include file='footer'}
index 86c0b375f31857e9adea3cb59b51433fee13a435..0b8588b4e3f7a210ef188f6f05844cd7c7d088a5 100755 (executable)
@@ -1,8 +1,11 @@
 <?php
 namespace wcf\acp\form;
+use wcf\data\user\authentication\failure\UserAuthenticationFailure;
+use wcf\data\user\authentication\failure\UserAuthenticationFailureAction;
 use wcf\data\user\User;
-use wcf\form\AbstractForm;
+use wcf\form\AbstractCaptchaForm;
 use wcf\system\application\ApplicationHandler;
+use wcf\system\exception\NamedUserException;
 use wcf\system\exception\PermissionDeniedException;
 use wcf\system\exception\UserInputException;
 use wcf\system\request\RequestHandler;
@@ -12,6 +15,7 @@ use wcf\system\user\authentication\UserAuthenticationFactory;
 use wcf\system\WCF;
 use wcf\util\HeaderUtil;
 use wcf\util\StringUtil;
+use wcf\util\UserUtil;
 
 /**
  * Shows the acp login form.
@@ -23,7 +27,7 @@ use wcf\util\StringUtil;
  * @subpackage acp.form
  * @category   Community Framework
  */
-class LoginForm extends AbstractForm {
+class LoginForm extends AbstractCaptchaForm {
        /**
         * given login username
         * @var string
@@ -40,7 +44,7 @@ class LoginForm extends AbstractForm {
         * user object
         * @var \wcf\data\user\User
         */
-       public $user;
+       public $user = null;
        
        /**
         * given forward url
@@ -48,6 +52,12 @@ class LoginForm extends AbstractForm {
         */
        public $url = null;
        
+       /**
+        * @todo
+        * @var unknown
+        */
+       public $useCaptcha = false;
+       
        /**
         * Creates a new LoginForm object.
         */
@@ -78,6 +88,30 @@ class LoginForm extends AbstractForm {
                                $this->url = '';
                        }
                }
+               
+               // check authentication failures
+               if (ENABLE_USER_AUTHENTICATION_FAILURE) {
+                       $failures = UserAuthenticationFailure::countIPFailures(UserUtil::getIpAddress());
+                       if (USER_AUTHENTICATION_FAILURE_IP_BLOCK && $failures >= USER_AUTHENTICATION_FAILURE_IP_BLOCK) {
+                               throw new NamedUserException(WCF::getLanguage()->getDynamicVariable('wcf.user.login.blocked'));
+                       }
+                       if (USER_AUTHENTICATION_FAILURE_IP_CAPTCHA && $failures >= USER_AUTHENTICATION_FAILURE_IP_CAPTCHA) {
+                               $this->captchaObjectTypeName = REGISTER_CAPTCHA_TYPE;
+                       }
+                       else if (USER_AUTHENTICATION_FAILURE_USER_CAPTCHA) {
+                               if (isset($_POST['username'])) {
+                                       $user = User::getUserByUsername(StringUtil::trim($_POST['username']));
+                                       if (!$user->userID) $user = User::getUserByEmail(StringUtil::trim($_POST['username']));
+                                       
+                                       if ($user->userID) {
+                                               $failures = UserAuthenticationFailure::countUserFailures($user->userID);
+                                               if (USER_AUTHENTICATION_FAILURE_USER_CAPTCHA && $failures >= USER_AUTHENTICATION_FAILURE_USER_CAPTCHA) {
+                                                       $this->captchaObjectTypeName = REGISTER_CAPTCHA_TYPE;
+                                               }
+                                       }
+                               }
+                       }
+               }
        }
        
        /**
@@ -113,6 +147,34 @@ class LoginForm extends AbstractForm {
                }
        }
        
+       /**
+        * @see \wcf\form\IForm::submit()
+        */
+       public function submit() {
+               parent::submit();
+               
+               // save authentication failure
+               if (ENABLE_USER_AUTHENTICATION_FAILURE) {
+                       if ($this->errorField == 'username' || $this->errorField == 'password') {
+                               $action = new UserAuthenticationFailureAction(array(), 'create', array(
+                                       'data' => array(
+                                               'environment' => (RequestHandler::getInstance()->isACPRequest() ? 'admin' : 'user'),
+                                               'userID' => ($this->user !== null ? $this->user->userID : null),
+                                               'username' => $this->username,
+                                               'time' => TIME_NOW,
+                                               'ipAddress' => UserUtil::getIpAddress(),
+                                               'userAgent' => UserUtil::getUserAgent()
+                                       )
+                               ));
+                               $action->executeAction();
+                               
+                               if ($this->captchaObjectType) {
+                                       $this->captchaObjectType->getProcessor()->reset();
+                               }
+                       }
+               }
+       }
+       
        /**
         * @see \wcf\form\IForm::validate()
         */
diff --git a/wcfsetup/install/files/lib/acp/page/UserAuthenticationFailureListPage.class.php b/wcfsetup/install/files/lib/acp/page/UserAuthenticationFailureListPage.class.php
new file mode 100644 (file)
index 0000000..29b219a
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+namespace wcf\acp\page;
+use wcf\page\SortablePage;
+
+/**
+ * Shows a list of user authentication failures.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage acp.page
+ * @category   Community Framework
+ */
+class UserAuthenticationFailureListPage extends SortablePage {
+       /**
+        * @see \wcf\page\AbstractPage::$activeMenuItem
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.log.authentication.failure';
+       
+       /**
+        * @see \wcf\page\AbstractPage::$neededPermissions
+        */
+       public $neededPermissions = array('admin.system.canViewLog');
+       
+       /**
+        * @see \wcf\page\AbstractPage::$neededModules
+        */
+       public $neededModules = array('ENABLE_USER_AUTHENTICATION_FAILURE');
+       
+       /**
+        * @see \wcf\page\SortablePage::$defaultSortField
+        */
+       public $defaultSortField = 'time';
+       
+       /**
+        * @see \wcf\page\SortablePage::$defaultSortOrder
+        */
+       public $defaultSortOrder = 'DESC';
+       
+       /**
+        * @see \wcf\page\SortablePage::$validSortFields
+        */
+       public $validSortFields = array('failureID', 'environment', 'userID', 'username', 'time', 'ipAddress', 'userAgent');
+       
+       /**
+        * @see \wcf\page\MultipleLinkPage::$objectListClassName
+        */
+       public $objectListClassName = 'wcf\data\user\authentication\failure\UserAuthenticationFailureList';
+}
diff --git a/wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailure.class.php b/wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailure.class.php
new file mode 100644 (file)
index 0000000..6021d19
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+namespace wcf\data\user\authentication\failure;
+use wcf\data\DatabaseObject;
+use wcf\util\UserUtil;
+use wcf\system\WCF;
+
+/**
+ * Represents a user authentication failure.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.user.authentication.failure
+ * @category   Community Framework
+ */
+class UserAuthenticationFailure extends DatabaseObject {
+       /**
+        * @see \wcf\data\DatabaseObject::$databaseTableName
+        */
+       protected static $databaseTableName = 'user_authentication_failure';
+       
+       /**
+        * @see \wcf\data\DatabaseObject::$databaseTableIndexName
+        */
+       protected static $databaseTableIndexName = 'failureID';
+       
+       /**
+        * Returns the ip address and attempts to convert into IPv4.
+        *
+        * @return      string
+        */
+       public function getIpAddress() {
+               return UserUtil::convertIPv6To4($this->ipAddress);
+       }
+       
+       /**
+        * Returns the number of authentication failures caused by given ip address.
+        * 
+        * @param       string          $ipAddress
+        * @return      boolean
+        */
+       public static function countIPFailures($ipAddress) {
+               $sql = "SELECT  COUNT(*) AS count
+                       FROM    wcf".WCF_N."_user_authentication_failure
+                       WHERE   ipAddress = ?
+                               AND time > ?";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute(array($ipAddress, TIME_NOW - USER_AUTHENTICATION_FAILURE_TIMEOUT));
+               return $statement->fetchColumn();
+       }
+       
+       /**
+        * Returns the number of authentication failures for given user account.
+        *
+        * @param       integer         $userID
+        * @return      boolean
+        */
+       public static function countUserFailures($userID) {
+               $sql = "SELECT  COUNT(*) AS count
+                       FROM    wcf".WCF_N."_user_authentication_failure
+                       WHERE   userID = ?
+                               AND time > ?";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute(array($userID, TIME_NOW - USER_AUTHENTICATION_FAILURE_TIMEOUT));
+               return $statement->fetchColumn();
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailureAction.class.php b/wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailureAction.class.php
new file mode 100644 (file)
index 0000000..4418d38
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+namespace wcf\data\user\authentication\failure;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes user authentication failure-related actions.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.user.authentication.failure
+ * @category   Community Framework
+ */
+class UserAuthenticationFailureAction extends AbstractDatabaseObjectAction {
+}
diff --git a/wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailureEditor.class.php b/wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailureEditor.class.php
new file mode 100644 (file)
index 0000000..0e36a3b
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace wcf\data\user\authentication\failure;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit user authentication failures.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.user.authentication.failure
+ * @category   Community Framework
+ */
+class UserAuthenticationFailureEditor extends DatabaseObjectEditor {
+       /**
+        * @see \wcf\data\DatabaseObjectDecorator::$baseClass
+        */
+       protected static $baseClass = 'wcf\data\user\authentication\failure\UserAuthenticationFailure';
+}
diff --git a/wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailureList.class.php b/wcfsetup/install/files/lib/data/user/authentication/failure/UserAuthenticationFailureList.class.php
new file mode 100644 (file)
index 0000000..2f42566
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+namespace wcf\data\user\authentication\failure;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of user authentication failures.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.user.authentication.failure
+ * @category   Community Framework
+ */
+class UserAuthenticationFailureList extends DatabaseObjectList { }
index 10ae1cef9225e250b65b128d14169d93f50a4c49..fcecee03d8b76f71e44d46d8f99314623d6f6867 100644 (file)
@@ -125,6 +125,16 @@ class DailyCleanUpCronjob extends AbstractCronjob {
                        (TIME_NOW - 86400)
                ));
                
+               // clean up user authentication failure log
+               if (ENABLE_USER_AUTHENTICATION_FAILURE) {
+                       $sql = "DELETE FROM     wcf".WCF_N."_user_authentication_failure
+                               WHERE           time < ?";
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       $statement->execute(array(
+                               (TIME_NOW - 86400 * USER_AUTHENTICATION_FAILURE_EXPIRATION)
+                       ));
+               }
+               
                // clean up error logs
                $files = @glob(WCF_DIR.'log/*.txt');
                if (is_array($files)) {
index 3552460a61eb1d8a9e19c1e95c6982521a5455e7..279a0bbbd4005e701d6b2d215ecb7f37a4a0a436 100644 (file)
                <item name="wcf.acp.menu.link.captcha"><![CDATA[Captchas]]></item>
                <item name="wcf.acp.menu.link.captcha.question.add"><![CDATA[Frage hinzufügen]]></item>
                <item name="wcf.acp.menu.link.captcha.question.list"><![CDATA[Fragen auflisten]]></item>
+               <item name="wcf.acp.menu.link.log.authentication.failure"><![CDATA[Fehlgeschlagene Anmeldungen]]></item>
        </category>
        
        <category name="wcf.acp.notice">
@@ -994,6 +995,19 @@ GmbH=Gesellschaft mit beschränkter Haftung]]></item>
                <item name="wcf.acp.option.search_captcha_type"><![CDATA[Suche]]></item>
                <item name="wcf.acp.option.message_captcha_type"><![CDATA[Nachrichten]]></item>
                <item name="wcf.acp.option.category.security.antispam.captcha"><![CDATA[Captchas]]></item>
+               <item name="wcf.acp.option.category.security.general.authentication"><![CDATA[Benutzer-Authentifikation]]></item>
+               <item name="wcf.acp.option.enable_user_authentication_failure"><![CDATA[Fehlgeschlagene Ammeldeversuche protokollieren]]></item>
+               <item name="wcf.acp.option.enable_user_authentication_failure.description"><![CDATA[TODO]]></item>
+               <item name="wcf.acp.option.user_authentication_failure_timeout"><![CDATA[Zeitraum der Überwachung]]></item>
+               <item name="wcf.acp.option.user_authentication_failure_timeout.description"><![CDATA[TODO]]></item>
+               <item name="wcf.acp.option.user_authentication_failure_ip_captcha"><![CDATA[Captcha bei Anmeldung (IP)]]></item>
+               <item name="wcf.acp.option.user_authentication_failure_ip_captcha.description"><![CDATA[TODO]]></item>
+               <item name="wcf.acp.option.user_authentication_failure_ip_block"><![CDATA[Blockierung der Anmeldung]]></item>
+               <item name="wcf.acp.option.user_authentication_failure_ip_block.description"><![CDATA[TODO]]></item>
+               <item name="wcf.acp.option.user_authentication_failure_user_captcha"><![CDATA[Captcha bei Anmeldung (Benutzer)]]></item>
+               <item name="wcf.acp.option.user_authentication_failure_user_captcha.description"><![CDATA[TODO]]></item>
+               <item name="wcf.acp.option.user_authentication_failure_expiration"><![CDATA[Löschung von alten Protokolleinträgen]]></item>
+               <item name="wcf.acp.option.user_authentication_failure_expiration.description"><![CDATA[TODO]]></item>
        </category>
        
        <category name="wcf.acp.package">
@@ -1562,6 +1576,11 @@ Ihr neues Kennwort lautet: {$password}
 Klicken Sie hier, um sich mit Ihrem neuen Kennwort anzumelden: {link controller='Login' isEmail=true}{/link}]]></item>
                <item name="wcf.acp.user.sendNewPassword.mail.subject"><![CDATA[Neues Kennwort für Ihr Benutzerkonto auf der Website: {@PAGE_TITLE|language}]]></item>
                <item name="wcf.acp.user.sendNewPassword.workerTitle"><![CDATA[Neue Passwörter zusenden]]></item>
+               <item name="wcf.acp.user.authentication.failure.list"><![CDATA[Fehlgeschlagene Anmeldungen]]></item>
+               <item name="wcf.acp.user.authentication.failure.environment"><![CDATA[Umgebung]]></item>
+               <item name="wcf.acp.user.authentication.failure.environment.user"><![CDATA[Benutzer]]></item>
+               <item name="wcf.acp.user.authentication.failure.environment.admin"><![CDATA[Administration]]></item>
+               <item name="wcf.acp.user.authentication.failure.time"><![CDATA[Datum]]></item>
        </category>
        
        <category name="wcf.acp.worker">
@@ -2607,6 +2626,7 @@ Wenn Sie Probleme mit der Aktivierung haben, wenden Sie sich bitte an den Admini
                <item name="wcf.user.enableSignature"><![CDATA[Signatur entsperren]]></item>
                <item name="wcf.user.edit"><![CDATA[Benutzer bearbeiten]]></item>
                <item name="wcf.user.birthdayToday"><![CDATA[Hat heute Geburtstag]]></item>
+               <item name="wcf.user.login.blocked"><![CDATA[Aufgrund einer hohen Anzahl von fehlgeschlagenen Anmeldeversuchen durch Ihre IP-Adresse steht Ihnen die Anmeldung aus Sicherheitsgründen vorübergehend nicht zur Verfügung. Bitte versuchen Sie es später erneut!]]></item>
        </category>
        
        <category name="wcf.user.menu">
index ddde915215ab53f7b553791eefb3696d5b4b7e20..4c9da63aa5825c7842c09cbc2b36235d0a23b185 100644 (file)
@@ -1095,6 +1095,19 @@ CREATE TABLE wcf1_user_activity_point (
        KEY (objectTypeID)
 );
 
+DROP TABLE IF EXISTS wcf1_user_authentication_failure;
+CREATE TABLE wcf1_user_authentication_failure (
+       failureID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       environment ENUM('user', 'admin') NOT NULL DEFAULT 'user',
+       userID INT(10),
+       username VARCHAR(255) NOT NULL DEFAULT '',
+       time INT(10) NOT NULL DEFAULT 0,
+       ipAddress VARCHAR(39) NOT NULL DEFAULT '',
+       userAgent VARCHAR(255) NOT NULL DEFAULT '',
+       KEY (ipAddress, time),
+       KEY (time)
+);
+
 DROP TABLE IF EXISTS wcf1_user_avatar;
 CREATE TABLE wcf1_user_avatar (
        avatarID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
@@ -1579,6 +1592,8 @@ ALTER TABLE wcf1_user_activity_event ADD FOREIGN KEY (languageID) REFERENCES wcf
 ALTER TABLE wcf1_user_activity_point ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;
 ALTER TABLE wcf1_user_activity_point ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
 
+ALTER TABLE wcf1_user_authentication_failure ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE SET NULL;
+
 ALTER TABLE wcf1_user_profile_visitor ADD FOREIGN KEY (ownerID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;
 ALTER TABLE wcf1_user_profile_visitor ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;