Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSP-2060-Use_processing_chain #43

Merged
merged 10 commits into from
Nov 18, 2024
45 changes: 31 additions & 14 deletions public/login.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

declare(strict_types=1);

use SimpleSAML\Auth\Simple;
use SimpleSAML\Configuration;
use SimpleSAML\Locale\Language;
use SimpleSAML\Logger;
Expand All @@ -44,8 +45,8 @@

$forceAuthn = isset($_GET['renew']) && $_GET['renew'];
$isPassive = isset($_GET['gateway']) && $_GET['gateway'];
// Determine if client wants us to post or redirect the response. Default is redirect.
$redirect = !(isset($_GET['method']) && 'POST' === $_GET['method']);
// Determine if the client wants us to post or redirect the response. Default is redirect.
$redirect = !(isset($_GET['method']) && $_GET['method'] === 'POST');

$casconfig = Configuration::getConfig('module_casserver.php');
$serviceValidator = new ServiceValidator($casconfig);
Expand All @@ -66,9 +67,6 @@
}
}


$as = new \SimpleSAML\Auth\Simple($casconfig->getValue('authsource'));

if (array_key_exists('scope', $_GET) && is_string($_GET['scope'])) {
$scopes = $casconfig->getOptionalValue('scopes', []);

Expand All @@ -87,16 +85,34 @@
Language::setLanguageCookie($_GET['language']);
}

/** Initializations */

// AuthSource Simple
$as = new Simple($casconfig->getValue('authsource'));

// Ticket Store
$ticketStoreConfig = $casconfig->getOptionalValue('ticketstore', ['class' => 'casserver:FileSystemTicketStore']);
$ticketStoreClass = Module::resolveClass($ticketStoreConfig['class'], 'Cas\Ticket');
/** @var $ticketStore TicketStore */
/** @psalm-suppress InvalidStringClass */
$ticketStore = new $ticketStoreClass($casconfig);

$ticketFactoryClass = Module::resolveClass('casserver:TicketFactory', 'Cas\Ticket');
// Ticket Factory
$ticketFactoryClass = Module::resolveClass('casserver:TicketFactory', 'Cas\Factories');
/** @var $ticketFactory TicketFactory */
/** @psalm-suppress InvalidStringClass */
$ticketFactory = new $ticketFactoryClass($casconfig);

// Processing Chain Factory
$processingChaingFactoryClass = Module::resolveClass('casserver:ProcessingChainFactory', 'Cas\Factories');
pradtke marked this conversation as resolved.
Show resolved Hide resolved
/** @var $processingChainFactory ProcessingChainFactory */
/** @psalm-suppress InvalidStringClass */
$processingChainFactory = new $processingChaingFactoryClass($casconfig);

// Attribute Extractor
$attributeExtractor = new AttributeExtractor($casconfig, $processingChainFactory);

// HTTP Utils
$httpUtils = new Utils\HTTP();
$session = Session::getSessionFromRequest();

Expand Down Expand Up @@ -177,19 +193,16 @@

if (isset($oldLanguagePreferred)) {
$parameters['language'] = $oldLanguagePreferred;
} else {
if (is_string($_GET['language'])) {
$parameters['language'] = $_GET['language'];
}
} elseif (is_string($_GET['language'])) {
$parameters['language'] = $_GET['language'];
}
}

