Skip to content
Snippets Groups Projects
Unverified Commit bd7d352d authored by Tim van Dijen's avatar Tim van Dijen Committed by GitHub
Browse files

Merge pull request #1502 from simplesamlphp/exampleauth-controllers

Exampleauth controllers
parents 31a49a8d a1af994c
No related branches found
No related tags found
No related merge requests found
...@@ -13,7 +13,7 @@ use SimpleSAML\Utils; ...@@ -13,7 +13,7 @@ use SimpleSAML\Utils;
* A simple processing filter for testing that redirection works as it should. * A simple processing filter for testing that redirection works as it should.
* *
*/ */
class RedirectTest extends \SimpleSAML\Auth\ProcessingFilter class RedirectTest extends Auth\ProcessingFilter
{ {
/** /**
* Initialize processing of the redirect test. * Initialize processing of the redirect test.
...@@ -29,7 +29,7 @@ class RedirectTest extends \SimpleSAML\Auth\ProcessingFilter ...@@ -29,7 +29,7 @@ class RedirectTest extends \SimpleSAML\Auth\ProcessingFilter
// Save state and redirect // Save state and redirect
$id = Auth\State::saveState($state, 'exampleauth:redirectfilter-test'); $id = Auth\State::saveState($state, 'exampleauth:redirectfilter-test');
$url = Module::getModuleURL('exampleauth/redirecttest.php'); $url = Module::getModuleURL('exampleauth/redirecttest');
$httpUtils = new Utils\HTTP(); $httpUtils = new Utils\HTTP();
$httpUtils->redirectTrustedURL($url, ['StateId' => $id]); $httpUtils->redirectTrustedURL($url, ['StateId' => $id]);
......
...@@ -9,6 +9,7 @@ use SimpleSAML\Auth; ...@@ -9,6 +9,7 @@ use SimpleSAML\Auth;
use SimpleSAML\Error; use SimpleSAML\Error;
use SimpleSAML\Module; use SimpleSAML\Module;
use SimpleSAML\Utils; use SimpleSAML\Utils;
use Symfony\Component\HttpFoundation\Session\Session as SymfonySession;
/** /**
* Example external authentication source. * Example external authentication source.
...@@ -19,9 +20,8 @@ use SimpleSAML\Utils; ...@@ -19,9 +20,8 @@ use SimpleSAML\Utils;
* To adapt this to your own web site, you should: * To adapt this to your own web site, you should:
* 1. Create your own module directory. * 1. Create your own module directory.
* 2. Enable to module in the config by adding '<module-dir>' => true to the $config['module.enable'] array. * 2. Enable to module in the config by adding '<module-dir>' => true to the $config['module.enable'] array.
* 3. Copy this file and modules/exampleauth/www/resume.php to their corresponding * 3. Copy this file to its corresponding location in the new module.
* location in the new module. * 4. Replace all occurrences of "exampleauth" in this file with the name of your module.
* 4. Replace all occurrences of "exampleauth" in this file and in resume.php with the name of your module.
* 5. Adapt the getUser()-function, the authenticate()-function and the logout()-function to your site. * 5. Adapt the getUser()-function, the authenticate()-function and the logout()-function to your site.
* 6. Add an entry in config/authsources.php referencing your module. E.g.: * 6. Add an entry in config/authsources.php referencing your module. E.g.:
* 'myauth' => [ * 'myauth' => [
...@@ -65,13 +65,12 @@ class External extends Auth\Source ...@@ -65,13 +65,12 @@ class External extends Auth\Source
* stored in the users PHP session, but this could be replaced * stored in the users PHP session, but this could be replaced
* with anything. * with anything.
*/ */
$session = new SymfonySession();
if (!session_id()) { if (!$session->getId()) {
// session_start not called before. Do it here $session->start();
session_start();
} }
if (!isset($_SESSION['uid'])) { if (!$session->has('uid')) {
// The user isn't authenticated // The user isn't authenticated
return null; return null;
} }
...@@ -81,16 +80,15 @@ class External extends Auth\Source ...@@ -81,16 +80,15 @@ class External extends Auth\Source
* Note that all attributes in SimpleSAMLphp are multivalued, so we need * Note that all attributes in SimpleSAMLphp are multivalued, so we need
* to store them as arrays. * to store them as arrays.
*/ */
$attributes = [ $attributes = [
'uid' => [$_SESSION['uid']], 'uid' => [$session->get('uid')],
'displayName' => [$_SESSION['name']], 'displayName' => [$session->get('name')],
'mail' => [$_SESSION['mail']], 'mail' => [$session->get('mail')],
]; ];
// Here we generate a multivalued attribute based on the account type // Here we generate a multivalued attribute based on the account type
$attributes['eduPersonAffiliation'] = [ $attributes['eduPersonAffiliation'] = [
$_SESSION['type'], /* In this example, either 'student' or 'employee'. */ $session->get('type'), /* In this example, either 'student' or 'employee'. */
'member', 'member',
]; ];
...@@ -148,7 +146,7 @@ class External extends Auth\Source ...@@ -148,7 +146,7 @@ class External extends Auth\Source
* We assume that whatever authentication page we send the user to has an * We assume that whatever authentication page we send the user to has an
* option to return the user to a specific page afterwards. * option to return the user to a specific page afterwards.
*/ */
$returnTo = Module::getModuleURL('exampleauth/resume.php', [ $returnTo = Module::getModuleURL('exampleauth/resume', [
'State' => $stateId, 'State' => $stateId,
]); ]);
...@@ -159,7 +157,7 @@ class External extends Auth\Source ...@@ -159,7 +157,7 @@ class External extends Auth\Source
* is also part of this module, but in a real example, this would likely be * is also part of this module, but in a real example, this would likely be
* the absolute URL of the login page for the site. * the absolute URL of the login page for the site.
*/ */
$authPage = Module::getModuleURL('exampleauth/authpage.php'); $authPage = Module::getModuleURL('exampleauth/authpage');
/* /*
* The redirect to the authentication page. * The redirect to the authentication page.
...@@ -185,16 +183,18 @@ class External extends Auth\Source ...@@ -185,16 +183,18 @@ class External extends Auth\Source
* This function resumes the authentication process after the user has * This function resumes the authentication process after the user has
* entered his or her credentials. * entered his or her credentials.
* *
* @param \Symfony\Component\HttpFoundation\Request $request
*
* @throws \SimpleSAML\Error\BadRequest * @throws \SimpleSAML\Error\BadRequest
* @throws \SimpleSAML\Error\Exception * @throws \SimpleSAML\Error\Exception
*/ */
public static function resume(): void public static function resume(Request $request): void
{ {
/* /*
* First we need to restore the $state-array. We should have the identifier for * First we need to restore the $state-array. We should have the identifier for
* it in the 'State' request parameter. * it in the 'State' request parameter.
*/ */
if (!isset($_REQUEST['State'])) { if (!$request->has('State')) {
throw new Error\BadRequest('Missing "State" parameter.'); throw new Error\BadRequest('Missing "State" parameter.');
} }
...@@ -203,7 +203,7 @@ class External extends Auth\Source ...@@ -203,7 +203,7 @@ class External extends Auth\Source
* match the string we used in the saveState-call above. * match the string we used in the saveState-call above.
*/ */
/** @var array $state */ /** @var array $state */
$state = Auth\State::loadState($_REQUEST['State'], 'exampleauth:External'); $state = Auth\State::loadState($request->get('State'), 'exampleauth:External');
/* /*
* Now we have the $state-array, and can use it to locate the authentication * Now we have the $state-array, and can use it to locate the authentication
...@@ -266,15 +266,12 @@ class External extends Auth\Source ...@@ -266,15 +266,12 @@ class External extends Auth\Source
*/ */
public function logout(array &$state): void public function logout(array &$state): void
{ {
if (!session_id()) { $session = new SymfonySession();
// session_start not called before. Do it here if (!$session->getId()) {
session_start(); $session->start();
} }
/* $session->clear();
* In this example we simply remove the 'uid' from the session.
*/
unset($_SESSION['uid']);
/* /*
* If we need to do a redirect to a different page, we could do this * If we need to do a redirect to a different page, we could do this
......
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\exampleauth\Controller;
use SimpleSAML\Auth;
use SimpleSAML\Configuration;
use SimpleSAML\Error;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\Module\exampleauth\Auth\Source\External;
use SimpleSAML\Session;
use SimpleSAML\Utils;
use SimpleSAML\XHTML\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session as SymfonySession;
use function array_key_exists;
use function preg_match;
use function session_id;
use function session_start;
use function urldecode;
/**
* Controller class for the exampleauth module.
*
* This class serves the different views available in the module.
*
* @package simplesamlphp/simplesamlphp
*/
class ExampleAuth
{
/** @var \SimpleSAML\Configuration */
protected Configuration $config;
/** @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.
*
* It initializes the global configuration and session for the controllers implemented here.
*
* @param \SimpleSAML\Configuration $config The configuration to use by the controllers.
* @param \SimpleSAML\Session $session The session to use by the controllers.
*
* @throws \Exception
*/
public function __construct(
Configuration $config,
Session $session
) {
$this->config = $config;
$this->session = $session;
}
/**
* Inject the \SimpleSAML\Auth\State dependency.
*
* @param \SimpleSAML\Auth\State $authState
*/
public function setAuthState(Auth\State $authState): void
{
$this->authState = $authState;
}
/**
* Auth testpage.
*
* @param \Symfony\Component\HttpFoundation\Request $request The current request.
*
* @return \SimpleSAML\XHTML\Template|\SimpleSAML\HTTP\RunnableResponse
*/
public function authpage(Request $request)
{
/**
* This page serves as a dummy login page.
*
* 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.
*/
$returnTo = $request->get('ReturnTo');
if ($returnTo === null) {
throw new Error\Exception('Missing ReturnTo parameter.');
}
$httpUtils = new Utils\HTTP();
$returnTo = $httpUtils->checkURLAllowed($returnTo);
/**
* The following piece of code would never be found in a real authentication page. Its
* purpose in this example is to make this example safer in the case where the
* administrator of the IdP leaves the exampleauth-module enabled in a production
* environment.
*
* What we do here is to extract the $state-array identifier, and check that it belongs to
* the exampleauth:External process.
*/
if (!preg_match('@State=(.*)@', $returnTo, $matches)) {
throw new Error\Exception('Invalid ReturnTo URL for this example.');
}
/**
* The loadState-function will not return if the second parameter does not
* match the parameter passed to saveState, so by now we know that we arrived here
* through the exampleauth:External authentication page.
*/
$this->authState::loadState(urldecode($matches[1]), 'exampleauth:External');
// our list of users.
$users = [
'student' => [
'password' => 'student',
'uid' => 'student',
'name' => 'Student Name',
'mail' => 'somestudent@example.org',
'type' => 'student',
],
'admin' => [
'password' => 'admin',
'uid' => 'admin',
'name' => 'Admin Name',
'mail' => 'someadmin@example.org',
'type' => 'employee',
],
];
// time to handle login responses; since this is a dummy example, we accept any data
$badUserPass = false;
if ($request->getMethod() === 'POST') {
$username = $request->get('username');
$password = $request->get('password');
if (!isset($users[$username]) || $users[$username]['password'] !== $password) {
$badUserPass = true;
} else {
$user = $users[$username];
$session = new SymfonySession();
if (!$session->getId()) {
$session->start();
}
$session->set('uid', $user['uid']);
$session->set('name', $user['name']);
$session->set('mail', $user['mail']);
$session->set('type', $user['type']);
return new RunnableResponse([$httpUtils, 'redirectTrustedURL'], [$returnTo]);
}
}
// if we get this far, we need to show the login page to the user
$t = new Template($this->config, 'exampleauth:authenticate.twig');
$t->data['badUserPass'] = $badUserPass;
$t->data['returnTo'] = $returnTo;
return $t;
}
/**
* Redirect testpage.
*
* @param \Symfony\Component\HttpFoundation\Request $request The current request.
*
* @return \SimpleSAML\HTTP\RunnableResponse
*/
public function redirecttest(Request $request): RunnableResponse
{
/**
* Request handler for redirect filter test.
*/
$stateId = $request->get('StateId');
if ($stateId === null) {
throw new Error\BadRequest('Missing required StateId query parameter.');
}
/** @var array $state */
$state = $this->authState::loadState($stateId, 'exampleauth:redirectfilter-test');
$state['Attributes']['RedirectTest2'] = ['OK'];
return new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]);
}
/**
* Resume testpage.
*
* @param \Symfony\Component\HttpFoundation\Request $request The current request.
*
* @return \SimpleSAML\HTTP\RunnableResponse
*/
public function resume(Request $request): RunnableResponse
{
/**
* This page serves as the point where the user's authentication
* process is resumed after the login page.
*
* It simply passes control back to the class.
*/
return new RunnableResponse([External::class, 'resume'], [$request]);
}
}
exampleauth-authpage:
path: /authpage
defaults: { _controller: 'SimpleSAML\Module\exampleauth\Controller\ExampleAuth::authpage' }
exampleauth-redirecttest:
path: /redirecttest
defaults: { _controller: 'SimpleSAML\Module\exampleauth\Controller\ExampleAuth::redirecttest' }
exampleauth-resume:
path: /resume
defaults: { _controller: 'SimpleSAML\Module\exampleauth\Controller\ExampleAuth::resume' }
<?php
/**
* This page serves as a dummy login page.
*
* 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.
*
* @package SimpleSAMLphp
*/
if (!isset($_REQUEST['ReturnTo'])) {
die('Missing ReturnTo parameter.');
}
$httpUtils = new \SimpleSAML\Utils\HTTP();
$returnTo = $httpUtils->checkURLAllowed($_REQUEST['ReturnTo']);
/**
* The following piece of code would never be found in a real authentication page. Its
* purpose in this example is to make this example safer in the case where the
* administrator of the IdP leaves the exampleauth-module enabled in a production
* environment.
*
* What we do here is to extract the $state-array identifier, and check that it belongs to
* the exampleauth:External process.
*/
if (!preg_match('@State=(.*)@', $returnTo, $matches)) {
die('Invalid ReturnTo URL for this example.');
}
/**
* The loadState-function will not return if the second parameter does not
* match the parameter passed to saveState, so by now we know that we arrived here
* through the exampleauth:External authentication page.
*/
\SimpleSAML\Auth\State::loadState(urldecode($matches[1]), 'exampleauth:External');
// our list of users.
$users = [
'student' => [
'password' => 'student',
'uid' => 'student',
'name' => 'Student Name',
'mail' => 'somestudent@example.org',
'type' => 'student',
],
'admin' => [
'password' => 'admin',
'uid' => 'admin',
'name' => 'Admin Name',
'mail' => 'someadmin@example.org',
'type' => 'employee',
],
];
// time to handle login responses; since this is a dummy example, we accept any data
$badUserPass = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = (string) $_REQUEST['username'];
$password = (string) $_REQUEST['password'];
if (!isset($users[$username]) || $users[$username]['password'] !== $password) {
$badUserPass = true;
} else {
$user = $users[$username];
if (!session_id()) {
// session_start not called before. Do it here.
session_start();
}
$_SESSION['uid'] = $user['uid'];
$_SESSION['name'] = $user['name'];
$_SESSION['mail'] = $user['mail'];
$_SESSION['type'] = $user['type'];
$httpUtils->redirectTrustedURL($returnTo);
}
}
// if we get this far, we need to show the login page to the user
$config = \SimpleSAML\Configuration::getInstance();
$t = new \SimpleSAML\XHTML\Template($config, 'exampleauth:authenticate.twig');
$t->data['badUserPass'] = $badUserPass;
$t->data['returnTo'] = $returnTo;
$t->send();
<?php
/**
* Request handler for redirect filter test.
*
* @package SimpleSAMLphp
*/
if (!array_key_exists('StateId', $_REQUEST)) {
throw new \SimpleSAML\Error\BadRequest('Missing required StateId query parameter.');
}
/** @var array $state */
$state = \SimpleSAML\Auth\State::loadState($_REQUEST['StateId'], 'exampleauth:redirectfilter-test');
$state['Attributes']['RedirectTest2'] = ['OK'];
\SimpleSAML\Auth\ProcessingChain::resumeProcessing($state);
<?php
/**
* This page serves as the point where the user's authentication
* process is resumed after the login page.
*
* It simply passes control back to the class.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Module\exampleauth\Auth\Source;
External::resume();
...@@ -62,7 +62,7 @@ class SQLPersistentNameID extends BaseNameIDGenerator ...@@ -62,7 +62,7 @@ class SQLPersistentNameID extends BaseNameIDGenerator
* *
* @throws \SimpleSAML\Error\Exception If the 'attribute' option is not specified. * @throws \SimpleSAML\Error\Exception If the 'attribute' option is not specified.
*/ */
public function __construct(array $config, $reserved) public function __construct(array &$config, $reserved)
{ {
parent::__construct($config, $reserved); parent::__construct($config, $reserved);
......
<?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 redirecttest-endpoint leads to a redirect
*
* @return void
*/
public function testRedirect(): void
{
$request = Request::create(
'/redirecttest',
'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->redirecttest($request);
$this->assertTrue($response->isSuccessful());
$this->assertInstanceOf(RunnableResponse::class, $response);
}
/**
* Test that accessing the redirecttest-endpoint without StateId leads to an exception
*
* @return void
*/
public function testRedirectMissingStateId(): void
{
$request = Request::create(
'/redirecttest',
'GET',
);
$c = new Controller\ExampleAuth($this->config, $this->session);
$this->expectException(Error\BadRequest::class);
$this->expectExceptionMessage('Missing required StateId query parameter.');
$c->redirecttest($request);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment