Viel neues

This commit is contained in:
Sven Steinert
2026-04-30 12:06:00 +02:00
parent 118809bfae
commit fce31ebcd7
1274 changed files with 181255 additions and 0 deletions

25
qa-tool/htdocs/vendor/autoload.php vendored Normal file
View File

@@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInite830279ee99617ca9ab69fa346d50d57::getLoader();

View File

@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
qa-tool/htdocs/vendor/composer/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,23 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
'DateInvalidTimeZoneException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php',
'DateMalformedIntervalStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php',
'DateMalformedPeriodStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php',
'DateMalformedStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php',
'DateObjectError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php',
'DateRangeError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php',
'Jumbojett\\OpenIDConnectClient' => $vendorDir . '/jumbojett/openid-connect-php/src/OpenIDConnectClient.php',
'Jumbojett\\OpenIDConnectClientException' => $vendorDir . '/jumbojett/openid-connect-php/src/OpenIDConnectClient.php',
'Override' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/Override.php',
'SQLite3Exception' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
);

View File

@@ -0,0 +1,12 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php',
);

View File

@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@@ -0,0 +1,17 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'phpseclib3\\' => array($vendorDir . '/phpseclib/phpseclib/phpseclib'),
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
'RobRichards\\XMLSecLibs\\' => array($vendorDir . '/robrichards/xmlseclibs/src'),
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
'LightSaml\\' => array($vendorDir . '/litesaml/lightsaml/src'),
);

View File

@@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInite830279ee99617ca9ab69fa346d50d57
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInite830279ee99617ca9ab69fa346d50d57', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInite830279ee99617ca9ab69fa346d50d57', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInite830279ee99617ca9ab69fa346d50d57::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInite830279ee99617ca9ab69fa346d50d57::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}

View File

@@ -0,0 +1,102 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInite830279ee99617ca9ab69fa346d50d57
{
public static $files = array (
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
'decc78cc4436b1292c6c0d151b19445c' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/bootstrap.php',
);
public static $prefixLengthsPsr4 = array (
'p' =>
array (
'phpseclib3\\' => 11,
),
'S' =>
array (
'Symfony\\Polyfill\\Php83\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Component\\HttpFoundation\\' => 33,
),
'R' =>
array (
'RobRichards\\XMLSecLibs\\' => 23,
),
'P' =>
array (
'Psr\\EventDispatcher\\' => 20,
'ParagonIE\\ConstantTime\\' => 23,
),
'L' =>
array (
'LightSaml\\' => 10,
),
);
public static $prefixDirsPsr4 = array (
'phpseclib3\\' =>
array (
0 => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib',
),
'Symfony\\Polyfill\\Php83\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php83',
),
'Symfony\\Polyfill\\Mbstring\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),
'Symfony\\Component\\HttpFoundation\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/http-foundation',
),
'RobRichards\\XMLSecLibs\\' =>
array (
0 => __DIR__ . '/..' . '/robrichards/xmlseclibs/src',
),
'Psr\\EventDispatcher\\' =>
array (
0 => __DIR__ . '/..' . '/psr/event-dispatcher/src',
),
'ParagonIE\\ConstantTime\\' =>
array (
0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src',
),
'LightSaml\\' =>
array (
0 => __DIR__ . '/..' . '/litesaml/lightsaml/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
'DateInvalidOperationException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
'DateInvalidTimeZoneException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php',
'DateMalformedIntervalStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php',
'DateMalformedPeriodStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php',
'DateMalformedStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php',
'DateObjectError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php',
'DateRangeError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php',
'Jumbojett\\OpenIDConnectClient' => __DIR__ . '/..' . '/jumbojett/openid-connect-php/src/OpenIDConnectClient.php',
'Jumbojett\\OpenIDConnectClientException' => __DIR__ . '/..' . '/jumbojett/openid-connect-php/src/OpenIDConnectClient.php',
'Override' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/Override.php',
'SQLite3Exception' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInite830279ee99617ca9ab69fa346d50d57::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInite830279ee99617ca9ab69fa346d50d57::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInite830279ee99617ca9ab69fa346d50d57::$classMap;
}, null, ClassLoader::class);
}
}

View File

@@ -0,0 +1,698 @@
{
"packages": [
{
"name": "jumbojett/openid-connect-php",
"version": "v1.0.0",
"version_normalized": "1.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/jumbojett/OpenID-Connect-PHP.git",
"reference": "4af1d11497ec765dccddf928c14c04535ca96017"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jumbojett/OpenID-Connect-PHP/zipball/4af1d11497ec765dccddf928c14c04535ca96017",
"reference": "4af1d11497ec765dccddf928c14c04535ca96017",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.0",
"phpseclib/phpseclib": "~3.0"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
"yoast/phpunit-polyfills": "^1.0"
},
"time": "2023-12-13T09:52:12+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Bare-bones OpenID Connect client",
"support": {
"issues": "https://github.com/jumbojett/OpenID-Connect-PHP/issues",
"source": "https://github.com/jumbojett/OpenID-Connect-PHP/tree/v1.0.0"
},
"install-path": "../jumbojett/openid-connect-php"
},
{
"name": "litesaml/lightsaml",
"version": "v4.2.0",
"version_normalized": "4.2.0.0",
"source": {
"type": "git",
"url": "https://github.com/litesaml/lightsaml.git",
"reference": "da88de24e699418918a128f0142dd09e2c41dd38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/litesaml/lightsaml/zipball/da88de24e699418918a128f0142dd09e2c41dd38",
"reference": "da88de24e699418918a128f0142dd09e2c41dd38",
"shasum": ""
},
"require": {
"php": ">=7.4",
"psr/event-dispatcher": "^1.0",
"robrichards/xmlseclibs": "~2.0|~3.0|~4.0",
"symfony/http-foundation": "~5.0|~6.0|~7.0"
},
"require-dev": {
"litesaml/schemas": "~1.0.0",
"marcocesarato/php-conventional-changelog": "^1.15",
"monolog/monolog": "^2.0|^3.0",
"phpstan/phpstan": "^1.8",
"phpunit/phpunit": "~8.4|~9.5",
"pimple/pimple": "~3.0",
"squizlabs/php_codesniffer": "^3.6",
"symfony/css-selector": "~5.0|~6.0|~7.0",
"symfony/dom-crawler": "~5.0|~6.0|~7.0"
},
"time": "2024-02-08T11:59:00+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"LightSaml\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "William",
"email": "work@suppo.fr"
},
{
"name": "Milos Tomic",
"email": "tmilos@gmail.com",
"homepage": "https://github.com/tmilos/",
"role": "Developer"
}
],
"description": "SAML 2.0 PHP library",
"keywords": [
"SAML 2.0",
"Single Logout",
"Single SignOn",
"library",
"lightSAML",
"php"
],
"support": {
"docs": "https://docs.litesaml.com",
"issues": "https://github.com/litesaml/lightsaml/issues",
"source": "https://github.com/litesaml/lightsaml"
},
"install-path": "../litesaml/lightsaml"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v3.0.0",
"version_normalized": "3.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "df1e7fde177501eee2037dd159cf04f5f301a512"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512",
"reference": "df1e7fde177501eee2037dd159cf04f5f301a512",
"shasum": ""
},
"require": {
"php": "^8"
},
"require-dev": {
"phpunit/phpunit": "^9",
"vimeo/psalm": "^4|^5"
},
"time": "2024-05-08T12:36:18+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"ParagonIE\\ConstantTime\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
"keywords": [
"base16",
"base32",
"base32_decode",
"base32_encode",
"base64",
"base64_decode",
"base64_encode",
"bin2hex",
"encoding",
"hex",
"hex2bin",
"rfc4648"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"install-path": "../paragonie/constant_time_encoding"
},
{
"name": "paragonie/random_compat",
"version": "v9.99.100",
"version_normalized": "9.99.100.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
"shasum": ""
},
"require": {
"php": ">= 7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"time": "2020-10-15T08:29:30+00:00",
"type": "library",
"installation-source": "dist",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/random_compat/issues",
"source": "https://github.com/paragonie/random_compat"
},
"install-path": "../paragonie/random_compat"
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.41",
"version_normalized": "3.0.41.0",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "621c73f7dcb310b61de34d1da4c4204e8ace6ceb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/621c73f7dcb310b61de34d1da4c4204e8ace6ceb",
"reference": "621c73f7dcb310b61de34d1da4c4204e8ace6ceb",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "^1|^2|^3",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": ">=5.6.1"
},
"require-dev": {
"phpunit/phpunit": "*"
},
"suggest": {
"ext-dom": "Install the DOM extension to load XML formatted public keys.",
"ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
"ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
"ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
"ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
},
"time": "2024-08-12T00:13:54+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"phpseclib/bootstrap.php"
],
"psr-4": {
"phpseclib3\\": "phpseclib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jim Wigginton",
"email": "terrafrost@php.net",
"role": "Lead Developer"
},
{
"name": "Patrick Monnerat",
"email": "pm@datasphere.ch",
"role": "Developer"
},
{
"name": "Andreas Fischer",
"email": "bantu@phpbb.com",
"role": "Developer"
},
{
"name": "Hans-Jürgen Petrich",
"email": "petrich@tronic-media.com",
"role": "Developer"
},
{
"name": "Graham Campbell",
"email": "graham@alt-three.com",
"role": "Developer"
}
],
"description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
"homepage": "http://phpseclib.sourceforge.net",
"keywords": [
"BigInteger",
"aes",
"asn.1",
"asn1",
"blowfish",
"crypto",
"cryptography",
"encryption",
"rsa",
"security",
"sftp",
"signature",
"signing",
"ssh",
"twofish",
"x.509",
"x509"
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.41"
},
"funding": [
{
"url": "https://github.com/terrafrost",
"type": "github"
},
{
"url": "https://www.patreon.com/phpseclib",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib",
"type": "tidelift"
}
],
"install-path": "../phpseclib/phpseclib"
},
{
"name": "psr/event-dispatcher",
"version": "1.0.0",
"version_normalized": "1.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/event-dispatcher.git",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"time": "2019-01-08T18:20:26+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\EventDispatcher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Standard interfaces for event handling.",
"keywords": [
"events",
"psr",
"psr-14"
],
"support": {
"issues": "https://github.com/php-fig/event-dispatcher/issues",
"source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
},
"install-path": "../psr/event-dispatcher"
},
{
"name": "robrichards/xmlseclibs",
"version": "3.1.1",
"version_normalized": "3.1.1.0",
"source": {
"type": "git",
"url": "https://github.com/robrichards/xmlseclibs.git",
"reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/f8f19e58f26cdb42c54b214ff8a820760292f8df",
"reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"php": ">= 5.4"
},
"time": "2020-09-05T13:00:25+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"RobRichards\\XMLSecLibs\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "A PHP library for XML Security",
"homepage": "https://github.com/robrichards/xmlseclibs",
"keywords": [
"security",
"signature",
"xml",
"xmldsig"
],
"support": {
"issues": "https://github.com/robrichards/xmlseclibs/issues",
"source": "https://github.com/robrichards/xmlseclibs/tree/3.1.1"
},
"install-path": "../robrichards/xmlseclibs"
},
{
"name": "symfony/http-foundation",
"version": "v7.1.3",
"version_normalized": "7.1.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "f602d5c17d1fa02f8019ace2687d9d136b7f4a1a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/f602d5c17d1fa02f8019ace2687d9d136b7f4a1a",
"reference": "f602d5c17d1fa02f8019ace2687d9d136b7f4a1a",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-mbstring": "~1.1",
"symfony/polyfill-php83": "^1.27"
},
"conflict": {
"doctrine/dbal": "<3.6",
"symfony/cache": "<6.4"
},
"require-dev": {
"doctrine/dbal": "^3.6|^4",
"predis/predis": "^1.1|^2.0",
"symfony/cache": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/expression-language": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/rate-limiter": "^6.4|^7.0"
},
"time": "2024-07-26T12:41:01+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpFoundation\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.1.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/http-foundation"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.30.0",
"version_normalized": "1.30.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"time": "2024-06-19T12:30:46+00:00",
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-mbstring"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.30.0",
"version_normalized": "1.30.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
"reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"time": "2024-06-19T12:35:24+00:00",
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php83\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-php83"
}
],
"dev": true,
"dev-package-names": []
}

View File

