diff --git a/modules/exampleauth/lib/Controller/ExampleAuth.php b/modules/exampleauth/lib/Controller/ExampleAuth.php index 5765622c52539d3036639b0911e582c46d5d0a80..00f178979ed0727418450a2546fbca38aa218b50 100644 --- a/modules/exampleauth/lib/Controller/ExampleAuth.php +++ b/modules/exampleauth/lib/Controller/ExampleAuth.php @@ -36,6 +36,12 @@ class ExampleAuth /** @var \SimpleSAML\Session */ protected Session $session; + /** + * @var \SimpleSAML\Auth\State|string + * @psalm-var \SimpleSAML\Auth\State|class-string + */ + protected $authState = Auth\State::class; + /** * Controller constructor. @@ -56,6 +62,17 @@ class ExampleAuth } + /** + * Inject the \SimpleSAML\Auth\State dependency. + * + * @param \SimpleSAML\Auth\State $authState + */ + public function setAuthState(Auth\State $authState): void + { + $this->authState = $authState; + } + + /** * Auth testpage. * @@ -71,8 +88,8 @@ class ExampleAuth * Note that we don't actually validate the user in this example. This page * just serves to make the example work out of the box. */ - if (!$request->query->has('ReturnTo')) { - die('Missing ReturnTo parameter.'); + if ($request->get('ReturnTo') === null) { + throw new Error\Exception('Missing ReturnTo parameter.'); } $httpUtils = new Utils\HTTP(); @@ -88,7 +105,7 @@ class ExampleAuth * the exampleauth:External process. */ if (!preg_match('@State=(.*)@', $returnTo, $matches)) { - die('Invalid ReturnTo URL for this example.'); + throw new Error\Exception('Invalid ReturnTo URL for this example.'); } /** @@ -96,7 +113,7 @@ class ExampleAuth * match the parameter passed to saveState, so by now we know that we arrived here * through the exampleauth:External authentication page. */ - Auth\State::loadState(urldecode($matches[1]), 'exampleauth:External'); + $this->authState::loadState(urldecode($matches[1]), 'exampleauth:External'); // our list of users. $users = [ @@ -119,8 +136,8 @@ class ExampleAuth // time to handle login responses; since this is a dummy example, we accept any data $badUserPass = false; if ($request->getMethod() === 'POST') { - $username = $request->request->get('username'); - $password = $request->request->get('password'); + $username = $request->get('username'); + $password = $request->get('password'); if (!isset($users[$username]) || $users[$username]['password'] !== $password) { $badUserPass = true; @@ -137,7 +154,7 @@ class ExampleAuth $session->set('mail', $user['mail']); $session->set('type', $user['type']); - $httpUtils->redirectTrustedURL($returnTo); + return new RunnableResponse([$httpUtils, 'redirectTrustedURL'], [$returnTo]); } } @@ -145,7 +162,8 @@ class ExampleAuth $t = new Template($this->config, 'exampleauth:authenticate.twig'); $t->data['badUserPass'] = $badUserPass; $t->data['returnTo'] = $returnTo; - $t->send(); + + return $t; } @@ -161,12 +179,12 @@ class ExampleAuth /** * Request handler for redirect filter test. */ - if (!$request->has('StateId')) { + if ($request->get('StateId') === null) { throw new Error\BadRequest('Missing required StateId query parameter.'); } /** @var array $state */ - $state = Auth\State::loadState($request->get('StateId'), 'exampleauth:redirectfilter-test'); + $state = $this->authState::loadState($request->get('StateId'), 'exampleauth:redirectfilter-test'); $state['Attributes']['RedirectTest2'] = ['OK']; return new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]); diff --git a/tests/modules/exampleauth/lib/Controller/ExampleAuthTest.php b/tests/modules/exampleauth/lib/Controller/ExampleAuthTest.php new file mode 100644 index 0000000000000000000000000000000000000000..44860436d6143c4c72ef27b9355a284bb35fcde7 --- /dev/null +++ b/tests/modules/exampleauth/lib/Controller/ExampleAuthTest.php @@ -0,0 +1,245 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\exampleauth\Controller; + +use PHPUnit\Framework\TestCase; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; +use SimpleSAML\Error; +use SimpleSAML\HTTP\RunnableResponse; +use SimpleSAML\Module\exampleauth\Controller; +use SimpleSAML\Session; +use SimpleSAML\XHTML\Template; +use Symfony\Component\HttpFoundation\Request; + +/** + * Set of tests for the controllers in the "exampleauth" module. + * + * @covers \SimpleSAML\Module\exampleauth\Controller\ExampleAuth + */ +class ExampleAuthTest extends TestCase +{ + /** @var \SimpleSAML\Configuration */ + protected Configuration $config; + + /** @var \SimpleSAML\Session */ + protected Session $session; + + + /** + * Set up for each test. + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = Configuration::loadFromArray( + [ + 'module.enable' => ['exampleauth' => true], + ], + '[ARRAY]', + 'simplesaml' + ); + + $this->session = Session::getSessionFromRequest(); + + Configuration::setPreLoadedConfig($this->config, 'config.php'); + } + + + /** + * Test that accessing the authpage-endpoint without ReturnTo parameter throws an exception + * + * @return void + */ + public function testAuthpageNoReturnTo(): void + { + $request = Request::create( + '/authpage', + 'GET', + ['NoReturnTo' => 'Limbo'], + ); + + $c = new Controller\ExampleAuth($this->config, $this->session); + + $this->expectException(Error\Exception::class); + $this->expectExceptionMessage('Missing ReturnTo parameter.'); + + $c->authpage($request); + } + + + /** + * Test that accessing the authpage-endpoint without a valid ReturnTo parameter throws an exception + * + * @return void + */ + public function testAuthpageInvalidReturnTo(): void + { + $request = Request::create( + '/authpage', + 'GET', + ['ReturnTo' => 'SomeBogusValue'], + ); + + $c = new Controller\ExampleAuth($this->config, $this->session); + + $this->expectException(Error\Exception::class); + $this->expectExceptionMessage('Invalid ReturnTo URL for this example.'); + + $c->authpage($request); + } + + + /** + * Test that accessing the authpage-endpoint using GET-method show a login-screen + * + * @return void + */ + public function testAuthpageGetMethod(): void + { + $request = Request::create( + '/authpage', + 'GET', + ['ReturnTo' => 'State=/'], + ); + + $c = new Controller\ExampleAuth($this->config, $this->session); + $c->setAuthState(new class () extends Auth\State { + public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array + { + return []; + } + }); + + $response = $c->authpage($request); + $this->assertTrue($response->isSuccessful()); + $this->assertInstanceOf(Template::class, $response); + } + + + /** + * Test that accessing the authpage-endpoint using POST-method and using the correct password triggers a redirect + * + * @return void + */ + public function testAuthpagePostMethodCorrectPassword(): void + { + $this->markTestSkipped('Needs debugging'); + + $request = Request::create( + '/authpage', + 'POST', + ['ReturnTo' => 'State=/', 'username' => 'student', 'password' => 'student'], + ); + + $c = new Controller\ExampleAuth($this->config, $this->session); + $c->setAuthState(new class () extends Auth\State { + public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array + { + return []; + } + }); + + $response = $c->authpage($request); + $this->assertTrue($response->isSuccessful()); + $this->assertInstanceOf(RunnableResponse::class, $response); + } + + + /** + * Test that accessing the authpage-endpoint using POST-method and an incorrect password shows the login-screen again + * + * @return void + */ + public function testAuthpagePostMethodIncorrectPassword(): void + { + $request = Request::create( + '/authpage', + 'POST', + ['ReturnTo' => 'State=/', 'username' => 'user', 'password' => 'something stupid'], + ); + + $c = new Controller\ExampleAuth($this->config, $this->session); + $c->setAuthState(new class () extends Auth\State { + public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array + { + return []; + } + }); + + $response = $c->authpage($request); + $this->assertTrue($response->isSuccessful()); + $this->assertInstanceOf(Template::class, $response); + } + + + /** + * Test that accessing the resume-endpoint leads to a redirect + * + * @return void + */ + public function testResume(): void + { + $request = Request::create( + '/resume', + 'GET', + ); + + $c = new Controller\ExampleAuth($this->config, $this->session); + + $response = $c->resume($request); + $this->assertTrue($response->isSuccessful()); + $this->assertInstanceOf(RunnableResponse::class, $response); + } + + + /** + * Test that accessing the redirect-endpoint leads to a redirect + * + * @return void + */ + public function testRedirect(): void + { + $request = Request::create( + '/redirect', + 'GET', + ['StateId' => 'someState'] + ); + + $c = new Controller\ExampleAuth($this->config, $this->session); + $c->setAuthState(new class () extends Auth\State { + public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array + { + return []; + } + }); + + $response = $c->redirect($request); + $this->assertTrue($response->isSuccessful()); + $this->assertInstanceOf(RunnableResponse::class, $response); + } + + + /** + * Test that accessing the redirect-endpoint without StateId leads to an exception + * + * @return void + */ + public function testRedirectMissingStateId(): void + { + $request = Request::create( + '/redirect', + 'GET', + ); + + $c = new Controller\ExampleAuth($this->config, $this->session); + + $this->expectException(Error\BadRequest::class); + $this->expectExceptionMessage('Missing required StateId query parameter.'); + + $c->redirect($request); + } +}