friendship ended with social-app. php is my new best friend

Compare changes

Choose any two refs to compare.

Changed files
+121 -75
lib
templates
vendor
chillerlan
php-oauth
src
+73 -72
index.php
···
use Matrix\Async;
use GuzzleHttp\Psr7\HttpFactory;
use chillerlan\OAuth\OAuthOptions;
+
use chillerlan\OAuth\Storage\SessionStorage;
use DateTimeImmutable;
use Lcobucci\JWT\Token\Builder;
use Lcobucci\JWT\Encoding\ChainedFormatter;
···
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
use Smallnest\Bsky\BskyProvider;
+
session_start();
$bskyToucher = new BskyToucher();
$favoriteFeeds = array_map(function ($feed) use ($bskyToucher) {
···
Flight::set('publicApi', PUBLIC_API);
Flight::set('frontpageFeed', FRONTPAGE_FEED);
Flight::set('defaultRelay', DEFAULT_RELAY);
-
Flight::set('userAuth', null);
+
Flight::set('userAuth', array_key_exists('sbs_'.SITE_DOMAIN, $_SESSION) ? $_SESSION['sbs_'.SITE_DOMAIN] : null);
+
Flight::set('userPds', array_key_exists('sbs_'.SITE_DOMAIN.'_pds', $_SESSION) ? $_SESSION['sbs_'.SITE_DOMAIN.'_pds'] : null);
+
Flight::set('userInfo', array_key_exists('sbs_'.SITE_DOMAIN.'_userinfo', $_SESSION) ? $_SESSION['sbs_'.SITE_DOMAIN.'_userinfo'] : null);
Flight::set('flight.log_errors', false);
Flight::set('flight.handle_errors', false);
Flight::set('flight.content_length', false);
···
'setTheme' => array_key_exists('sbs_theme', $_COOKIE) ? $_COOKIE['sbs_theme'] : DEFAULT_THEME,
'setFont' => array_key_exists('sbs_font', $_COOKIE) ? $_COOKIE['sbs_font'] : DEFAULT_FONT,
'userAuth' => Flight::get('userAuth'),
+
'userPds' => Flight::get('userPds'),
+
'userInfo' => Flight::Get('userInfo'),
'favFeeds' => $favoriteFeeds,
'pages' => PAGES,
'links' => LINKS,
···
});
Flight::route('/login', function(): void {
-
if (!array_key_exists('username', $_GET)) {
+
if (isset($_GET['username'])) {
+
$username = $_GET['username'];
+
$bskyToucher = new BskyToucher();
+
$userInfo = $bskyToucher->getUserInfo($username);
+
if (!$userInfo) die(1);
+
$pds = $userInfo->pds;
+
$options = new OAuthOptions([
+
'key' => 'https://'.SITE_DOMAIN.CLIENT_ID,
+
'secret' => CLIENT_SECRET,
+
'callbackURL' => 'https://'.SITE_DOMAIN.'/login',
+
'sessionStart' => true,
+
'sessionStorageVar' => 'sbs_'.SITE_DOMAIN
+
]);
+
$storage = new SessionStorage($options);
+
$connector = new React\Socket\Connector([
+
'dns' => '1.1.1.1'
+
]);
+
$http = new React\Http\Browser($connector);
+
$httpFactory = new HttpFactory();
+
$token_builder = Builder::new(new JoseEncoder(), ChainedFormatter::default());
+
$algorithm = new Sha256();
+
$signing_key = InMemory::file(CERT_PATH);
+
$now = new DateTimeImmutable();
+
$token = $token_builder
+
->withHeader('alg', 'ES256')
+
->withHeader('typ', 'JWT')
+
->withHeader('kid', 'ocwgKj_O7H9at1sL6yWf9ZZ82NOM7D0xlN8HGIyWH6M')
+
->issuedBy('https://'.SITE_DOMAIN.CLIENT_ID)
+
->identifiedBy(uniqid())
+
->relatedTo('https://'.SITE_DOMAIN.CLIENT_ID)
+
->permittedFor($pds)
+
->issuedAt($now->modify('-5 seconds'))
+
->getToken($algorithm, $signing_key);
+
$client = new GuzzleHttp\Client([
+
'verify' => true,
+
'headers' => [
+
'User-Agent' => USER_AGENT_STR,
+
'Authorization' => 'Bearer: '.$token->toString()
+
]
+
]);
+
$provider = new BskyProvider($options, $client, $httpFactory, $httpFactory, $httpFactory);
+
$provider->setPds($pds);
+
$name = $provider->getName();
+
if (isset($_GET['login']) && $_GET['login'] === $name) {
+
$auth_url = $provider->getAuthorizationUrl([
+
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
+
'client_assertion' => $token->toString()
+
]);
+
Flight::redirect($auth_url);
+
die(1);
+
} else if (isset($_GET['code'], $_GET['iss'])) {
+
$storage->storeAccessToken($_GET['code'], $name);
+
$_SESSION['sbs_'.SITE_DOMAIN.'_pds'] = $_GET['iss'];
+
$_SESSION['sbs_'.SITE_DOMAIN.'_userinfo'] = $bskyToucher->getUserInfo();
+
Flight::redirect('/');
+
die(1);
+
} else if (isset($_GET['error'])) {
+
die(1);
+
}
+
} else {
$latte = new Latte\Engine;
$latte->render('./templates/login.latte', array_merge(Flight::get('standardParams'), [
'mainClass' => 'form',
···
'ogimage' => '',
'ogurl' => 'https://'.SITE_DOMAIN.'/login'
]));
-
die(1);
}
-
$username = $_GET['username'];
-
$bskyToucher = new BskyToucher();
-
$userInfo = $bskyToucher->getUserInfo($username);
-
if (!$userInfo) die(1);
-
$pds = $userInfo->pds;
-
$options = new OAuthOptions([
-
'key' => 'https://'.SITE_DOMAIN.CLIENT_ID,
-
'secret' => CLIENT_SECRET,
-
'callbackURL' => 'http://127.0.0.1/login',
-
'sessionStart' => true,
-
]);
-
$connector = new React\Socket\Connector([
-
'dns' => '1.1.1.1'
-
]);
-
$http = new React\Http\Browser($connector);
-
$httpFactory = new HttpFactory();
-
$token_builder = Builder::new(new JoseEncoder(), ChainedFormatter::default());
-
$algorithm = new Sha256();
-
$signing_key = InMemory::file(CERT_PATH);
-
$now = new DateTimeImmutable();
-
$token = $token_builder
-
->withHeader('alg', 'ES256')
-
->withHeader('typ', 'JWT')
-
->issuedBy($userInfo->did)
-
->relatedTo('https://'.SITE_DOMAIN.CLIENT_ID)
-
->permittedFor('did:web:'.str_replace("/", "", str_replace("https://", "", $pds)))
-
->issuedAt($now)
-
->getToken($algorithm, $signing_key);
-
$client = new GuzzleHttp\Client([
-
'verify' => true,
-
'headers' => [
-
'User-Agent' => USER_AGENT_STR,
-
'Authorization' => 'Bearer: '.$token->toString()
-
]
-
]);
-
$provider = new BskyProvider($options, $client, $httpFactory, $httpFactory, $httpFactory);
-
$provider->setPds($pds);
-
$name = $provider->getName();
-
-
if (isset($_GET['login']) && $_GET['login'] === $name) {
-
$auth_url = $provider->getAuthorizationUrl([
-
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
-
'client_assertion' => $token->toString()
-
]);
-
header('Location: '.$auth_url);
-
die(1);
-
} else if (isset($_GET['code'], $_GET['state'])) {
-
$token = $provider->getAccessToken($_GET['code'], $_GET['state']);
+
});
-
// save the token in a permanent storage
-
// [...]
-
-
// access granted, redirect
-
header('Location: ?granted='.$name);
-
die(1);
-
} else if (isset($_GET['granted']) && $_GET['granted'] === $name) {
-
die(1);
-
} else if (isset($_GET['error'])) {
-
die(1);
-
}
-
$latte = new Latte\Engine;
-
$latte->render('./templates/login.latte', array_merge(Flight::get('standardParams'), [
-
'mainClass' => 'form',
-
'ogtitle' => SITE_TITLE." | login",
-
'ogdesc' => SITE_DESC,
-
'ogimage' => '',
-
'ogurl' => 'https://'.SITE_DOMAIN.'/login'
-
]));
+
Flight::route('/logout', function(): void {
+
unset($_SESSION['sbs_'.SITE_DOMAIN]);
+
unset($_SESSION['sbs_'.SITE_DOMAIN.'_pds']);
+
unset($_SESSION['sbs_'.SITE_DOMAIN.'_userinfo']);
+
Flight::redirect('/');
});
-
-
// https://shimaenaga.veryroundbird.house/oauth/authorize?client_id=https%3A%2F%2Ftangled.org%2Foauth%2Fclient-metadata.json&request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Areq-2399ff42af66498132ebf8de809254b7
Flight::route('/createaccount', function(): void {
$latte = new Latte\Engine;
+40 -2
lib/bskyProvider.php
···
use chillerlan\OAuth\Core\OAuth2Interface;
use chillerlan\OAuth\Core\OAuth2Provider;
-
use chillerlan\OAuth\Core\PARTrait;
use chillerlan\OAuth\Core\PKCETrait;
use chillerlan\OAuth\OAuthOptions;
use chillerlan\OAuth\Storage\SessionStorage;
+
use chillerlan\HTTP\Utils\MessageUtil;
+
use chillerlan\HTTP\Utils\QueryUtil;
+
use chillerlan\OAuth\Providers\ProviderException;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
+
use Psr\Http\Message\UriInterface;
+
use function sprintf;
class BskyProvider extends OAuth2Provider implements \chillerlan\OAuth\Core\PAR, \chillerlan\OAuth\Core\PKCE {
-
use \chillerlan\OAuth\Core\PARTrait;
use \chillerlan\OAuth\Core\PKCETrait;
public const IDENTIFIER = 'BSKYPROVIDER';
···
$this->parAuthorizationURL = (string)$pds->withPath('/oauth/par');
return $this;
+
}
+
+
public function getParRequestUri(array $body):UriInterface{
+
// send the request with the same method and parameters as the token requests
+
// @link https://datatracker.ietf.org/doc/html/rfc9126#name-request
+
$response = $this->sendAccessTokenRequest($this->parAuthorizationURL, $body);
+
$status = $response->getStatusCode();
+
$json = MessageUtil::decodeJSON($response, true);
+
+
// something went horribly wrong
+
if($status !== 201){
+
+
// @link https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
+
if(isset($json['error'], $json['error_description'])){
+
throw new ProviderException(sprintf('PAR error: "%s" (%s)', $json['error'], $json['error_description']));
+
}
+
+
throw new ProviderException(sprintf('PAR request error: (HTTP/%s)', $status)); // @codeCoverageIgnore
+
}
+
+
$url = QueryUtil::merge($this->authorizationURL, $this->getParAuthorizationURLRequestParams($json));
+
+
return $this->uriFactory->createUri($url);
+
}
+
+
protected function getParAuthorizationURLRequestParams(array $response):array{
+
+
if(!isset($response['request_uri'])){
+
throw new ProviderException('PAR response error: "request_uri" missing');
+
}
+
+
return [
+
'client_id' => $this->options->key,
+
'request_uri' => $response['request_uri'],
+
];
}
}
?>
+1
lib/bskyToucher.php
···
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Fusonic\OpenGraph\Consumer;
+
use chillerlan\OAuth\Storage\SessionStorage;
class BskyToucher {
private $bskyApiBase = 'https://public.api.bsky.app/xrpc/';
+2 -1
templates/_partials/nav.latte
···
<nav>
<ul>
{if $userAuth}
+
<li><a href="/u/{$userInfo->handle}">profile</a></li>
<li><a href="/settings">settings</a></li>
-
<li><a href="#">log out</a></li>
+
<li><a href="/logout">log out</a></li>
{else}
<li><a href="/createaccount">create</a></li>
<li><a href="/login">log in</a></li>
+4
templates/layout.latte
···
data-theme="{$setTheme}"
data-font="{$setFont}"
>
+
<!--
+
{print_r($_SESSION)}
+
{print_r(PHP_SESSION_DISABLED)}
+
-->
<div id="page">
<header>
<h1><a href="/">{include '_partials/logo.latte'}{$siteTitle}</a></h1>
+1
vendor/chillerlan/php-oauth/src/Core/PARTrait.php
···
// something went horribly wrong
if($status !== 200){
+
print_r($json);
// @link https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
if(isset($json['error'], $json['error_description'])){