Initial Commit
This commit is contained in:
12
dependencies/jasny/immutable/phpcs.xml
vendored
Normal file
12
dependencies/jasny/immutable/phpcs.xml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<ruleset name="Jasny">
|
||||
<description>The Jasny coding standard.</description>
|
||||
|
||||
<!-- Include the whole PSR-1 standard -->
|
||||
<rule ref="PSR1"/>
|
||||
<!-- Include the whole PSR-2 standard -->
|
||||
<rule ref="PSR2"/>
|
||||
|
||||
<!-- TODO: Add own rules -->
|
||||
</ruleset>
|
||||
|
10
dependencies/jasny/immutable/phpstan.neon
vendored
Normal file
10
dependencies/jasny/immutable/phpstan.neon
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
parameters:
|
||||
level: 7
|
||||
paths:
|
||||
- src
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
ignoreErrors:
|
||||
- /^Variable property access/
|
||||
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
22
dependencies/jasny/immutable/src/NoDynamicProperties.php
vendored
Normal file
22
dependencies/jasny/immutable/src/NoDynamicProperties.php
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\Immutable;
|
||||
|
||||
/**
|
||||
* Disable dynamic properties.
|
||||
*/
|
||||
trait NoDynamicProperties
|
||||
{
|
||||
/**
|
||||
* Magic method called when trying to set a non-existing property.
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function __set(string $property, $value) : void
|
||||
{
|
||||
throw new \LogicException(\sprintf('%s has no property "%s"', \get_class($this), $property));
|
||||
}
|
||||
}
|
154
dependencies/jasny/immutable/src/With.php
vendored
Normal file
154
dependencies/jasny/immutable/src/With.php
vendored
Normal file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\Immutable;
|
||||
|
||||
/**
|
||||
* Trait with the `withProperty` methods that can be used by classes of immutable objects.
|
||||
*
|
||||
* {@internal Some lines are expected to be covered, which should be ignored. Added codeCoverageIgnore. }}
|
||||
*/
|
||||
trait With
|
||||
{
|
||||
/**
|
||||
* Return a copy with a changed property.
|
||||
* Returns this object if the resulting object would be the same as the current one.
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @return static
|
||||
* @throws \BadMethodCallException if property doesn't exist
|
||||
*/
|
||||
private function withProperty(string $property, $value)
|
||||
{
|
||||
if (!\property_exists($this, $property)) {
|
||||
throw new \BadMethodCallException(\sprintf('%s has no property "%s"', \get_class($this), $property));
|
||||
}
|
||||
if (isset($this->{$property}) && $this->{$property} === $value || !isset($this->{$property}) && $value === null) {
|
||||
return $this;
|
||||
}
|
||||
$clone = clone $this;
|
||||
$clone->{$property} = $value;
|
||||
return $clone;
|
||||
}
|
||||
/**
|
||||
* Return a copy with a property unset.
|
||||
* Returns this object if the resulting object would be the same as the current one.
|
||||
*
|
||||
* @param string $property
|
||||
* @return static
|
||||
* @throws \BadMethodCallException if property doesn't exist
|
||||
*/
|
||||
private function withoutProperty(string $property)
|
||||
{
|
||||
if (!\property_exists($this, $property)) {
|
||||
throw new \BadMethodCallException(\sprintf('%s has no property "%s"', \get_class($this), $property));
|
||||
}
|
||||
if (!isset($this->{$property})) {
|
||||
return $this;
|
||||
}
|
||||
$clone = clone $this;
|
||||
unset($clone->{$property});
|
||||
return $clone;
|
||||
}
|
||||
/**
|
||||
* Return a copy with an added item for a property.
|
||||
* Returns this object if the resulting object would be the same as the current one.
|
||||
*
|
||||
* @param string $property
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return static
|
||||
* @throws \BadMethodCallException if property doesn't exist
|
||||
*/
|
||||
private function withPropertyKey(string $property, string $key, $value)
|
||||
{
|
||||
if (!\property_exists($this, $property)) {
|
||||
throw new \BadMethodCallException(\sprintf('%s has no property "%s"', \get_class($this), $property));
|
||||
}
|
||||
// @codeCoverageIgnore
|
||||
if (!\is_array($this->{$property}) && !$this->{$property} instanceof \ArrayAccess) {
|
||||
throw new \BadMethodCallException(\sprintf('%s::$%s is not an array', \get_class($this), $property));
|
||||
}
|
||||
if (isset($this->{$property}[$key]) && $this->{$property}[$key] === $value) {
|
||||
return $this;
|
||||
}
|
||||
$clone = clone $this;
|
||||
$clone->{$property}[$key] = $value;
|
||||
return $clone;
|
||||
}
|
||||
/**
|
||||
* Return a copy with a removed item from a property.
|
||||
* Returns this object if the resulting object would be the same as the current one.
|
||||
*
|
||||
* @param string $property
|
||||
* @param string $key
|
||||
* @return static
|
||||
* @throws \BadMethodCallException if property doesn't exist or isn't an array
|
||||
*/
|
||||
private function withoutPropertyKey(string $property, string $key)
|
||||
{
|
||||
if (!\property_exists($this, $property)) {
|
||||
throw new \BadMethodCallException(\sprintf('%s has no property "%s"', \get_class($this), $property));
|
||||
}
|
||||
// @codeCoverageIgnore
|
||||
if (!\is_array($this->{$property}) && !$this->{$property} instanceof \ArrayAccess) {
|
||||
throw new \BadMethodCallException(\sprintf('%s::$%s is not an array', \get_class($this), $property));
|
||||
}
|
||||
if (!isset($this->{$property}[$key])) {
|
||||
return $this;
|
||||
}
|
||||
$clone = clone $this;
|
||||
unset($clone->{$property}[$key]);
|
||||
return $clone;
|
||||
}
|
||||
/**
|
||||
* Return a copy with a value added to a sequential array.
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @param mixed $unique Don't add if the array already has a copy of the value.
|
||||
* @return static
|
||||
* @throws \BadMethodCallException if property doesn't exist or isn't an array
|
||||
*/
|
||||
private function withPropertyItem(string $property, $value, bool $unique = \false)
|
||||
{
|
||||
if (!\property_exists($this, $property)) {
|
||||
throw new \BadMethodCallException(\sprintf('%s has no property "%s"', \get_class($this), $property));
|
||||
}
|
||||
if (!\is_array($this->{$property})) {
|
||||
throw new \BadMethodCallException(\sprintf('%s::$%s is not an array', \get_class($this), $property));
|
||||
}
|
||||
if ($unique && \in_array($value, $this->{$property}, \true)) {
|
||||
return $this;
|
||||
}
|
||||
$clone = clone $this;
|
||||
$clone->{$property}[] = $value;
|
||||
return $clone;
|
||||
}
|
||||
/**
|
||||
* Return a copy with a value removed from a sequential array.
|
||||
* Returns this object if the resulting object would be the same as the current one.
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @return static
|
||||
* @throws \BadMethodCallException if property doesn't exist or isn't an array
|
||||
*/
|
||||
private function withoutPropertyItem(string $property, $value)
|
||||
{
|
||||
if (!\property_exists($this, $property)) {
|
||||
throw new \BadMethodCallException(\sprintf('%s has no property "%s"', \get_class($this), $property));
|
||||
}
|
||||
if (!\is_array($this->{$property})) {
|
||||
throw new \BadMethodCallException(\sprintf('%s::$%s is not an array', \get_class($this), $property));
|
||||
}
|
||||
$keys = \array_keys($this->{$property}, $value, \true);
|
||||
if ($keys === []) {
|
||||
return $this;
|
||||
}
|
||||
$clone = clone $this;
|
||||
$clone->{$property} = \array_values(\array_diff_key($clone->{$property}, \array_fill_keys($keys, null)));
|
||||
return $clone;
|
||||
}
|
||||
}
|
14
dependencies/jasny/sso/codeception.yml
vendored
Normal file
14
dependencies/jasny/sso/codeception.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
actor: Tester
|
||||
paths:
|
||||
tests: tests
|
||||
log: tests/_output
|
||||
data: tests/_data
|
||||
support: tests/_support
|
||||
envs: tests/_envs
|
||||
bootstrap: _bootstrap.php
|
||||
settings:
|
||||
colors: true
|
||||
memory_limit: 1024M
|
||||
extensions:
|
||||
enabled:
|
||||
- WP_Ultimo\Dependencies\Codeception\Extension\RunFailed
|
12
dependencies/jasny/sso/phpcs.xml
vendored
Normal file
12
dependencies/jasny/sso/phpcs.xml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<ruleset name="Jasny">
|
||||
<description>The Jasny coding standard.</description>
|
||||
|
||||
<!-- Include the whole PSR-1 standard -->
|
||||
<rule ref="PSR1"/>
|
||||
<!-- Include the whole PSR-2 standard -->
|
||||
<rule ref="PSR2"/>
|
||||
|
||||
<!-- TODO: Add own rules -->
|
||||
</ruleset>
|
||||
|
281
dependencies/jasny/sso/src/Broker/Broker.php
vendored
Normal file
281
dependencies/jasny/sso/src/Broker/Broker.php
vendored
Normal file
@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Broker;
|
||||
|
||||
use WP_Ultimo\Dependencies\Jasny\Immutable;
|
||||
/**
|
||||
* Single sign-on broker.
|
||||
*
|
||||
* The broker lives on the website visited by the user. The broken doesn't have any user credentials stored. Instead it
|
||||
* will talk to the SSO server in name of the user, verifying credentials and getting user information.
|
||||
*/
|
||||
class Broker
|
||||
{
|
||||
use Immutable\With;
|
||||
/**
|
||||
* URL of SSO server.
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
/**
|
||||
* My identifier, given by SSO provider.
|
||||
* @var string
|
||||
*/
|
||||
protected $broker;
|
||||
/**
|
||||
* My secret word, given by SSO provider.
|
||||
* @var string
|
||||
*/
|
||||
protected $secret;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $initialized = \false;
|
||||
/**
|
||||
* Session token of the client.
|
||||
* @var string|null
|
||||
*/
|
||||
protected $token;
|
||||
/**
|
||||
* Verification code returned by the server.
|
||||
* @var string|null
|
||||
*/
|
||||
protected $verificationCode;
|
||||
/**
|
||||
* @var \ArrayAccess<string,mixed>
|
||||
*/
|
||||
protected $state;
|
||||
/**
|
||||
* @var Curl
|
||||
*/
|
||||
protected $curl;
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string $url Url of SSO server
|
||||
* @param string $broker My identifier, given by SSO provider.
|
||||
* @param string $secret My secret word, given by SSO provider.
|
||||
*/
|
||||
public function __construct(string $url, string $broker, string $secret)
|
||||
{
|
||||
if (!(bool) \preg_match('~^https?://~', $url)) {
|
||||
throw new \InvalidArgumentException("Invalid SSO server URL '{$url}'");
|
||||
}
|
||||
if ((bool) \preg_match('/\\W/', $broker)) {
|
||||
throw new \InvalidArgumentException("Invalid broker id '{$broker}': must be alphanumeric");
|
||||
}
|
||||
$this->url = $url;
|
||||
$this->broker = $broker;
|
||||
$this->secret = $secret;
|
||||
$this->state = new Cookies();
|
||||
}
|
||||
/**
|
||||
* Get a copy with a different handler for the user state (like cookie or session).
|
||||
*
|
||||
* @param \ArrayAccess<string,mixed> $handler
|
||||
* @return static
|
||||
*/
|
||||
public function withTokenIn(\ArrayAccess $handler) : self
|
||||
{
|
||||
return $this->withProperty('state', $handler);
|
||||
}
|
||||
/**
|
||||
* Set a custom wrapper for cURL.
|
||||
*
|
||||
* @param Curl $curl
|
||||
* @return static
|
||||
*/
|
||||
public function withCurl(Curl $curl) : self
|
||||
{
|
||||
return $this->withProperty('curl', $curl);
|
||||
}
|
||||
/**
|
||||
* Get Wrapped cURL.
|
||||
*/
|
||||
protected function getCurl() : Curl
|
||||
{
|
||||
if (!isset($this->curl)) {
|
||||
$this->curl = new Curl();
|
||||
// @codeCoverageIgnore
|
||||
}
|
||||
return $this->curl;
|
||||
}
|
||||
/**
|
||||
* Get the broker identifier.
|
||||
*/
|
||||
public function getBrokerId() : string
|
||||
{
|
||||
return $this->broker;
|
||||
}
|
||||
/**
|
||||
* Get information from cookie.
|
||||
*/
|
||||
protected function initialize() : void
|
||||
{
|
||||
if ($this->initialized) {
|
||||
return;
|
||||
}
|
||||
$this->token = $this->state[$this->getCookieName('token')] ?? null;
|
||||
$this->verificationCode = $this->state[$this->getCookieName('verify')] ?? null;
|
||||
$this->initialized = \true;
|
||||
}
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getToken() : ?string
|
||||
{
|
||||
$this->initialize();
|
||||
return $this->token;
|
||||
}
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getVerificationCode() : ?string
|
||||
{
|
||||
$this->initialize();
|
||||
return $this->verificationCode;
|
||||
}
|
||||
/**
|
||||
* Get the cookie name.
|
||||
* The broker name is part of the cookie name. This resolves issues when multiple brokers are on the same domain.
|
||||
*/
|
||||
protected function getCookieName(string $type) : string
|
||||
{
|
||||
$brokerName = \preg_replace('/[_\\W]+/', '_', \strtolower($this->broker));
|
||||
return "sso_{$type}_{$brokerName}";
|
||||
}
|
||||
/**
|
||||
* Generate session id from session key
|
||||
*
|
||||
* @throws NotAttachedException
|
||||
*/
|
||||
public function getBearerToken() : string
|
||||
{
|
||||
$token = $this->getToken();
|
||||
$verificationCode = $this->getVerificationCode();
|
||||
if ($verificationCode === null) {
|
||||
throw new NotAttachedException("The client isn't attached to the SSO server for this broker. " . "Make sure that the '" . $this->getCookieName('verify') . "' cookie is set.");
|
||||
}
|
||||
return "SSO-{$this->broker}-{$token}-" . $this->generateChecksum("bearer:{$verificationCode}");
|
||||
}
|
||||
/**
|
||||
* Generate session token.
|
||||
*/
|
||||
protected function generateToken() : void
|
||||
{
|
||||
$this->token = \base_convert(\bin2hex(\random_bytes(32)), 16, 36);
|
||||
$this->state[$this->getCookieName('token')] = $this->token;
|
||||
}
|
||||
/**
|
||||
* Clears session token.
|
||||
*/
|
||||
public function clearToken() : void
|
||||
{
|
||||
unset($this->state[$this->getCookieName('token')]);
|
||||
unset($this->state[$this->getCookieName('verify')]);
|
||||
$this->token = null;
|
||||
$this->verificationCode = null;
|
||||
}
|
||||
/**
|
||||
* Check if we have an SSO token.
|
||||
*/
|
||||
public function isAttached() : bool
|
||||
{
|
||||
return $this->getVerificationCode() !== null;
|
||||
}
|
||||
/**
|
||||
* Get URL to attach session at SSO server.
|
||||
*
|
||||
* @param array<string,mixed> $params
|
||||
* @return string
|
||||
*/
|
||||
public function getAttachUrl(array $params = []) : string
|
||||
{
|
||||
if ($this->getToken() === null) {
|
||||
$this->generateToken();
|
||||
}
|
||||
$data = ['broker' => $this->broker, 'token' => $this->getToken(), 'checksum' => $this->generateChecksum('attach')];
|
||||
return $this->url . "?" . \http_build_query($data + $params);
|
||||
}
|
||||
/**
|
||||
* Verify attaching to the SSO server by providing the verification code.
|
||||
*/
|
||||
public function verify(string $code) : void
|
||||
{
|
||||
$this->initialize();
|
||||
if ($this->verificationCode === $code) {
|
||||
return;
|
||||
}
|
||||
if ($this->verificationCode !== null) {
|
||||
\trigger_error("SSO attach already verified", \E_USER_WARNING);
|
||||
return;
|
||||
}
|
||||
$this->verificationCode = $code;
|
||||
$this->state[$this->getCookieName('verify')] = $code;
|
||||
}
|
||||
/**
|
||||
* Generate checksum for a broker.
|
||||
*/
|
||||
protected function generateChecksum(string $command) : string
|
||||
{
|
||||
return \base_convert(\hash_hmac('sha256', $command . ':' . $this->token, $this->secret), 16, 36);
|
||||
}
|
||||
/**
|
||||
* Get the request url for a command
|
||||
*
|
||||
* @param string $path
|
||||
* @param array<string,mixed>|string $params Query parameters
|
||||
* @return string
|
||||
*/
|
||||
protected function getRequestUrl(string $path, $params = '') : string
|
||||
{
|
||||
$query = \is_array($params) ? \http_build_query($params) : $params;
|
||||
$base = $path[0] === '/' ? \preg_replace('~^(\\w+://[^/]+).*~', '$1', $this->url) : \preg_replace('~/[^/]*$~', '', $this->url);
|
||||
return $base . '/' . \ltrim($path, '/') . ($query !== '' ? '?' . $query : '');
|
||||
}
|
||||
/**
|
||||
* Send an HTTP request to the SSO server.
|
||||
*
|
||||
* @param string $method HTTP method: 'GET', 'POST', 'DELETE'
|
||||
* @param string $path Relative path
|
||||
* @param array<string,mixed>|string $data Query or post parameters
|
||||
* @return mixed
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function request(string $method, string $path, $data = '')
|
||||
{
|
||||
$url = $this->getRequestUrl($path, $method === 'POST' ? '' : $data);
|
||||
$headers = ['Accept: application/json', 'Authorization: Bearer ' . $this->getBearerToken()];
|
||||
['httpCode' => $httpCode, 'contentType' => $contentType, 'body' => $body] = $this->getCurl()->request($method, $url, $headers, $method === 'POST' ? $data : '');
|
||||
return $this->handleResponse($httpCode, $contentType, $body);
|
||||
}
|
||||
/**
|
||||
* Handle the response of the cURL request.
|
||||
*
|
||||
* @param int $httpCode HTTP status code
|
||||
* @param string|null $ctHeader Content-Type header
|
||||
* @param string $body Response body
|
||||
* @return mixed
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function handleResponse(int $httpCode, $ctHeader, string $body)
|
||||
{
|
||||
if ($httpCode === 204) {
|
||||
return null;
|
||||
}
|
||||
[$contentType] = \explode(';', $ctHeader, 2);
|
||||
if ($contentType != 'application/json') {
|
||||
throw new RequestException("Expected 'application/json' response, got '{$contentType}'", 500, new RequestException($body, $httpCode));
|
||||
}
|
||||
try {
|
||||
$data = \json_decode($body, \true, 512, \JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $exception) {
|
||||
throw new RequestException("Invalid JSON response from server", 500, $exception);
|
||||
}
|
||||
if ($httpCode >= 400) {
|
||||
throw new RequestException($data['error'] ?? $body, $httpCode);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
73
dependencies/jasny/sso/src/Broker/Cookies.php
vendored
Normal file
73
dependencies/jasny/sso/src/Broker/Cookies.php
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Broker;
|
||||
|
||||
/**
|
||||
* Use global $_COOKIE and setcookie() to persist the client token.
|
||||
*
|
||||
* @implements \ArrayAccess<string,mixed>
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Cookies implements \ArrayAccess
|
||||
{
|
||||
/** @var int */
|
||||
protected $ttl;
|
||||
/** @var string */
|
||||
protected $path;
|
||||
/** @var string */
|
||||
protected $domain;
|
||||
/** @var bool */
|
||||
protected $secure;
|
||||
/**
|
||||
* Cookies constructor.
|
||||
*
|
||||
* @param int $ttl Cookie TTL in seconds
|
||||
* @param string $path
|
||||
* @param string $domain
|
||||
* @param bool $secure
|
||||
*/
|
||||
public function __construct(int $ttl = 3600, string $path = '', string $domain = '', bool $secure = \false)
|
||||
{
|
||||
$this->ttl = $ttl;
|
||||
$this->path = $path;
|
||||
$this->domain = $domain;
|
||||
$this->secure = $secure;
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetSet($name, $value)
|
||||
{
|
||||
$success = \setcookie($name, $value, \time() + $this->ttl, $this->path, $this->domain, $this->secure, \true);
|
||||
if (!$success) {
|
||||
throw new \RuntimeException("Failed to set cookie '{$name}'");
|
||||
}
|
||||
$_COOKIE[$name] = $value;
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function offsetUnset($name) : void
|
||||
{
|
||||
\setcookie($name, '', 1, $this->path, $this->domain, $this->secure, \true);
|
||||
unset($_COOKIE[$name]);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetGet($name)
|
||||
{
|
||||
return $_COOKIE[$name] ?? null;
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetExists($name)
|
||||
{
|
||||
return isset($_COOKIE[$name]);
|
||||
}
|
||||
}
|
56
dependencies/jasny/sso/src/Broker/Curl.php
vendored
Normal file
56
dependencies/jasny/sso/src/Broker/Curl.php
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/** @noinspection PhpComposerExtensionStubsInspection */
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Broker;
|
||||
|
||||
/**
|
||||
* Wrapper for cURL.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Curl
|
||||
{
|
||||
/**
|
||||
* Curl constructor.
|
||||
*
|
||||
* @throws \Exception if curl extension isn't loaded
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if (!\extension_loaded('curl')) {
|
||||
throw new \Exception("cURL extension not loaded");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Send an HTTP request to the SSO server.
|
||||
*
|
||||
* @param string $method HTTP method: 'GET', 'POST', 'DELETE'
|
||||
* @param string $url Full URL
|
||||
* @param string[] $headers HTTP headers
|
||||
* @param array<string,mixed>|string $data Query or post parameters
|
||||
* @return array{httpCode:int,contentType:string,body:string}
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function request(string $method, string $url, array $headers, $data = '')
|
||||
{
|
||||
$ch = \curl_init($url);
|
||||
if ($ch === \false) {
|
||||
throw new \RuntimeException("Failed to initialize a cURL session");
|
||||
}
|
||||
if ($data !== [] && $data !== '') {
|
||||
$post = \is_string($data) ? $data : \http_build_query($data);
|
||||
\curl_setopt($ch, \CURLOPT_POSTFIELDS, $post);
|
||||
}
|
||||
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, \true);
|
||||
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $method);
|
||||
\curl_setopt($ch, \CURLOPT_HTTPHEADER, $headers);
|
||||
$responseBody = (string) \curl_exec($ch);
|
||||
if (\curl_errno($ch) != 0) {
|
||||
throw new RequestException('Server request failed: ' . \curl_error($ch));
|
||||
}
|
||||
$httpCode = \curl_getinfo($ch, \CURLINFO_HTTP_CODE);
|
||||
$contentType = \curl_getinfo($ch, \CURLINFO_CONTENT_TYPE) ?? 'text/html';
|
||||
return ['httpCode' => $httpCode, 'contentType' => $contentType, 'body' => $responseBody];
|
||||
}
|
||||
}
|
11
dependencies/jasny/sso/src/Broker/NotAttachedException.php
vendored
Normal file
11
dependencies/jasny/sso/src/Broker/NotAttachedException.php
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Broker;
|
||||
|
||||
/**
|
||||
* Exception thrown when a request is done while no session is attached
|
||||
*/
|
||||
class NotAttachedException extends \RuntimeException
|
||||
{
|
||||
}
|
11
dependencies/jasny/sso/src/Broker/RequestException.php
vendored
Normal file
11
dependencies/jasny/sso/src/Broker/RequestException.php
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Broker;
|
||||
|
||||
/**
|
||||
* SSO Request failed.
|
||||
*/
|
||||
class RequestException extends \RuntimeException
|
||||
{
|
||||
}
|
42
dependencies/jasny/sso/src/Broker/Session.php
vendored
Normal file
42
dependencies/jasny/sso/src/Broker/Session.php
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Broker;
|
||||
|
||||
/**
|
||||
* Use global $_SESSION to persist the client token.
|
||||
*
|
||||
* @implements \ArrayAccess<string,mixed>
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Session implements \ArrayAccess
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function offsetSet($name, $value) : void
|
||||
{
|
||||
$_SESSION[$name] = $value;
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function offsetUnset($name) : void
|
||||
{
|
||||
unset($_SESSION[$name]);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function offsetGet($name)
|
||||
{
|
||||
return $_SESSION[$name] ?? null;
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function offsetExists($name) : bool
|
||||
{
|
||||
return isset($_SESSION[$name]);
|
||||
}
|
||||
}
|
12
dependencies/jasny/sso/src/Server/BrokerException.php
vendored
Normal file
12
dependencies/jasny/sso/src/Server/BrokerException.php
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Server;
|
||||
|
||||
/**
|
||||
* Exception that's thrown if request from broker is invalid.
|
||||
* Should result in an HTTP 4xx response.
|
||||
*/
|
||||
class BrokerException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
20
dependencies/jasny/sso/src/Server/ExceptionInterface.php
vendored
Normal file
20
dependencies/jasny/sso/src/Server/ExceptionInterface.php
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Server;
|
||||
|
||||
interface ExceptionInterface
|
||||
{
|
||||
/**
|
||||
* Gets the Exception message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMessage();
|
||||
/**
|
||||
* Gets the Exception code.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCode();
|
||||
}
|
70
dependencies/jasny/sso/src/Server/GlobalSession.php
vendored
Normal file
70
dependencies/jasny/sso/src/Server/GlobalSession.php
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Server;
|
||||
|
||||
/**
|
||||
* Interact with the global session using PHP's session_* functions.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class GlobalSession implements SessionInterface
|
||||
{
|
||||
/**
|
||||
* Options passed to session_start().
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
protected $options;
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param array<string,mixed> $options Options passed to session_start().
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = $options + ['cookie_samesite' => 'None', 'cookie_secure' => \true];
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getId() : string
|
||||
{
|
||||
return \session_id();
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function start() : void
|
||||
{
|
||||
$started = \session_status() !== \PHP_SESSION_ACTIVE ? \session_start($this->options) : \true;
|
||||
if (!$started) {
|
||||
$err = \error_get_last() ?? ['message' => 'Failed to start session'];
|
||||
throw new ServerException($err['message'], 500);
|
||||
}
|
||||
// Session shouldn't be empty when resumed.
|
||||
$_SESSION['_sso_init'] = 1;
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function resume(string $id) : void
|
||||
{
|
||||
\session_id($id);
|
||||
$started = \session_start($this->options);
|
||||
if (!$started) {
|
||||
$err = \error_get_last() ?? ['message' => 'Failed to start session'];
|
||||
throw new ServerException($err['message'], 500);
|
||||
}
|
||||
if ($_SESSION === []) {
|
||||
\session_abort();
|
||||
throw new BrokerException("Session has expired. Client must attach with new token.", 401);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isActive() : bool
|
||||
{
|
||||
return \session_status() === \PHP_SESSION_ACTIVE;
|
||||
}
|
||||
}
|
273
dependencies/jasny/sso/src/Server/Server.php
vendored
Normal file
273
dependencies/jasny/sso/src/Server/Server.php
vendored
Normal file
@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Server;
|
||||
|
||||
use WP_Ultimo\Dependencies\Jasny\Immutable;
|
||||
use WP_Ultimo\Dependencies\Psr\Http\Message\ServerRequestInterface;
|
||||
use WP_Ultimo\Dependencies\Psr\Log\LoggerInterface;
|
||||
use WP_Ultimo\Dependencies\Psr\Log\NullLogger;
|
||||
use WP_Ultimo\Dependencies\Psr\SimpleCache\CacheInterface;
|
||||
/**
|
||||
* Single sign-on server.
|
||||
* The SSO server is responsible of managing users sessions which are available for brokers.
|
||||
*/
|
||||
class Server
|
||||
{
|
||||
use Immutable\With;
|
||||
/**
|
||||
* Callback to get the secret for a broker.
|
||||
* @var \Closure
|
||||
*/
|
||||
protected $getBrokerInfo;
|
||||
/**
|
||||
* Storage for broker session links.
|
||||
* @var CacheInterface
|
||||
*/
|
||||
protected $cache;
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
/**
|
||||
* Service to interact with sessions.
|
||||
* @var SessionInterface
|
||||
*/
|
||||
protected $session;
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @phpstan-param callable(string):?array{secret:string,domains:string[]} $getBrokerInfo
|
||||
* @phpstan-param CacheInterface $cache
|
||||
*/
|
||||
public function __construct(callable $getBrokerInfo, CacheInterface $cache)
|
||||
{
|
||||
$this->getBrokerInfo = \Closure::fromCallable($getBrokerInfo);
|
||||
$this->cache = $cache;
|
||||
$this->logger = new NullLogger();
|
||||
$this->session = new GlobalSession();
|
||||
}
|
||||
/**
|
||||
* Get a copy of the service with logging.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withLogger(LoggerInterface $logger) : self
|
||||
{
|
||||
return $this->withProperty('logger', $logger);
|
||||
}
|
||||
/**
|
||||
* Get a copy of the service with a custom session service.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withSession(SessionInterface $session) : self
|
||||
{
|
||||
return $this->withProperty('session', $session);
|
||||
}
|
||||
/**
|
||||
* Start the session for broker requests to the SSO server.
|
||||
*
|
||||
* @throws BrokerException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function startBrokerSession(?ServerRequestInterface $request = null) : void
|
||||
{
|
||||
if ($this->session->isActive()) {
|
||||
throw new ServerException("Session is already started", 500);
|
||||
}
|
||||
$bearer = $this->getBearerToken($request);
|
||||
[$brokerId, $token, $checksum] = $this->parseBearer($bearer);
|
||||
$sessionId = $this->cache->get($this->getCacheKey($brokerId, $token));
|
||||
if ($sessionId === null) {
|
||||
$this->logger->warning("Bearer token isn't attached to a client session", ['broker' => $brokerId, 'token' => $token]);
|
||||
throw new BrokerException("Bearer token isn't attached to a client session", 403);
|
||||
}
|
||||
$code = $this->getVerificationCode($brokerId, $token, $sessionId);
|
||||
$this->validateChecksum($checksum, 'bearer', $brokerId, $token, $code);
|
||||
$this->session->resume($sessionId);
|
||||
$this->logger->debug("Broker request with session", ['broker' => $brokerId, 'token' => $token, 'session' => $sessionId]);
|
||||
}
|
||||
/**
|
||||
* Get bearer token from Authorization header.
|
||||
*/
|
||||
protected function getBearerToken(?ServerRequestInterface $request = null) : string
|
||||
{
|
||||
$authorization = $request === null ? $_SERVER['HTTP_AUTHORIZATION'] ?? '' : $request->getHeaderLine('Authorization');
|
||||
[$type, $token] = \explode(' ', $authorization, 2) + ['', ''];
|
||||
if ($type !== 'Bearer') {
|
||||
$this->logger->warning("Broker didn't use bearer authentication: " . ($authorization === '' ? "No 'Authorization' header" : "{$type} authorization used"));
|
||||
throw new BrokerException("Broker didn't use bearer authentication", 401);
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
/**
|
||||
* Get the broker id and token from the bearer token used by the broker.
|
||||
*
|
||||
* @return string[]
|
||||
* @throws BrokerException
|
||||
*/
|
||||
protected function parseBearer(string $bearer) : array
|
||||
{
|
||||
$matches = null;
|
||||
if (!(bool) \preg_match('/^SSO-(\\w*+)-(\\w*+)-([a-z0-9]*+)$/', $bearer, $matches)) {
|
||||
$this->logger->warning("Invalid bearer token", ['bearer' => $bearer]);
|
||||
throw new BrokerException("Invalid bearer token", 403);
|
||||
}
|
||||
return \array_slice($matches, 1);
|
||||
}
|
||||
/**
|
||||
* Generate cache key for linking the broker token to the client session.
|
||||
*/
|
||||
protected function getCacheKey(string $brokerId, string $token) : string
|
||||
{
|
||||
return "SSO-{$brokerId}-{$token}";
|
||||
}
|
||||
/**
|
||||
* Get the broker secret using the configured callback.
|
||||
*
|
||||
* @param string $brokerId
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getBrokerSecret(string $brokerId) : ?string
|
||||
{
|
||||
return ($this->getBrokerInfo)($brokerId)['secret'] ?? null;
|
||||
}
|
||||
/**
|
||||
* Generate the verification code based on the token using the server secret.
|
||||
*/
|
||||
protected function getVerificationCode(string $brokerId, string $token, string $sessionId) : string
|
||||
{
|
||||
return \base_convert(\hash('sha256', $brokerId . $token . $sessionId), 16, 36);
|
||||
}
|
||||
/**
|
||||
* Generate checksum for a broker.
|
||||
*/
|
||||
protected function generateChecksum(string $command, string $brokerId, string $token) : string
|
||||
{
|
||||
$secret = $this->getBrokerSecret($brokerId);
|
||||
if ($secret === null) {
|
||||
$this->logger->warning("Unknown broker", ['broker' => $brokerId, 'token' => $token]);
|
||||
throw new BrokerException("Broker is unknown or disabled", 403);
|
||||
}
|
||||
return \base_convert(\hash_hmac('sha256', $command . ':' . $token, $secret), 16, 36);
|
||||
}
|
||||
/**
|
||||
* Assert that the checksum matches the expected checksum.
|
||||
*
|
||||
* @throws BrokerException
|
||||
*/
|
||||
protected function validateChecksum(string $checksum, string $command, string $brokerId, string $token, ?string $code = null) : void
|
||||
{
|
||||
$expected = $this->generateChecksum($command . ($code !== null ? ":{$code}" : ''), $brokerId, $token);
|
||||
if ($checksum !== $expected) {
|
||||
$this->logger->warning("Invalid {$command} checksum", ['expected' => $expected, 'received' => $checksum, 'broker' => $brokerId, 'token' => $token] + ($code !== null ? ['verification_code' => $code] : []));
|
||||
throw new BrokerException("Invalid {$command} checksum", 403);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Validate that the URL has a domain that is allowed for the broker.
|
||||
*/
|
||||
public function validateDomain(string $type, string $url, string $brokerId, ?string $token = null) : void
|
||||
{
|
||||
$domains = ($this->getBrokerInfo)($brokerId)['domains'] ?? [];
|
||||
$host = \parse_url($url, \PHP_URL_HOST);
|
||||
if (!\in_array($host, $domains, \true)) {
|
||||
$this->logger->warning("Domain of {$type} is not allowed for broker", [$type => $url, 'broker' => $brokerId] + ($token !== null ? ['token' => $token] : []));
|
||||
throw new BrokerException("Domain of {$type} is not allowed", 400);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Attach a client session to a broker session.
|
||||
* Returns the verification code.
|
||||
*
|
||||
* @throws BrokerException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function attach(?ServerRequestInterface $request = null) : string
|
||||
{
|
||||
['broker' => $brokerId, 'token' => $token] = $this->processAttachRequest($request);
|
||||
$this->session->start();
|
||||
$this->assertNotAttached($brokerId, $token);
|
||||
$key = $this->getCacheKey($brokerId, $token);
|
||||
$cached = $this->cache->set($key, $this->session->getId());
|
||||
$info = ['broker' => $brokerId, 'token' => $token, 'session' => $this->session->getId()];
|
||||
if (!$cached) {
|
||||
$this->logger->error("Failed to attach bearer token to session id due to cache issue", $info);
|
||||
throw new ServerException("Failed to attach bearer token to session id", 500);
|
||||
}
|
||||
$this->logger->info("Attached broker token to session", $info);
|
||||
return $this->getVerificationCode($brokerId, $token, $this->session->getId());
|
||||
}
|
||||
/**
|
||||
* Assert that the token isn't already attached to a different session.
|
||||
*/
|
||||
protected function assertNotAttached(string $brokerId, string $token) : void
|
||||
{
|
||||
$key = $this->getCacheKey($brokerId, $token);
|
||||
$attached = $this->cache->get($key);
|
||||
if ($attached !== null && $attached !== $this->session->getId()) {
|
||||
$this->logger->warning("Token is already attached", ['broker' => $brokerId, 'token' => $token, 'attached_to' => $attached, 'session' => $this->session->getId()]);
|
||||
throw new BrokerException("Token is already attached", 400);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Validate attach request and return broker id and token.
|
||||
*
|
||||
* @param ServerRequestInterface|null $request
|
||||
* @return array{broker:string,token:string}
|
||||
* @throws BrokerException
|
||||
*/
|
||||
protected function processAttachRequest(?ServerRequestInterface $request) : array
|
||||
{
|
||||
$brokerId = $this->getRequiredQueryParam($request, 'broker');
|
||||
$token = $this->getRequiredQueryParam($request, 'token');
|
||||
$checksum = $this->getRequiredQueryParam($request, 'checksum');
|
||||
$this->validateChecksum($checksum, 'attach', $brokerId, $token);
|
||||
$origin = $this->getHeader($request, 'Origin');
|
||||
if ($origin !== '') {
|
||||
$this->validateDomain('origin', $origin, $brokerId, $token);
|
||||
}
|
||||
$referer = $this->getHeader($request, 'Referer');
|
||||
if ($referer !== '') {
|
||||
$this->validateDomain('referer', $referer, $brokerId, $token);
|
||||
}
|
||||
$returnUrl = $this->getQueryParam($request, 'return_url');
|
||||
if ($returnUrl !== null) {
|
||||
$this->validateDomain('return_url', $returnUrl, $brokerId, $token);
|
||||
}
|
||||
return ['broker' => $brokerId, 'token' => $token];
|
||||
}
|
||||
/**
|
||||
* Get query parameter from PSR-7 request or $_GET.
|
||||
*/
|
||||
protected function getQueryParam(?ServerRequestInterface $request, string $key) : ?string
|
||||
{
|
||||
$params = $request === null ? $_GET : $request->getQueryParams();
|
||||
return $params[$key] ?? null;
|
||||
}
|
||||
/**
|
||||
* Get required query parameter from PSR-7 request or $_GET.
|
||||
*
|
||||
* @throws BrokerException if query parameter isn't set
|
||||
*/
|
||||
protected function getRequiredQueryParam(?ServerRequestInterface $request, string $key) : string
|
||||
{
|
||||
$value = $this->getQueryParam($request, $key);
|
||||
if ($value === null) {
|
||||
throw new BrokerException("Missing '{$key}' query parameter", 400);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
/**
|
||||
* Get HTTP Header from PSR-7 request or $_SERVER.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function getHeader(?ServerRequestInterface $request, string $key) : string
|
||||
{
|
||||
return $request === null ? $_SERVER['HTTP_' . \str_replace('-', '_', \strtoupper($key))] ?? '' : $request->getHeaderLine($key);
|
||||
}
|
||||
}
|
12
dependencies/jasny/sso/src/Server/ServerException.php
vendored
Normal file
12
dependencies/jasny/sso/src/Server/ServerException.php
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Server;
|
||||
|
||||
/**
|
||||
* Exception that's thrown if something unexpectedly went wrong on the server.
|
||||
* Should result in an HTTP 5xx response.
|
||||
*/
|
||||
class ServerException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
34
dependencies/jasny/sso/src/Server/SessionInterface.php
vendored
Normal file
34
dependencies/jasny/sso/src/Server/SessionInterface.php
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare (strict_types=1);
|
||||
namespace WP_Ultimo\Dependencies\Jasny\SSO\Server;
|
||||
|
||||
/**
|
||||
* Interface to start a session.
|
||||
*/
|
||||
interface SessionInterface
|
||||
{
|
||||
/**
|
||||
* @see session_id()
|
||||
*/
|
||||
public function getId() : string;
|
||||
/**
|
||||
* Start a new session.
|
||||
* @see session_start()
|
||||
*
|
||||
* @throws ServerException if session can't be started.
|
||||
*/
|
||||
public function start() : void;
|
||||
/**
|
||||
* Resume an existing session.
|
||||
*
|
||||
* @throws ServerException if session can't be started.
|
||||
* @throws BrokerException if session is expired
|
||||
*/
|
||||
public function resume(string $id) : void;
|
||||
/**
|
||||
* Check if a session is active. (status PHP_SESSION_ACTIVE)
|
||||
* @see session_status()
|
||||
*/
|
||||
public function isActive() : bool;
|
||||
}
|
Reference in New Issue
Block a user