Update of /cvsroot/php-blog/additional_plugins/serendipity_event_browserid/src
In directory sfp-cvs-1.v30.ch3.sourceforge.com:/tmp/cvs-serv18870/serendipity_event_browserid/src
Added Files:
AbstractStore.php Client.php RedisStore.php StoreInterface.php
Log Message:
gitclone.sh autocommit
--- NEW FILE: RedisStore.php ---
<?php
namespace Portier\Client;
/**
* A store implementation that uses Redis as the backend.
*/
class RedisStore extends AbstractStore
{
public $redis;
/**
* Constructor
* @param \Redis $redis The Redis instance to use.
*/
public function __construct(\Redis $redis)
{
parent::__construct();
$this->redis = $redis;
}
/**
* {@inheritDoc}
*/
public function fetchCached($cacheId, $url)
{
$key = 'cache:' . $cacheId;
$data = $this->redis->get($key);
if ($data) {
return json_decode($data);
}
$res = $this->fetch($url);
$this->redis->setEx($key, $res->ttl, json_encode($res->data));
return $res->data;
}
/**
* {@inheritDoc}
*/
public function createNonce($email)
{
$nonce = $this->generateNonce($email);
$key = 'nonce:' . $nonce;
$this->redis->setEx($key, $this->nonceTtl, $email);
return $nonce;
}
/**
* {@inheritDoc}
*/
public function consumeNonce($nonce, $email)
{
$key = 'nonce:' . $nonce;
$res = $this->redis->multi()
->get($key)
->del($key)
->exec();
if ($res[0] !== $email) {
throw new \Exception('Invalid or expired nonce');
}
}
}
--- NEW FILE: Client.php ---
<?php
namespace Portier\Client;
/**
* Client for a Portier broker.
*/
class Client
{
/**
* Default Portier broker origin.
* @var string
*/
const DEFAULT_BROKER = 'https://broker.portier.io';
private $store;
private $redirectUri;
private $clientId;
/**
* The origin of the Portier broker.
* @var string
*/
public $broker = self::DEFAULT_BROKER;
/**
* The number of seconds of clock drift to allow.
* @var int
*/
public $leeway = 3 * 60;
/**
* Constructor
* @param Store $store Store implementation to use.
* @param string $redirectUri URL that Portier will redirect to.
*/
public function __construct($store, $redirectUri)
{
$this->store = $store;
$this->redirectUri = $redirectUri;
$this->clientId = self::getOrigin($this->redirectUri);
}
/**
* Start authentication of an email address.
* @param string $email Email address to authenticate.
* @return string URL to redirect the browser to.
*/
public function authenticate($email)
{
$nonce = $this->store->createNonce($email);
$query = http_build_query([
'login_hint' => $email,
'scope' => 'openid email',
'nonce' => $nonce,
'response_type' => 'id_token',
'response_mode' => 'form_post',
'client_id' => $this->clientId,
'redirect_uri' => $this->redirectUri,
]);
return $this->broker . '/auth?' . $query;
}
/**
* Verify a token received on our `redirect_uri`.
* @param string $token The received `id_token` parameter value.
* @return string The verified email address.
*/
public function verify($token)
{
// Parse token and get the key ID from its header.
$parser = new \Lcobucci\JWT\Parser();
$token = $parser->parse($token);
$kid = $token->getHeader('kid');
// Fetch broker keys.
$discoveryUrl = $this->broker . '/.well-known/openid-configuration';
$discoveryDoc = $this->store->fetchCached('discovery', $discoveryUrl);
if (!isset($discoveryDoc->jwks_uri) || !is_string($discoveryDoc->jwks_uri)) {
throw new \Exception('Discovery document incorrectly formatted');
}
$keysDoc = $this->store->fetchCached('keys', $discoveryDoc->jwks_uri);
if (!isset($keysDoc->keys) || !is_array($keysDoc->keys)) {
throw new \Exception('Keys document incorrectly formatted');
}
// Find the matching public key, and verify the signature.
$publicKey = null;
foreach ($keysDoc->keys as $key) {
if (isset($key->alg) && $key->alg === 'RS256' &&
isset($key->kid) && $key->kid === $kid &&
isset($key->n) && isset($key->e)) {
$publicKey = $key;
break;
}
}
if ($publicKey === null) {
throw new \Exception('Cannot find the public key used to sign the token');
}
if (!$token->verify(
new \Lcobucci\JWT\Signer\Rsa\Sha256(),
self::parseJwk($publicKey)
)) {
throw new \Exception('Token signature did not validate');
}
// Validate the token claims.
$vdata = new \Lcobucci\JWT\ValidationData();
$vdata->setIssuer($this->broker);
$vdata->setAudience($this->clientId);
if (!$token->validate($vdata)) {
throw new \Exception('Token claims did not validate');
}
// Get the email and consume the nonce.
$nonce = $token->getClaim('nonce');
$email = $token->getClaim('sub');
$this->store->consumeNonce($nonce, $email);
return $email;
}
/**
* Parse a JWK into an OpenSSL public key.
* @param object $jwk
* @return resource
*/
private static function parseJwk($jwk)
{
$n = gmp_init(bin2hex(\Base64Url\Base64Url::decode($jwk->n)), 16);
$e = gmp_init(bin2hex(\Base64Url\Base64Url::decode($jwk->e)), 16);
$seq = new \FG\ASN1\Universal\Sequence();
$seq->addChild(new \FG\ASN1\Universal\Integer(gmp_strval($n)));
$seq->addChild(new \FG\ASN1\Universal\Integer(gmp_strval($e)));
$pkey = new \FG\X509\PublicKey(bin2hex($seq->getBinary()));
$encoded = base64_encode($pkey->getBinary());
return new \Lcobucci\JWT\Signer\Key(
"-----BEGIN PUBLIC KEY-----\n" .
chunk_split($encoded, 64, "\n") .
"-----END PUBLIC KEY-----\n"
);
}
/**
* Get the origin for a URL
* @param string $url
* @return string
*/
private static function getOrigin($url)
{
$components = parse_url($url);
if ($components === false) {
throw new \Exception('Could not parse the redirect URI');
}
if (!isset($components['scheme'])) {
throw new \Exception('No scheme set in redirect URI');
}
$scheme = $components['scheme'];
if (!isset($components['host'])) {
throw new \Exception('No host set in redirect URI');
}
$host = $components['host'];
$res = $scheme . '://' . $host;
if (isset($components['port'])) {
$port = $components['port'];
if (($scheme === 'http' && $port !== 80) ||
($scheme === 'https' && $port !== 443)) {
$res .= ':' . $port;
}
}
return $res;
}
}
--- NEW FILE: AbstractStore.php ---
<?php
namespace Portier\Client;
/**
* An abstract base class for stores.
*
* Offers default implementations for fetching and generating nonces.
*/
abstract class AbstractStore implements StoreInterface
{
/**
* The Guzzle instance to use.
* @var \GuzzleHttp\Client
*/
public $guzzle;
/**
* Lifespan of a nonce.
* @var float
*/
public $nonceTtl = 15 * 60;
/**
* Minimum time to cache a HTTP response
* @var float
*/
public $cacheMinTtl = 60 * 60;
/**
* Constructor
*/
public function __construct()
{
$this->guzzle = new \GuzzleHttp\Client([
'timeout' => 10
]);
}
/**
* Generate a nonce value.
* @param string $email Optional email context
* @return string The generated nonce.
*/
public function generateNonce($email)
{
return bin2hex(random_bytes(16));
}
/**
* Fetch a URL using HTTP GET.
* @param string $url The URL to fetch.
* @return object An object with `ttl` and `data` properties.
*/
public function fetch($url)
{
$res = $this->guzzle->get($url);
$data = json_decode($res->getBody());
if (!($data instanceof \stdClass)) {
throw new \Exception('Invalid response body');
}
$ttl = 0;
if ($res->hasHeader('Cache-Control')) {
$cacheControl = $res->getHeaderLine('Cache-Control');
if (preg_match(
'/max-age\s*=\s*(\d+)/',
$cacheControl,
$matches
)) {
$ttl = intval($matches[1]);
}
}
$ttl = max($this->cacheMinTtl, $ttl);
return (object) [
'ttl' => $ttl,
'data' => $data,
];
}
}
--- NEW FILE: StoreInterface.php ---
<?php
namespace Portier\Client;
/**
* Interface for stores used by the client.
*/
interface StoreInterface
{
/**
* Fetch JSON from cache or using HTTP GET.
* @param string $cacheId The cache ID to use for this request.
* @param string $url The URL to fetch of the ID is not available.
* @return object The JSON object from the response body.
*/
public function fetchCached($cacheId, $url);
/**
* Generate and store a nonce.
* @param string $email Email address to associate with the nonce.
* @return string The generated nonce.
*/
public function createNonce($email);
/**
* Consume a nonce, and check if it's valid for the given email address.
* @param string $nonce The nonce to resolve.
* @param string $email The email address that is being verified.
*/
public function consumeNonce($nonce, $email);
}
|