Initial Commit
This commit is contained in:
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]);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user