if (isset($serviceUrl)) {
$defaultTicketName = isset($_GET['service']) ? 'ticket' : 'SAMLart';
$ticketName = $casconfig->getOptionalValue('ticketName', $defaultTicketName);

$attributeExtractor = new AttributeExtractor();
$mappedAttributes = $attributeExtractor->extractUserAndAttributes($as->getAttributes(), $casconfig);
$mappedAttributes = $attributeExtractor->extractUserAndAttributes($as->getAttributes());
ioigoume marked this conversation as resolved.
Show resolved Hide resolved

$serviceTicket = $ticketFactory->createServiceTicket([
'service' => $serviceUrl,
Expand All @@ -207,7 +220,7 @@
$validDebugModes = ['true', 'samlValidate'];
if (
array_key_exists('debugMode', $_GET) &&
in_array($_GET['debugMode'], $validDebugModes) &&
in_array($_GET['debugMode'], $validDebugModes, true) &&
$casconfig->getOptionalBoolean('debugMode', false)
) {
if ($_GET['debugMode'] === 'samlValidate') {
Expand All @@ -229,7 +242,11 @@
} elseif ($redirect) {
$httpUtils->redirectTrustedURL($httpUtils->addURLParameters($serviceUrl, $parameters));
} else {
$httpUtils->submitPOSTData($serviceUrl, $parameters);
try {
$httpUtils->submitPOSTData($serviceUrl, $parameters);
} catch (\SimpleSAML\Error\Exception $e) {

}
}
} else {
$httpUtils->redirectTrustedURL(
Expand Down
2 changes: 1 addition & 1 deletion public/proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
/** @psalm-suppress InvalidStringClass */
$ticketStore = new $ticketStoreClass($casconfig);

$ticketFactoryClass = \SimpleSAML\Module::resolveClass('casserver:TicketFactory', 'Cas\Ticket');
$ticketFactoryClass = \SimpleSAML\Module::resolveClass('casserver:TicketFactory', 'Cas\Factories');
/** @psalm-suppress InvalidStringClass */
$ticketFactory = new $ticketFactoryClass($casconfig);

Expand Down
2 changes: 1 addition & 1 deletion public/utility/validateTicket.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
/** @psalm-suppress InvalidStringClass */
$ticketStore = new $ticketStoreClass($casconfig);

$ticketFactoryClass = Module::resolveClass('casserver:TicketFactory', 'Cas\Ticket');
$ticketFactoryClass = Module::resolveClass('casserver:TicketFactory', 'Cas\Factories');
/** @var TicketFactory $ticketFactory */
/** @psalm-suppress InvalidStringClass */
$ticketFactory = new $ticketFactoryClass($casconfig);
Expand Down
2 changes: 1 addition & 1 deletion public/validate.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
/** @psalm-suppress InvalidStringClass */
$ticketStore = new $ticketStoreClass($casconfig);

$ticketFactoryClass = \SimpleSAML\Module::resolveClass('casserver:TicketFactory', 'Cas\Ticket');
$ticketFactoryClass = \SimpleSAML\Module::resolveClass('casserver:TicketFactory', 'Cas\Factories');
/** @psalm-suppress InvalidStringClass */
$ticketFactory = new $ticketFactoryClass($casconfig);

Expand Down
125 changes: 92 additions & 33 deletions src/Cas/AttributeExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,32 @@
namespace SimpleSAML\Module\casserver\Cas;

use SimpleSAML\Auth;
use SimpleSAML\Auth\ProcessingChain;
use SimpleSAML\Configuration;
use SimpleSAML\Error\Exception;
use SimpleSAML\Error\NoState;
use SimpleSAML\Module;
use SimpleSAML\Module\casserver\Cas\Factories\ProcessingChainFactory;

/**
* Extract the user and any mapped attributes from the AuthSource attributes
*/
class AttributeExtractor
{
/** @var Configuration */
private readonly Configuration $casconfig;

/** @var ProcessingChainFactory */
private readonly ProcessingChainFactory $processingChainFactory;

public function __construct(
Configuration $casconfig,
ProcessingChainFactory $processingChainFactory
) {
$this->casconfig = $casconfig;
$this->processingChainFactory = $processingChainFactory;
}

/**
* Determine the user and any CAS attributes based on the attributes from the
* authsource and the CAS configuration.
Expand All @@ -24,32 +42,34 @@ class AttributeExtractor
* // any attributes
* ]
*
* If no CAS attributes are configured then the attributes array is empty
* @param array $attributes
* @param \SimpleSAML\Configuration $casconfig
* If no CAS attributes are configured, then the attributes' array is empty
*
* @param array $attributes
*
* @return array
* @throws \Exception
*/
public function extractUserAndAttributes(array $attributes, Configuration $casconfig): array
public function extractUserAndAttributes(array $attributes): array
{
if ($casconfig->hasValue('authproc')) {
$attributes = $this->invokeAuthProc($attributes, $casconfig);
if ($this->casconfig->hasValue('authproc')) {
$attributes = $this->runAuthProcs($attributes);
}

$casUsernameAttribute = $casconfig->getOptionalValue('attrname', 'eduPersonPrincipalName');
$casUsernameAttribute = $this->casconfig->getOptionalValue('attrname', 'eduPersonPrincipalName');

$userName = $attributes[$casUsernameAttribute][0];
if (empty($userName)) {
throw new \Exception("No cas user defined for attribute $casUsernameAttribute");
}

if ($casconfig->getOptionalValue('attributes', true)) {
$attributesToTransfer = $casconfig->getOptionalValue('attributes_to_transfer', []);
if ($this->casconfig->getOptionalValue('attributes', true)) {
$attributesToTransfer = $this->casconfig->getOptionalValue('attributes_to_transfer', []);

if (sizeof($attributesToTransfer) > 0) {
$casAttributes = [];

foreach ($attributesToTransfer as $key) {
if (array_key_exists($key, $attributes)) {
if (\array_key_exists($key, $attributes)) {
$casAttributes[$key] = $attributes[$key];
}
}
Expand All @@ -66,35 +86,74 @@ public function extractUserAndAttributes(array $attributes, Configuration $casco
];
}

/**
* Run authproc filters with the processing chain
* Creating the ProcessingChain require metadata.
* - For the idp metadata use the OIDC issuer as the entityId (and the authprocs from the main config file)
* - For the sp metadata use the client id as the entityId (and don’t set authprocs).
*
* @param array $state
*
* @return void
* @throws Exception
* @throws Error\UnserializableException
* @throws \Exception
*/
protected function runAuthProcs(array &$state): void
{
$filters = $this->casconfig->getOptionalArray('authproc', []);
$idpMetadata = [
'entityid' => $state['Source']['entityid'] ?? '',
// ProcessChain needs to know the list of authproc filters we defined in module_oidc configuration
'authproc' => $filters,
];
$spMetadata = [
'entityid' => $state['Destination']['entityid'] ?? '',
];

$state['ReturnURL'] = Module::getModuleURL('casserver/login.php');
$state['Destination'] = $spMetadata;
$state['Source'] = $idpMetadata;

$this->processingChainFactory->build($state)->processState($state);
}

/**
* Process any authproc filters defined in the configuration. The Authproc filters must only
* rely on 'Attributes' being available and not on additional SAML state.
* @see \SimpleSAML_Auth_ProcessingChain::parseFilter() For the original, SAML side implementation
* @param array $attributes The current attributes
* @param \SimpleSAML\Configuration $casconfig The cas configuration
* @return array The attributes post processing.
* This is a wrapper around Auth/State::loadState that facilitates testing by
* hiding the static method
*
* @param array $queryParameters
*
* @return array|null
* @throws NoState
*/
private function invokeAuthProc(array $attributes, Configuration $casconfig): array
public function manageState(array $queryParameters): ?array
{
$filters = $casconfig->getOptionalArray('authproc', []);
if (empty($queryParameters[ProcessingChain::AUTHPARAM])) {
throw new NoState();
}

$state = [
'Attributes' => $attributes,
];
foreach ($filters as $config) {
$className = Module::resolveClass(
$config['class'],
'Auth\Process',
Auth\ProcessingFilter::class,
);
// Unset 'class' to prevent the filter from interpreting it as an option
unset($config['class']);
/** @psalm-suppress InvalidStringClass */
$filter = new $className($config, null);
$filter->process($state);
$stateId = (string)$queryParameters[ProcessingChain::AUTHPARAM];
$state = $this->loadState($stateId, ProcessingChain::COMPLETED_STAGE);

if (!empty($state['authSourceId'])) {
$this->authSourceId = (string)$state['authSourceId'];
unset($state['authSourceId']);
}

return $state['Attributes'];
return $state;
}

/**
* @param string $id
* @param string $stage
* @param bool $allowMissing
*
* @return array|null
* @throws \SimpleSAML\Error\NoState
*/
public function loadState(string $id, string $stage, bool $allowMissing = false): ?array
{
return $this->authState::loadState($id, $stage, $allowMissing);
}
}
49 changes: 49 additions & 0 deletions src/Cas/Factories/ProcessingChainFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

/*
* This file is part of the simplesamlphp-module-casserver.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace SimpleSAML\Module\casserver\Cas\Factories;

use SimpleSAML\Auth\ProcessingChain;
use SimpleSAML\Configuration;

class ProcessingChainFactory
{
/** @var Configuration */
private readonly Configuration $casconfig;

public function __construct(
Configuration $casconfig,
) {
$this->casconfig = $casconfig;
}

/**
* @codeCoverageIgnore
* @throws \Exception
*/
public function build(array $state): ProcessingChain
{
$idpMetadata = [
ioigoume marked this conversation as resolved.
Show resolved Hide resolved
'entityid' => $state['Source']['entityid'] ?? '',
// ProcessChain needs to know the list of authproc filters we defined in casserver configuration
'authproc' => $this->casconfig->getOptionalArray('authproc', []),
];
$spMetadata = [
'entityid' => $state['Destination']['entityid'] ?? '',
];

return new ProcessingChain(
$idpMetadata,
$spMetadata,
'casserver',
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

declare(strict_types=1);

namespace SimpleSAML\Module\casserver\Cas\Ticket;
namespace SimpleSAML\Module\casserver\Cas\Factories;

use SimpleSAML\Configuration;
use SimpleSAML\XML\Utils\Random;
Expand Down
Loading