aboutsummaryrefslogtreecommitdiffstats
path: root/tests/lib/AppFramework/Middleware/Security
diff options
context:
space:
mode:
Diffstat (limited to 'tests/lib/AppFramework/Middleware/Security')
-rw-r--r--tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php328
-rw-r--r--tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php344
-rw-r--r--tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php122
-rw-r--r--tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php69
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php145
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php17
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php15
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php38
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php171
-rw-r--r--tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php209
-rw-r--r--tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php322
-rw-r--r--tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php120
-rw-r--r--tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php701
13 files changed, 2601 insertions, 0 deletions
diff --git a/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php
new file mode 100644
index 00000000000..3fd2cb38a33
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/BruteForceMiddlewareTest.php
@@ -0,0 +1,328 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security;
+
+use OC\AppFramework\Middleware\Security\BruteForceMiddleware;
+use OC\AppFramework\Utility\ControllerMethodReflector;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\BruteForceProtection;
+use OCP\AppFramework\Http\Response;
+use OCP\IRequest;
+use OCP\Security\Bruteforce\IThrottler;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class TestController extends Controller {
+ /**
+ * @BruteForceProtection(action=login)
+ */
+ public function testMethodWithAnnotation() {
+ }
+
+ public function testMethodWithoutAnnotation() {
+ }
+
+ #[BruteForceProtection(action: 'single')]
+ public function singleAttribute(): void {
+ }
+
+ #[BruteForceProtection(action: 'first')]
+ #[BruteForceProtection(action: 'second')]
+ public function multipleAttributes(): void {
+ }
+}
+
+class BruteForceMiddlewareTest extends TestCase {
+ /** @var ControllerMethodReflector */
+ private $reflector;
+ /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */
+ private $throttler;
+ /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
+ private $request;
+ /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $logger;
+ private BruteForceMiddleware $bruteForceMiddleware;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->reflector = new ControllerMethodReflector();
+ $this->throttler = $this->createMock(IThrottler::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->bruteForceMiddleware = new BruteForceMiddleware(
+ $this->reflector,
+ $this->throttler,
+ $this->request,
+ $this->logger,
+ );
+ }
+
+ public function testBeforeControllerWithAnnotation(): void {
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('127.0.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelayOrThrowOnMax')
+ ->with('127.0.0.1', 'login');
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'testMethodWithAnnotation');
+ $this->bruteForceMiddleware->beforeController($controller, 'testMethodWithAnnotation');
+ }
+
+ public function testBeforeControllerWithSingleAttribute(): void {
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('::1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelayOrThrowOnMax')
+ ->with('::1', 'single');
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'singleAttribute');
+ $this->bruteForceMiddleware->beforeController($controller, 'singleAttribute');
+ }
+
+ public function testBeforeControllerWithMultipleAttributes(): void {
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('::1');
+
+ $calls = [
+ ['::1', 'first'],
+ ['::1', 'second'],
+ ];
+ $this->throttler
+ ->expects($this->exactly(2))
+ ->method('sleepDelayOrThrowOnMax')
+ ->willReturnCallback(function () use (&$calls) {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ return 0;
+ });
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'multipleAttributes');
+ $this->bruteForceMiddleware->beforeController($controller, 'multipleAttributes');
+ }
+
+ public function testBeforeControllerWithoutAnnotation(): void {
+ $this->request
+ ->expects($this->never())
+ ->method('getRemoteAddress');
+ $this->throttler
+ ->expects($this->never())
+ ->method('sleepDelayOrThrowOnMax');
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'testMethodWithoutAnnotation');
+ $this->bruteForceMiddleware->beforeController($controller, 'testMethodWithoutAnnotation');
+ }
+
+ public function testAfterControllerWithAnnotationAndThrottledRequest(): void {
+ /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
+ $response = $this->createMock(Response::class);
+ $response
+ ->expects($this->once())
+ ->method('isThrottled')
+ ->willReturn(true);
+ $response
+ ->expects($this->once())
+ ->method('getThrottleMetadata')
+ ->willReturn([]);
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('127.0.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelayOrThrowOnMax')
+ ->with('127.0.0.1', 'login');
+ $this->throttler
+ ->expects($this->once())
+ ->method('registerAttempt')
+ ->with('login', '127.0.0.1');
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'testMethodWithAnnotation');
+ $this->bruteForceMiddleware->afterController($controller, 'testMethodWithAnnotation', $response);
+ }
+
+ public function testAfterControllerWithAnnotationAndNotThrottledRequest(): void {
+ /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
+ $response = $this->createMock(Response::class);
+ $response
+ ->expects($this->once())
+ ->method('isThrottled')
+ ->willReturn(false);
+ $this->request
+ ->expects($this->never())
+ ->method('getRemoteAddress');
+ $this->throttler
+ ->expects($this->never())
+ ->method('sleepDelayOrThrowOnMax');
+ $this->throttler
+ ->expects($this->never())
+ ->method('registerAttempt');
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'testMethodWithAnnotation');
+ $this->bruteForceMiddleware->afterController($controller, 'testMethodWithAnnotation', $response);
+ }
+
+ public function testAfterControllerWithSingleAttribute(): void {
+ /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
+ $response = $this->createMock(Response::class);
+ $response
+ ->expects($this->once())
+ ->method('isThrottled')
+ ->willReturn(true);
+ $response
+ ->expects($this->once())
+ ->method('getThrottleMetadata')
+ ->willReturn([]);
+
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('::1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelayOrThrowOnMax')
+ ->with('::1', 'single');
+ $this->throttler
+ ->expects($this->once())
+ ->method('registerAttempt')
+ ->with('single', '::1');
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'singleAttribute');
+ $this->bruteForceMiddleware->afterController($controller, 'singleAttribute', $response);
+ }
+
+ public function testAfterControllerWithMultipleAttributesGeneralMatch(): void {
+ /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
+ $response = $this->createMock(Response::class);
+ $response
+ ->expects($this->once())
+ ->method('isThrottled')
+ ->willReturn(true);
+ $response
+ ->expects($this->once())
+ ->method('getThrottleMetadata')
+ ->willReturn([]);
+
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('::1');
+
+ $sleepCalls = [
+ ['::1', 'first'],
+ ['::1', 'second'],
+ ];
+ $this->throttler
+ ->expects($this->exactly(2))
+ ->method('sleepDelayOrThrowOnMax')
+ ->willReturnCallback(function () use (&$sleepCalls) {
+ $expected = array_shift($sleepCalls);
+ $this->assertEquals($expected, func_get_args());
+ return 0;
+ });
+
+ $attemptCalls = [
+ ['first', '::1', []],
+ ['second', '::1', []],
+ ];
+ $this->throttler
+ ->expects($this->exactly(2))
+ ->method('registerAttempt')
+ ->willReturnCallback(function () use (&$attemptCalls): void {
+ $expected = array_shift($attemptCalls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'multipleAttributes');
+ $this->bruteForceMiddleware->afterController($controller, 'multipleAttributes', $response);
+ }
+
+ public function testAfterControllerWithMultipleAttributesSpecificMatch(): void {
+ /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
+ $response = $this->createMock(Response::class);
+ $response
+ ->expects($this->once())
+ ->method('isThrottled')
+ ->willReturn(true);
+ $response
+ ->expects($this->once())
+ ->method('getThrottleMetadata')
+ ->willReturn(['action' => 'second']);
+
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('::1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelayOrThrowOnMax')
+ ->with('::1', 'second');
+ $this->throttler
+ ->expects($this->once())
+ ->method('registerAttempt')
+ ->with('second', '::1');
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'multipleAttributes');
+ $this->bruteForceMiddleware->afterController($controller, 'multipleAttributes', $response);
+ }
+
+ public function testAfterControllerWithoutAnnotation(): void {
+ $this->request
+ ->expects($this->never())
+ ->method('getRemoteAddress');
+ $this->throttler
+ ->expects($this->never())
+ ->method('sleepDelayOrThrowOnMax');
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'testMethodWithoutAnnotation');
+ /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
+ $response = $this->createMock(Response::class);
+ $this->bruteForceMiddleware->afterController($controller, 'testMethodWithoutAnnotation', $response);
+ }
+
+ public function testAfterControllerWithThrottledResponseButUnhandled(): void {
+ $this->request
+ ->expects($this->never())
+ ->method('getRemoteAddress');
+ $this->throttler
+ ->expects($this->never())
+ ->method('sleepDelayOrThrowOnMax');
+
+ $controller = new TestController('test', $this->request);
+ $this->reflector->reflect($controller, 'testMethodWithoutAnnotation');
+ /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
+ $response = $this->createMock(Response::class);
+ $response->method('isThrottled')
+ ->willReturn(true);
+
+ $this->logger->expects($this->once())
+ ->method('debug')
+ ->with('Response for Test\AppFramework\Middleware\Security\TestController::testMethodWithoutAnnotation got bruteforce throttled but has no annotation nor attribute defined.');
+
+ $this->bruteForceMiddleware->afterController($controller, 'testMethodWithoutAnnotation', $response);
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php
new file mode 100644
index 00000000000..c325ae638fb
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php
@@ -0,0 +1,344 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2014-2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace Test\AppFramework\Middleware\Security;
+
+use OC\AppFramework\Http\Request;
+use OC\AppFramework\Middleware\Security\CORSMiddleware;
+use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
+use OC\AppFramework\Utility\ControllerMethodReflector;
+use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
+use OC\User\Session;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\Http\Response;
+use OCP\IConfig;
+use OCP\IRequest;
+use OCP\IRequestId;
+use OCP\Security\Bruteforce\IThrottler;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\AppFramework\Middleware\Security\Mock\CORSMiddlewareController;
+
+class CORSMiddlewareTest extends \Test\TestCase {
+ /** @var ControllerMethodReflector */
+ private $reflector;
+ /** @var Session|MockObject */
+ private $session;
+ /** @var IThrottler|MockObject */
+ private $throttler;
+ /** @var CORSMiddlewareController */
+ private $controller;
+ private LoggerInterface $logger;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->reflector = new ControllerMethodReflector();
+ $this->session = $this->createMock(Session::class);
+ $this->throttler = $this->createMock(IThrottler::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->controller = new CORSMiddlewareController(
+ 'test',
+ $this->createMock(IRequest::class)
+ );
+ }
+
+ public static function dataSetCORSAPIHeader(): array {
+ return [
+ ['testSetCORSAPIHeader'],
+ ['testSetCORSAPIHeaderAttribute'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSetCORSAPIHeader')]
+ public function testSetCORSAPIHeader(string $method): void {
+ $request = new Request(
+ [
+ 'server' => [
+ 'HTTP_ORIGIN' => 'test'
+ ]
+ ],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $this->reflector->reflect($this->controller, $method);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+
+ $response = $middleware->afterController($this->controller, $method, new Response());
+ $headers = $response->getHeaders();
+ $this->assertEquals('test', $headers['Access-Control-Allow-Origin']);
+ }
+
+ public function testNoAnnotationNoCORSHEADER(): void {
+ $request = new Request(
+ [
+ 'server' => [
+ 'HTTP_ORIGIN' => 'test'
+ ]
+ ],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+
+ $response = $middleware->afterController($this->controller, __FUNCTION__, new Response());
+ $headers = $response->getHeaders();
+ $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers));
+ }
+
+ public static function dataNoOriginHeaderNoCORSHEADER(): array {
+ return [
+ ['testNoOriginHeaderNoCORSHEADER'],
+ ['testNoOriginHeaderNoCORSHEADERAttribute'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoOriginHeaderNoCORSHEADER')]
+ public function testNoOriginHeaderNoCORSHEADER(string $method): void {
+ $request = new Request(
+ [],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $this->reflector->reflect($this->controller, $method);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+
+ $response = $middleware->afterController($this->controller, $method, new Response());
+ $headers = $response->getHeaders();
+ $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers));
+ }
+
+ public static function dataCorsIgnoredIfWithCredentialsHeaderPresent(): array {
+ return [
+ ['testCorsIgnoredIfWithCredentialsHeaderPresent'],
+ ['testCorsAttributeIgnoredIfWithCredentialsHeaderPresent'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCorsIgnoredIfWithCredentialsHeaderPresent')]
+ public function testCorsIgnoredIfWithCredentialsHeaderPresent(string $method): void {
+ $this->expectException(SecurityException::class);
+
+ $request = new Request(
+ [
+ 'server' => [
+ 'HTTP_ORIGIN' => 'test'
+ ]
+ ],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $this->reflector->reflect($this->controller, $method);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+
+ $response = new Response();
+ $response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE');
+ $middleware->afterController($this->controller, $method, $response);
+ }
+
+ public static function dataNoCORSOnAnonymousPublicPage(): array {
+ return [
+ ['testNoCORSOnAnonymousPublicPage'],
+ ['testNoCORSOnAnonymousPublicPageAttribute'],
+ ['testNoCORSAttributeOnAnonymousPublicPage'],
+ ['testNoCORSAttributeOnAnonymousPublicPageAttribute'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCORSOnAnonymousPublicPage')]
+ public function testNoCORSOnAnonymousPublicPage(string $method): void {
+ $request = new Request(
+ [],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $this->reflector->reflect($this->controller, $method);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+ $this->session->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $this->session->expects($this->never())
+ ->method('logout');
+ $this->session->expects($this->never())
+ ->method('logClientIn')
+ ->with($this->equalTo('user'), $this->equalTo('pass'))
+ ->willReturn(true);
+ $this->reflector->reflect($this->controller, $method);
+
+ $middleware->beforeController($this->controller, $method);
+ }
+
+ public static function dataCORSShouldNeverAllowCookieAuth(): array {
+ return [
+ ['testCORSShouldNeverAllowCookieAuth'],
+ ['testCORSShouldNeverAllowCookieAuthAttribute'],
+ ['testCORSAttributeShouldNeverAllowCookieAuth'],
+ ['testCORSAttributeShouldNeverAllowCookieAuthAttribute'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNeverAllowCookieAuth')]
+ public function testCORSShouldNeverAllowCookieAuth(string $method): void {
+ $request = new Request(
+ [],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $this->reflector->reflect($this->controller, $method);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+ $this->session->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->session->expects($this->once())
+ ->method('logout');
+ $this->session->expects($this->never())
+ ->method('logClientIn')
+ ->with($this->equalTo('user'), $this->equalTo('pass'))
+ ->willReturn(true);
+
+ $this->expectException(SecurityException::class);
+ $middleware->beforeController($this->controller, $method);
+ }
+
+ public static function dataCORSShouldRelogin(): array {
+ return [
+ ['testCORSShouldRelogin'],
+ ['testCORSAttributeShouldRelogin'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldRelogin')]
+ public function testCORSShouldRelogin(string $method): void {
+ $request = new Request(
+ ['server' => [
+ 'PHP_AUTH_USER' => 'user',
+ 'PHP_AUTH_PW' => 'pass'
+ ]],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $this->session->expects($this->once())
+ ->method('logout');
+ $this->session->expects($this->once())
+ ->method('logClientIn')
+ ->with($this->equalTo('user'), $this->equalTo('pass'))
+ ->willReturn(true);
+ $this->reflector->reflect($this->controller, $method);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+
+ $middleware->beforeController($this->controller, $method);
+ }
+
+ public static function dataCORSShouldFailIfPasswordLoginIsForbidden(): array {
+ return [
+ ['testCORSShouldFailIfPasswordLoginIsForbidden'],
+ ['testCORSAttributeShouldFailIfPasswordLoginIsForbidden'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldFailIfPasswordLoginIsForbidden')]
+ public function testCORSShouldFailIfPasswordLoginIsForbidden(string $method): void {
+ $this->expectException(SecurityException::class);
+
+ $request = new Request(
+ ['server' => [
+ 'PHP_AUTH_USER' => 'user',
+ 'PHP_AUTH_PW' => 'pass'
+ ]],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $this->session->expects($this->once())
+ ->method('logout');
+ $this->session->expects($this->once())
+ ->method('logClientIn')
+ ->with($this->equalTo('user'), $this->equalTo('pass'))
+ ->willThrowException(new PasswordLoginForbiddenException);
+ $this->reflector->reflect($this->controller, $method);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+
+ $middleware->beforeController($this->controller, $method);
+ }
+
+ public static function dataCORSShouldNotAllowCookieAuth(): array {
+ return [
+ ['testCORSShouldNotAllowCookieAuth'],
+ ['testCORSAttributeShouldNotAllowCookieAuth'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNotAllowCookieAuth')]
+ public function testCORSShouldNotAllowCookieAuth(string $method): void {
+ $this->expectException(SecurityException::class);
+
+ $request = new Request(
+ ['server' => [
+ 'PHP_AUTH_USER' => 'user',
+ 'PHP_AUTH_PW' => 'pass'
+ ]],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $this->session->expects($this->once())
+ ->method('logout');
+ $this->session->expects($this->once())
+ ->method('logClientIn')
+ ->with($this->equalTo('user'), $this->equalTo('pass'))
+ ->willReturn(false);
+ $this->reflector->reflect($this->controller, $method);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+
+ $middleware->beforeController($this->controller, $method);
+ }
+
+ public function testAfterExceptionWithSecurityExceptionNoStatus(): void {
+ $request = new Request(
+ ['server' => [
+ 'PHP_AUTH_USER' => 'user',
+ 'PHP_AUTH_PW' => 'pass'
+ ]],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+ $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception'));
+
+ $expected = new JSONResponse(['message' => 'A security exception'], 500);
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testAfterExceptionWithSecurityExceptionWithStatus(): void {
+ $request = new Request(
+ ['server' => [
+ 'PHP_AUTH_USER' => 'user',
+ 'PHP_AUTH_PW' => 'pass'
+ ]],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+ $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception', 501));
+
+ $expected = new JSONResponse(['message' => 'A security exception'], 501);
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testAfterExceptionWithRegularException(): void {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('A regular exception');
+
+ $request = new Request(
+ ['server' => [
+ 'PHP_AUTH_USER' => 'user',
+ 'PHP_AUTH_PW' => 'pass'
+ ]],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
+ $middleware->afterException($this->controller, __FUNCTION__, new \Exception('A regular exception'));
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php
new file mode 100644
index 00000000000..b0b41b27cb9
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/CSPMiddlewareTest.php
@@ -0,0 +1,122 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security;
+
+use OC\AppFramework\Middleware\Security\CSPMiddleware;
+use OC\Security\CSP\ContentSecurityPolicy;
+use OC\Security\CSP\ContentSecurityPolicyManager;
+use OC\Security\CSP\ContentSecurityPolicyNonceManager;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\EmptyContentSecurityPolicy;
+use OCP\AppFramework\Http\Response;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class CSPMiddlewareTest extends \Test\TestCase {
+ /** @var CSPMiddleware&MockObject */
+ private $middleware;
+ /** @var Controller&MockObject */
+ private $controller;
+ /** @var ContentSecurityPolicyManager&MockObject */
+ private $contentSecurityPolicyManager;
+ /** @var ContentSecurityPolicyNonceManager&MockObject */
+ private $cspNonceManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->controller = $this->createMock(Controller::class);
+ $this->contentSecurityPolicyManager = $this->createMock(ContentSecurityPolicyManager::class);
+ $this->cspNonceManager = $this->createMock(ContentSecurityPolicyNonceManager::class);
+ $this->middleware = new CSPMiddleware(
+ $this->contentSecurityPolicyManager,
+ $this->cspNonceManager,
+ );
+ }
+
+ public function testAfterController(): void {
+ $this->cspNonceManager
+ ->expects($this->once())
+ ->method('browserSupportsCspV3')
+ ->willReturn(false);
+ $response = $this->createMock(Response::class);
+ $defaultPolicy = new ContentSecurityPolicy();
+ $defaultPolicy->addAllowedImageDomain('defaultpolicy');
+ $currentPolicy = new ContentSecurityPolicy();
+ $currentPolicy->addAllowedConnectDomain('currentPolicy');
+ $mergedPolicy = new ContentSecurityPolicy();
+ $mergedPolicy->addAllowedMediaDomain('mergedPolicy');
+ $response
+ ->expects($this->exactly(2))
+ ->method('getContentSecurityPolicy')
+ ->willReturn($currentPolicy);
+ $this->contentSecurityPolicyManager
+ ->expects($this->once())
+ ->method('getDefaultPolicy')
+ ->willReturn($defaultPolicy);
+ $this->contentSecurityPolicyManager
+ ->expects($this->once())
+ ->method('mergePolicies')
+ ->with($defaultPolicy, $currentPolicy)
+ ->willReturn($mergedPolicy);
+ $response->expects($this->once())
+ ->method('setContentSecurityPolicy')
+ ->with($mergedPolicy);
+
+ $this->middleware->afterController($this->controller, 'test', $response);
+ }
+
+ public function testAfterControllerEmptyCSP(): void {
+ $response = $this->createMock(Response::class);
+ $emptyPolicy = new EmptyContentSecurityPolicy();
+ $response->expects($this->any())
+ ->method('getContentSecurityPolicy')
+ ->willReturn($emptyPolicy);
+ $response->expects($this->never())
+ ->method('setContentSecurityPolicy');
+
+ $this->middleware->afterController($this->controller, 'test', $response);
+ }
+
+ public function testAfterControllerWithContentSecurityPolicy3Support(): void {
+ $this->cspNonceManager
+ ->expects($this->once())
+ ->method('browserSupportsCspV3')
+ ->willReturn(true);
+ $token = base64_encode('the-nonce');
+ $this->cspNonceManager
+ ->expects($this->once())
+ ->method('getNonce')
+ ->willReturn($token);
+ $response = $this->createMock(Response::class);
+ $defaultPolicy = new ContentSecurityPolicy();
+ $defaultPolicy->addAllowedImageDomain('defaultpolicy');
+ $currentPolicy = new ContentSecurityPolicy();
+ $currentPolicy->addAllowedConnectDomain('currentPolicy');
+ $mergedPolicy = new ContentSecurityPolicy();
+ $mergedPolicy->addAllowedMediaDomain('mergedPolicy');
+ $response
+ ->expects($this->exactly(2))
+ ->method('getContentSecurityPolicy')
+ ->willReturn($currentPolicy);
+ $this->contentSecurityPolicyManager
+ ->expects($this->once())
+ ->method('getDefaultPolicy')
+ ->willReturn($defaultPolicy);
+ $this->contentSecurityPolicyManager
+ ->expects($this->once())
+ ->method('mergePolicies')
+ ->with($defaultPolicy, $currentPolicy)
+ ->willReturn($mergedPolicy);
+ $response->expects($this->once())
+ ->method('setContentSecurityPolicy')
+ ->with($mergedPolicy);
+
+ $this->assertEquals($response, $this->middleware->afterController($this->controller, 'test', $response));
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php
new file mode 100644
index 00000000000..55a70d4c040
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security;
+
+use OC\AppFramework\Middleware\Security\FeaturePolicyMiddleware;
+use OC\Security\FeaturePolicy\FeaturePolicy;
+use OC\Security\FeaturePolicy\FeaturePolicyManager;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\EmptyFeaturePolicy;
+use OCP\AppFramework\Http\Response;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class FeaturePolicyMiddlewareTest extends \Test\TestCase {
+ /** @var FeaturePolicyMiddleware|MockObject */
+ private $middleware;
+ /** @var Controller|MockObject */
+ private $controller;
+ /** @var FeaturePolicyManager|MockObject */
+ private $manager;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->controller = $this->createMock(Controller::class);
+ $this->manager = $this->createMock(FeaturePolicyManager::class);
+ $this->middleware = new FeaturePolicyMiddleware(
+ $this->manager
+ );
+ }
+
+ public function testAfterController(): void {
+ $response = $this->createMock(Response::class);
+ $defaultPolicy = new FeaturePolicy();
+ $defaultPolicy->addAllowedCameraDomain('defaultpolicy');
+ $currentPolicy = new FeaturePolicy();
+ $currentPolicy->addAllowedAutoplayDomain('currentPolicy');
+ $mergedPolicy = new FeaturePolicy();
+ $mergedPolicy->addAllowedGeoLocationDomain('mergedPolicy');
+ $response->method('getFeaturePolicy')
+ ->willReturn($currentPolicy);
+ $this->manager->method('getDefaultPolicy')
+ ->willReturn($defaultPolicy);
+ $this->manager->method('mergePolicies')
+ ->with($defaultPolicy, $currentPolicy)
+ ->willReturn($mergedPolicy);
+ $response->expects($this->once())
+ ->method('setFeaturePolicy')
+ ->with($mergedPolicy);
+
+ $this->middleware->afterController($this->controller, 'test', $response);
+ }
+
+ public function testAfterControllerEmptyCSP(): void {
+ $response = $this->createMock(Response::class);
+ $emptyPolicy = new EmptyFeaturePolicy();
+ $response->method('getFeaturePolicy')
+ ->willReturn($emptyPolicy);
+ $response->expects($this->never())
+ ->method('setFeaturePolicy');
+
+ $this->middleware->afterController($this->controller, 'test', $response);
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php b/tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php
new file mode 100644
index 00000000000..8ab3a48b62e
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/Mock/CORSMiddlewareController.php
@@ -0,0 +1,145 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security\Mock;
+
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\CORS;
+use OCP\AppFramework\Http\Attribute\PublicPage;
+
+class CORSMiddlewareController extends Controller {
+ /**
+ * @CORS
+ */
+ public function testSetCORSAPIHeader() {
+ }
+
+ #[CORS]
+ public function testSetCORSAPIHeaderAttribute() {
+ }
+
+ public function testNoAnnotationNoCORSHEADER() {
+ }
+
+ /**
+ * @CORS
+ */
+ public function testNoOriginHeaderNoCORSHEADER() {
+ }
+
+ #[CORS]
+ public function testNoOriginHeaderNoCORSHEADERAttribute() {
+ }
+
+ /**
+ * @CORS
+ */
+ public function testCorsIgnoredIfWithCredentialsHeaderPresent() {
+ }
+
+ #[CORS]
+ public function testCorsAttributeIgnoredIfWithCredentialsHeaderPresent() {
+ }
+
+ /**
+ * CORS must not be enforced for anonymous users on public pages
+ *
+ * @CORS
+ * @PublicPage
+ */
+ public function testNoCORSOnAnonymousPublicPage() {
+ }
+
+ /**
+ * CORS must not be enforced for anonymous users on public pages
+ *
+ * @CORS
+ */
+ #[PublicPage]
+ public function testNoCORSOnAnonymousPublicPageAttribute() {
+ }
+
+ /**
+ * @PublicPage
+ */
+ #[CORS]
+ public function testNoCORSAttributeOnAnonymousPublicPage() {
+ }
+
+ #[CORS]
+ #[PublicPage]
+ public function testNoCORSAttributeOnAnonymousPublicPageAttribute() {
+ }
+
+ /**
+ * @CORS
+ * @PublicPage
+ */
+ public function testCORSShouldNeverAllowCookieAuth() {
+ }
+
+ /**
+ * @CORS
+ */
+ #[PublicPage]
+ public function testCORSShouldNeverAllowCookieAuthAttribute() {
+ }
+
+ /**
+ * @PublicPage
+ */
+ #[CORS]
+ public function testCORSAttributeShouldNeverAllowCookieAuth() {
+ }
+
+ #[CORS]
+ #[PublicPage]
+ public function testCORSAttributeShouldNeverAllowCookieAuthAttribute() {
+ }
+
+ /**
+ * @CORS
+ */
+ public function testCORSShouldRelogin() {
+ }
+
+ #[CORS]
+ public function testCORSAttributeShouldRelogin() {
+ }
+
+ /**
+ * @CORS
+ */
+ public function testCORSShouldFailIfPasswordLoginIsForbidden() {
+ }
+
+ #[CORS]
+ public function testCORSAttributeShouldFailIfPasswordLoginIsForbidden() {
+ }
+
+ /**
+ * @CORS
+ */
+ public function testCORSShouldNotAllowCookieAuth() {
+ }
+
+ #[CORS]
+ public function testCORSAttributeShouldNotAllowCookieAuth() {
+ }
+
+ public function testAfterExceptionWithSecurityExceptionNoStatus() {
+ }
+
+ public function testAfterExceptionWithSecurityExceptionWithStatus() {
+ }
+
+
+ public function testAfterExceptionWithRegularException() {
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php b/tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php
new file mode 100644
index 00000000000..4d6778e98b9
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/Mock/NormalController.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security\Mock;
+
+use OCP\AppFramework\Controller;
+
+class NormalController extends Controller {
+ public function foo() {
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php b/tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php
new file mode 100644
index 00000000000..93e793ecca9
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/Mock/OCSController.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security\Mock;
+
+class OCSController extends \OCP\AppFramework\OCSController {
+ public function foo() {
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php b/tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php
new file mode 100644
index 00000000000..cd1cdaa49ca
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/Mock/PasswordConfirmationMiddlewareController.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security\Mock;
+
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
+
+class PasswordConfirmationMiddlewareController extends Controller {
+ public function testNoAnnotationNorAttribute() {
+ }
+
+ /**
+ * @TestAnnotation
+ */
+ public function testDifferentAnnotation() {
+ }
+
+ /**
+ * @PasswordConfirmationRequired
+ */
+ public function testAnnotation() {
+ }
+
+ #[PasswordConfirmationRequired]
+ public function testAttribute() {
+ }
+
+ #[PasswordConfirmationRequired]
+ public function testSSO() {
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php b/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php
new file mode 100644
index 00000000000..c8f9878b0c1
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php
@@ -0,0 +1,171 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security\Mock;
+
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\ExAppRequired;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\PublicPage;
+use OCP\AppFramework\Http\Attribute\StrictCookiesRequired;
+use OCP\AppFramework\Http\Attribute\SubAdminRequired;
+
+class SecurityMiddlewareController extends Controller {
+ /**
+ * @PublicPage
+ * @NoCSRFRequired
+ */
+ public function testAnnotationNoCSRFRequiredPublicPage() {
+ }
+
+ /**
+ * @NoCSRFRequired
+ */
+ #[PublicPage]
+ public function testAnnotationNoCSRFRequiredAttributePublicPage() {
+ }
+
+ /**
+ * @PublicPage
+ */
+ #[NoCSRFRequired]
+ public function testAnnotationPublicPageAttributeNoCSRFRequired() {
+ }
+
+ #[NoCSRFRequired]
+ #[PublicPage]
+ public function testAttributeNoCSRFRequiredPublicPage() {
+ }
+
+ public function testNoAnnotationNorAttribute() {
+ }
+
+ /**
+ * @NoCSRFRequired
+ */
+ public function testAnnotationNoCSRFRequired() {
+ }
+
+ #[NoCSRFRequired]
+ public function testAttributeNoCSRFRequired() {
+ }
+
+ /**
+ * @PublicPage
+ */
+ public function testAnnotationPublicPage() {
+ }
+
+ #[PublicPage]
+ public function testAttributePublicPage() {
+ }
+
+ /**
+ * @PublicPage
+ * @StrictCookieRequired
+ */
+ public function testAnnotationPublicPageStrictCookieRequired() {
+ }
+
+ /**
+ * @StrictCookieRequired
+ */
+ #[PublicPage]
+ public function testAnnotationStrictCookieRequiredAttributePublicPage() {
+ }
+
+ /**
+ * @PublicPage
+ */
+ #[StrictCookiesRequired]
+ public function testAnnotationPublicPageAttributeStrictCookiesRequired() {
+ }
+
+ #[PublicPage]
+ #[StrictCookiesRequired]
+ public function testAttributePublicPageStrictCookiesRequired() {
+ }
+
+ /**
+ * @PublicPage
+ * @NoCSRFRequired
+ * @StrictCookieRequired
+ */
+ public function testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired() {
+ }
+
+ #[NoCSRFRequired]
+ #[PublicPage]
+ #[StrictCookiesRequired]
+ public function testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired() {
+ }
+
+ /**
+ * @NoCSRFRequired
+ * @NoAdminRequired
+ */
+ public function testAnnotationNoAdminRequiredNoCSRFRequired() {
+ }
+
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ public function testAttributeNoAdminRequiredNoCSRFRequired() {
+ }
+
+ /**
+ * @NoCSRFRequired
+ * @SubAdminRequired
+ */
+ public function testAnnotationNoCSRFRequiredSubAdminRequired() {
+ }
+
+ /**
+ * @SubAdminRequired
+ */
+ #[NoCSRFRequired]
+ public function testAnnotationNoCSRFRequiredAttributeSubAdminRequired() {
+ }
+
+ /**
+ * @NoCSRFRequired
+ */
+ #[SubAdminRequired]
+ public function testAnnotationSubAdminRequiredAttributeNoCSRFRequired() {
+ }
+
+ #[NoCSRFRequired]
+ #[SubAdminRequired]
+ public function testAttributeNoCSRFRequiredSubAdminRequired() {
+ }
+
+ /**
+ * @PublicPage
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ */
+ public function testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage() {
+ }
+
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ #[PublicPage]
+ public function testAttributeNoAdminRequiredNoCSRFRequiredPublicPage() {
+ }
+
+ /**
+ * @ExAppRequired
+ */
+ public function testAnnotationExAppRequired() {
+ }
+
+ #[ExAppRequired]
+ public function testAttributeExAppRequired() {
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php
new file mode 100644
index 00000000000..90e801ca471
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/PasswordConfirmationMiddlewareTest.php
@@ -0,0 +1,209 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security;
+
+use OC\AppFramework\Middleware\Security\Exceptions\NotConfirmedException;
+use OC\AppFramework\Middleware\Security\PasswordConfirmationMiddleware;
+use OC\AppFramework\Utility\ControllerMethodReflector;
+use OC\Authentication\Token\IProvider;
+use OC\User\Manager;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Authentication\Token\IToken;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IUser;
+use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
+use Test\AppFramework\Middleware\Security\Mock\PasswordConfirmationMiddlewareController;
+use Test\TestCase;
+
+class PasswordConfirmationMiddlewareTest extends TestCase {
+ /** @var ControllerMethodReflector */
+ private $reflector;
+ /** @var ISession&\PHPUnit\Framework\MockObject\MockObject */
+ private $session;
+ /** @var IUserSession&\PHPUnit\Framework\MockObject\MockObject */
+ private $userSession;
+ /** @var IUser&\PHPUnit\Framework\MockObject\MockObject */
+ private $user;
+ /** @var PasswordConfirmationMiddleware */
+ private $middleware;
+ /** @var PasswordConfirmationMiddlewareController */
+ private $controller;
+ /** @var ITimeFactory&\PHPUnit\Framework\MockObject\MockObject */
+ private $timeFactory;
+ private IProvider&\PHPUnit\Framework\MockObject\MockObject $tokenProvider;
+ private LoggerInterface $logger;
+ /** @var IRequest&\PHPUnit\Framework\MockObject\MockObject */
+ private IRequest $request;
+ /** @var Manager&\PHPUnit\Framework\MockObject\MockObject */
+ private Manager $userManager;
+
+ protected function setUp(): void {
+ $this->reflector = new ControllerMethodReflector();
+ $this->session = $this->createMock(ISession::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->user = $this->createMock(IUser::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->tokenProvider = $this->createMock(IProvider::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->userManager = $this->createMock(Manager::class);
+ $this->controller = new PasswordConfirmationMiddlewareController(
+ 'test',
+ $this->createMock(IRequest::class)
+ );
+
+ $this->middleware = new PasswordConfirmationMiddleware(
+ $this->reflector,
+ $this->session,
+ $this->userSession,
+ $this->timeFactory,
+ $this->tokenProvider,
+ $this->logger,
+ $this->request,
+ $this->userManager,
+ );
+ }
+
+ public function testNoAnnotationNorAttribute(): void {
+ $this->reflector->reflect($this->controller, __FUNCTION__);
+ $this->session->expects($this->never())
+ ->method($this->anything());
+ $this->userSession->expects($this->never())
+ ->method($this->anything());
+
+ $this->middleware->beforeController($this->controller, __FUNCTION__);
+ }
+
+ public function testDifferentAnnotation(): void {
+ $this->reflector->reflect($this->controller, __FUNCTION__);
+ $this->session->expects($this->never())
+ ->method($this->anything());
+ $this->userSession->expects($this->never())
+ ->method($this->anything());
+
+ $this->middleware->beforeController($this->controller, __FUNCTION__);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')]
+ public function testAnnotation($backend, $lastConfirm, $currentTime, $exception): void {
+ $this->reflector->reflect($this->controller, __FUNCTION__);
+
+ $this->user->method('getBackendClassName')
+ ->willReturn($backend);
+ $this->userSession->method('getUser')
+ ->willReturn($this->user);
+
+ $this->session->method('get')
+ ->with('last-password-confirm')
+ ->willReturn($lastConfirm);
+
+ $this->timeFactory->method('getTime')
+ ->willReturn($currentTime);
+
+ $token = $this->createMock(IToken::class);
+ $token->method('getScopeAsArray')
+ ->willReturn([]);
+ $this->tokenProvider->expects($this->once())
+ ->method('getToken')
+ ->willReturn($token);
+
+ $thrown = false;
+ try {
+ $this->middleware->beforeController($this->controller, __FUNCTION__);
+ } catch (NotConfirmedException $e) {
+ $thrown = true;
+ }
+
+ $this->assertSame($exception, $thrown);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')]
+ public function testAttribute($backend, $lastConfirm, $currentTime, $exception): void {
+ $this->reflector->reflect($this->controller, __FUNCTION__);
+
+ $this->user->method('getBackendClassName')
+ ->willReturn($backend);
+ $this->userSession->method('getUser')
+ ->willReturn($this->user);
+
+ $this->session->method('get')
+ ->with('last-password-confirm')
+ ->willReturn($lastConfirm);
+
+ $this->timeFactory->method('getTime')
+ ->willReturn($currentTime);
+
+ $token = $this->createMock(IToken::class);
+ $token->method('getScopeAsArray')
+ ->willReturn([]);
+ $this->tokenProvider->expects($this->once())
+ ->method('getToken')
+ ->willReturn($token);
+
+ $thrown = false;
+ try {
+ $this->middleware->beforeController($this->controller, __FUNCTION__);
+ } catch (NotConfirmedException $e) {
+ $thrown = true;
+ }
+
+ $this->assertSame($exception, $thrown);
+ }
+
+
+
+ public static function dataProvider(): array {
+ return [
+ ['foo', 2000, 4000, true],
+ ['foo', 2000, 3000, false],
+ ['user_saml', 2000, 4000, false],
+ ['user_saml', 2000, 3000, false],
+ ['foo', 2000, 3815, false],
+ ['foo', 2000, 3816, true],
+ ];
+ }
+
+ public function testSSO(): void {
+ static $sessionId = 'mySession1d';
+
+ $this->reflector->reflect($this->controller, __FUNCTION__);
+
+ $this->user->method('getBackendClassName')
+ ->willReturn('fictional_backend');
+ $this->userSession->method('getUser')
+ ->willReturn($this->user);
+
+ $this->session->method('get')
+ ->with('last-password-confirm')
+ ->willReturn(0);
+ $this->session->method('getId')
+ ->willReturn($sessionId);
+
+ $this->timeFactory->method('getTime')
+ ->willReturn(9876);
+
+ $token = $this->createMock(IToken::class);
+ $token->method('getScopeAsArray')
+ ->willReturn([IToken::SCOPE_SKIP_PASSWORD_VALIDATION => true]);
+ $this->tokenProvider->expects($this->once())
+ ->method('getToken')
+ ->with($sessionId)
+ ->willReturn($token);
+
+ $thrown = false;
+ try {
+ $this->middleware->beforeController($this->controller, __FUNCTION__);
+ } catch (NotConfirmedException) {
+ $thrown = true;
+ }
+
+ $this->assertSame(false, $thrown);
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php
new file mode 100644
index 00000000000..c42baadcb1c
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php
@@ -0,0 +1,322 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security;
+
+use OC\AppFramework\Middleware\Security\RateLimitingMiddleware;
+use OC\AppFramework\Utility\ControllerMethodReflector;
+use OC\Security\Ip\BruteforceAllowList;
+use OC\Security\RateLimiting\Exception\RateLimitExceededException;
+use OC\Security\RateLimiting\Limiter;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\AnonRateLimit;
+use OCP\AppFramework\Http\Attribute\UserRateLimit;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\IAppConfig;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class TestRateLimitController extends Controller {
+ /**
+ * @UserRateThrottle(limit=20, period=200)
+ * @AnonRateThrottle(limit=10, period=100)
+ */
+ public function testMethodWithAnnotation() {
+ }
+
+ /**
+ * @AnonRateThrottle(limit=10, period=100)
+ */
+ public function testMethodWithAnnotationFallback() {
+ }
+
+ public function testMethodWithoutAnnotation() {
+ }
+
+ #[UserRateLimit(limit: 20, period: 200)]
+ #[AnonRateLimit(limit: 10, period: 100)]
+ public function testMethodWithAttributes() {
+ }
+
+ #[AnonRateLimit(limit: 10, period: 100)]
+ public function testMethodWithAttributesFallback() {
+ }
+}
+
+/**
+ * @group DB
+ */
+class RateLimitingMiddlewareTest extends TestCase {
+ private IRequest|MockObject $request;
+ private IUserSession|MockObject $userSession;
+ private ControllerMethodReflector $reflector;
+ private Limiter|MockObject $limiter;
+ private ISession|MockObject $session;
+ private IAppConfig|MockObject $appConfig;
+ private BruteforceAllowList|MockObject $bruteForceAllowList;
+ private RateLimitingMiddleware $rateLimitingMiddleware;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->reflector = new ControllerMethodReflector();
+ $this->limiter = $this->createMock(Limiter::class);
+ $this->session = $this->createMock(ISession::class);
+ $this->appConfig = $this->createMock(IAppConfig::class);
+ $this->bruteForceAllowList = $this->createMock(BruteforceAllowList::class);
+
+ $this->rateLimitingMiddleware = new RateLimitingMiddleware(
+ $this->request,
+ $this->userSession,
+ $this->reflector,
+ $this->limiter,
+ $this->session,
+ $this->appConfig,
+ $this->bruteForceAllowList,
+ );
+ }
+
+ public function testBeforeControllerWithoutAnnotationForAnon(): void {
+ $this->limiter
+ ->expects($this->never())
+ ->method('registerUserRequest');
+ $this->limiter
+ ->expects($this->never())
+ ->method('registerAnonRequest');
+
+ $this->userSession->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+
+ /** @var TestRateLimitController|MockObject $controller */
+ $controller = $this->createMock(TestRateLimitController::class);
+ $this->reflector->reflect($controller, 'testMethodWithoutAnnotation');
+ $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithoutAnnotation');
+ }
+
+ public function testBeforeControllerWithoutAnnotationForLoggedIn(): void {
+ $this->limiter
+ ->expects($this->never())
+ ->method('registerUserRequest');
+ $this->limiter
+ ->expects($this->never())
+ ->method('registerAnonRequest');
+
+ $this->userSession->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+
+ /** @var TestRateLimitController|MockObject $controller */
+ $controller = $this->createMock(TestRateLimitController::class);
+ $this->reflector->reflect($controller, 'testMethodWithoutAnnotation');
+ $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithoutAnnotation');
+ }
+
+ public function testBeforeControllerForAnon(): void {
+ $controller = new TestRateLimitController('test', $this->request);
+
+ $this->request
+ ->method('getRemoteAddress')
+ ->willReturn('127.0.0.1');
+
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+
+ $this->limiter
+ ->expects($this->never())
+ ->method('registerUserRequest');
+ $this->limiter
+ ->expects($this->once())
+ ->method('registerAnonRequest')
+ ->with(get_class($controller) . '::testMethodWithAnnotation', '10', '100', '127.0.0.1');
+
+ $this->reflector->reflect($controller, 'testMethodWithAnnotation');
+ $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotation');
+ }
+
+ public function testBeforeControllerForLoggedIn(): void {
+ $controller = new TestRateLimitController('test', $this->request);
+ /** @var IUser|MockObject $user */
+ $user = $this->createMock(IUser::class);
+
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->limiter
+ ->expects($this->never())
+ ->method('registerAnonRequest');
+ $this->limiter
+ ->expects($this->once())
+ ->method('registerUserRequest')
+ ->with(get_class($controller) . '::testMethodWithAnnotation', '20', '200', $user);
+
+
+ $this->reflector->reflect($controller, 'testMethodWithAnnotation');
+ $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotation');
+ }
+
+ public function testBeforeControllerAnonWithFallback(): void {
+ $controller = new TestRateLimitController('test', $this->request);
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('127.0.0.1');
+
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+
+
+ $this->limiter
+ ->expects($this->never())
+ ->method('registerUserRequest');
+ $this->limiter
+ ->expects($this->once())
+ ->method('registerAnonRequest')
+ ->with(get_class($controller) . '::testMethodWithAnnotationFallback', '10', '100', '127.0.0.1');
+
+ $this->reflector->reflect($controller, 'testMethodWithAnnotationFallback');
+ $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotationFallback');
+ }
+
+ public function testBeforeControllerAttributesForAnon(): void {
+ $controller = new TestRateLimitController('test', $this->request);
+
+ $this->request
+ ->method('getRemoteAddress')
+ ->willReturn('127.0.0.1');
+
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+
+ $this->limiter
+ ->expects($this->never())
+ ->method('registerUserRequest');
+ $this->limiter
+ ->expects($this->once())
+ ->method('registerAnonRequest')
+ ->with(get_class($controller) . '::testMethodWithAttributes', '10', '100', '127.0.0.1');
+
+ $this->reflector->reflect($controller, 'testMethodWithAttributes');
+ $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributes');
+ }
+
+ public function testBeforeControllerAttributesForLoggedIn(): void {
+ $controller = new TestRateLimitController('test', $this->request);
+ /** @var IUser|MockObject $user */
+ $user = $this->createMock(IUser::class);
+
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->limiter
+ ->expects($this->never())
+ ->method('registerAnonRequest');
+ $this->limiter
+ ->expects($this->once())
+ ->method('registerUserRequest')
+ ->with(get_class($controller) . '::testMethodWithAttributes', '20', '200', $user);
+
+
+ $this->reflector->reflect($controller, 'testMethodWithAttributes');
+ $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributes');
+ }
+
+ public function testBeforeControllerAttributesAnonWithFallback(): void {
+ $controller = new TestRateLimitController('test', $this->request);
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('127.0.0.1');
+
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+
+
+ $this->limiter
+ ->expects($this->never())
+ ->method('registerUserRequest');
+ $this->limiter
+ ->expects($this->once())
+ ->method('registerAnonRequest')
+ ->with(get_class($controller) . '::testMethodWithAttributesFallback', '10', '100', '127.0.0.1');
+
+ $this->reflector->reflect($controller, 'testMethodWithAttributesFallback');
+ $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributesFallback');
+ }
+
+ public function testAfterExceptionWithOtherException(): void {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('My test exception');
+
+ $controller = new TestRateLimitController('test', $this->request);
+
+ $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new \Exception('My test exception'));
+ }
+
+ public function testAfterExceptionWithJsonBody(): void {
+ $controller = new TestRateLimitController('test', $this->request);
+ $this->request
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('Accept')
+ ->willReturn('JSON');
+
+ $result = $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new RateLimitExceededException());
+ $expected = new DataResponse([], 429
+ );
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testAfterExceptionWithHtmlBody(): void {
+ $controller = new TestRateLimitController('test', $this->request);
+ $this->request
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('Accept')
+ ->willReturn('html');
+
+ $result = $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new RateLimitExceededException());
+ $expected = new TemplateResponse(
+ 'core',
+ '429',
+ [],
+ TemplateResponse::RENDER_AS_GUEST
+ );
+ $expected->setStatus(429);
+ $this->assertEquals($expected, $result);
+ $this->assertIsString($result->render());
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php
new file mode 100644
index 00000000000..7800371f68f
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\AppFramework\Middleware\Security;
+
+use OC\AppFramework\Http\Request;
+use OC\AppFramework\Middleware\Security\Exceptions\LaxSameSiteCookieFailedException;
+use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
+use OC\AppFramework\Middleware\Security\SameSiteCookieMiddleware;
+use OC\AppFramework\Utility\ControllerMethodReflector;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use Test\TestCase;
+
+class SameSiteCookieMiddlewareTest extends TestCase {
+ /** @var SameSiteCookieMiddleware */
+ private $middleware;
+
+ /** @var Request|\PHPUnit\Framework\MockObject\MockObject */
+ private $request;
+
+ /** @var ControllerMethodReflector|\PHPUnit\Framework\MockObject\MockObject */
+ private $reflector;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(Request::class);
+ $this->reflector = $this->createMock(ControllerMethodReflector::class);
+ $this->middleware = new SameSiteCookieMiddleware($this->request, $this->reflector);
+ }
+
+ public function testBeforeControllerNoIndex(): void {
+ $this->request->method('getScriptName')
+ ->willReturn('/ocs/v2.php');
+
+ $this->middleware->beforeController($this->createMock(Controller::class), 'foo');
+ $this->addToAssertionCount(1);
+ }
+
+ public function testBeforeControllerIndexHasAnnotation(): void {
+ $this->request->method('getScriptName')
+ ->willReturn('/index.php');
+
+ $this->reflector->method('hasAnnotation')
+ ->with('NoSameSiteCookieRequired')
+ ->willReturn(true);
+
+ $this->middleware->beforeController($this->createMock(Controller::class), 'foo');
+ $this->addToAssertionCount(1);
+ }
+
+ public function testBeforeControllerIndexNoAnnotationPassingCheck(): void {
+ $this->request->method('getScriptName')
+ ->willReturn('/index.php');
+
+ $this->reflector->method('hasAnnotation')
+ ->with('NoSameSiteCookieRequired')
+ ->willReturn(false);
+
+ $this->request->method('passesLaxCookieCheck')
+ ->willReturn(true);
+
+ $this->middleware->beforeController($this->createMock(Controller::class), 'foo');
+ $this->addToAssertionCount(1);
+ }
+
+ public function testBeforeControllerIndexNoAnnotationFailingCheck(): void {
+ $this->expectException(LaxSameSiteCookieFailedException::class);
+
+ $this->request->method('getScriptName')
+ ->willReturn('/index.php');
+
+ $this->reflector->method('hasAnnotation')
+ ->with('NoSameSiteCookieRequired')
+ ->willReturn(false);
+
+ $this->request->method('passesLaxCookieCheck')
+ ->willReturn(false);
+
+ $this->middleware->beforeController($this->createMock(Controller::class), 'foo');
+ }
+
+ public function testAfterExceptionNoLaxCookie(): void {
+ $ex = new SecurityException();
+
+ try {
+ $this->middleware->afterException($this->createMock(Controller::class), 'foo', $ex);
+ $this->fail();
+ } catch (\Exception $e) {
+ $this->assertSame($ex, $e);
+ }
+ }
+
+ public function testAfterExceptionLaxCookie(): void {
+ $ex = new LaxSameSiteCookieFailedException();
+
+ $this->request->method('getRequestUri')
+ ->willReturn('/myrequri');
+
+ $middleware = $this->getMockBuilder(SameSiteCookieMiddleware::class)
+ ->setConstructorArgs([$this->request, $this->reflector])
+ ->onlyMethods(['setSameSiteCookie'])
+ ->getMock();
+
+ $middleware->expects($this->once())
+ ->method('setSameSiteCookie');
+
+ $resp = $middleware->afterException($this->createMock(Controller::class), 'foo', $ex);
+
+ $this->assertSame(Http::STATUS_FOUND, $resp->getStatus());
+
+ $headers = $resp->getHeaders();
+ $this->assertSame('/myrequri', $headers['Location']);
+ }
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
new file mode 100644
index 00000000000..0c6fc21357d
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
@@ -0,0 +1,701 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Test\AppFramework\Middleware\Security;
+
+use OC\AppFramework\Http;
+use OC\AppFramework\Http\Request;
+use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
+use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
+use OC\AppFramework\Middleware\Security\Exceptions\ExAppRequiredException;
+use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
+use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
+use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
+use OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException;
+use OC\AppFramework\Middleware\Security\SecurityMiddleware;
+use OC\AppFramework\Utility\ControllerMethodReflector;
+use OC\Settings\AuthorizedGroupMapper;
+use OC\User\Session;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\Group\ISubAdmin;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\INavigationManager;
+use OCP\IRequest;
+use OCP\IRequestId;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Security\Ip\IRemoteAddress;
+use Psr\Log\LoggerInterface;
+use Test\AppFramework\Middleware\Security\Mock\NormalController;
+use Test\AppFramework\Middleware\Security\Mock\OCSController;
+use Test\AppFramework\Middleware\Security\Mock\SecurityMiddlewareController;
+
+class SecurityMiddlewareTest extends \Test\TestCase {
+ /** @var SecurityMiddleware|\PHPUnit\Framework\MockObject\MockObject */
+ private $middleware;
+ /** @var SecurityMiddlewareController */
+ private $controller;
+ /** @var SecurityException */
+ private $secException;
+ /** @var SecurityException */
+ private $secAjaxException;
+ /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
+ private $request;
+ /** @var ControllerMethodReflector */
+ private $reader;
+ /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $logger;
+ /** @var INavigationManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $navigationManager;
+ /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
+ private $urlGenerator;
+ /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $appManager;
+ /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
+ private $l10n;
+ /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
+ private $userSession;
+ /** @var AuthorizedGroupMapper|\PHPUnit\Framework\MockObject\MockObject */
+ private $authorizedGroupMapper;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->authorizedGroupMapper = $this->createMock(AuthorizedGroupMapper::class);
+ $this->userSession = $this->createMock(Session::class);
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('test');
+ $this->userSession->method('getUser')->willReturn($user);
+ $this->request = $this->createMock(IRequest::class);
+ $this->controller = new SecurityMiddlewareController(
+ 'test',
+ $this->request
+ );
+ $this->reader = new ControllerMethodReflector();
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->navigationManager = $this->createMock(INavigationManager::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->middleware = $this->getMiddleware(true, true, false);
+ $this->secException = new SecurityException('hey', false);
+ $this->secAjaxException = new SecurityException('hey', true);
+ }
+
+ private function getMiddleware(bool $isLoggedIn, bool $isAdminUser, bool $isSubAdmin, bool $isAppEnabledForUser = true): SecurityMiddleware {
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->appManager->expects($this->any())
+ ->method('isEnabledForUser')
+ ->willReturn($isAppEnabledForUser);
+ $remoteIpAddress = $this->createMock(IRemoteAddress::class);
+ $remoteIpAddress->method('allowsAdminActions')->willReturn(true);
+
+ $groupManager = $this->createMock(IGroupManager::class);
+ $groupManager->method('isAdmin')
+ ->willReturn($isAdminUser);
+ $subAdminManager = $this->createMock(ISubAdmin::class);
+ $subAdminManager->method('isSubAdmin')
+ ->willReturn($isSubAdmin);
+
+ return new SecurityMiddleware(
+ $this->request,
+ $this->reader,
+ $this->navigationManager,
+ $this->urlGenerator,
+ $this->logger,
+ 'files',
+ $isLoggedIn,
+ $groupManager,
+ $subAdminManager,
+ $this->appManager,
+ $this->l10n,
+ $this->authorizedGroupMapper,
+ $this->userSession,
+ $remoteIpAddress
+ );
+ }
+
+ public static function dataNoCSRFRequiredPublicPage(): array {
+ return [
+ ['testAnnotationNoCSRFRequiredPublicPage'],
+ ['testAnnotationNoCSRFRequiredAttributePublicPage'],
+ ['testAnnotationPublicPageAttributeNoCSRFRequired'],
+ ['testAttributeNoCSRFRequiredPublicPage'],
+ ];
+ }
+
+ public static function dataPublicPage(): array {
+ return [
+ ['testAnnotationPublicPage'],
+ ['testAttributePublicPage'],
+ ];
+ }
+
+ public static function dataNoCSRFRequired(): array {
+ return [
+ ['testAnnotationNoCSRFRequired'],
+ ['testAttributeNoCSRFRequired'],
+ ];
+ }
+
+ public static function dataPublicPageStrictCookieRequired(): array {
+ return [
+ ['testAnnotationPublicPageStrictCookieRequired'],
+ ['testAnnotationStrictCookieRequiredAttributePublicPage'],
+ ['testAnnotationPublicPageAttributeStrictCookiesRequired'],
+ ['testAttributePublicPageStrictCookiesRequired'],
+ ];
+ }
+
+ public static function dataNoCSRFRequiredPublicPageStrictCookieRequired(): array {
+ return [
+ ['testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired'],
+ ['testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired'],
+ ];
+ }
+
+ public static function dataNoAdminRequiredNoCSRFRequired(): array {
+ return [
+ ['testAnnotationNoAdminRequiredNoCSRFRequired'],
+ ['testAttributeNoAdminRequiredNoCSRFRequired'],
+ ];
+ }
+
+ public static function dataNoAdminRequiredNoCSRFRequiredPublicPage(): array {
+ return [
+ ['testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage'],
+ ['testAttributeNoAdminRequiredNoCSRFRequiredPublicPage'],
+ ];
+ }
+
+ public static function dataNoCSRFRequiredSubAdminRequired(): array {
+ return [
+ ['testAnnotationNoCSRFRequiredSubAdminRequired'],
+ ['testAnnotationNoCSRFRequiredAttributeSubAdminRequired'],
+ ['testAnnotationSubAdminRequiredAttributeNoCSRFRequired'],
+ ['testAttributeNoCSRFRequiredSubAdminRequired'],
+ ];
+ }
+
+ public static function dataExAppRequired(): array {
+ return [
+ ['testAnnotationExAppRequired'],
+ ['testAttributeExAppRequired'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
+ public function testSetNavigationEntry(string $method): void {
+ $this->navigationManager->expects($this->once())
+ ->method('setActiveEntry')
+ ->with($this->equalTo('files'));
+
+ $this->reader->reflect($this->controller, $method);
+ $this->middleware->beforeController($this->controller, $method);
+ }
+
+
+ /**
+ * @param string $method
+ * @param string $test
+ */
+ private function ajaxExceptionStatus($method, $test, $status) {
+ $isLoggedIn = false;
+ $isAdminUser = false;
+
+ // isAdminUser requires isLoggedIn call to return true
+ if ($test === 'isAdminUser') {
+ $isLoggedIn = true;
+ }
+
+ $sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false);
+
+ try {
+ $this->reader->reflect($this->controller, $method);
+ $sec->beforeController($this->controller, $method);
+ } catch (SecurityException $ex) {
+ $this->assertEquals($status, $ex->getCode());
+ }
+
+ // add assertion if everything should work fine otherwise phpunit will
+ // complain
+ if ($status === 0) {
+ $this->addToAssertionCount(1);
+ }
+ }
+
+ public function testAjaxStatusLoggedInCheck(): void {
+ $this->ajaxExceptionStatus(
+ 'testNoAnnotationNorAttribute',
+ 'isLoggedIn',
+ Http::STATUS_UNAUTHORIZED
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')]
+ public function testAjaxNotAdminCheck(string $method): void {
+ $this->ajaxExceptionStatus(
+ $method,
+ 'isAdminUser',
+ Http::STATUS_FORBIDDEN
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
+ public function testAjaxStatusCSRFCheck(string $method): void {
+ $this->ajaxExceptionStatus(
+ $method,
+ 'passesCSRFCheck',
+ Http::STATUS_PRECONDITION_FAILED
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
+ public function testAjaxStatusAllGood(string $method): void {
+ $this->ajaxExceptionStatus(
+ $method,
+ 'isLoggedIn',
+ 0
+ );
+ $this->ajaxExceptionStatus(
+ $method,
+ 'isAdminUser',
+ 0
+ );
+ $this->ajaxExceptionStatus(
+ $method,
+ 'passesCSRFCheck',
+ 0
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
+ public function testNoChecks(string $method): void {
+ $this->request->expects($this->never())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+
+ $sec = $this->getMiddleware(false, false, false);
+
+ $this->reader->reflect($this->controller, $method);
+ $sec->beforeController($this->controller, $method);
+ }
+
+ /**
+ * @param string $method
+ * @param string $expects
+ */
+ private function securityCheck($method, $expects, $shouldFail = false) {
+ // admin check requires login
+ if ($expects === 'isAdminUser') {
+ $isLoggedIn = true;
+ $isAdminUser = !$shouldFail;
+ } else {
+ $isLoggedIn = !$shouldFail;
+ $isAdminUser = false;
+ }
+
+ $sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false);
+
+ if ($shouldFail) {
+ $this->expectException(SecurityException::class);
+ } else {
+ $this->addToAssertionCount(1);
+ }
+
+ $this->reader->reflect($this->controller, $method);
+ $sec->beforeController($this->controller, $method);
+ }
+
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
+ public function testCsrfCheck(string $method): void {
+ $this->expectException(CrossSiteRequestForgeryException::class);
+
+ $this->request->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->willReturn(true);
+ $this->reader->reflect($this->controller, $method);
+ $this->middleware->beforeController($this->controller, $method);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
+ public function testNoCsrfCheck(string $method): void {
+ $this->request->expects($this->never())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+
+ $this->reader->reflect($this->controller, $method);
+ $this->middleware->beforeController($this->controller, $method);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
+ public function testPassesCsrfCheck(string $method): void {
+ $this->request->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->willReturn(true);
+
+ $this->reader->reflect($this->controller, $method);
+ $this->middleware->beforeController($this->controller, $method);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
+ public function testFailCsrfCheck(string $method): void {
+ $this->expectException(CrossSiteRequestForgeryException::class);
+
+ $this->request->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->willReturn(true);
+
+ $this->reader->reflect($this->controller, $method);
+ $this->middleware->beforeController($this->controller, $method);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPageStrictCookieRequired')]
+ public function testStrictCookieRequiredCheck(string $method): void {
+ $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException::class);
+
+ $this->request->expects($this->never())
+ ->method('passesCSRFCheck');
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->willReturn(false);
+
+ $this->reader->reflect($this->controller, $method);
+ $this->middleware->beforeController($this->controller, $method);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
+ public function testNoStrictCookieRequiredCheck(string $method): void {
+ $this->request->expects($this->never())
+ ->method('passesStrictCookieCheck')
+ ->willReturn(false);
+
+ $this->reader->reflect($this->controller, $method);
+ $this->middleware->beforeController($this->controller, $method);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPageStrictCookieRequired')]
+ public function testPassesStrictCookieRequiredCheck(string $method): void {
+ $this->request
+ ->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->willReturn(true);
+
+ $this->reader->reflect($this->controller, $method);
+ $this->middleware->beforeController($this->controller, $method);
+ }
+
+ public static function dataCsrfOcsController(): array {
+ return [
+ [NormalController::class, false, false, true],
+ [NormalController::class, false, true, true],
+ [NormalController::class, true, false, true],
+ [NormalController::class, true, true, true],
+
+ [OCSController::class, false, false, true],
+ [OCSController::class, false, true, false],
+ [OCSController::class, true, false, false],
+ [OCSController::class, true, true, false],
+ ];
+ }
+
+ /**
+ * @param string $controllerClass
+ * @param bool $hasOcsApiHeader
+ * @param bool $hasBearerAuth
+ * @param bool $exception
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCsrfOcsController')]
+ public function testCsrfOcsController(string $controllerClass, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception): void {
+ $this->request
+ ->method('getHeader')
+ ->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) {
+ if ($header === 'OCS-APIREQUEST' && $hasOcsApiHeader) {
+ return 'true';
+ }
+ if ($header === 'Authorization' && $hasBearerAuth) {
+ return 'Bearer TOKEN!';
+ }
+ return '';
+ });
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->willReturn(true);
+
+ $controller = new $controllerClass('test', $this->request);
+
+ try {
+ $this->middleware->beforeController($controller, 'foo');
+ $this->assertFalse($exception);
+ } catch (CrossSiteRequestForgeryException $e) {
+ $this->assertTrue($exception);
+ }
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')]
+ public function testLoggedInCheck(string $method): void {
+ $this->securityCheck($method, 'isLoggedIn');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')]
+ public function testFailLoggedInCheck(string $method): void {
+ $this->securityCheck($method, 'isLoggedIn', true);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')]
+ public function testIsAdminCheck(string $method): void {
+ $this->securityCheck($method, 'isAdminUser');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')]
+ public function testIsNotSubAdminCheck(string $method): void {
+ $this->reader->reflect($this->controller, $method);
+ $sec = $this->getMiddleware(true, false, false);
+
+ $this->expectException(SecurityException::class);
+ $sec->beforeController($this->controller, $method);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')]
+ public function testIsSubAdminCheck(string $method): void {
+ $this->reader->reflect($this->controller, $method);
+ $sec = $this->getMiddleware(true, false, true);
+
+ $sec->beforeController($this->controller, $method);
+ $this->addToAssertionCount(1);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')]
+ public function testIsSubAdminAndAdminCheck(string $method): void {
+ $this->reader->reflect($this->controller, $method);
+ $sec = $this->getMiddleware(true, true, true);
+
+ $sec->beforeController($this->controller, $method);
+ $this->addToAssertionCount(1);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')]
+ public function testFailIsAdminCheck(string $method): void {
+ $this->securityCheck($method, 'isAdminUser', true);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequiredPublicPage')]
+ public function testRestrictedAppLoggedInPublicPage(string $method): void {
+ $middleware = $this->getMiddleware(true, false, false);
+ $this->reader->reflect($this->controller, $method);
+
+ $this->appManager->method('getAppPath')
+ ->with('files')
+ ->willReturn('foo');
+
+ $this->appManager->method('isEnabledForUser')
+ ->with('files')
+ ->willReturn(false);
+
+ $middleware->beforeController($this->controller, $method);
+ $this->addToAssertionCount(1);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequiredPublicPage')]
+ public function testRestrictedAppNotLoggedInPublicPage(string $method): void {
+ $middleware = $this->getMiddleware(false, false, false);
+ $this->reader->reflect($this->controller, $method);
+
+ $this->appManager->method('getAppPath')
+ ->with('files')
+ ->willReturn('foo');
+
+ $this->appManager->method('isEnabledForUser')
+ ->with('files')
+ ->willReturn(false);
+
+ $middleware->beforeController($this->controller, $method);
+ $this->addToAssertionCount(1);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')]
+ public function testRestrictedAppLoggedIn(string $method): void {
+ $middleware = $this->getMiddleware(true, false, false, false);
+ $this->reader->reflect($this->controller, $method);
+
+ $this->appManager->method('getAppPath')
+ ->with('files')
+ ->willReturn('foo');
+
+ $this->expectException(AppNotEnabledException::class);
+ $middleware->beforeController($this->controller, $method);
+ }
+
+
+ public function testAfterExceptionNotCaughtThrowsItAgain(): void {
+ $ex = new \Exception();
+ $this->expectException(\Exception::class);
+ $this->middleware->afterException($this->controller, 'test', $ex);
+ }
+
+ public function testAfterExceptionReturnsRedirectForNotLoggedInUser(): void {
+ $this->request = new Request(
+ [
+ 'server'
+ => [
+ 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp'
+ ]
+ ],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $this->middleware = $this->getMiddleware(false, false, false);
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRoute')
+ ->with(
+ 'core.login.showLoginForm',
+ [
+ 'redirect_url' => 'nextcloud/index.php/apps/specialapp',
+ ]
+ )
+ ->willReturn('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp');
+ $this->logger
+ ->expects($this->once())
+ ->method('debug');
+ $response = $this->middleware->afterException(
+ $this->controller,
+ 'test',
+ new NotLoggedInException()
+ );
+ $expected = new RedirectResponse('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp');
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testAfterExceptionRedirectsToWebRootAfterStrictCookieFail(): void {
+ $this->request = new Request(
+ [
+ 'server' => [
+ 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp',
+ ],
+ ],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+
+ $this->middleware = $this->getMiddleware(false, false, false);
+ $response = $this->middleware->afterException(
+ $this->controller,
+ 'test',
+ new StrictCookieMissingException()
+ );
+
+ $expected = new RedirectResponse(\OC::$WEBROOT . '/');
+ $this->assertEquals($expected, $response);
+ }
+
+
+ /**
+ * @return array
+ */
+ public static function exceptionProvider(): array {
+ return [
+ [
+ new AppNotEnabledException(),
+ ],
+ [
+ new CrossSiteRequestForgeryException(),
+ ],
+ [
+ new NotAdminException(''),
+ ],
+ ];
+ }
+
+ /**
+ * @param SecurityException $exception
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('exceptionProvider')]
+ public function testAfterExceptionReturnsTemplateResponse(SecurityException $exception): void {
+ $this->request = new Request(
+ [
+ 'server'
+ => [
+ 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp'
+ ]
+ ],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+ $this->middleware = $this->getMiddleware(false, false, false);
+ $this->logger
+ ->expects($this->once())
+ ->method('debug');
+ $response = $this->middleware->afterException(
+ $this->controller,
+ 'test',
+ $exception
+ );
+ $expected = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
+ $expected->setStatus($exception->getCode());
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testAfterAjaxExceptionReturnsJSONError(): void {
+ $response = $this->middleware->afterException($this->controller, 'test',
+ $this->secAjaxException);
+
+ $this->assertTrue($response instanceof JSONResponse);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')]
+ public function testExAppRequired(string $method): void {
+ $middleware = $this->getMiddleware(true, false, false);
+ $this->reader->reflect($this->controller, $method);
+
+ $session = $this->createMock(ISession::class);
+ $session->method('get')->with('app_api')->willReturn(true);
+ $this->userSession->method('getSession')->willReturn($session);
+
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->willReturn(true);
+ $this->request->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+
+ $middleware->beforeController($this->controller, $method);
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')]
+ public function testExAppRequiredError(string $method): void {
+ $middleware = $this->getMiddleware(true, false, false, false);
+ $this->reader->reflect($this->controller, $method);
+
+ $session = $this->createMock(ISession::class);
+ $session->method('get')->with('app_api')->willReturn(false);
+ $this->userSession->method('getSession')->willReturn($session);
+
+ $this->expectException(ExAppRequiredException::class);
+ $middleware->beforeController($this->controller, $method);
+ }
+}