mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
ENH PasswordExpirationMiddleware implementation (#9207)
This commit is contained in:
parent
6d6c4c652c
commit
233e0e7aa0
@ -27,6 +27,7 @@ SilverStripe\Core\Injector\Injector:
|
||||
Middlewares:
|
||||
AuthenticationMiddleware: '%$SilverStripe\Security\AuthenticationMiddleware'
|
||||
BasicAuthMiddleware: '%$SilverStripe\Security\BasicAuthMiddleware'
|
||||
PasswordExpirationMiddleware: '%$SilverStripe\Security\PasswordExpirationMiddleware'
|
||||
SilverStripe\Security\AuthenticationMiddleware:
|
||||
properties:
|
||||
AuthenticationHandler: '%$SilverStripe\Security\AuthenticationHandler'
|
||||
@ -42,3 +43,11 @@ SilverStripe\Core\Injector\Injector:
|
||||
Authenticators:
|
||||
cms: '%$SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator'
|
||||
SilverStripe\Security\IdentityStore: '%$SilverStripe\Security\AuthenticationHandler'
|
||||
|
||||
SilverStripe\Security\PasswordExpirationMiddleware:
|
||||
default_redirect: Security/changepassword
|
||||
whitelisted_url_startswith:
|
||||
- Security/basicauthlogin/
|
||||
- Security/changepassword/
|
||||
- Security/login/
|
||||
- Security/logout/
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
* [Installer UI has been removed](#installer-ui) and offered as a separate module.
|
||||
* [Generic login form styling](#login-forms)
|
||||
* Removed `use_gzip` option on `HtmlEditorField` which used to compress the rich text editor dependency.
|
||||
* Removed `use_gzip` option on `HtmlEditorField` which used to compress the rich text editor dependency.
|
||||
No longer required since compression is performed as part of the CMS build automatically.
|
||||
See (#832)(https://github.com/silverstripe/silverstripe-admin/issues/832)
|
||||
|
||||
@ -48,7 +48,34 @@ composer require silverstripe/login-forms
|
||||
Note that any customisations you might have in `Page.ss` or `Layout/Security.ss`
|
||||
no longer apply when this module is installed. If you have customised the login process
|
||||
by adding form fields, or through custom handlers such as SAML or LDAP,
|
||||
you'll need to review those before starting to use the module.
|
||||
you'll need to review those before starting to use the module.
|
||||
|
||||
|
||||
## New PasswordExpirationMiddleware now proactively invalidates members with expired passwords
|
||||
|
||||
A new PasswordExpirationMiddleware has been implemented.
|
||||
It checks passwords of authenticated users for expiration and either enforces a redirection
|
||||
to a change password form, or resets the user for a request being processed (sets current user to null).
|
||||
|
||||
This is considered to be a security enhancement, but potentially might interfere with some custom logic
|
||||
around password expiration if you have it implemented.
|
||||
|
||||
Ideally you should test your setup when upgrading if you use the password expiration functionality.
|
||||
|
||||
If you'd like to deactivate the middleware, you can unregister it in your application config like this:
|
||||
|
||||
```yml
|
||||
---
|
||||
Name: disable-passwordExpirationMiddleware
|
||||
After:
|
||||
- '#coresecurity'
|
||||
---
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\Control\Director:
|
||||
properties:
|
||||
Middlewares:
|
||||
PasswordExpirationMiddleware: null
|
||||
```
|
||||
|
||||
## Deprecation
|
||||
|
||||
|
@ -2,9 +2,12 @@
|
||||
|
||||
namespace SilverStripe\Security\MemberAuthenticator;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Security\PasswordExpirationMiddleware;
|
||||
use SilverStripe\Security\CMSSecurity;
|
||||
use SilverStripe\Security\Security;
|
||||
|
||||
@ -65,7 +68,15 @@ class CMSLoginHandler extends LoginHandler
|
||||
);
|
||||
|
||||
// Get redirect url
|
||||
$changePasswordURL = $this->addBackURLParam(Security::singleton()->Link('changepassword'));
|
||||
$changedPasswordLink = Security::singleton()->Link('changepassword');
|
||||
$changePasswordURL = $this->addBackURLParam($changedPasswordLink);
|
||||
|
||||
if (Injector::inst()->has(PasswordExpirationMiddleware::class)) {
|
||||
$session = $this->getRequest()->getSession();
|
||||
$passwordExpirationMiddleware = Injector::inst()->get(PasswordExpirationMiddleware::class);
|
||||
$passwordExpirationMiddleware->allowCurrentRequest($session);
|
||||
}
|
||||
|
||||
$changePasswordURLATT = Convert::raw2att($changePasswordURL);
|
||||
$changePasswordURLJS = Convert::raw2js($changePasswordURL);
|
||||
$message = _t(
|
||||
|
@ -9,6 +9,7 @@ use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\ORM\ValidationResult;
|
||||
use SilverStripe\Security\Authenticator;
|
||||
use SilverStripe\Security\PasswordExpirationMiddleware;
|
||||
use SilverStripe\Security\IdentityStore;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
@ -263,7 +264,8 @@ class LoginHandler extends RequestHandler
|
||||
'good'
|
||||
);
|
||||
$changedPasswordLink = Security::singleton()->Link('changepassword');
|
||||
$changePasswordUrl = $this->addBackURLParam($changedPasswordLink);
|
||||
|
||||
return $this->redirect($this->addBackURLParam($changedPasswordLink));
|
||||
return $this->redirect($changePasswordUrl);
|
||||
}
|
||||
}
|
||||
|
247
src/Security/PasswordExpirationMiddleware.php
Normal file
247
src/Security/PasswordExpirationMiddleware.php
Normal file
@ -0,0 +1,247 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Control\Middleware\HTTPMiddleware;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\ORM\Connect\DatabaseException;
|
||||
|
||||
/**
|
||||
* Check if authenticated user has password expired.
|
||||
* Depending on the configuration there are following outcomes:
|
||||
* - if the current requested URL whitelisted, then allow to process further
|
||||
* - else if the change password form URL is set, then redirect to it
|
||||
* - else set current user to null (deauthenticate for the current request) and process further
|
||||
*/
|
||||
class PasswordExpirationMiddleware implements HTTPMiddleware
|
||||
{
|
||||
use Configurable;
|
||||
|
||||
/**
|
||||
* Session key for persisting URL of the password change form
|
||||
*/
|
||||
const SESSION_KEY_REDIRECT = __CLASS__.'.change password redirect';
|
||||
|
||||
/**
|
||||
* Session key for persisting a flag allowing to process the current request
|
||||
* without performing password expiration check
|
||||
*/
|
||||
const SESSION_KEY_ALLOW_CURRENT_REQUEST = __CLASS__.'.allow current request';
|
||||
|
||||
/**
|
||||
* List of URL patterns allowed for users to visit where
|
||||
* URL starts with the pattern
|
||||
*
|
||||
* @var string[]
|
||||
*
|
||||
* @config
|
||||
*/
|
||||
private static $whitelisted_url_startswith = [];
|
||||
|
||||
/**
|
||||
* Where users with expired passwords get redirected by default
|
||||
* when login form didn't register a custom one with
|
||||
* {@see SilverStripe\Security\AuthenticationMiddleware::setRedirect}
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @config
|
||||
*/
|
||||
private static $default_redirect = null;
|
||||
|
||||
/**
|
||||
* The list of mimetypes allowing a redirect to a change password form.
|
||||
* By default this is (x)HTML
|
||||
*
|
||||
* @var string[]
|
||||
*
|
||||
* @config
|
||||
*/
|
||||
private static $mimetypes_allowing_redirect = [
|
||||
'*/*',
|
||||
'text/*',
|
||||
'text/html',
|
||||
'application/xhtml+xml',
|
||||
'text/xml',
|
||||
'application/xml'
|
||||
];
|
||||
|
||||
public function process(HTTPRequest $request, callable $delegate)
|
||||
{
|
||||
try {
|
||||
if ($response = $this->checkForExpiredPassword($request)) {
|
||||
return $response;
|
||||
}
|
||||
} catch (DatabaseException $e) {
|
||||
// Database isn't ready, carry on.
|
||||
}
|
||||
|
||||
return $delegate($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the just authenticated member has the password expired.
|
||||
* Returns a response if the current request should not be
|
||||
* processed as usual.
|
||||
*
|
||||
* @param HTTPRequest $request
|
||||
*
|
||||
* @return HTTPResponse|null
|
||||
*/
|
||||
protected function checkForExpiredPassword(HTTPRequest $request): ?HTTPResponse
|
||||
{
|
||||
$session = $request->getSession();
|
||||
|
||||
if ($session && $session->get(static::SESSION_KEY_ALLOW_CURRENT_REQUEST)) {
|
||||
// allow current request and skip the expiration check, but for only the current
|
||||
// request, so we're deleting the flag from the session so it's not affecting other
|
||||
// requests.
|
||||
// This flag would usually be set from within $handler->authenticateRequest()
|
||||
$session->clear(static::SESSION_KEY_ALLOW_CURRENT_REQUEST);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = Security::getCurrentUser();
|
||||
if ($user && $user->isPasswordExpired()) {
|
||||
if ($response = $this->handleExpiredPassword($request)) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have a redirect to a password change form registered
|
||||
* and redirect there if possible.
|
||||
* Otherwise, deauthenticate the user by resetting it for this request,
|
||||
* since we should treat ones with expired passwords as unauthorised.
|
||||
*
|
||||
* @param HTTPRequest $request
|
||||
*
|
||||
* @return HTTPResponse|null
|
||||
*/
|
||||
protected function handleExpiredPassword(HTTPRequest $request): ?HTTPResponse
|
||||
{
|
||||
$session = $request->getSession();
|
||||
|
||||
$sessionRedirectUrl = $session->get(static::SESSION_KEY_REDIRECT);
|
||||
$defaultRedirectUrl = static::config()->get('default_redirect');
|
||||
|
||||
if ($sessionRedirectUrl || $defaultRedirectUrl) {
|
||||
$redirectUrl = $this->absoluteUrl($sessionRedirectUrl ?? $defaultRedirectUrl);
|
||||
} else {
|
||||
$redirectUrl = null;
|
||||
}
|
||||
|
||||
if (!$session || !$redirectUrl) {
|
||||
Security::setCurrentUser(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentUrl = $this->absoluteUrl($request->getURL(true));
|
||||
if ($currentUrl === $redirectUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$allowedStartswith = static::config()->get('whitelisted_url_startswith');
|
||||
if (is_array($allowedStartswith)) {
|
||||
foreach ($allowedStartswith as $pattern) {
|
||||
$startswith = $this->absoluteUrl($pattern);
|
||||
|
||||
if (strncmp($currentUrl, $startswith, strlen($startswith)) === 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectOrForbid($request, $redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an absolute URL for the given path, adds base url
|
||||
* if the path configured as absolute
|
||||
*
|
||||
* @param string $url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function absoluteUrl($url): string
|
||||
{
|
||||
if (substr($url, 0, 1) === '/' && substr($url, 1, 1) !== '/') {
|
||||
// add BASE_URL explicitly if not absolute
|
||||
$url = Controller::join_links(Director::absoluteBaseURL(), $url);
|
||||
} else {
|
||||
$url = Director::absoluteURL($url) ?: Controller::join_links(Director::absoluteBaseURL(), $url);
|
||||
}
|
||||
|
||||
if (substr($url, -1) === '/') {
|
||||
$url = substr($url, 0, -1);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a redirect to the URL if text/html is acceptable, otherwise
|
||||
* deauthenticates the current request by Security::setCurrentUser(null)
|
||||
*
|
||||
* @param HTTPRequest $request
|
||||
* @param string $redirectUrl
|
||||
*
|
||||
* @return HTTPResponse|null
|
||||
*/
|
||||
private function redirectOrForbid(HTTPRequest $request, $redirectUrl): ?HTTPResponse
|
||||
{
|
||||
$acceptableTypes = $request->getAcceptMimetypes();
|
||||
|
||||
$allowedTypes = static::config()->get('mimetypes_allowing_redirect') ?? [];
|
||||
|
||||
if (count(array_intersect($allowedTypes, $acceptableTypes)) > 0) {
|
||||
$redirectAllowed = true;
|
||||
} else {
|
||||
// if browser didn't send the Accept header
|
||||
// with mimetypes, let's redirect anyway
|
||||
$redirectAllowed = count($acceptableTypes) === 0;
|
||||
}
|
||||
|
||||
if ($redirectAllowed) {
|
||||
$response = new HTTPResponse();
|
||||
$response->redirect($redirectUrl);
|
||||
return $response;
|
||||
}
|
||||
|
||||
Security::setCurrentUser(null);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserve the password change URL in the session
|
||||
* That URL is to be redirected to to force users change expired passwords
|
||||
*
|
||||
* @param Session $session Session where we persist the redirect URL
|
||||
* @param string $url change password form address
|
||||
*/
|
||||
public static function setRedirect(Session $session, $url)
|
||||
{
|
||||
$session->set(static::SESSION_KEY_REDIRECT, $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the current request to be finished without password expiration check
|
||||
*
|
||||
* @param Session $session Session where we persist the redirect URL
|
||||
* @param string $url change password form address
|
||||
*/
|
||||
public static function allowCurrentRequest(Session $session)
|
||||
{
|
||||
$session->set(static::SESSION_KEY_ALLOW_CURRENT_REQUEST, true);
|
||||
}
|
||||
}
|
446
tests/php/Security/PasswordExpirationMiddlewareTest.php
Normal file
446
tests/php/Security/PasswordExpirationMiddlewareTest.php
Normal file
@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Security\Tests;
|
||||
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\Tests\HttpRequestMockBuilder;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\ValidationException;
|
||||
use SilverStripe\Security\PasswordExpirationMiddleware;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
|
||||
class PasswordExpirationMiddlewareTest extends SapphireTest
|
||||
{
|
||||
use HttpRequestMockBuilder;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Director::config()->set('alternate_base_url', 'http://localhost/custom-base/');
|
||||
PasswordExpirationMiddleware::config()->set('default_redirect', null);
|
||||
PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Member mock object
|
||||
*
|
||||
* @param bool $isPasswordExpired result of the function {@see SilverStripe\Security\Member::isPasswordExpired}
|
||||
*
|
||||
* @return Member
|
||||
*/
|
||||
private function getMemberMock($isPasswordExpired) : Member
|
||||
{
|
||||
$mock = $this->createMock(Member::class);
|
||||
$mock->method('isPasswordExpired')->will($this->returnValue($isPasswordExpired));
|
||||
|
||||
return $mock;
|
||||
}
|
||||
|
||||
public function test200()
|
||||
{
|
||||
$member = $this->getMemberMock(false);
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/');
|
||||
$executed = false;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed) {
|
||||
$executed = true;
|
||||
return "delegated";
|
||||
});
|
||||
|
||||
$this->assertEquals($member, Security::getCurrentUser());
|
||||
$this->assertTrue($executed);
|
||||
$this->assertEquals("delegated", $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a member with an expired password is allowed to process the request in
|
||||
* deauthorised mode (Security::getCurrentUser() === null) if there are no
|
||||
* change password redirects registered
|
||||
*
|
||||
* @depends test200
|
||||
*/
|
||||
public function testDeauthorised()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/');
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertTrue($executed);
|
||||
$this->assertNull($activeMember);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a member with an expired password is redirected to a change password form
|
||||
* instead of processing its original request
|
||||
*
|
||||
* @depends test200
|
||||
*/
|
||||
public function testRedirected()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/');
|
||||
$request->method('getAcceptMimetypes')->will($this->returnValue(['*/*']));
|
||||
$session = $request->getSession();
|
||||
|
||||
$a->setRedirect($session, '/redirect-address-custom');
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertFalse($executed);
|
||||
$this->assertNull($activeMember);
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertEquals(Director::absoluteURL('redirect-address-custom'), $response->getHeader('Location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check we handle network locations correctly (the relative urls starting with //)
|
||||
*
|
||||
* @depends testRedirected
|
||||
*/
|
||||
public function testNetworkLocationRedirect()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/');
|
||||
$request->method('getAcceptMimetypes')->will($this->returnValue(['*/*']));
|
||||
$session = $request->getSession();
|
||||
|
||||
$a->setRedirect($session, '//localhost/custom-base/redirect-address-custom');
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertFalse($executed);
|
||||
$this->assertNull($activeMember);
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertEquals(Director::absoluteURL('redirect-address-custom'), $response->getHeader('Location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check we can allow the current request handling even with an expired password
|
||||
*
|
||||
* @depends test200
|
||||
* @depends testDeauthorised
|
||||
*/
|
||||
public function testAllowRequest()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/');
|
||||
$session = $request->getSession();
|
||||
PasswordExpirationMiddleware::allowCurrentRequest($session);
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertTrue($executed);
|
||||
$this->assertEquals($member, $activeMember);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a member with an expired password is redirected to a default change password form
|
||||
* if a custom not set
|
||||
*
|
||||
* @depends testRedirected
|
||||
*/
|
||||
public function testDefaultRedirect()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
PasswordExpirationMiddleware::config()->set('default_redirect', 'redirect-address-default');
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/');
|
||||
$request->method('getAcceptMimetypes')->will($this->returnValue(['*/*']));
|
||||
$session = $request->getSession();
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertFalse($executed);
|
||||
$this->assertNull($activeMember);
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertEquals(Director::absoluteURL('redirect-address-default'), $response->getHeader('Location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a member with an expired password is redirected to a default change password form
|
||||
* if a custom not set
|
||||
*
|
||||
* @depends testDefaultRedirect
|
||||
*/
|
||||
public function testCustomRedirect()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
PasswordExpirationMiddleware::config()->set('default_redirect', '/redirect-address-default');
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/');
|
||||
$request->method('getAcceptMimetypes')->will($this->returnValue(['*/*']));
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertFalse($executed);
|
||||
$this->assertNull($activeMember);
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertEquals(Director::absoluteURL('redirect-address-default'), $response->getHeader('Location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a custom redirect URL overrides the default one
|
||||
*
|
||||
* @depends testCustomRedirect
|
||||
*/
|
||||
public function testCustomOverDefaultRedirect()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
PasswordExpirationMiddleware::config()->set('default_redirect', '/redirect-address-default');
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/');
|
||||
$request->method('getAcceptMimetypes')->will($this->returnValue(['*/*']));
|
||||
$session = $request->getSession();
|
||||
$a->setRedirect($session, '/redirect-address-custom');
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertFalse($executed);
|
||||
$this->assertNull($activeMember);
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertEquals(Director::absoluteURL('redirect-address-custom'), $response->getHeader('Location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test we can allow URLs to be visited without redirections through config
|
||||
*
|
||||
* @depends testRedirected
|
||||
*/
|
||||
public function testAllowedUrlStartswithNegative()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', [
|
||||
'/allowed-address-configured/'
|
||||
]);
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/not-allowed');
|
||||
$request->method('getAcceptMimetypes')->will($this->returnValue(['*/*']));
|
||||
$session = $request->getSession();
|
||||
|
||||
$a->setRedirect($session, '/redirect-address-custom');
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertFalse($executed);
|
||||
$this->assertNull($activeMember);
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertEquals(Director::absoluteURL('redirect-address-custom'), $response->getHeader('Location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test we can allow URLs to be visited without redirections through config
|
||||
*
|
||||
* @depends testRedirected
|
||||
*/
|
||||
public function testAllowedUrlStartswithPositivePattern()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', [
|
||||
'/allowed-address-configured/'
|
||||
]);
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/allowed-address-configured/subsection1/subsection2/');
|
||||
$request->method('getAcceptMimetypes')->will($this->returnValue(['*/*']));
|
||||
$session = $request->getSession();
|
||||
|
||||
$a->setRedirect($session, '/redirect-address-custom');
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertTrue($executed);
|
||||
$this->assertEquals($member, $activeMember);
|
||||
}
|
||||
|
||||
public function testAllowedUrlStartswithPositiveTrailingSlash()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', [
|
||||
'/allowed-address-configured/'
|
||||
]);
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/allowed-address-configured?foo=bar');
|
||||
$request->method('getAcceptMimetypes')->will($this->returnValue(['*/*']));
|
||||
$session = $request->getSession();
|
||||
|
||||
$a->setRedirect($session, '/redirect-address-custom');
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertTrue($executed);
|
||||
$this->assertEquals($member, $activeMember);
|
||||
}
|
||||
|
||||
public function testAllowedUrlStartswithPositiveRelativeUrl()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', [
|
||||
'allowed-address-configured/'
|
||||
]);
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/allowed-address-configured?foo=bar');
|
||||
$request->method('getAcceptMimetypes')->will($this->returnValue(['*/*']));
|
||||
$session = $request->getSession();
|
||||
|
||||
$a->setRedirect($session, 'redirect-address-custom');
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertTrue($executed);
|
||||
$this->assertEquals($member, $activeMember);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test we can allow URLs to be visited without redirections through config
|
||||
*
|
||||
* @depends testRedirected
|
||||
*/
|
||||
public function testAllowedUrlStartswithPositiveExactUrl()
|
||||
{
|
||||
$member = $this->getMemberMock(true);
|
||||
$this->assertTrue($member->isPasswordExpired());
|
||||
|
||||
PasswordExpirationMiddleware::config()->set('whitelisted_url_startswith', [
|
||||
'/allowed-address-configured/'
|
||||
]);
|
||||
|
||||
$a = new PasswordExpirationMiddleware();
|
||||
|
||||
$request = $this->buildRequestMock('/allowed-address-configured/');
|
||||
$request->method('getAcceptMimetypes')->will($this->returnValue(['*/*']));
|
||||
$session = $request->getSession();
|
||||
|
||||
$a->setRedirect($session, '/redirect-address-custom');
|
||||
|
||||
$executed = false;
|
||||
$activeMember = null;
|
||||
Security::setCurrentUser($member);
|
||||
$response = $a->process($request, static function () use (&$executed, &$activeMember) {
|
||||
$executed = true;
|
||||
$activeMember = Security::getCurrentUser();
|
||||
});
|
||||
|
||||
$this->assertTrue($executed);
|
||||
$this->assertEquals($member, $activeMember);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user