@@ -0,0 +1,113 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'jumbojett/openid-connect-php' => array(
'pretty_version' => 'v1.0.0',
'version' => '1.0.0.0',
'reference' => '4af1d11497ec765dccddf928c14c04535ca96017',
'type' => 'library',
'install_path' => __DIR__ . '/../jumbojett/openid-connect-php',
'aliases' => array(),
'dev_requirement' => false,
),
'litesaml/lightsaml' => array(
'pretty_version' => 'v4.2.0',
'version' => '4.2.0.0',
'reference' => 'da88de24e699418918a128f0142dd09e2c41dd38',
'type' => 'library',
'install_path' => __DIR__ . '/../litesaml/lightsaml',
'aliases' => array(),
'dev_requirement' => false,
),
'paragonie/constant_time_encoding' => array(
'pretty_version' => 'v3.0.0',
'version' => '3.0.0.0',
'reference' => 'df1e7fde177501eee2037dd159cf04f5f301a512',
'type' => 'library',
'install_path' => __DIR__ . '/../paragonie/constant_time_encoding',
'aliases' => array(),
'dev_requirement' => false,
),
'paragonie/random_compat' => array(
'pretty_version' => 'v9.99.100',
'version' => '9.99.100.0',
'reference' => '996434e5492cb4c3edcb9168db6fbb1359ef965a',
'type' => 'library',
'install_path' => __DIR__ . '/../paragonie/random_compat',
'aliases' => array(),
'dev_requirement' => false,
),
'phpseclib/phpseclib' => array(
'pretty_version' => '3.0.41',
'version' => '3.0.41.0',
'reference' => '621c73f7dcb310b61de34d1da4c4204e8ace6ceb',
'type' => 'library',
'install_path' => __DIR__ . '/../phpseclib/phpseclib',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/event-dispatcher' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => 'dbefd12671e8a14ec7f180cab83036ed26714bb0',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/event-dispatcher',
'aliases' => array(),
'dev_requirement' => false,
),
'robrichards/xmlseclibs' => array(
'pretty_version' => '3.1.1',
'version' => '3.1.1.0',
'reference' => 'f8f19e58f26cdb42c54b214ff8a820760292f8df',
'type' => 'library',
'install_path' => __DIR__ . '/../robrichards/xmlseclibs',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/http-foundation' => array(
'pretty_version' => 'v7.1.3',
'version' => '7.1.3.0',
'reference' => 'f602d5c17d1fa02f8019ace2687d9d136b7f4a1a',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/http-foundation',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-mbstring' => array(
'pretty_version' => 'v1.30.0',
'version' => '1.30.0.0',
'reference' => 'fd22ab50000ef01661e2a31d850ebaa297f8e03c',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php83' => array(
'pretty_version' => 'v1.30.0',
'version' => '1.30.0.0',
'reference' => 'dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php83',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

View File

@@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@@ -0,0 +1,2 @@
**List of common tasks a pull request require complete**
- [ ] Changelog entry is added or the pull request don't alter library's functionality

View File

@@ -0,0 +1,38 @@
---
name: build
on: [push, pull_request]
env:
DEFAULT_COMPOSER_FLAGS: "--prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi"
jobs:
phpunit:
name: PHP ${{ matrix.php }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
php: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer update $DEFAULT_COMPOSER_FLAGS
- name: Run unit tests
run: vendor/bin/phpunit --verbose --colors=always tests

View File

@@ -0,0 +1,4 @@
/.idea
/vendor
/composer.lock
.phpunit.result.cache

View File

@@ -0,0 +1,180 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2023-12-13
### Added
- PHP 7.0 is required. #327
- Support for signed and encrypted UserInfo response and ID Token. #305
- Allow to set User-Agent header. #370
### Fixed
- User-Agent is set for any HTTP method in fetchURL() (not just POST). #382
- Update visibility of getWellKnownConfigValue to protected. #363
- Fixed issue on authentication for php8. #354
- Update construct typehint in docblock. #364
- Fixed LogoutToken verification for single value aud claims. #334
- Update well known config value function response types. #376
## [0.9.10] - 2022-09-30
### Fixed
- `private_key_jwt` and `client_secret_jwt` need to explicitly be enabled #331
## [0.9.9] - 2022-09-28
### Added
- Added support for back-channel logout. #302
- Added support for `private_key_jwt` Client Authentication method #322
- Added support for `client_secret_jwt` Client Authentication method #324
- Added PS512 encryption support #342
### Fixed
- Harden self-signed JWK header usage. #323
## [0.9.8] - 2022-08-05
### Fixed
- Do not use PKCE if IdP does not support it. #317
## [0.9.7] - 2022-07-13
### Added
- Support for Self-Contained JWTs. #308
- Support for RFC8693 Token Exchange Request. #275
### Fixed
- PHP 5.4 compatibility. #304
- Use session_status(). #306
## [0.9.6] - 2022-05-08
### Added
- Support for [phpseclib/phpseclib](https://phpseclib.com/) version **3**. #260
- Support client_secret on token endpoint with PKCE. #293
- Added new parameter to `requestTokens()` to pass custom HTTP headers #297
### Changed
- Allow serializing `OpenIDConnectClient` using `serialize()` #295
## [0.9.5] - 2021-11-24
### Changed
- signOut() Method parameter $accessToken -> $idToken to prevent confusion about access and id tokens usage. #127
- Fixed issue where missing nonce within the claims was causing an exception. #280
## [0.9.4] - 2021-11-21
### Added
- Enabled `client_secret_basic` authentication on `refreshToken()` #215
- Basic auth support for requestResourceOwnerToken #271
## [0.9.3] - 2021-11-20
### Added
- getRedirectURL() will not log a warning for PHP 7.1+ #179
- it is now possible to disable upgrading from HTTP to HTTPS for development purposes by calling `setHttpUpgradeInsecureRequests(false)` #241
- bugfix in getSessionKey when _SESSION key does not exist #251
- Added scope parameter to refresh token request #225
- bugfix in `verifyJWTclaims` when $accessToken is empty and $claims->at_hash is not #276
- bugfix with the `empty` function in PHP 5.4 #267
## [0.9.2] - 2020-11-16
### Added
- Support for [PKCE](https://tools.ietf.org/html/rfc7636). Currently, the supported methods are 'plain' and 'S256'.
## [0.9.1] - 2020-08-27
### Added
- Add support for MS Azure Active Directory B2C user flows
### Changed
- Fix at_hash verification #200
- Getters for public parameters #204
- Removed client ID query parameter when making a token request using Basic Auth
- Use of `random_bytes()` for token generation instead of `uniqid()`; polyfill for PHP < 7.0 provided.
### Removed
- Removed explicit content-length header - caused issues with proxy servers
## [0.9.0] - 2020-03-09
### Added
- php 7.4 deprecates array_key_exists on objects, use property_exists in getVerifiedClaims and requestUserInfo
- Adding a header to indicate JSON as the return type for userinfo endpoint #151
- ~Updated OpenIDConnectClient to conditionally verify nonce #146~
- Add possibility to change enc_type parameter for http_build_query #155
- Adding OAuth 2.0 Token Introspection #156
- Add optional parameters clientId/clientSecret for introspection #157 & #158
- Adding OAuth 2.0 Token Revocation #160
- Adding issuer validator #145
- Adding signing algorithm PS256 #180
- Check http status of request user info #186
- URL encode clientId and clientSecret when using basic authentication, according to https://tools.ietf.org/html/rfc6749#section-2.3.1 #192
- Adjust PHPDoc to state that null is also allowed #193
### Changed
- Bugfix/code cleanup #152
- Cleanup PHPDoc #46e5b59
- Replace unnecessary double quotes with single quotes #2a76b57
- Use original function names instead of aliases #1f37892
- Remove unnecessary default values #5ab801e
- Explicit declare field $redirectURL #9187c0b
- Remove unused code #1e65384
- Fix indent #e9cdf56
- Cleanup conditional code flow for better readability #107f3fb
- Added strict type comparisons #167
- Bugfix: required `openid` scope was omitted when additional scopes were registered using `addScope` method. This resulted in failing OpenID process.
## [0.8.0] - 2019-01-02
### Added
- Fix `verifyJWTsignature()`: verify JWT to prevent php errors and warnings on invalid token
### Changed
- Decouple session manipulation, it's allow use of other session libraries #134
- Broaden version requirements of the phpseclib/phpseclib package. #144
## [0.7.0] - 2018-10-15
### Added
- Add "license" field to composer.json #138
- Ensure key_alg is set when getting key #139
- Add option to send additional registration parameters like post_logout_redirect_uris. #140
### Changed
- disabled autoload for Crypt_RSA + make refreshToken() method tolerant for errors #137
## [0.6.0] - 2018-07-17
### Added
- Added five minutes leeway due to clock skew between openidconnect server and client.
- Fix save access_token from request in implicit flow authentication #129
- `verifyJWTsignature()` method private -> public #126
- Support for providers where provider/login URL is not the same as the issuer URL. #125
- Support for providers that has a different login URL from the issuer URL, for instance Azure Active Directory. Here, the provider URL is on the format: https://login.windows.net/(tenant-id), while the issuer claim actually is on the format: https://sts.windows.net/(tenant-id).
### Changed
- refreshToken method update #124
## [0.5.0] - 2018-04-09
### Added
- Implement Azure AD B2C Implicit Workflow
## [0.4.1] - 2018-02-16
### Changed
- Documentation updates for include path.
## [0.4.0] - 2018-02-15
### Added
- Timeout is configurable via setTimeout method. This addresses issue #94.
- Add the ability to authenticate using the Resource Owner flow (with or without the Client ID and ClientSecret). This addresses issue #98
- Add support for HS256, HS512 and HS384 signatures
- Removed unused calls to $this->getProviderConfigValue("token_endpoint_…

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,238 @@
PHP OpenID Connect Basic Client
========================
A simple library that allows an application to authenticate a user through the basic OpenID Connect flow.
This library hopes to encourage OpenID Connect use by making it simple enough for a developer with little knowledge of
the OpenID Connect protocol to set up authentication.
A special thanks goes to Justin Richer and Amanda Anganes for their help and support of the protocol.
# Requirements #
1. PHP 5.4 or greater
2. CURL extension
3. JSON extension
## Install ##
1. Install library using composer
```
composer require jumbojett/openid-connect-php
```
2. Include composer autoloader
```php
require __DIR__ . '/vendor/autoload.php';
```
## Example 1: Basic Client ##
```php
use Jumbojett\OpenIDConnectClient;
$oidc = new OpenIDConnectClient('https://id.provider.com',
'ClientIDHere',
'ClientSecretHere');
$oidc->setCertPath('/path/to/my.cert');
$oidc->authenticate();
$name = $oidc->requestUserInfo('given_name');
```
[See openid spec for available user attributes][1]
## Example 2: Dynamic Registration ##
```php
use Jumbojett\OpenIDConnectClient;
$oidc = new OpenIDConnectClient("https://id.provider.com");
$oidc->register();
$client_id = $oidc->getClientID();
$client_secret = $oidc->getClientSecret();
// Be sure to add logic to store the client id and client secret
```
## Example 3: Network and Security ##
```php
// Configure a proxy
$oidc->setHttpProxy("http://my.proxy.com:80/");
// Configure a cert
$oidc->setCertPath("/path/to/my.cert");
```
## Example 4: Request Client Credentials Token ##
```php
use Jumbojett\OpenIDConnectClient;
$oidc = new OpenIDConnectClient('https://id.provider.com',
'ClientIDHere',
'ClientSecretHere');
$oidc->providerConfigParam(array('token_endpoint'=>'https://id.provider.com/connect/token'));
$oidc->addScope('my_scope');
// this assumes success (to validate check if the access_token property is there and a valid JWT) :
$clientCredentialsToken = $oidc->requestClientCredentialsToken()->access_token;
```
## Example 5: Request Resource Owners Token (with client auth) ##
```php
use Jumbojett\OpenIDConnectClient;
$oidc = new OpenIDConnectClient('https://id.provider.com',
'ClientIDHere',
'ClientSecretHere');
$oidc->providerConfigParam(array('token_endpoint'=>'https://id.provider.com/connect/token'));
$oidc->addScope('my_scope');
//Add username and password
$oidc->addAuthParam(array('username'=>'<Username>'));
$oidc->addAuthParam(array('password'=>'<Password>'));
//Perform the auth and return the token (to validate check if the access_token property is there and a valid JWT) :
$token = $oidc->requestResourceOwnerToken(TRUE)->access_token;
```
## Example 6: Basic client for implicit flow e.g. with Azure AD B2C (see http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth) ##
```php
use Jumbojett\OpenIDConnectClient;
$oidc = new OpenIDConnectClient('https://id.provider.com',
'ClientIDHere',
'ClientSecretHere');
$oidc->setResponseTypes(array('id_token'));
$oidc->addScope(array('openid'));
$oidc->setAllowImplicitFlow(true);
$oidc->addAuthParam(array('response_mode' => 'form_post'));
$oidc->setCertPath('/path/to/my.cert');
$oidc->authenticate();
$sub = $oidc->getVerifiedClaims('sub');
```
## Example 7: Introspection of an access token (see https://tools.ietf.org/html/rfc7662) ##
```php
use Jumbojett\OpenIDConnectClient;
$oidc = new OpenIDConnectClient('https://id.provider.com',
'ClientIDHere',
'ClientSecretHere');
$data = $oidc->introspectToken('an.access-token.as.given');
if (!$data->active) {
// the token is no longer usable
}
```
## Example 8: PKCE Client ##
```php
use Jumbojett\OpenIDConnectClient;
$oidc = new OpenIDConnectClient('https://id.provider.com',
'ClientIDHere',
null);
$oidc->setCodeChallengeMethod('S256');
$oidc->authenticate();
$name = $oidc->requestUserInfo('given_name');
```
## Example 9: Back-channel logout ##
Back-channel authentication assumes you can end a session on the server side on behalf of the user (without relying
on their browser). The request is a POST from the OP direct to your RP. In this way, the use of this library can
ensure your RP performs 'single sign out' for the user even if they didn't have your RP open in a browser or other
device, but still had an active session there.
Either the sid or the sub may be accessible from the logout token sent from the OP. You can use either
`getSidFromBackChannel()` or `getSubjectFromBackChannel()` to retrieve them if it is helpful to match them to a session
in order to destroy it.
The below ensures the use of this library to ensure validation of the back-channel logout token, but is afterward
just a hypothetical way of finding such a session and destroying it. Adjust it to the needs of your RP.
```php
function handleLogout() {
// NOTE: assumes that $this->oidc is an instance of OpenIDConnectClient()
if ($this->oidc->verifyLogoutToken()) {
$sid = $this->oidc->getSidFromBackChannel();
if (isset($sid)) {
// Somehow find the session based on the $sid and
// destroy it. This depends on your RP's design,
// there is nothing in the OIDC spec to mandate how.
//
// In this example, we find a Redis key, which was
// previously stored using the sid we obtained from
// the access token after login.
//
// The value of the Redis key is that of the user's
// session ID specific to this hypothetical RP app.
//
// We then switch to that session and destroy it.
$this->redis->connect('127.0.0.1', 6379);
$session_id_to_destroy = $this->redis->get($sid);
if ($session_id_to_destroy) {
session_commit();
session_id($session_id_to_destroy); // switches to that session
session_start();
$_SESSION = array(); // effectively ends the session
}
}
}
}
```
## Example 10: Enable Token Endpoint Auth Methods ##
By default, only `client_secret_basic` is enabled on client side which was the only supported for a long time.
Recently `client_secret_jwt` and `private_key_jwt` have been added, but they remain disabled until explicitly enabled.
```php
use Jumbojett\OpenIDConnectClient;
$oidc = new OpenIDConnectClient('https://id.provider.com',
'ClientIDHere',
null);
# enable 'client_secret_basic' and 'client_secret_jwt'
$oidc->setTokenEndpointAuthMethodsSupported(['client_secret_basic', 'client_secret_jwt']);
# for 'private_key_jwt' in addition also the generator function has to be set.
$oidc->setTokenEndpointAuthMethodsSupported(['private_key_jwt']);
$oidc->setPrivateKeyJwtGenerator(function(string $token_endpoint) {
# TODO: what ever is necessary
})
```
## Development Environments ##
In some cases you may need to disable SSL security on your development systems.
Note: This is not recommended on production systems.
```php
$oidc->setVerifyHost(false);
$oidc->setVerifyPeer(false);
```
Also, your local system might not support HTTPS, so you might disable upgrading to it:
```php
$oidc->setHttpUpgradeInsecureRequests(false);
```
### Todo ###
- Dynamic registration does not support registration auth tokens and endpoints
[1]: http://openid.net/specs/openid-connect-basic-1_0-15.html#id_res
## Contributing ###
- All pull requests, once merged, should be added to the CHANGELOG.md file.

View File

@@ -0,0 +1,56 @@
<?php
/**
*
* Copyright MITRE 2012
*
* OpenIDConnectClient for PHP5
* Author: Michael Jett <mjett@mitre.org>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
*/
require __DIR__ . '/vendor/autoload.php';
use Jumbojett\OpenIDConnectClient;
$oidc = new OpenIDConnectClient(
'http://myproviderURL.com/',
'ClientIDHere',
'ClientSecretHere'
);
$oidc->authenticate();
$name = $oidc->requestUserInfo('given_name');
?>
<html>
<head>
<title>Example OpenID Connect Client Use</title>
<style>
body {
font-family: 'Lucida Grande', Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<div>
Hello <?php echo $name; ?>
</div>
</body>
</html>

View File

@@ -0,0 +1,23 @@
{
"name": "jumbojett/openid-connect-php",
"description": "Bare-bones OpenID Connect client",
"license": "Apache-2.0",
"require": {
"php": ">=7.0",
"ext-json": "*",
"ext-curl": "*",
"phpseclib/phpseclib": "~3.0"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
"yoast/phpunit-polyfills": "^1.0"
},
"archive" : {
"exclude" : [
".*"
]
},
"autoload" : {
"classmap": [ "src/"]
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
bootstrap="./vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
verbose="true"
stopOnFailure="false"
processIsolation="false"
backupGlobals="false"
syntaxCheck="true"
>
<testsuites>
<testsuite name="Tests">
<directory>./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
<exclude>
<directory>./vendor</directory>
<directory>./tests</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,240 @@
<?php
use Jumbojett\OpenIDConnectClient;
use Jumbojett\OpenIDConnectClientException;
use PHPUnit\Framework\MockObject\MockObject;
use Yoast\PHPUnitPolyfills\TestCases\TestCase;
class OpenIDConnectClientTest extends TestCase
{
/**
* @return void
*/
public function testGetRedirectURL()
{
$client = new OpenIDConnectClient();
self::assertSame('http:///', $client->getRedirectURL());
$_SERVER['SERVER_NAME'] = 'domain.test';
$_SERVER['REQUEST_URI'] = '/path/index.php?foo=bar&baz#fragment';
self::assertSame('http://domain.test/path/index.php', $client->getRedirectURL());
}
public function testAuthenticateDoesNotThrowExceptionIfClaimsIsMissingNonce()
{
$fakeClaims = new StdClass();
$fakeClaims->iss = 'fake-issuer';
$fakeClaims->aud = 'fake-client-id';
$fakeClaims->nonce = null;
$_REQUEST['id_token'] = 'abc.123.xyz';
$_REQUEST['state'] = false;
$_SESSION['openid_connect_state'] = false;
/** @var OpenIDConnectClient | MockObject $client */
$client = $this->getMockBuilder(OpenIDConnectClient::class)->setMethods(['decodeJWT', 'getProviderConfigValue', 'verifyJWTSignature'])->getMock();
$client->method('decodeJWT')->willReturn($fakeClaims);
$client->method('getProviderConfigValue')->with('jwks_uri')->willReturn(true);
$client->method('verifyJWTSignature')->willReturn(true);
$client->setClientID('fake-client-id');
$client->setIssuer('fake-issuer');
$client->setIssuerValidator(function() {
return true;
});
$client->setAllowImplicitFlow(true);
$client->setProviderURL('https://jwt.io/');
try {
$authenticated = $client->authenticate();
$this->assertTrue($authenticated);
} catch ( OpenIDConnectClientException $e ) {
if ( $e->getMessage() === 'Unable to verify JWT claims' ) {
self::fail( 'OpenIDConnectClientException was thrown when it should not have been.' );
}
}
}
public function testSerialize()
{
$client = new OpenIDConnectClient('https://example.com', 'foo', 'bar', 'baz');
$serialized = serialize($client);
$this->assertInstanceOf(OpenIDConnectClient::class, unserialize($serialized));
}
/**
* @dataProvider provider
*/
public function testAuthMethodSupport($expected, $authMethod, $clientAuthMethods, $idpAuthMethods)
{
$client = new OpenIDConnectClient();
if ($clientAuthMethods !== null) {
$client->setTokenEndpointAuthMethodsSupported($clientAuthMethods);
}
$this->assertEquals($expected, $client->supportsAuthMethod($authMethod, $idpAuthMethods));
}
public function provider(): array
{
return [
'client_secret_basic - default config' => [true, 'client_secret_basic', null, ['client_secret_basic']],
'client_secret_jwt - default config' => [false, 'client_secret_jwt', null, ['client_secret_basic', 'client_secret_jwt']],
'client_secret_jwt - explicitly enabled' => [true, 'client_secret_jwt', ['client_secret_jwt'], ['client_secret_basic', 'client_secret_jwt']],
'private_key_jwt - default config' => [false, 'private_key_jwt', null, ['client_secret_basic', 'client_secret_jwt', 'private_key_jwt']],
'private_key_jwt - explicitly enabled' => [true, 'private_key_jwt', ['private_key_jwt'], ['client_secret_basic', 'client_secret_jwt', 'private_key_jwt']],
];
}
/**
* @covers Jumbojett\\OpenIDConnectClient::verifyLogoutTokenClaims
* @dataProvider provideTestVerifyLogoutTokenClaimsData
* @throws OpenIDConnectClientException
*/
public function testVerifyLogoutTokenClaims( $claims, $expectedResult )
{
/** @var OpenIDConnectClient | MockObject $client */
$client = $this->getMockBuilder(OpenIDConnectClient::class)->setMethods(['decodeJWT'])->getMock();
$client->setClientID('fake-client-id');
$client->setIssuer('fake-issuer');
$client->setIssuerValidator(function() {
return true;
});
$client->setProviderURL('https://jwt.io/');
$actualResult = $client->verifyLogoutTokenClaims( $claims );
$this->assertEquals( $expectedResult, $actualResult );
}
/**
* @return array
*/
public function provideTestVerifyLogoutTokenClaimsData(): array
{
return [
'valid-single-aud' => [
(object)[
'iss' => 'fake-issuer',
'aud' => 'fake-client-id',
'sid' => 'fake-client-sid',
'sub' => 'fake-client-sub',
'iat' => time(),
'events' => (object) [
'http://schemas.openid.net/event/backchannel-logout' => (object)[]
],
],
true
],
'valid-multiple-auds' => [
(object)[
'iss' => 'fake-issuer',
'aud' => [ 'fake-client-id', 'some-other-aud' ],
'sid' => 'fake-client-sid',
'sub' => 'fake-client-sub',
'iat' => time(),
'events' => (object) [
'http://schemas.openid.net/event/backchannel-logout' => (object)[]
],
],
true
],
'invalid-no-sid-and-no-sub' => [
(object)[
'iss' => 'fake-issuer',
'aud' => [ 'fake-client-id', 'some-other-aud' ],
'iat' => time(),
'events' => (object) [
'http://schemas.openid.net/event/backchannel-logout' => (object)[]
],
],
false
],
'valid-no-sid' => [
(object)[
'iss' => 'fake-issuer',
'aud' => [ 'fake-client-id', 'some-other-aud' ],
'sub' => 'fake-client-sub',
'iat' => time(),
'events' => (object) [
'http://schemas.openid.net/event/backchannel-logout' => (object)[]
],
],
true
],
'valid-no-sub' => [
(object)[
'iss' => 'fake-issuer',
'aud' => [ 'fake-client-id', 'some-other-aud' ],
'sid' => 'fake-client-sid',
'iat' => time(),
'events' => (object) [
'http://schemas.openid.net/event/backchannel-logout' => (object)[]
],
],
true
],
'invalid-with-nonce' => [
(object)[
'iss' => 'fake-issuer',
'aud' => [ 'fake-client-id', 'some-other-aud' ],
'sid' => 'fake-client-sid',
'iat' => time(),
'events' => (object) [
'http://schemas.openid.net/event/backchannel-logout' => (object)[]
],
'nonce' => 'must-not-be-set'
],
false
],
'invalid-no-events' => [
(object)[
'iss' => 'fake-issuer',
'aud' => [ 'fake-client-id', 'some-other-aud' ],
'sid' => 'fake-client-sid',
'iat' => time(),
'nonce' => 'must-not-be-set'
],
false
],
'invalid-no-backchannel-event' => [
(object)[
'iss' => 'fake-issuer',
'aud' => [ 'fake-client-id', 'some-other-aud' ],
'sid' => 'fake-client-sid',
'iat' => time(),
'events' => (object) [],
'nonce' => 'must-not-be-set'
],
false
],
'invalid-no-iat' => [
(object)[
'iss' => 'fake-issuer',
'aud' => [ 'fake-client-id', 'some-other-aud' ],
'sid' => 'fake-client-sid',
'events' => (object) [
'http://schemas.openid.net/event/backchannel-logout' => (object)[]
]
],
false
],
'invalid-bad-iat' => [
(object)[
'iss' => 'fake-issuer',
'aud' => [ 'fake-client-id', 'some-other-aud' ],
'sid' => 'fake-client-sid',
'iat' => time() + 301,
'events' => (object) [
'http://schemas.openid.net/event/backchannel-logout' => (object)[]
]
],
false
],
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
use Jumbojett\OpenIDConnectClient;
use Jumbojett\OpenIDConnectClientException;
use PHPUnit\Framework\MockObject\MockObject;
use Yoast\PHPUnitPolyfills\TestCases\TestCase;
class TokenVerificationTest extends TestCase
{
/**
* @param $alg
* @param $jwt
* @throws OpenIDConnectClientException
* @dataProvider providesTokens
*/
public function testTokenVerification($alg, $jwt)
{
/** @var OpenIDConnectClient | MockObject $client */
$client = $this->getMockBuilder(OpenIDConnectClient::class)->setMethods(['fetchUrl'])->getMock();
$client->method('fetchUrl')->willReturn(file_get_contents(__DIR__ . "/data/jwks-$alg.json"));
$client->setProviderURL('https://jwt.io/');
$client->providerConfigParam(['jwks_uri' => 'https://jwt.io/.well-known/jwks.json']);
$verified = $client->verifyJWTSignature($jwt);
self::assertTrue($verified);
$client->setAccessToken($jwt);
}
public function providesTokens(): array
{
return [
'PS256' => ['ps256', 'eyJhbGciOiJQUzI1NiIsImtpZCI6Imtvbm5lY3RkLXRva2Vucy1zaWduaW5nLWtleSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJrcG9wLWh0dHBzOi8va29wYW5vLmRlbW8vbWVldC8iLCJleHAiOjE1NjgzNzE0NjEsImp0aSI6IkpkR0tDbEdOTXl2VXJpcmlRRUlWUXZCVmttT2FfQkRjIiwiaWF0IjoxNTY4MzcxMjIxLCJpc3MiOiJodHRwczovL2tvcGFuby5kZW1vIiwic3ViIjoiUHpUVWp3NHBlXzctWE5rWlBILXJxVHE0MTQ1Z3lDdlRvQmk4V1E5bFBrcW5rbEc1aktvRU5LM21Qb0I1WGY1ZTM5dFRMR2RKWXBMNEJubXFnelpaX0FAa29ubmVjdCIsImtjLmlzQWNjZXNzVG9rZW4iOnRydWUsImtjLmF1dGhvcml6ZWRTY29wZXMiOlsicHJvZmlsZSIsImVtYWlsIiwia29wYW5vL2t3bSIsImtvcGFuby9nYyIsImtvcGFuby9rdnMiLCJvcGVuaWQiXSwia2MuYXV0aG9yaXplZENsYWltcyI6eyJpZF90b2tlbiI6eyJuYW1lIjpudWxsfX0sImtjLmlkZW50aXR5Ijp7ImtjLmkuZG4iOiJKb25hcyBCcmVra2UiLCJrYy5pLmlkIjoiQUFBQUFLd2hxVkJBMCs1SXN4bjdwMU13UkNVQkFBQUFCZ0FBQUJzQUFBQk5VVDA5QUFBQUFBPT0iLCJrYy5pLnVuIjoidXNlcjEiLCJrYy5pLnVzIjoiTVEifSwia2MucHJvdmlkZXIiOiJpZGVudGlmaWVyLWtjIn0.hGRuXvul2kOiALHexwYp5MBEJVwz1YV3ehyM3AOuwCoK2w5sJxdciqqY_TfXCKyO6nAEbYLK3J0CBOjfup_IG0aCZcwzjto8khYlc4ezXkGnFsbJBNQdDGkpHtWnioWx-OJ3cXvY9F8aOvjaq0gw11ZDAcqQl0g7LTbJ9-J_yx0pmy3NGai2JB30Fh1OgSDzYfxWnE0RRgZG-x68e65RXfSBaEGW85OUh4wihxO2zdTGAHJ3Iq_-QAG4yRbXZtLx3ZspG7LNmqG-YE3huy3Rd8u3xrJNhmUOfEnz3x07q7VW0cj9NedX98BAbj3iNvksQsE0oG0J_f_Tu8Ai8VbWB72sJuXZWxANDKdz0BBYLzXhsjXkNByRq9x3zqDVsX-cVHei_XudxEOVRBjhkvW2MmIjcAHNKCKsdar865-gFG9McP4PCcBlY28tC0Cvnzyi83LBfpGRXdl6MJunnUsKQ1C79iCoVI1doK1erFN959Q-TGJfJA3Tr5LNpuGawB5rpe1nDGWvmYhg3uYfNl8uTTyvNgvvejcflEb2DURuXdqABuSiP7RkDWYtzx6mq49G0tRxelBbvyjQ2id2QjmRRdQ6dHEZ2NCJ51b8OFoDJBtxN1CD62TTxa3FUqCdZAPAUR3hHn_69vYq82MR514s-Gb67A6j2PbMPFATQP2UdK8']
];
}
}

View File

@@ -0,0 +1,12 @@
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "konnectd-tokens-signing-key",
"n": "10hb3pFUVcqJcS-d1pLCkFTyTqVD1GavlAai582CoRwFcyIQxCPJz0LJVgkUNwxSRkY0g0PcgFN_MmuuzpFXMkkiMIC9O_KwnuL34FrbijZvcGpnDn7kb9KAM883OVTr_w3wFeQIyh0ksSwVQ9CxVQ-ZeCXP73CCGk99uDb8SeF8_vncXJmaak99pK6HKJteSLkA-Ywxo9HOINZK2vW06UYcSkeoQnSI27Cd5-T6GVgqKH0Su4c5Ydou_w0tL_UkbZA4fIbMZC6dtWmBQf6tyYsCM9fbWNIVOj_7WlWcAOSTFNF2We2dxJrOzt6vDND3k1nCgg_EEM6cgBO3swUCktTFuQxo1sryYX5WXz9wnJb38b9mTXhOeF0bd9y_VQq8erSlcyRu8UGzX65tIf534hLL16KQaHbjROGSQvzqFrISmSBjBTjkPedTZSYOhiVJ95-em_Y6uLi-T7V4bs4dcg3oa0H_glXltoC9JxzS6gfMGGLgh-NpGEOdC_QosyzVVfzT70TurOGnsB1_VcAm_fK-T1Zv_ztpr5OZNfXWXC3Pfq_3sxP5HDKMk8luZ7LOWk7HVSYBdCFmOM1A3KmHNS2fEs-QHIr-XjYQ7QrXsRFP3dmoEPfiYlu03m8Xs3UMB70eGeGQx7OhZSuogxV_oCfApV5EJfuz97tVmOg8iMs",
"e": "AQAB"
}
],
"kty": ""
}

View File

@@ -0,0 +1,59 @@
{
"name": "litesaml/lightsaml",
"license": "MIT",
"type": "library",
"description": "SAML 2.0 PHP library",
"keywords": ["SAML 2.0", "PHP", "library", "lightSAML", "Single SignOn", "Single Logout"],
"authors": [
{
"name": "William",
"email": "work@suppo.fr"
},
{
"name": "Milos Tomic",
"email": "tmilos@gmail.com",
"homepage": "https://github.com/tmilos/",
"role": "Developer"
}
],
"support": {
"issues": "https://github.com/litesaml/lightsaml/issues",
"source": "https://github.com/litesaml/lightsaml",
"docs": "https://docs.litesaml.com"
},
"autoload": {
"psr-4": {
"LightSaml\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LightSaml\\Tests\\": "tests/"
}
},
"require": {
"php": ">=7.4",
"robrichards/xmlseclibs": "~2.0|~3.0|~4.0",
"symfony/http-foundation": "~5.0|~6.0|~7.0",
"psr/event-dispatcher": "^1.0"
},
"require-dev": {
"symfony/dom-crawler": "~5.0|~6.0|~7.0",
"symfony/css-selector": "~5.0|~6.0|~7.0",
"pimple/pimple": "~3.0",
"phpunit/phpunit": "~8.4|~9.5",
"monolog/monolog": "^2.0|^3.0",
"squizlabs/php_codesniffer": "^3.6",
"litesaml/schemas": "~1.0.0",
"phpstan/phpstan": "^1.8",
"marcocesarato/php-conventional-changelog": "^1.15"
},
"prefer-stable": true,
"minimum-stability": "stable",
"scripts": {
"test": "vendor/bin/phpunit",
"phpcs": "vendor/bin/phpcs --standard=PSR12 --exclude=Generic.Files.LineLength ./src",
"phpstan": "vendor/bin/phpstan analyse --memory-limit 512M --ansi",
"tag": "vendor/bin/conventional-changelog --commit"
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace LightSaml\Action;
use LightSaml\Context\ContextInterface;
interface ActionInterface
{
public function execute(ContextInterface $context);
}

View File

@@ -0,0 +1,26 @@
<?php
namespace LightSaml\Action;
use Psr\Log\LoggerInterface;
class ActionLogWrapper implements ActionWrapperInterface
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* @return ActionInterface
*/
public function wrap(ActionInterface $action)
{
return new LoggableAction($action, $this->logger);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace LightSaml\Action;
interface ActionWrapperInterface
{
/**
* @return ActionInterface
*/
public function wrap(ActionInterface $action);
}

View File

@@ -0,0 +1,31 @@
<?php
namespace LightSaml\Action\Assertion;
use LightSaml\Action\ActionInterface;
use LightSaml\Context\ContextInterface;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Error\LightSamlContextException;
use Psr\Log\LoggerInterface;
abstract class AbstractAssertionAction implements ActionInterface
{
/** @var LoggerInterface */
protected $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function execute(ContextInterface $context)
{
if ($context instanceof AssertionContext) {
$this->doExecute($context);
} else {
throw new LightSamlContextException($context, 'Expected AssertionContext');
}
}
abstract protected function doExecute(AssertionContext $context);
}

View File

@@ -0,0 +1,51 @@
<?php
namespace LightSaml\Action\Assertion\Inbound;
use LightSaml\Action\Assertion\AbstractAssertionAction;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Error\LightSamlContextException;
use LightSaml\SamlConstants;
use Psr\Log\LoggerInterface;
class AssertionIssuerFormatValidatorAction extends AbstractAssertionAction
{
/** @var string */
private $expectedIssuerFormat = SamlConstants::NAME_ID_FORMAT_ENTITY;
/**
* @param string $expectedIssuerFormat
*/
public function __construct(LoggerInterface $logger, $expectedIssuerFormat)
{
parent::__construct($logger);
$this->expectedIssuerFormat = $expectedIssuerFormat;
}
protected function doExecute(AssertionContext $context)
{
if (null == $context->getAssertion()->getIssuer()) {
$message = 'Assertion element must have an issuer element';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
if (
$context->getAssertion()->getIssuer()->getFormat() &&
$context->getAssertion()->getIssuer()->getFormat() != $this->expectedIssuerFormat
) {
$message = sprintf(
"Response Issuer Format if set must have value '%s' but it was '%s'",
$this->expectedIssuerFormat,
$context->getAssertion()->getIssuer()->getFormat()
);
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this, [
'actualFormat' => $context->getAssertion()->getIssuer()->getFormat(),
'expectedFormat' => $this->expectedIssuerFormat,
]));
throw new LightSamlContextException($context, $message);
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace LightSaml\Action\Assertion\Inbound;
use LightSaml\Action\Assertion\AbstractAssertionAction;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Credential\Criteria\MetadataCriteria;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Error\LightSamlModelException;
use LightSaml\Model\XmlDSig\AbstractSignatureReader;
use LightSaml\Validator\Model\Signature\SignatureValidatorInterface;
use Psr\Log\LoggerInterface;
class AssertionSignatureValidatorAction extends AbstractAssertionAction
{
/** @var SignatureValidatorInterface */
protected $signatureValidator;
/** @var bool */
protected $requireSignature;
/**
* @param bool $requireSignature
*/
public function __construct(LoggerInterface $logger, SignatureValidatorInterface $signatureValidator, $requireSignature = true)
{
parent::__construct($logger);
$this->signatureValidator = $signatureValidator;
$this->requireSignature = $requireSignature;
}
/**
* @return void
*/
protected function doExecute(AssertionContext $context)
{
$signature = $context->getAssertion()->getSignature();
if (null === $signature) {
if ($this->requireSignature) {
$message = 'Assertions must be signed';
$this->logger->critical($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
} else {
$this->logger->debug('Assertion is not signed', LogHelper::getActionContext($context, $this));
return;
}
}
if ($signature instanceof AbstractSignatureReader) {
$metadataType = ProfileContext::ROLE_IDP === $context->getProfileContext()->getOwnRole() ? MetadataCriteria::TYPE_SP : MetadataCriteria::TYPE_IDP;
$credential = $this->signatureValidator->validate($signature, $context->getAssertion()->getIssuer()->getValue(), $metadataType);
if ($credential) {
$keyNames = $credential->getKeyNames();
$this->logger->debug(
sprintf('Assertion signature validated with key "%s"', implode(', ', $keyNames)),
LogHelper::getActionContext($context, $this, [
'credential' => $credential,
])
);
} else {
$this->logger->warning(
'Assertion signature verification was not performed',
LogHelper::getActionContext($context, $this)
);
}
} else {
$message = 'Expected AbstractSignatureReader';
$this->logger->critical($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlModelException($message);
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace LightSaml\Action\Assertion\Inbound;
use LightSaml\Action\Assertion\AbstractAssertionAction;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Validator\Model\Assertion\AssertionValidatorInterface;
use Psr\Log\LoggerInterface;
class AssertionValidatorAction extends AbstractAssertionAction
{
/** @var AssertionValidatorInterface */
protected $assertionValidator;
public function __construct(LoggerInterface $logger, AssertionValidatorInterface $assertionValidator)
{
parent::__construct($logger);
$this->assertionValidator = $assertionValidator;
}
protected function doExecute(AssertionContext $context)
{
$this->assertionValidator->validateAssertion($context->getAssertion());
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace LightSaml\Action\Assertion\Inbound;
use LightSaml\Action\Assertion\AbstractAssertionAction;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\ProfileContexts;
use LightSaml\Context\Profile\RequestStateContext;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Store\Request\RequestStateStoreInterface;
use Psr\Log\LoggerInterface;
class InResponseToValidatorAction extends AbstractAssertionAction
{
/** @var RequestStateStoreInterface */
protected $requestStore;
public function __construct(LoggerInterface $logger, RequestStateStoreInterface $requestStore)
{
parent::__construct($logger);
$this->requestStore = $requestStore;
}
protected function doExecute(AssertionContext $context)
{
if (null === $context->getAssertion()->getSubject()) {
return;
}
foreach ($context->getAssertion()->getSubject()->getAllSubjectConfirmations() as $subjectConfirmation) {
if (
$subjectConfirmation->getSubjectConfirmationData() &&
$subjectConfirmation->getSubjectConfirmationData()->getInResponseTo()
) {
$requestState = $this->validateInResponseTo(
$subjectConfirmation->getSubjectConfirmationData()->getInResponseTo(),
$context
);
/** @var RequestStateContext $requestStateContext */
$requestStateContext = $context->getSubContext(ProfileContexts::REQUEST_STATE, RequestStateContext::class);
$requestStateContext->setRequestState($requestState);
}
}
}
/**
* @param string $inResponseTo
*
* @return \LightSaml\State\Request\RequestState
*/
protected function validateInResponseTo($inResponseTo, AssertionContext $context)
{
$requestState = $this->requestStore->get($inResponseTo);
if (null == $requestState) {
$message = sprintf("Unknown InResponseTo '%s'", $inResponseTo);
$this->logger->emergency($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
return $requestState;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace LightSaml\Action\Assertion\Inbound;
use LightSaml\Action\Assertion\AbstractAssertionAction;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Store\EntityDescriptor\EntityDescriptorStoreInterface;
use Psr\Log\LoggerInterface;
class KnownAssertionIssuerAction extends AbstractAssertionAction
{
/** @var EntityDescriptorStoreInterface */
private $idpEntityDescriptorProvider;
public function __construct(LoggerInterface $logger, EntityDescriptorStoreInterface $idpEntityDescriptorProvider)
{
parent::__construct($logger);
$this->idpEntityDescriptorProvider = $idpEntityDescriptorProvider;
}
/**
* @return void
*/
protected function doExecute(AssertionContext $context)
{
if (null === $context->getAssertion()->getIssuer()) {
$message = 'Assertion element must have an issuer element';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
if (false == $this->idpEntityDescriptorProvider->has($context->getAssertion()->getIssuer()->getValue())) {
$message = sprintf("Unknown issuer '%s'", $context->getAssertion()->getIssuer()->getValue());
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this, [
'messageIssuer' => $context->getAssertion()->getIssuer()->getValue(),
]));
throw new LightSamlContextException($context, $message);
}
$this->logger->debug(
sprintf('Known assertion issuer: "%s"', $context->getAssertion()->getIssuer()->getValue()),
LogHelper::getActionContext($context, $this)
);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace LightSaml\Action\Assertion\Inbound;
use LightSaml\Action\Assertion\AbstractAssertionAction;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Criteria\CriteriaSet;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Model\Assertion\SubjectConfirmation;
use LightSaml\Model\Metadata\AssertionConsumerService;
use LightSaml\Model\Metadata\SpSsoDescriptor;
use LightSaml\Resolver\Endpoint\Criteria\DescriptorTypeCriteria;
use LightSaml\Resolver\Endpoint\Criteria\LocationCriteria;
use LightSaml\Resolver\Endpoint\Criteria\ServiceTypeCriteria;
use LightSaml\Resolver\Endpoint\EndpointResolverInterface;
use Psr\Log\LoggerInterface;
class RecipientValidatorAction extends AbstractAssertionAction
{
/** @var EndpointResolverInterface */
private $endpointResolver;
public function __construct(LoggerInterface $logger, EndpointResolverInterface $endpointResolver)
{
parent::__construct($logger);
$this->endpointResolver = $endpointResolver;
}
/**
* @return void
*/
protected function doExecute(AssertionContext $context)
{
if ($context->getAssertion()->getAllAuthnStatements() && $context->getAssertion()->hasBearerSubject()) {
$this->validateBearerAssertion($context);
}
}
protected function validateBearerAssertion(AssertionContext $context)
{
foreach ($context->getAssertion()->getSubject()->getBearerConfirmations() as $subjectConfirmation) {
$this->validateSubjectConfirmation($context, $subjectConfirmation);
}
}
protected function validateSubjectConfirmation(AssertionContext $context, SubjectConfirmation $subjectConfirmation)
{
$recipient = $subjectConfirmation->getSubjectConfirmationData()->getRecipient();
if (null == $recipient) {
$message = 'Bearer SubjectConfirmation must contain Recipient attribute';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
$criteriaSet = new CriteriaSet([
new DescriptorTypeCriteria(SpSsoDescriptor::class),
new ServiceTypeCriteria(AssertionConsumerService::class),
new LocationCriteria($recipient),
]);
$ownEntityDescriptor = $context->getProfileContext()->getOwnEntityDescriptor();
$arrEndpoints = $this->endpointResolver->resolve($criteriaSet, $ownEntityDescriptor->getAllEndpoints());
if (empty($arrEndpoints)) {
$message = sprintf("Recipient '%s' does not match SP descriptor", $recipient);
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this, [
'recipient' => $recipient,
]));
throw new LightSamlContextException($context, $message);
}
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace LightSaml\Action\Assertion\Inbound;
use LightSaml\Action\Assertion\AbstractAssertionAction;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Store\Id\IdStoreInterface;
use Psr\Log\LoggerInterface;
/**
* 4.1.4.5 POST-Specific Processing Rules
* The service provider MUST ensure that bearer assertions are not replayed, by maintaining the set of used
* ID values for the length of time for which the assertion would be considered valid based on the
* NotOnOrAfter attribute in the <SubjectConfirmationData>.
*/
class RepeatedIdValidatorAction extends AbstractAssertionAction
{
/** @var IdStoreInterface */
protected $idStore;
public function __construct(LoggerInterface $logger, IdStoreInterface $idStore)
{
parent::__construct($logger);
$this->idStore = $idStore;
}
/**
* @return void
*/
protected function doExecute(AssertionContext $context)
{
if ($context->getAssertion()->hasBearerSubject()) {
$this->validateBearerAssertion($context);
}
}
/**
* @throws \LightSaml\Error\LightSamlContextException
*/
protected function validateBearerAssertion(AssertionContext $context)
{
if (null == $context->getAssertion()->getId()) {
$message = 'Bearer Assertion must have ID attribute';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
if (null == $context->getAssertion()->getIssuer()) {
$message = 'Bearer Assertion must have Issuer element';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
if ($this->idStore->has($context->getAssertion()->getIssuer()->getValue(), $context->getAssertion()->getId())) {
$message = sprintf(
"Repeated assertion id '%s' of issuer '%s'",
$context->getAssertion()->getId(),
$context->getAssertion()->getIssuer()->getValue()
);
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this, [
'id' => $context->getAssertion()->getId(),
'issuer' => $context->getAssertion()->getIssuer()->getValue(),
]));
throw new LightSamlContextException($context, $message);
}
$this->idStore->set(
$context->getAssertion()->getIssuer()->getValue(),
$context->getAssertion()->getId(),
$this->getIdExpiryTime($context)
);
}
/**
* @throws \LogicException
* @throws \LightSaml\Error\LightSamlValidationException
*
* @return \DateTime
*/
protected function getIdExpiryTime(AssertionContext $context)
{
/** @var \DateTime $result */
$result = null;
$bearerConfirmations = $context->getAssertion()->getSubject()->getBearerConfirmations();
if (null == $bearerConfirmations) {
throw new \LogicException('Bearer assertion must have bearer subject confirmations');
}
foreach ($bearerConfirmations as $subjectConfirmation) {
if (null == $subjectConfirmation->getSubjectConfirmationData()) {
$message = 'Bearer SubjectConfirmation must have SubjectConfirmationData element';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
$dt = $subjectConfirmation->getSubjectConfirmationData()->getNotOnOrAfterDateTime();
if (null == $dt) {
$message = 'Bearer SubjectConfirmation must have NotOnOrAfter attribute';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
if (null == $result || $result->getTimestamp() < $dt->getTimestamp()) {
$result = $dt;
}
}
if (null == $result) {
$message = 'Unable to find NotOnOrAfter attribute in bearer assertion';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
return $result;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace LightSaml\Action\Assertion\Inbound;
use LightSaml\Action\Assertion\AbstractAssertionAction;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Provider\TimeProvider\TimeProviderInterface;
use LightSaml\Validator\Model\Assertion\AssertionTimeValidatorInterface;
use Psr\Log\LoggerInterface;
class TimeValidatorAction extends AbstractAssertionAction
{
/** @var AssertionTimeValidatorInterface */
protected $assertionTimeValidator;
/** @var TimeProviderInterface */
protected $timeProvider;
/** @var int */
protected $allowedSecondsSkew;
/**
* @param int $allowedSecondsSkew
*/
public function __construct(
LoggerInterface $logger,
AssertionTimeValidatorInterface $assertionTimeValidator,
TimeProviderInterface $timeProvider,
$allowedSecondsSkew = 120
) {
parent::__construct($logger);
$this->assertionTimeValidator = $assertionTimeValidator;
$this->timeProvider = $timeProvider;
$this->allowedSecondsSkew = $allowedSecondsSkew;
}
/**
* @return void
*/
protected function doExecute(AssertionContext $context)
{
$this->assertionTimeValidator->validateTimeRestrictions(
$context->getAssertion(),
$this->timeProvider->getTimestamp(),
$this->allowedSecondsSkew
);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace LightSaml\Action;
use LightSaml\Context\ContextInterface;
use LightSaml\Context\Profile\ExceptionContext;
use LightSaml\Context\Profile\ProfileContexts;
class CatchableErrorAction implements ActionInterface
{
/** @var ActionInterface */
protected $mainAction;
/** @var ActionInterface */
protected $errorAction;
public function __construct(ActionInterface $mainAction, ActionInterface $errorAction)
{
$this->mainAction = $mainAction;
$this->errorAction = $errorAction;
}
/**
* @return void
*/
public function execute(ContextInterface $context)
{
try {
$this->mainAction->execute($context);
} catch (\Exception $ex) {
/** @var ExceptionContext $exceptionContext */
$exceptionContext = $context->getSubContext(ProfileContexts::EXCEPTION, ExceptionContext::class);
$exceptionContext->addException($ex);
$this->errorAction->execute($context);
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace LightSaml\Action;
use LightSaml\Context\ContextInterface;
class CompositeAction implements ActionInterface, DebugPrintTreeActionInterface, CompositeActionInterface
{
/** @var ActionInterface[] */
protected $children = [];
/**
* @param ActionInterface[] $children
*/
public function __construct(array $children = [])
{
foreach ($children as $action) {
$this->add($action);
}
}
/**
* @return ActionInterface[]
*/
public function getChildren()
{
return $this->children;
}
/**
* @return CompositeAction
*/
public function add(ActionInterface $action)
{
$this->children[] = $action;
return $this;
}
/**
* @param callable $callable
*
* @return ActionInterface|null
*/
public function map($callable)
{
foreach ($this->children as $k => $action) {
$newAction = call_user_func($callable, $action);
if ($newAction) {
$this->children[$k] = $newAction;
}
}
}
/**
* @return void
*/
public function execute(ContextInterface $context)
{
foreach ($this->children as $action) {
$action->execute($context);
}
}
/**
* @return array
*/
public function debugPrintTree()
{
$arr = [];
foreach ($this->children as $childAction) {
if ($childAction instanceof DebugPrintTreeActionInterface) {
$arr = array_merge($arr, $childAction->debugPrintTree());
} else {
$arr = array_merge($arr, [get_class($childAction) => []]);
}
}
$result = [
static::class => $arr,
];
return $result;
}
/**
* @return string
*/
public function __toString()
{
return json_encode($this->debugPrintTree(), JSON_PRETTY_PRINT);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace LightSaml\Action;
interface CompositeActionInterface extends ActionInterface
{
/**
* @return CompositeActionInterface
*/
public function add(ActionInterface $action);
/**
* @param callable $callable
*
* @return ActionInterface|null
*/
public function map($callable);
}

View File

@@ -0,0 +1,11 @@
<?php
namespace LightSaml\Action;
interface DebugPrintTreeActionInterface
{
/**
* @return array
*/
public function debugPrintTree();
}

View File

@@ -0,0 +1,26 @@
<?php
namespace LightSaml\Action;
use LightSaml\Context\ContextInterface;
use LightSaml\Event\ActionOccurred;
use Psr\EventDispatcher\EventDispatcherInterface;
class DispatchEventAction implements ActionInterface
{
/** @var EventDispatcherInterface */
protected $eventDispatcher;
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* @return void
*/
public function execute(ContextInterface $context)
{
$this->eventDispatcher->dispatch(new ActionOccurred($context));
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace LightSaml\Action;
use LightSaml\Context\ContextInterface;
use Psr\Log\LoggerInterface;
class LoggableAction extends WrappedAction
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(ActionInterface $action, LoggerInterface $logger)
{
parent::__construct($action);
$this->logger = $logger;
}
protected function beforeAction(ContextInterface $context)
{
$this->logger->debug(sprintf('Executing action "%s"', get_class($this->action)), [
'context' => $context,
'action' => $this->action,
]);
}
protected function afterAction(ContextInterface $context)
{
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace LightSaml\Action;
use LightSaml\Context\ContextInterface;
class NullAction implements ActionInterface
{
/**
* @return void
*/
public function execute(ContextInterface $context)
{
// null
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace LightSaml\Action\Profile;
use LightSaml\Action\ActionInterface;
use LightSaml\Context\ContextInterface;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
use Psr\Log\LoggerInterface;
abstract class AbstractProfileAction implements ActionInterface
{
/** @var LoggerInterface */
protected $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* @return void
*/
public function execute(ContextInterface $context)
{
if ($context instanceof ProfileContext) {
$this->doExecute($context);
} else {
$message = sprintf('Expected ProfileContext but got %s', get_class($context));
$this->logger->emergency($message, ['context' => $context]);
throw new LightSamlContextException($context, $message);
}
}
abstract protected function doExecute(ProfileContext $context);
}

View File

@@ -0,0 +1,43 @@
<?php
namespace LightSaml\Action\Profile\Entity;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Context\Profile\ProfileContexts;
use LightSaml\Model\Context\SerializationContext;
use Symfony\Component\HttpFoundation\Response;
class SerializeOwnEntityAction extends AbstractProfileAction
{
/** @var string[] */
protected $supportedContextTypes = ['application/samlmetadata+xml', 'application/xml', 'text/xml'];
protected function doExecute(ProfileContext $context)
{
$ownEntityDescriptor = $context->getOwnEntityDescriptor();
/** @var SerializationContext $serializationContext */
$serializationContext = $context->getSubContext(ProfileContexts::SERIALIZATION, SerializationContext::class);
$serializationContext->getDocument()->formatOutput = true;
$ownEntityDescriptor->serialize($serializationContext->getDocument(), $serializationContext);
$xml = $serializationContext->getDocument()->saveXML();
$response = new Response($xml);
$contentType = 'text/xml';
$acceptableContentTypes = array_flip($context->getHttpRequest()->getAcceptableContentTypes());
foreach ($this->supportedContextTypes as $supportedContentType) {
if (isset($acceptableContentTypes[$supportedContentType])) {
$contentType = $supportedContentType;
break;
}
}
$response->headers->replace(['Content-Type' => $contentType]);
$context->getHttpResponseContext()->setResponse($response);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace LightSaml\Action\Profile;
use LightSaml\Context\ContextInterface;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Context\Profile\ProfileContexts;
use LightSaml\Context\Profile\RequestStateContext;
use LightSaml\Store\Request\RequestStateStoreInterface;
use Psr\Log\LoggerInterface;
class FlushRequestStatesAction extends AbstractProfileAction
{
/** @var RequestStateStoreInterface */
protected $requestStore;
public function __construct(LoggerInterface $logger, RequestStateStoreInterface $requestStore)
{
parent::__construct($logger);
$this->requestStore = $requestStore;
}
/**
* @return void
*/
protected function doExecute(ProfileContext $context)
{
$this->flush($context->getInboundContext()->getSubContext(ProfileContexts::REQUEST_STATE, null));
foreach ($context as $child) {
if ($child instanceof AssertionContext) {
$this->flush($child->getSubContext(ProfileContexts::REQUEST_STATE, null));
}
}
}
/**
* @param ContextInterface|null $requestStateContext
*/
protected function flush($requestStateContext = null)
{
if (
$requestStateContext instanceof RequestStateContext &&
$requestStateContext->getRequestState() &&
$requestStateContext->getRequestState()->getId()
) {
$existed = $this->requestStore->remove($requestStateContext->getRequestState()->getId());
if ($existed) {
$this->logger->debug(
sprintf('Removed request state "%s"', $requestStateContext->getRequestState()->getId()),
LogHelper::getActionContext($requestStateContext, $this)
);
} else {
$this->logger->warning(
sprintf('Request state "%s" does not exist', $requestStateContext->getRequestState()->getId()),
LogHelper::getActionContext($requestStateContext, $this)
);
}
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Criteria\CriteriaSet;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Model\Metadata\IdpSsoDescriptor;
use LightSaml\Model\Metadata\SpSsoDescriptor;
use LightSaml\Resolver\Endpoint\Criteria\DescriptorTypeCriteria;
use LightSaml\Resolver\Endpoint\Criteria\LocationCriteria;
use LightSaml\Resolver\Endpoint\EndpointResolverInterface;
use Psr\Log\LoggerInterface;
abstract class AbstractDestinationValidatorAction extends AbstractProfileAction
{
/** @var EndpointResolverInterface */
protected $endpointResolver;
public function __construct(LoggerInterface $logger, EndpointResolverInterface $endpointResolver)
{
parent::__construct($logger);
$this->endpointResolver = $endpointResolver;
}
/**
* @return void
*/
protected function doExecute(ProfileContext $context)
{
$message = MessageContextHelper::asSamlMessage($context->getInboundContext());
$destination = $message->getDestination();
if (null == $destination) {
return;
}
$criteriaSet = $this->getCriteriaSet($context, $destination);
$endpoints = $this->endpointResolver->resolve($criteriaSet, $context->getOwnEntityDescriptor()->getAllEndpoints());
if ($endpoints) {
return;
}
$message = sprintf('Invalid inbound message destination "%s"', $destination);
$this->logger->emergency($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
/**
* @param string $location
*
* @return CriteriaSet
*/
protected function getCriteriaSet(ProfileContext $context, $location)
{
$criteriaSet = new CriteriaSet([
new DescriptorTypeCriteria(
ProfileContext::ROLE_IDP === $context->getOwnRole()
? IdpSsoDescriptor::class
: SpSsoDescriptor::class
),
new LocationCriteria($location),
]);
return $criteriaSet;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
use Psr\Log\LoggerInterface;
class AssertBindingTypeAction extends AbstractProfileAction
{
/** @var string[] */
protected $expectedBindingTypes;
/**
* @param string[] $expectedBindingTypes
*/
public function __construct(LoggerInterface $logger, array $expectedBindingTypes)
{
parent::__construct($logger);
$this->expectedBindingTypes = $expectedBindingTypes;
}
protected function doExecute(ProfileContext $context)
{
if (false === in_array($context->getInboundContext()->getBindingType(), $this->expectedBindingTypes)) {
$message = sprintf(
'Unexpected binding type "%s" - expected binding types are: %s',
$context->getInboundContext()->getBindingType(),
implode(' ', $this->expectedBindingTypes)
);
$this->logger->critical($message, LogHelper::getActionErrorContext($context, $this, [
'actualBindingType' => $context->getInboundContext()->getBindingType(),
'expectedBindingTypes' => $this->expectedBindingTypes,
]));
throw new LightSamlContextException($context, $message);
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Message;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Criteria\CriteriaSet;
use LightSaml\Model\Metadata\SingleSignOnService;
use LightSaml\Resolver\Endpoint\Criteria\ServiceTypeCriteria;
class DestinationValidatorAuthnRequestAction extends AbstractDestinationValidatorAction
{
/**
* @param string $location
*
* @return CriteriaSet
*/
protected function getCriteriaSet(ProfileContext $context, $location)
{
$result = parent::getCriteriaSet($context, $location);
$result->add(new ServiceTypeCriteria(SingleSignOnService::class));
return $result;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Message;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Criteria\CriteriaSet;
use LightSaml\Model\Metadata\AssertionConsumerService;
use LightSaml\Resolver\Endpoint\Criteria\ServiceTypeCriteria;
class DestinationValidatorResponseAction extends AbstractDestinationValidatorAction
{
/**
* @param string $location
*
* @return CriteriaSet
*/
protected function getCriteriaSet(ProfileContext $context, $location)
{
$result = parent::getCriteriaSet($context, $location);
$result->add(new ServiceTypeCriteria(AssertionConsumerService::class));
return $result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
class EntityIdFromMessageIssuerAction extends AbstractProfileAction
{
protected function doExecute(ProfileContext $context)
{
$message = MessageContextHelper::asSamlMessage($context->getInboundContext());
if (null == $message->getIssuer()) {
throw new LightSamlContextException($context, 'Inbound messages does not have Issuer');
}
$context->getPartyEntityContext()->setEntityId($message->getIssuer()->getValue());
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Error\LightSamlValidationException;
use LightSaml\Validator\Model\NameId\NameIdValidatorInterface;
use Psr\Log\LoggerInterface;
class IssuerValidatorAction extends AbstractProfileAction
{
/** @var NameIdValidatorInterface */
protected $nameIdValidator;
/** @var string */
protected $allowedFormat;
/**
* @param string $allowedFormat
*/
public function __construct(LoggerInterface $logger, NameIdValidatorInterface $nameIdValidator, $allowedFormat)
{
parent::__construct($logger);
$this->nameIdValidator = $nameIdValidator;
$this->allowedFormat = $allowedFormat;
}
/**
* @return void
*/
protected function doExecute(ProfileContext $context)
{
$message = MessageContextHelper::asSamlMessage($context->getInboundContext());
if (false == $message->getIssuer()) {
$message = 'Inbound message must have Issuer element';
$this->logger->emergency($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
if (
$this->allowedFormat &&
$message->getIssuer()->getValue() &&
$message->getIssuer()->getFormat() &&
$message->getIssuer()->getFormat() != $this->allowedFormat
) {
$message = sprintf(
"Response Issuer Format if set must have value '%s' but it was '%s'",
$this->allowedFormat,
$message->getIssuer()->getFormat()
);
$this->logger->emergency($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
try {
$this->nameIdValidator->validateNameId($message->getIssuer());
} catch (LightSamlValidationException $ex) {
throw new LightSamlContextException($context, $ex->getMessage(), 0, $ex);
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Credential\Criteria\MetadataCriteria;
use LightSaml\Error\LightSamlModelException;
use LightSaml\Model\XmlDSig\AbstractSignatureReader;
use LightSaml\Validator\Model\Signature\SignatureValidatorInterface;
use Psr\Log\LoggerInterface;
/**
* Validates the signature, if any, of the inbound message.
*/
class MessageSignatureValidatorAction extends AbstractProfileAction
{
/** @var SignatureValidatorInterface */
protected $signatureValidator;
public function __construct(LoggerInterface $logger, SignatureValidatorInterface $signatureValidator)
{
parent::__construct($logger);
$this->signatureValidator = $signatureValidator;
}
/**
* @return void
*/
protected function doExecute(ProfileContext $context)
{
$message = MessageContextHelper::asSamlMessage($context->getInboundContext());
$signature = $message->getSignature();
if (null === $signature) {
$this->logger->debug('Message is not signed', LogHelper::getActionContext($context, $this));
return;
}
if ($signature instanceof AbstractSignatureReader) {
$metadataType = ProfileContext::ROLE_IDP === $context->getOwnRole() ? MetadataCriteria::TYPE_SP : MetadataCriteria::TYPE_IDP;
$credential = $this->signatureValidator->validate($signature, $message->getIssuer()->getValue(), $metadataType);
if ($credential) {
$keyNames = $credential->getKeyNames();
$this->logger->debug(
sprintf('Message signature validated with key "%s"', implode(', ', $keyNames)),
LogHelper::getActionContext($context, $this, [
'credential' => $credential,
])
);
} else {
$this->logger->warning(
'Signature verification was not performed',
LogHelper::getActionContext($context, $this)
);
}
} else {
$message = 'Expected AbstractSignatureReader';
$this->logger->critical($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlModelException($message);
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Binding\BindingFactoryInterface;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlBindingException;
use Psr\Log\LoggerInterface;
/**
* Receives message from HTTP Request into inbound context,
* optionally enforces biding type to the one specified in the inbound context.
*/
class ReceiveMessageAction extends AbstractProfileAction
{
/** @var BindingFactoryInterface */
protected $bindingFactory;
public function __construct(LoggerInterface $logger, BindingFactoryInterface $bindingFactory)
{
parent::__construct($logger);
$this->bindingFactory = $bindingFactory;
}
/**
* @return void
*/
protected function doExecute(ProfileContext $context)
{
$bindingType = $this->bindingFactory->detectBindingType($context->getHttpRequest());
if (null == $bindingType) {
$message = 'Unable to resolve binding type, invalid or unsupported http request';
$this->logger->critical($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlBindingException($message);
}
$this->logger->debug(sprintf('Detected binding type: %s', $bindingType), LogHelper::getActionContext($context, $this));
$binding = $this->bindingFactory->create($bindingType);
$binding->receive($context->getHttpRequest(), $context->getInboundContext());
$context->getInboundContext()->setBindingType($bindingType);
$this->logger->info(
'Received message',
LogHelper::getActionContext($context, $this, [
'message' => $context->getInboundContext()->getDeserializationContext()->getDocument()->saveXML(),
])
);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Meta\TrustOptions\TrustOptions;
use LightSaml\Store\EntityDescriptor\EntityDescriptorStoreInterface;
use LightSaml\Store\TrustOptions\TrustOptionsStoreInterface;
use Psr\Log\LoggerInterface;
/**
* Looks up inbound message Issuer in entity descriptor providers and sets it to the party context.
*/
class ResolvePartyEntityIdAction extends AbstractProfileAction
{
/** @var EntityDescriptorStoreInterface */
private $spEntityDescriptorProvider;
/** @var EntityDescriptorStoreInterface */
private $idpEntityDescriptorProvider;
/** @var TrustOptionsStoreInterface */
protected $trustOptionsProvider;
public function __construct(
LoggerInterface $logger,
EntityDescriptorStoreInterface $spEntityDescriptorProvider,
EntityDescriptorStoreInterface $idpEntityDescriptorProvider,
TrustOptionsStoreInterface $trustOptionsProvider
) {
parent::__construct($logger);
$this->spEntityDescriptorProvider = $spEntityDescriptorProvider;
$this->idpEntityDescriptorProvider = $idpEntityDescriptorProvider;
$this->trustOptionsProvider = $trustOptionsProvider;
}
protected function doExecute(ProfileContext $context)
{
$partyContext = $context->getPartyEntityContext();
if ($partyContext->getEntityDescriptor() && $partyContext->getTrustOptions()) {
$this->logger->debug(
sprintf('Party EntityDescriptor and TrustOptions already set for "%s"', $partyContext->getEntityDescriptor()->getEntityID()),
LogHelper::getActionContext($context, $this, [
'partyEntityId' => $partyContext->getEntityDescriptor()->getEntityID(),
])
);
return;
}
$entityId = $partyContext->getEntityDescriptor() ? $partyContext->getEntityDescriptor()->getEntityID() : null;
$entityId = $entityId ? $entityId : $partyContext->getEntityId();
if (null == $entityId) {
$message = 'EntityID is not set in the party context';
$this->logger->critical($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
if (null == $partyContext->getEntityDescriptor()) {
$partyEntityDescriptor = $this->getPartyEntityDescriptor(
$context,
ProfileContext::ROLE_IDP === $context->getOwnRole()
? $this->spEntityDescriptorProvider
: $this->idpEntityDescriptorProvider,
$context->getPartyEntityContext()->getEntityId()
);
$partyContext->setEntityDescriptor($partyEntityDescriptor);
$this->logger->debug(
sprintf('Known issuer resolved: "%s"', $partyEntityDescriptor->getEntityID()),
LogHelper::getActionContext($context, $this, [
'partyEntityId' => $partyEntityDescriptor->getEntityID(),
])
);
}
if (null == $partyContext->getTrustOptions()) {
$trustOptions = $this->trustOptionsProvider->get($partyContext->getEntityDescriptor()->getEntityID());
if (null === $trustOptions) {
$trustOptions = new TrustOptions();
}
$partyContext->setTrustOptions($trustOptions);
}
}
/**
* @param string $entityId
*
* @return \LightSaml\Model\Metadata\EntityDescriptor
*/
protected function getPartyEntityDescriptor(
ProfileContext $context,
EntityDescriptorStoreInterface $entityDescriptorProvider,
$entityId
) {
$partyEntityDescriptor = $entityDescriptorProvider->get($entityId);
if (null === $partyEntityDescriptor) {
$message = sprintf("Unknown issuer '%s'", $entityId);
$this->logger->emergency($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
return $partyEntityDescriptor;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Response;
use LightSaml\Action\ActionInterface;
use LightSaml\Action\DebugPrintTreeActionInterface;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use Psr\Log\LoggerInterface;
class AssertionAction extends AbstractProfileAction implements DebugPrintTreeActionInterface
{
/** @var ActionInterface */
private $assertionAction;
public function __construct(LoggerInterface $logger, ActionInterface $assertionAction)
{
parent::__construct($logger);
$this->assertionAction = $assertionAction;
}
protected function doExecute(ProfileContext $context)
{
$response = MessageContextHelper::asResponse($context->getInboundContext());
foreach ($response->getAllAssertions() as $index => $assertion) {
$name = sprintf('assertion_%s', $index);
/** @var AssertionContext $assertionContext */
$assertionContext = $context->getSubContext($name, AssertionContext::class);
$assertionContext
->setAssertion($assertion)
->setId($name)
;
$this->assertionAction->execute($assertionContext);
}
}
/**
* @param int $depth
*
* @return array
*/
public function debugPrintTree($depth = 0)
{
$arr = [];
if ($this->assertionAction instanceof DebugPrintTreeActionInterface) {
$arr = array_merge($arr, $this->assertionAction->debugPrintTree());
} else {
$arr[get_class($this->assertionAction)] = [];
}
$result = [
static::class => $arr,
];
return $result;
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Response;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Credential\CredentialInterface;
use LightSaml\Credential\Criteria\EntityIdCriteria;
use LightSaml\Credential\Criteria\MetadataCriteria;
use LightSaml\Credential\Criteria\UsageCriteria;
use LightSaml\Credential\UsageType;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Model\Assertion\EncryptedAssertionReader;
use LightSaml\Model\Context\DeserializationContext;
use LightSaml\Resolver\Credential\CredentialResolverInterface;
use LightSaml\SamlConstants;
use Psr\Log\LoggerInterface;
class DecryptAssertionsAction extends AbstractProfileAction
{
/** @var CredentialResolverInterface */
protected $credentialResolver;
public function __construct(LoggerInterface $logger, CredentialResolverInterface $credentialResolver)
{
parent::__construct($logger);
$this->credentialResolver = $credentialResolver;
}
protected function doExecute(ProfileContext $context)
{
$response = MessageContextHelper::asResponse($context->getInboundContext());
if (0 === count($response->getAllEncryptedAssertions())) {
$this->logger->debug('Response has no encrypted assertions', LogHelper::getActionContext($context, $this));
return;
}
$ownEntityDescriptor = $context->getOwnEntityDescriptor();
$query = $this->credentialResolver->query();
$query
->add(new EntityIdCriteria($ownEntityDescriptor->getEntityID()))
->add(new MetadataCriteria(
ProfileContext::ROLE_IDP === $context->getOwnRole()
? MetadataCriteria::TYPE_IDP
: MetadataCriteria::TYPE_SP,
SamlConstants::PROTOCOL_SAML2
))
->add(new UsageCriteria(UsageType::ENCRYPTION))
;
$query->resolve();
$privateKeys = $query->getPrivateKeys();
if (empty($privateKeys)) {
$message = 'No credentials resolved for assertion decryption';
$this->logger->emergency($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
$this->logger->info('Trusted decryption candidates', LogHelper::getActionContext($context, $this, [
'credentials' => array_map(function (CredentialInterface $credential) {
return sprintf(
"Entity: '%s'; PK X509 Thumb: '%s'",
$credential->getEntityId(),
$credential->getPublicKey() ? $credential->getPublicKey()->getX509Thumbprint() : ''
);
}, $privateKeys),
]));
foreach ($response->getAllEncryptedAssertions() as $index => $encryptedAssertion) {
if ($encryptedAssertion instanceof EncryptedAssertionReader) {
$name = sprintf('assertion_encrypted_%s', $index);
/** @var DeserializationContext $deserializationContext */
$deserializationContext = $context->getInboundContext()->getSubContext($name, DeserializationContext::class);
$assertion = $encryptedAssertion->decryptMultiAssertion($privateKeys, $deserializationContext);
$response->addAssertion($assertion);
$this->logger->info(
'Assertion decrypted',
LogHelper::getActionContext($context, $this, [
'assertion' => $deserializationContext->getDocument()->saveXML(),
])
);
}
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Response;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
class HasAssertionsValidatorAction extends AbstractProfileAction
{
protected function doExecute(ProfileContext $context)
{
$response = MessageContextHelper::asResponse($context->getInboundContext());
if ($response->getAllAssertions()) {
return;
}
$message = 'Response must contain at least one assertion';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Response;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
class HasAuthnStatementValidatorAction extends AbstractProfileAction
{
protected function doExecute(ProfileContext $context)
{
$response = MessageContextHelper::asResponse($context->getInboundContext());
foreach ($response->getAllAssertions() as $assertion) {
if ($assertion->getAllAuthnStatements()) {
return;
}
}
$message = 'Response must have at least one Assertion containing AuthnStatement element';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Response;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
class HasBearerAssertionsValidatorAction extends AbstractProfileAction
{
protected function doExecute(ProfileContext $context)
{
$response = MessageContextHelper::asResponse($context->getInboundContext());
if ($response->getBearerAssertions()) {
return;
}
$message = 'Response must contain at least one bearer assertion';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace LightSaml\Action\Profile\Inbound\Response;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Resolver\Session\SessionProcessorInterface;
use Psr\Log\LoggerInterface;
class SpSsoStateAction extends AbstractProfileAction
{
/** @var SessionProcessorInterface */
private $sessionProcessor;
public function __construct(LoggerInterface $logger, SessionProcessorInterface $sessionProcessor)
{
parent::__construct($logger);
$this->sessionProcessor = $sessionProcessor;
}
protected function doExecute(ProfileContext $context)
{
$response = MessageContextHelper::asResponse($context->getInboundContext());
$this->sessionProcessor->processAssertions(
$response->getAllAssertions(),
$context->getOwnEntityDescriptor()->getEntityID(),
$context->getPartyEntityDescriptor()->getEntityID()
);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace LightSaml\Action\Profile\Inbound\StatusResponse;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Context\Profile\ProfileContexts;
use LightSaml\Context\Profile\RequestStateContext;
use LightSaml\Error\LightSamlContextException;
use LightSaml\State\Request\RequestStateParameters;
use LightSaml\Store\Request\RequestStateStoreInterface;
use Psr\Log\LoggerInterface;
class InResponseToValidatorAction extends AbstractProfileAction
{
/** @var RequestStateStoreInterface */
protected $requestStore;
public function __construct(LoggerInterface $logger, RequestStateStoreInterface $requestStore)
{
parent::__construct($logger);
$this->requestStore = $requestStore;
}
protected function doExecute(ProfileContext $context)
{
$response = MessageContextHelper::asStatusResponse($context->getInboundContext());
$inResponseTo = $response->getInResponseTo();
if ($inResponseTo) {
$requestState = $this->requestStore->get($inResponseTo);
if (null == $requestState) {
$message = sprintf("Unknown InResponseTo '%s'", $inResponseTo);
$this->logger->critical($message, LogHelper::getActionErrorContext($context, $this, [
'in_response_to' => $inResponseTo,
]));
throw new LightSamlContextException($context, $message);
}
$sentToParty = $requestState->getParameters()->get(RequestStateParameters::PARTY);
if ($sentToParty && $response->getIssuer() && $response->getIssuer()->getValue() != $sentToParty) {
$message = sprintf('AuthnRequest with id "%s" sent to party "%s" but StatusResponse for that request issued by party "%s"', $inResponseTo, $sentToParty, $response->getIssuer()->getValue());
$this->logger->critical($message, LogHelper::getActionErrorContext($context, $this, [
'sent_to' => $sentToParty,
'received_from' => $response->getIssuer()->getValue(),
]));
throw new LightSamlContextException($context, $message);
}
/** @var RequestStateContext $requestStateContext */
$requestStateContext = $context->getInboundContext()->getSubContext(ProfileContexts::REQUEST_STATE, RequestStateContext::class);
$requestStateContext->setRequestState($requestState);
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace LightSaml\Action\Profile\Inbound\StatusResponse;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlAuthenticationException;
use LightSaml\Error\LightSamlContextException;
/**
* Throws LightSamlAuthenticationException if status of inbound message is not successful.
*/
class StatusAction extends AbstractProfileAction
{
protected function doExecute(ProfileContext $context)
{
$statusResponse = MessageContextHelper::asStatusResponse($context->getInboundContext());
if ($statusResponse->getStatus() && $statusResponse->getStatus()->isSuccess()) {
return;
}
if (null == $statusResponse->getStatus()) {
$message = 'Status response does not have Status set';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
$status = $statusResponse->getStatus()->getStatusCode()->getValue();
$status .= "\n" . $statusResponse->getStatus()->getStatusMessage();
if ($statusResponse->getStatus()->getStatusCode()->getStatusCode()) {
$status .= "\n" . $statusResponse->getStatus()->getStatusCode()->getStatusCode()->getValue();
}
$message = 'Unsuccessful SAML response: ' . $status;
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this, ['status' => $status]));
throw new LightSamlAuthenticationException($statusResponse, $message);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace LightSaml\Action\Profile\Outbound\AuthnRequest;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Criteria\CriteriaSet;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Model\Metadata\AssertionConsumerService;
use LightSaml\Model\Metadata\SpSsoDescriptor;
use LightSaml\Resolver\Endpoint\Criteria\BindingCriteria;
use LightSaml\Resolver\Endpoint\Criteria\DescriptorTypeCriteria;
use LightSaml\Resolver\Endpoint\Criteria\ServiceTypeCriteria;
use LightSaml\Resolver\Endpoint\EndpointResolverInterface;
use LightSaml\SamlConstants;
use Psr\Log\LoggerInterface;
// TODO ACSUrlAction not used in profile builder, has to be added
class ACSUrlAction extends AbstractProfileAction
{
/** @var EndpointResolverInterface */
private $endpointResolver;
public function __construct(LoggerInterface $logger, EndpointResolverInterface $endpointResolver)
{
parent::__construct($logger);
$this->endpointResolver = $endpointResolver;
}
protected function doExecute(ProfileContext $context)
{
$ownEntityDescriptor = $context->getOwnEntityDescriptor();
$criteriaSet = new CriteriaSet([
new DescriptorTypeCriteria(SpSsoDescriptor::class),
new ServiceTypeCriteria(AssertionConsumerService::class),
new BindingCriteria([SamlConstants::BINDING_SAML2_HTTP_POST]),
]);
$endpoints = $this->endpointResolver->resolve($criteriaSet, $ownEntityDescriptor->getAllEndpoints());
if (empty($endpoints)) {
$message = 'Missing ACS Service with HTTP POST binding in own SP SSO Descriptor';
$this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
MessageContextHelper::asAuthnRequest($context->getOutboundContext())
->setAssertionConsumerServiceURL($endpoints[0]->getEndpoint()->getLocation());
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace LightSaml\Action\Profile\Outbound\AuthnRequest;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Model\Protocol\AuthnRequest;
/**
* Creates empty AuthnRequest in outbound context.
*/
class CreateAuthnRequestAction extends AbstractProfileAction
{
protected function doExecute(ProfileContext $context)
{
$context->getOutboundContext()->setMessage(new AuthnRequest());
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Model\Assertion\Issuer;
use LightSaml\SamlConstants;
/**
* Sets the Issuer of the outbound message to the value of own entityID.
*/
class CreateMessageIssuerAction extends AbstractProfileAction
{
/**
* @return void
*/
protected function doExecute(ProfileContext $context)
{
$ownEntityDescriptor = $context->getOwnEntityDescriptor();
$issuer = new Issuer($ownEntityDescriptor->getEntityID());
$issuer->setFormat(SamlConstants::NAME_ID_FORMAT_ENTITY);
MessageContextHelper::asSamlMessage($context->getOutboundContext())
->setIssuer($issuer);
$this->logger->debug(
sprintf('Issuer set to "%s"', $ownEntityDescriptor->getEntityID()),
LogHelper::getActionContext($context, $this)
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
/**
* Sets destination of the outbound message to the value of location of endpoint from the context.
*/
class DestinationAction extends AbstractProfileAction
{
/**
* @return void
*/
protected function doExecute(ProfileContext $context)
{
$endpoint = $context->getEndpoint();
MessageContextHelper::asSamlMessage($context->getOutboundContext())
->setDestination($endpoint->getLocation());
$this->logger->debug(
sprintf('Destination set to "%s"', $endpoint->getLocation()),
LogHelper::getActionContext($context, $this)
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\ProfileContext;
class ForwardRelayStateAction extends AbstractProfileAction
{
protected function doExecute(ProfileContext $context)
{
if (null == $context->getInboundContext()->getMessage()) {
return;
}
if ($context->getInboundMessage()->getRelayState()) {
$this->logger->debug(sprintf('Forwarding relay state from inbound message: "%s"', $context->getInboundMessage()->getRelayState()));
$context->getOutboundMessage()->setRelayState(
$context->getInboundMessage()->getRelayState()
);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Helper;
/**
* Sets the ID of the message in the outbound context.
*/
class MessageIdAction extends AbstractProfileAction
{
protected function doExecute(ProfileContext $context)
{
$id = Helper::generateID();
MessageContextHelper::asSamlMessage($context->getOutboundContext())
->setId($id);
$this->logger->info(
sprintf('Message ID set to "%s"', $id),
LogHelper::getActionContext($context, $this, ['message_id' => $id])
);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Provider\TimeProvider\TimeProviderInterface;
use Psr\Log\LoggerInterface;
/**
* Sets outbound message IssueInstant to the value provided by given time provider.
*/
class MessageIssueInstantAction extends AbstractProfileAction
{
/** @var TimeProviderInterface */
protected $timeProvider;
public function __construct(LoggerInterface $logger, TimeProviderInterface $timeProvider)
{
parent::__construct($logger);
$this->timeProvider = $timeProvider;
}
/**
* @return void
*/
protected function doExecute(ProfileContext $context)
{
MessageContextHelper::asSamlMessage($context->getOutboundContext())
->setIssueInstant($this->timeProvider->getTimestamp());
$this->logger->info(
sprintf('Message IssueInstant set to "%s"', MessageContextHelper::asSamlMessage($context->getOutboundContext())->getIssueInstantString()),
LogHelper::getActionContext($context, $this)
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use Psr\Log\LoggerInterface;
/**
* Sets the Version of the outbound message to the given value.
*/
class MessageVersionAction extends AbstractProfileAction
{
/** @var string */
private $version;
/**
* @param string $version
*/
public function __construct(LoggerInterface $logger, $version)
{
parent::__construct($logger);
$this->version = $version;
}
/**
* @return void
*/
protected function doExecute(ProfileContext $context)
{
MessageContextHelper::asSamlMessage($context->getOutboundContext())
->setVersion($this->version);
$this->logger->debug(
sprintf('Message Version set to "%s"', $this->version),
LogHelper::getActionContext($context, $this)
);
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Criteria\CriteriaSet;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Model\Metadata\EndpointReference;
use LightSaml\Model\Metadata\IdpSsoDescriptor;
use LightSaml\Model\Metadata\SpSsoDescriptor;
use LightSaml\Model\Protocol\AuthnRequest;
use LightSaml\Resolver\Endpoint\Criteria\BindingCriteria;
use LightSaml\Resolver\Endpoint\Criteria\DescriptorTypeCriteria;
use LightSaml\Resolver\Endpoint\Criteria\IndexCriteria;
use LightSaml\Resolver\Endpoint\Criteria\LocationCriteria;
use LightSaml\Resolver\Endpoint\Criteria\ServiceTypeCriteria;
use LightSaml\Resolver\Endpoint\EndpointResolverInterface;
use LightSaml\SamlConstants;
use Psr\Log\LoggerInterface;
/**
* Determines to which endpoint outbound message will be sent.
*/
abstract class ResolveEndpointBaseAction extends AbstractProfileAction
{
/** @var EndpointResolverInterface */
protected $endpointResolver;
public function __construct(LoggerInterface $logger, EndpointResolverInterface $endpointResolver)
{
parent::__construct($logger);
$this->endpointResolver = $endpointResolver;
}
protected function doExecute(ProfileContext $context)
{
if ($context->getEndpointContext()->getEndpoint()) {
$this->logger->debug(
sprintf(
'Endpoint already set with location "%s" and binding "%s"',
$context->getEndpoint()->getLocation(),
$context->getEndpoint()->getBinding()
),
LogHelper::getActionContext($context, $this, [
'endpointLocation' => $context->getEndpoint()->getLocation(),
'endpointBinding' => $context->getEndpoint()->getBinding(),
])
);
return;
}
$criteriaSet = $this->getCriteriaSet($context);
$message = $context->getInboundContext()->getMessage();
if ($message instanceof AuthnRequest) {
if (null !== $message->getAssertionConsumerServiceIndex()) {
$criteriaSet->add(new IndexCriteria($message->getAssertionConsumerServiceIndex()));
}
if (null !== $message->getAssertionConsumerServiceURL()) {
$criteriaSet->add(new LocationCriteria($message->getAssertionConsumerServiceURL()));
}
}
$candidates = $this->endpointResolver->resolve($criteriaSet, $context->getPartyEntityDescriptor()->getAllEndpoints());
/** @var EndpointReference $endpointReference */
$endpointReference = array_shift($candidates);
if (null == $endpointReference) {
$message = sprintf(
"Unable to determine endpoint for entity '%s'",
$context->getPartyEntityDescriptor()->getEntityID()
);
$this->logger->emergency($message, LogHelper::getActionErrorContext($context, $this));
throw new LightSamlContextException($context, $message);
}
$this->logger->debug(
sprintf(
'Endpoint resolved to location "%s" and binding "%s"',
$endpointReference->getEndpoint()->getLocation(),
$endpointReference->getEndpoint()->getBinding()
),
LogHelper::getActionContext($context, $this, [
'endpointLocation' => $endpointReference->getEndpoint()->getLocation(),
'endpointBinding' => $endpointReference->getEndpoint()->getBinding(),
])
);
$context->getEndpointContext()->setEndpoint($endpointReference->getEndpoint());
}
/**
* @return CriteriaSet
*/
protected function getCriteriaSet(ProfileContext $context)
{
$criteriaSet = new CriteriaSet();
$bindings = $this->getBindings($context);
if ($bindings) {
$criteriaSet->add(new BindingCriteria($bindings));
}
$descriptorType = $this->getDescriptorType($context);
if ($descriptorType) {
$criteriaSet->add(new DescriptorTypeCriteria($descriptorType));
}
$serviceType = $this->getServiceType($context);
if ($serviceType) {
$criteriaSet->add(new ServiceTypeCriteria($serviceType));
}
return $criteriaSet;
}
/**
* @return string[]
*/
protected function getBindings(ProfileContext $context)
{
return [
SamlConstants::BINDING_SAML2_HTTP_POST,
SamlConstants::BINDING_SAML2_HTTP_REDIRECT,
];
}
/**
* @return string|null
*/
protected function getDescriptorType(ProfileContext $context)
{
return ProfileContext::ROLE_IDP == $context->getOwnRole()
? SpSsoDescriptor::class
: IdpSsoDescriptor::class;
}
/**
* @return string|null
*/
protected function getServiceType(ProfileContext $context)
{
return;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Model\Metadata\SingleSignOnService;
class ResolveEndpointIdpSsoAction extends ResolveEndpointBaseAction
{
protected function getServiceType(ProfileContext $context)
{
return SingleSignOnService::class;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Model\Metadata\IdpSsoDescriptor;
use LightSaml\Model\Metadata\SingleLogoutService;
use LightSaml\Model\Metadata\SpSsoDescriptor;
class ResolveEndpointSloAction extends ResolveEndpointBaseAction
{
protected function getServiceType(ProfileContext $context)
{
return SingleLogoutService::class;
}
protected function getDescriptorType(ProfileContext $context)
{
$ssoSessionState = $context->getLogoutSsoSessionState();
$ownEntityId = $context->getOwnEntityDescriptor()->getEntityID();
if ($ssoSessionState->getIdpEntityId() == $ownEntityId) {
return SpSsoDescriptor::class;
} elseif ($ssoSessionState->getSpEntityId() == $ownEntityId) {
return IdpSsoDescriptor::class;
} else {
throw new LightSamlContextException($context, 'Unable to resolve logout target descriptor type');
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Model\Metadata\AssertionConsumerService;
class ResolveEndpointSpAcsAction extends ResolveEndpointBaseAction
{
protected function getServiceType(ProfileContext $context)
{
return AssertionConsumerService::class;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Model\Protocol\LogoutRequest;
use LightSaml\State\Request\RequestState;
use LightSaml\State\Request\RequestStateParameters;
use LightSaml\Store\Request\RequestStateStoreInterface;
use Psr\Log\LoggerInterface;
class SaveRequestStateAction extends AbstractProfileAction
{
/** @var RequestStateStoreInterface */
protected $requestStore;
public function __construct(LoggerInterface $logger, RequestStateStoreInterface $requestStore)
{
parent::__construct($logger);
$this->requestStore = $requestStore;
}
protected function doExecute(ProfileContext $context)
{
$message = MessageContextHelper::asSamlMessage($context->getOutboundContext());
$state = new RequestState();
$state->setId($message->getID());
$partyEntityId = $context->getPartyEntityContext() ? $context->getPartyEntityContext()->getEntityId() : '';
if ($context->getPartyEntityContext() && $context->getPartyEntityContext()->getEntityDescriptor()) {
$partyEntityId = $context->getPartyEntityContext()->getEntityDescriptor()->getEntityID();
}
$state->getParameters()->add([
RequestStateParameters::ID => $message->getID(),
RequestStateParameters::TYPE => get_class($message),
RequestStateParameters::TIMESTAMP => $message->getIssueInstantTimestamp(),
RequestStateParameters::PARTY => $partyEntityId,
RequestStateParameters::RELAY_STATE => $message->getRelayState(),
]);
if ($message instanceof LogoutRequest) {
$state->getParameters()->add([
RequestStateParameters::NAME_ID => $message->getNameID()->getValue(),
RequestStateParameters::NAME_ID_FORMAT => $message->getNameID()->getFormat(),
RequestStateParameters::SESSION_INDEX => $message->getSessionIndex(),
]);
}
$this->requestStore->set($state);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Binding\BindingFactoryInterface;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\ProfileContext;
use Psr\Log\LoggerInterface;
class SendMessageAction extends AbstractProfileAction
{
/** @var BindingFactoryInterface */
protected $bindingFactory;
public function __construct(LoggerInterface $logger, BindingFactoryInterface $bindingFactory)
{
parent::__construct($logger);
$this->bindingFactory = $bindingFactory;
}
/**
* @return void
*/
public function doExecute(ProfileContext $context)
{
$binding = $this->bindingFactory->create($context->getEndpoint()->getBinding());
$outboundContext = $context->getOutboundContext();
$context->getHttpResponseContext()->setResponse(
$binding->send($outboundContext)
);
$this->logger->info(
'Sending message',
LogHelper::getActionContext($context, $this, [
'message' => $outboundContext->getSerializationContext()->getDocument()->saveXML(),
])
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
class SetRelayStateAction extends AbstractProfileAction
{
protected function doExecute(ProfileContext $context)
{
if ($context->getRelayState()) {
$this->logger->debug(
sprintf('RelayState from context set to outbound message: "%s"', $context->getRelayState()),
LogHelper::getActionContext($context, $this)
);
MessageContextHelper::asSamlMessage($context->getOutboundContext())
->setRelayState($context->getRelayState());
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace LightSaml\Action\Profile\Outbound\Message;
use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Model\Protocol\AuthnRequest;
use LightSaml\Model\Protocol\LogoutRequest;
use LightSaml\Model\Protocol\Response;
use LightSaml\Resolver\Signature\SignatureResolverInterface;
use Psr\Log\LoggerInterface;
/**
* Signs the outbound message, according to TrustOptions settings.
*/
class SignMessageAction extends AbstractProfileAction
{
/** @var SignatureResolverInterface */
protected $signatureResolver;
public function __construct(LoggerInterface $logger, SignatureResolverInterface $signatureResolver)
{
parent::__construct($logger);
$this->signatureResolver = $signatureResolver;
}
protected function doExecute(ProfileContext $context)
{
$shouldSign = $this->shouldSignMessage($context);
if ($shouldSign) {
$signature = $this->signatureResolver->getSignature($context);
if ($signature) {
MessageContextHelper::asSamlMessage($context->getOutboundContext())
->setSignature($signature)
;
$this->logger->debug(
sprintf('Message signed with fingerprint "%s"', $signature->getCertificate()->getFingerprint()),
LogHelper::getActionContext($context, $this, [
'certificate' => $signature->getCertificate()->getInfo(),
])
);
} else {
$this->logger->critical(
'No signature resolved, although signing enabled',
LogHelper::getActionErrorContext($context, $this, [])
);
}
} else {
$this->logger->debug('Signing disabled', LogHelper::getActionContext($context, $this));
}
}
/**
* @return bool
*/
private function shouldSignMessage(ProfileContext $context)
{
$message = $context->getOutboundMessage();
if ($message instanceof LogoutRequest) {
return true;
}
$trustOptions = $context->getTrustOptions();
if ($message instanceof AuthnRequest) {
return $trustOptions->getSignAuthnRequest();
} elseif ($message instanceof Response) {
return $trustOptions->getSignResponse();
}
throw new \LogicException(sprintf('Unexpected message type "%s"', get_class($message)));
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace LightSaml\Action;
use LightSaml\Context\ContextInterface;
abstract class WrappedAction implements ActionInterface
{
/**
* @var ActionInterface
*/
protected $action;
public function __construct(ActionInterface $action)
{
$this->action = $action;
}
/**
* @return void
*/
public function execute(ContextInterface $context)
{
$this->beforeAction($context);
$this->action->execute($context);
$this->afterAction($context);
}
abstract protected function beforeAction(ContextInterface $context);
abstract protected function afterAction(ContextInterface $context);
}

View File

@@ -0,0 +1,62 @@
<?php
namespace LightSaml\Binding;
use LightSaml\Context\Profile\MessageContext;
use LightSaml\Event\MessageReceived;
use LightSaml\Event\MessageSent;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
abstract class AbstractBinding
{
/** @var EventDispatcherInterface|null */
protected $eventDispatcher;
/**
* @return AbstractBinding
*/
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher = null)
{
$this->eventDispatcher = $eventDispatcher;
return $this;
}
/**
* @return EventDispatcherInterface|null
*/
public function getEventDispatcher()
{
return $this->eventDispatcher;
}
/**
* @param string $messageString
*/
protected function dispatchReceive($messageString)
{
if ($this->eventDispatcher) {
$this->eventDispatcher->dispatch(new MessageReceived($messageString));
}
}
/**
* @param string $messageString
*/
protected function dispatchSend($messageString)
{
if ($this->eventDispatcher) {
$this->eventDispatcher->dispatch(new MessageSent($messageString));
}
}
/**
* @param string|null $destination
*
* @return \Symfony\Component\HttpFoundation\Response
*/
abstract public function send(MessageContext $context, $destination = null);
abstract public function receive(Request $request, MessageContext $context);
}

View File

@@ -0,0 +1,133 @@
<?php
namespace LightSaml\Binding;
use LightSaml\Error\LightSamlBindingException;
use LightSaml\SamlConstants;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
class BindingFactory implements BindingFactoryInterface
{
/** @var EventDispatcherInterface|null */
protected $eventDispatcher;
/**
* @param EventDispatcherInterface $eventDispatcher
*/
public function __construct(EventDispatcherInterface $eventDispatcher = null)
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* @return BindingFactoryInterface
*/
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher = null)
{
$this->eventDispatcher = $eventDispatcher;
return $this;
}
/**
* @return AbstractBinding
*/
public function getBindingByRequest(Request $request)
{
$bindingType = $this->detectBindingType($request);
return $this->create($bindingType);
}
/**
* @param string $bindingType
*
* @throws \LogicException
* @throws \LightSaml\Error\LightSamlBindingException
*
* @return AbstractBinding
*/
public function create($bindingType)
{
$result = null;
switch ($bindingType) {
case SamlConstants::BINDING_SAML2_HTTP_REDIRECT:
$result = new HttpRedirectBinding();
break;
case SamlConstants::BINDING_SAML2_HTTP_POST:
$result = new HttpPostBinding();
break;
case SamlConstants::BINDING_SAML2_HTTP_ARTIFACT:
throw new \LogicException('Artifact binding not implemented');
case SamlConstants::BINDING_SAML2_SOAP:
throw new \LogicException('SOAP binding not implemented');
}
if ($result) {
$result->setEventDispatcher($this->eventDispatcher);
return $result;
}
throw new LightSamlBindingException(sprintf("Unknown binding type '%s'", $bindingType));
}
/**
* @return string|null
*/
public function detectBindingType(Request $request)
{
$requestMethod = trim(strtoupper($request->getMethod()));
if ('GET' == $requestMethod) {
return $this->processGET($request);
} elseif ('POST' == $requestMethod) {
return $this->processPOST($request);
}
return null;
}
/**
* @return string|null
*/
protected function processGET(Request $request)
{
$get = $request->query->all();
if (array_key_exists('SAMLRequest', $get) || array_key_exists('SAMLResponse', $get)) {
return SamlConstants::BINDING_SAML2_HTTP_REDIRECT;
} elseif (array_key_exists('SAMLart', $get)) {
return SamlConstants::BINDING_SAML2_HTTP_ARTIFACT;
}
return null;
}
/**
* @return string|null
*/
protected function processPOST(Request $request)
{
$post = $request->request->all();
if (array_key_exists('SAMLRequest', $post) || array_key_exists('SAMLResponse', $post)) {
return SamlConstants::BINDING_SAML2_HTTP_POST;
} elseif (array_key_exists('SAMLart', $post)) {
return SamlConstants::BINDING_SAML2_HTTP_ARTIFACT;
} else {
if ($contentType = $request->headers->get('CONTENT_TYPE')) {
// Remove charset
if (false !== $pos = strpos($contentType, ';')) {
$contentType = substr($contentType, 0, $pos);
}
if ('text/xml' === $contentType) {
return SamlConstants::BINDING_SAML2_SOAP;
}
}
}
return null;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace LightSaml\Binding;
use Symfony\Component\HttpFoundation\Request;
interface BindingFactoryInterface
{
/**
* @return AbstractBinding
*/
public function getBindingByRequest(Request $request);
/**
* @param string $bindingType
*
* @throws \LightSaml\Error\LightSamlBindingException
*
* @return AbstractBinding
*/
public function create($bindingType);
/**
* @return string|null
*/
public function detectBindingType(Request $request);
}

View File

@@ -0,0 +1,69 @@
<?php
namespace LightSaml\Binding;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\MessageContext;
use LightSaml\Error\LightSamlBindingException;
use LightSaml\Model\Protocol\AbstractRequest;
use LightSaml\Model\Protocol\SamlMessage;
use Symfony\Component\HttpFoundation\Request;
class HttpPostBinding extends AbstractBinding
{
/**
* @param string|null $destination
*
* @return SamlPostResponse
*/
public function send(MessageContext $context, $destination = null)
{
$message = MessageContextHelper::asSamlMessage($context);
$destination = $message->getDestination() ? $message->getDestination() : $destination;
$serializationContext = $context->getSerializationContext();
$message->serialize($serializationContext->getDocument(), $serializationContext);
$msgStr = $serializationContext->getDocument()->saveXML();
$this->dispatchSend($msgStr);
$msgStr = base64_encode($msgStr);
$type = $message instanceof AbstractRequest ? 'SAMLRequest' : 'SAMLResponse';
$data = [$type => $msgStr];
if ($message->getRelayState()) {
$data['RelayState'] = $message->getRelayState();
}
$result = new SamlPostResponse($destination, $data);
$result->renderContent();
return $result;
}
public function receive(Request $request, MessageContext $context)
{
$post = $request->request->all();
if (array_key_exists('SAMLRequest', $post)) {
$msg = $post['SAMLRequest'];
} elseif (array_key_exists('SAMLResponse', $post)) {
$msg = $post['SAMLResponse'];
} else {
throw new LightSamlBindingException('Missing SAMLRequest or SAMLResponse parameter');
}
$msg = base64_decode($msg);
$this->dispatchReceive($msg);
$deserializationContext = $context->getDeserializationContext();
$result = SamlMessage::fromXML($msg, $deserializationContext);
if (array_key_exists('RelayState', $post)) {
$result->setRelayState($post['RelayState']);
}
$context->setMessage($result);
}
}

View File

@@ -0,0 +1,277 @@
<?php
namespace LightSaml\Binding;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\MessageContext;
use LightSaml\Error\LightSamlBindingException;
use LightSaml\Model\Protocol\AbstractRequest;
use LightSaml\Model\Protocol\SamlMessage;
use LightSaml\Model\XmlDSig\SignatureStringReader;
use LightSaml\Model\XmlDSig\SignatureWriter;
use LightSaml\SamlConstants;
use RobRichards\XMLSecLibs\XMLSecurityKey;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
class HttpRedirectBinding extends AbstractBinding
{
/**
* @param string|null $destination
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function send(MessageContext $context, $destination = null)
{
$destination = $context->getMessage()->getDestination() ? $context->getMessage()->getDestination() : $destination;
$url = $this->getRedirectURL($context, $destination);
return new RedirectResponse($url);
}
public function receive(Request $request, MessageContext $context)
{
$data = $this->parseQuery($request);
$this->processData($data, $context);
}
/**
* @throws \Exception
*/
protected function processData(array $data, MessageContext $context)
{
$msg = $this->getMessageStringFromData($data);
$encoding = $this->getEncodingFromData($data);
$msg = $this->decodeMessageString($msg, $encoding);
$this->dispatchReceive($msg);
$deserializationContext = $context->getDeserializationContext();
$message = SamlMessage::fromXML($msg, $deserializationContext);
$this->loadRelayState($message, $data);
$this->loadSignature($message, $data);
$context->setMessage($message);
}
/**
* @return string
*
* @throws LightSamlBindingException
*/
protected function getMessageStringFromData(array $data)
{
if (array_key_exists('SAMLRequest', $data)) {
return $data['SAMLRequest'];
} elseif (array_key_exists('SAMLResponse', $data)) {
return $data['SAMLResponse'];
} else {
throw new LightSamlBindingException('Missing SAMLRequest or SAMLResponse parameter');
}
}
/**
* @return string
*/
protected function getEncodingFromData(array $data)
{
if (array_key_exists('SAMLEncoding', $data)) {
return $data['SAMLEncoding'];
} else {
return SamlConstants::ENCODING_DEFLATE;
}
}
/**
* @param string $msg
* @param string $encoding
*
* @throws \LightSaml\Error\LightSamlBindingException
*
* @return string
*/
protected function decodeMessageString($msg, $encoding)
{
$msg = base64_decode($msg);
switch ($encoding) {
case SamlConstants::ENCODING_DEFLATE:
return gzinflate($msg);
break;
default:
throw new LightSamlBindingException(sprintf("Unknown encoding '%s'", $encoding));
}
}
protected function loadRelayState(SamlMessage $message, array $data)
{
if (array_key_exists('RelayState', $data)) {
$message->setRelayState($data['RelayState']);
}
}
protected function loadSignature(SamlMessage $message, array $data)
{
if (array_key_exists('Signature', $data)) {
if (false == array_key_exists('SigAlg', $data)) {
throw new LightSamlBindingException('Missing signature algorithm');
}
$message->setSignature(
new SignatureStringReader($data['Signature'], $data['SigAlg'], $data['SignedQuery'])
);
}
}
/**
* @param string|null $destination
*
* @return string
*/
protected function getRedirectURL(MessageContext $context, $destination)
{
$message = MessageContextHelper::asSamlMessage($context);
$signature = $message->getSignature();
if ($signature && false == $signature instanceof SignatureWriter) {
throw new LightSamlBindingException('Signature must be SignatureWriter');
}
$xml = $this->getMessageEncodedXml($message, $context);
$msg = $this->addMessageToUrl($message, $xml);
$this->addRelayStateToUrl($msg, $message);
$this->addSignatureToUrl($msg, $signature);
return $this->getDestinationUrl($msg, $message, $destination);
}
/**
* @return string
*/
protected function getMessageEncodedXml(SamlMessage $message, MessageContext $context)
{
$message->setSignature(null);
$serializationContext = $context->getSerializationContext();
$message->serialize($serializationContext->getDocument(), $serializationContext);
$xml = $serializationContext->getDocument()->saveXML();
$this->dispatchSend($xml);
$xml = gzdeflate($xml);
$xml = base64_encode($xml);
return $xml;
}
/**
* @param string $xml
*
* @return string
*/
protected function addMessageToUrl(SamlMessage $message, $xml)
{
if ($message instanceof AbstractRequest) {
$msg = 'SAMLRequest=';
} else {
$msg = 'SAMLResponse=';
}
$msg .= urlencode($xml);
return $msg;
}
/**
* @param string $msg
*/
protected function addRelayStateToUrl(&$msg, SamlMessage $message)
{
if (null !== $message->getRelayState()) {
$msg .= '&RelayState=' . urlencode($message->getRelayState());
}
}
/**
* @param string $msg
*/
protected function addSignatureToUrl(&$msg, SignatureWriter $signature = null)
{
/** @var $key XMLSecurityKey */
$key = $signature ? $signature->getXmlSecurityKey() : null;
if (null != $key) {
$msg .= '&SigAlg=' . urlencode($key->type);
$signature = $key->signData($msg);
$msg .= '&Signature=' . urlencode(base64_encode($signature));
}
}
/**
* @param string $msg
* @param string|null $destination
*
* @return string
*/
protected function getDestinationUrl($msg, SamlMessage $message, $destination)
{
$destination = $message->getDestination() ? $message->getDestination() : $destination;
if (false === strpos($destination, '?')) {
$destination .= '?' . $msg;
} else {
$destination .= '&' . $msg;
}
return $destination;
}
/**
* @return array
*/
protected function parseQuery(Request $request)
{
/*
* Parse the query string. We need to do this ourself, so that we get access
* to the raw (urlencoded) values. This is required because different software
* can urlencode to different values.
*/
$sigQuery = $relayState = $sigAlg = '';
$data = $this->parseQueryString($request->server->get('QUERY_STRING'));
$result = [];
foreach ($data as $name => $value) {
$result[$name] = urldecode($value);
switch ($name) {
case 'SAMLRequest':
case 'SAMLResponse':
$sigQuery = $name . '=' . $value;
break;
case 'RelayState':
$relayState = '&RelayState=' . $value;
break;
case 'SigAlg':
$sigAlg = '&SigAlg=' . $value;
break;
}
}
$result['SignedQuery'] = $sigQuery . $relayState . $sigAlg;
return $result;
}
/**
* @param string $queryString
* @return array
*/
protected function parseQueryString($queryString)
{
$result = [];
foreach (explode('&', $queryString ?: '') as $e) {
$tmp = explode('=', $e, 2);
$name = $tmp[0];
$value = 2 === count($tmp) ? $tmp[1] : '';
$name = urldecode($name);
$result[$name] = $value;
}
return $result;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace LightSaml\Binding;
use Symfony\Component\HttpFoundation\Response;
class SamlPostResponse extends Response
{
/** @var string */
protected $destination;
/** @var array */
protected $data;
/**
* @param string $destination
* @param int $status
* @param array $headers
*/
public function __construct($destination, array $data, $status = 200, $headers = [])
{
parent::__construct('', $status, $headers);
$this->destination = $destination;
$this->data = $data;
}
/**
* @return array
*/
public function getData()
{
return $this->data;
}
/**
* @return string
*/
public function getDestination()
{
return $this->destination;
}
public function renderContent()
{
$content = <<<'EOT'
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>POST data</title>
</head>
<body onload="document.getElementById('a-very-unique-input-id#lightSAML').click();">
<noscript>
<p><strong>Note:</strong> Since your browser does not support JavaScript, you must press the button below once to proceed.</p>
</noscript>
<form method="post" action="%s">
<input id="a-very-unique-input-id#lightSAML" type="submit" style="display:none;"/>
%s
<noscript>
<input type="submit" value="Submit" />
</noscript>
</form>
</body>
</html>
EOT;
$fields = '';
foreach ($this->data as $name => $value) {
$fields .= sprintf(
'<input type="hidden" name="%s" value="%s" />',
htmlspecialchars($name),
htmlspecialchars($value)
);
}
$content = sprintf($content, htmlspecialchars($this->destination ?? ''), $fields);
$this->setContent($content);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace LightSaml\Bridge\Pimple\Container;
use Pimple\Container;
abstract class AbstractPimpleContainer
{
/** @var Container */
protected $pimple;
public function __construct(Container $pimple)
{
$this->pimple = $pimple;
}
/**
* @return Container
*/
public function getPimple()
{
return $this->pimple;
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace LightSaml\Bridge\Pimple\Container;
use LightSaml\Build\Container\BuildContainerInterface;
use LightSaml\Build\Container\CredentialContainerInterface;
use LightSaml\Build\Container\OwnContainerInterface;
use LightSaml\Build\Container\PartyContainerInterface;
use LightSaml\Build\Container\ProviderContainerInterface;
use LightSaml\Build\Container\ServiceContainerInterface;
use LightSaml\Build\Container\StoreContainerInterface;
use LightSaml\Build\Container\SystemContainerInterface;
class BuildContainer extends AbstractPimpleContainer implements BuildContainerInterface
{
/** @var SystemContainerInterface */
private $systemContainer;
/** @var PartyContainerInterface */
private $partyContainer;
/** @var StoreContainerInterface */
private $storeContainer;
/** @var ProviderContainerInterface */
private $providerContainer;
/** @var CredentialContainerInterface */
private $credentialContainer;
/** @var ServiceContainerInterface */
private $serviceContainer;
/** @var OwnContainerInterface */
private $ownContainer;
/**
* @return SystemContainerInterface
*/
public function getSystemContainer()
{
if (null == $this->systemContainer) {
$this->systemContainer = new SystemContainer($this->pimple);
}
return $this->systemContainer;
}
/**
* @return PartyContainerInterface
*/
public function getPartyContainer()
{
if (null == $this->partyContainer) {
$this->partyContainer = new PartyContainer($this->pimple);
}
return $this->partyContainer;
}
/**
* @return StoreContainerInterface
*/
public function getStoreContainer()
{
if (null == $this->storeContainer) {
$this->storeContainer = new StoreContainer($this->pimple);
}
return $this->storeContainer;
}
/**
* @return ProviderContainerInterface
*/
public function getProviderContainer()
{
if (null == $this->providerContainer) {
$this->providerContainer = new ProviderContainer($this->pimple);
}
return $this->providerContainer;
}
/**
* @return CredentialContainerInterface
*/
public function getCredentialContainer()
{
if (null == $this->credentialContainer) {
$this->credentialContainer = new CredentialContainer($this->pimple);
}
return $this->credentialContainer;
}
/**
* @return ServiceContainerInterface
*/
public function getServiceContainer()
{
if (null == $this->serviceContainer) {
$this->serviceContainer = new ServiceContainer($this->pimple);
}
return $this->serviceContainer;
}
/**
* @return OwnContainerInterface
*/
public function getOwnContainer()
{
if (null == $this->ownContainer) {
$this->ownContainer = new OwnContainer($this->pimple);
}
return $this->ownContainer;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace LightSaml\Bridge\Pimple\Container;
use LightSaml\Build\Container\CredentialContainerInterface;
use LightSaml\Store\Credential\CredentialStoreInterface;
class CredentialContainer extends AbstractPimpleContainer implements CredentialContainerInterface
{
public const CREDENTIAL_STORE = 'lightsaml.container.credential_store';
/**
* @return CredentialStoreInterface
*/
public function getCredentialStore()
{
return $this->pimple[self::CREDENTIAL_STORE];
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace LightSaml\Bridge\Pimple\Container\Factory;
use LightSaml\Bridge\Pimple\Container\CredentialContainer;
use LightSaml\Build\Container\OwnContainerInterface;
use LightSaml\Build\Container\PartyContainerInterface;
use LightSaml\Credential\CredentialInterface;
use LightSaml\Error\LightSamlBuildException;
use LightSaml\Store\Credential\Factory\CredentialFactory;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
class CredentialContainerProvider implements ServiceProviderInterface
{
/** @var PartyContainerInterface */
private $partyContainer;
/** @var OwnContainerInterface */
private $ownContainer;
/** @var CredentialInterface[] */
private $extraCredentials = [];
public function __construct(PartyContainerInterface $partyContainer, OwnContainerInterface $ownContainer)
{
$this->ownContainer = $ownContainer;
$this->partyContainer = $partyContainer;
}
/**
* @return CredentialContainerProvider
*/
public function addExtraCredential(CredentialInterface $credential)
{
if (null === $credential->getEntityId()) {
throw new \InvalidArgumentException('Extra credential must have entityID');
}
$this->extraCredentials[] = $credential;
return $this;
}
/**
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$ownCredentials = $this->ownContainer->getOwnCredentials();
if (empty($ownCredentials)) {
throw new LightSamlBuildException('There are no own credentials');
}
$pimple[CredentialContainer::CREDENTIAL_STORE] = function () {
$factory = new CredentialFactory();
return $factory->build(
$this->partyContainer->getIdpEntityDescriptorStore(),
$this->partyContainer->getSpEntityDescriptorStore(),
$this->ownContainer->getOwnCredentials(),
$this->extraCredentials
);
};
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace LightSaml\Bridge\Pimple\Container\Factory;
use LightSaml\Bridge\Pimple\Container\OwnContainer;
use LightSaml\Credential\CredentialInterface;
use LightSaml\Error\LightSamlBuildException;
use LightSaml\Provider\EntityDescriptor\EntityDescriptorProviderInterface;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
class OwnContainerProvider implements ServiceProviderInterface
{
/** @var CredentialInterface[] */
private $ownCredentials = [];
/** @var EntityDescriptorProviderInterface */
private $ownEntityDescriptorProvider;
/**
* @param CredentialInterface[] $ownCredentials
*/
public function __construct(EntityDescriptorProviderInterface $ownEntityDescriptorProvider, array $ownCredentials = null)
{
$this->ownEntityDescriptorProvider = $ownEntityDescriptorProvider;
if ($ownCredentials) {
foreach ($ownCredentials as $credential) {
$this->addOwnCredential($credential);
}
}
}
/**
* @return OwnContainerProvider
*/
public function addOwnCredential(CredentialInterface $credential)
{
if (null == $credential->getPrivateKey()) {
throw new LightSamlBuildException('Own credential must have private key');
}
$this->ownCredentials[] = $credential;
return $this;
}
/**
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple[OwnContainer::OWN_CREDENTIALS] = function () {
return $this->ownCredentials;
};
$pimple[OwnContainer::OWN_ENTITY_DESCRIPTOR_PROVIDER] = function () {
return $this->ownEntityDescriptorProvider;
};
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace LightSaml\Bridge\Pimple\Container\Factory;
use LightSaml\Bridge\Pimple\Container\PartyContainer;
use LightSaml\Meta\TrustOptions\TrustOptions;
use LightSaml\Store\EntityDescriptor\FixedEntityDescriptorStore;
use LightSaml\Store\TrustOptions\FixedTrustOptionsStore;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
class PartyContainerProvider implements ServiceProviderInterface
{
/**
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple[PartyContainer::IDP_ENTITY_DESCRIPTOR] = function () {
return new FixedEntityDescriptorStore();
};
$pimple[PartyContainer::SP_ENTITY_DESCRIPTOR] = function () {
return new FixedEntityDescriptorStore();
};
$pimple[PartyContainer::TRUST_OPTIONS_STORE] = function () {
return new FixedTrustOptionsStore(new TrustOptions());
};
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace LightSaml\Bridge\Pimple\Container\Factory;
use LightSaml\Bridge\Pimple\Container\ProviderContainer;
use LightSaml\Error\LightSamlBuildException;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
class ProviderContainerProvider implements ServiceProviderInterface
{
/**
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple[ProviderContainer::ATTRIBUTE_VALUE_PROVIDER] = function () {
throw new LightSamlBuildException('Attribute value provider not set');
};
$pimple[ProviderContainer::SESSION_INFO_PROVIDER] = function () {
throw new LightSamlBuildException('Session info provider not set');
};
$pimple[ProviderContainer::NAME_ID_PROVIDER] = function () {
throw new LightSamlBuildException('Name ID provider not set');
};
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace LightSaml\Bridge\Pimple\Container\Factory;
use LightSaml\Binding\BindingFactory;
use LightSaml\Bridge\Pimple\Container\ServiceContainer;
use LightSaml\Build\Container\CredentialContainerInterface;
use LightSaml\Build\Container\StoreContainerInterface;
use LightSaml\Build\Container\SystemContainerInterface;
use LightSaml\Resolver\Credential\Factory\CredentialResolverFactory;
use LightSaml\Resolver\Endpoint\BindingEndpointResolver;
use LightSaml\Resolver\Endpoint\CompositeEndpointResolver;
use LightSaml\Resolver\Endpoint\DescriptorTypeEndpointResolver;
use LightSaml\Resolver\Endpoint\IndexEndpointResolver;
use LightSaml\Resolver\Endpoint\LocationEndpointResolver;
use LightSaml\Resolver\Endpoint\ServiceTypeEndpointResolver;
use LightSaml\Resolver\Session\SessionProcessor;
use LightSaml\Resolver\Signature\OwnSignatureResolver;
use LightSaml\Validator\Model\Assertion\AssertionTimeValidator;
use LightSaml\Validator\Model\Assertion\AssertionValidator;
use LightSaml\Validator\Model\NameId\NameIdValidator;
use LightSaml\Validator\Model\Signature\SignatureValidator;
use LightSaml\Validator\Model\Statement\StatementValidator;
use LightSaml\Validator\Model\Subject\SubjectValidator;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
class ServiceContainerProvider implements ServiceProviderInterface
{
/** @var CredentialContainerInterface */
private $credentialContainer;
/** @var SystemContainerInterface */
private $systemContainer;
/** @var StoreContainerInterface */
private $storeContainer;
public function __construct(
CredentialContainerInterface $credentialContainer,
StoreContainerInterface $storeContainer,
SystemContainerInterface $systemContainer
) {
$this->credentialContainer = $credentialContainer;
$this->storeContainer = $storeContainer;
$this->systemContainer = $systemContainer;
}
/**
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple[ServiceContainer::NAME_ID_VALIDATOR] = function () {
return new NameIdValidator();
};
$pimple[ServiceContainer::ASSERTION_TIME_VALIDATOR] = function () {
return new AssertionTimeValidator();
};
$pimple[ServiceContainer::ASSERTION_VALIDATOR] = function (Container $c) {
$nameIdValidator = $c[ServiceContainer::NAME_ID_VALIDATOR];
return new AssertionValidator(
$nameIdValidator,
new SubjectValidator($nameIdValidator),
new StatementValidator()
);
};
$pimple[ServiceContainer::ENDPOINT_RESOLVER] = function () {
return new CompositeEndpointResolver([
new BindingEndpointResolver(),
new DescriptorTypeEndpointResolver(),
new ServiceTypeEndpointResolver(),
new IndexEndpointResolver(),
new LocationEndpointResolver(),
]);
};
$pimple[ServiceContainer::BINDING_FACTORY] = function () {
return new BindingFactory($this->systemContainer->getEventDispatcher());
};
$pimple[ServiceContainer::CREDENTIAL_RESOLVER] = function () {
$factory = new CredentialResolverFactory($this->credentialContainer->getCredentialStore());
return $factory->build();
};
$pimple[ServiceContainer::SIGNATURE_RESOLVER] = function (Container $c) {
$credentialResolver = $c[ServiceContainer::CREDENTIAL_RESOLVER];
return new OwnSignatureResolver($credentialResolver);
};
$pimple[ServiceContainer::SIGNATURE_VALIDATOR] = function (Container $c) {
$credentialResolver = $c[ServiceContainer::CREDENTIAL_RESOLVER];
return new SignatureValidator($credentialResolver);
};
$pimple[ServiceContainer::SESSION_PROCESSOR] = function () {
return new SessionProcessor($this->storeContainer->getSsoStateStore(), $this->systemContainer->getTimeProvider());
};
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace LightSaml\Bridge\Pimple\Container\Factory;
use LightSaml\Bridge\Pimple\Container\StoreContainer;
use LightSaml\Build\Container\SystemContainerInterface;
use LightSaml\Store\Id\NullIdStore;
use LightSaml\Store\Request\RequestStateSessionStore;
use LightSaml\Store\Sso\SsoStateSessionStore;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
class StoreContainerProvider implements ServiceProviderInterface
{
/** @var SystemContainerInterface */
private $systemContainer;
public function __construct(SystemContainerInterface $systemContainer)
{
$this->systemContainer = $systemContainer;
}
/**
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple[StoreContainer::REQUEST_STATE_STORE] = function () {
return new RequestStateSessionStore($this->systemContainer->getSession(), 'main');
};
$pimple[StoreContainer::ID_STATE_STORE] = function () {
return new NullIdStore();
};
$pimple[StoreContainer::SSO_STATE_STORE] = function () {
return new SsoStateSessionStore($this->systemContainer->getSession(), 'samlsso');
};
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace LightSaml\Bridge\Pimple\Container\Factory;
use LightSaml\Bridge\Pimple\Container\SystemContainer;
use LightSaml\Provider\TimeProvider\SystemTimeProvider;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\NullLogger;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
class SystemContainerProvider implements ServiceProviderInterface
{
/** @var bool */
private $mockSession;
/** @var EventDispatcherInterface|null */
private $eventDispatcher;
public function __construct($mockSession = false, EventDispatcherInterface $eventDispatcher = null)
{
$this->mockSession = $mockSession;
$this->eventDispatcher = $eventDispatcher;
}
/**
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple[SystemContainer::REQUEST] = function () {
return Request::createFromGlobals();
};
$pimple[SystemContainer::SESSION] = function () {
if ($this->mockSession) {
$session = new Session(new MockArraySessionStorage());
} else {
$session = new Session();
}
$session->setName(sprintf('SID%s', mt_rand(1000, 9999)));
$session->start();
return $session;
};
$pimple[SystemContainer::TIME_PROVIDER] = function () {
return new SystemTimeProvider();
};
$pimple[SystemContainer::EVENT_DISPATCHER] = function () {
return $this->eventDispatcher;
};
$pimple[SystemContainer::LOGGER] = function () {
return new NullLogger();
};
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace LightSaml\Bridge\Pimple\Container;
use LightSaml\Build\Container\OwnContainerInterface;
use LightSaml\Credential\CredentialInterface;
use LightSaml\Provider\EntityDescriptor\EntityDescriptorProviderInterface;
class OwnContainer extends AbstractPimpleContainer implements OwnContainerInterface
{
public const OWN_ENTITY_DESCRIPTOR_PROVIDER = 'lightsaml.container.own_entity_descriptor_provider';
public const OWN_CREDENTIALS = 'lightsaml.container.own_credentials';
/**
* @return EntityDescriptorProviderInterface
*/
public function getOwnEntityDescriptorProvider()
{
return $this->pimple[self::OWN_ENTITY_DESCRIPTOR_PROVIDER];
}
/**
* @return CredentialInterface[]
*/
public function getOwnCredentials()
{
return $this->pimple[self::OWN_CREDENTIALS];
}
}

Some files were not shown because too many files have changed in this diff Show More