friendship ended with social-app. php is my new best friend
1<?php
2namespace Smallnest\Bsky;
3
4require_once('config.php');
5require_once('vendor/autoload.php');
6
7use chillerlan\OAuth\Core\OAuth2Interface;
8use chillerlan\OAuth\Core\OAuth2Provider;
9use chillerlan\OAuth\Core\PKCETrait;
10use chillerlan\OAuth\OAuthOptions;
11use chillerlan\OAuth\Storage\SessionStorage;
12use chillerlan\HTTP\Utils\MessageUtil;
13use chillerlan\HTTP\Utils\QueryUtil;
14use chillerlan\OAuth\Providers\ProviderException;
15use GuzzleHttp\Client;
16use GuzzleHttp\Psr7\HttpFactory;
17use Psr\Http\Message\UriInterface;
18use function sprintf;
19
20class BskyProvider extends OAuth2Provider implements \chillerlan\OAuth\Core\PAR, \chillerlan\OAuth\Core\PKCE {
21 use \chillerlan\OAuth\Core\PKCETrait;
22
23 public const IDENTIFIER = 'BSKYPROVIDER';
24 public const SCOPE_ATPROTO = 'atproto';
25 public const SCOPE_TRANSITION_GENERIC = 'transition:generic';
26 public const AUTH_METHOD = self::AUTH_METHOD_HEADER;
27
28 protected string $authorizationURL = 'https://bsky.app/oauth/authorize';
29 protected string $accessTokenURL = 'https://bsky.app/oauth/token';
30 protected string $apiURL = 'https://bsky.app/xrpc';
31 protected string $parAuthorizationURL = 'https://bsky.app/oauth/par';
32
33 public const DEFAULT_SCOPES = [
34 self::SCOPE_ATPROTO,
35 self::SCOPE_TRANSITION_GENERIC,
36 ];
37
38 public function setPds(UriInterface|string $pds):static{
39 if(!$pds instanceof UriInterface){
40 $pds = $this->uriFactory->createUri($pds);
41 }
42
43 // throw if the host is empty
44 if($pds->getHost() === ''){
45 throw new OAuthException('invalid PDS URL');
46 }
47
48 // enforce https and remove unnecessary parts
49 $pds = $pds->withScheme('https')->withQuery('')->withFragment('');
50
51 // set the provider URLs
52 $this->authorizationURL = (string)$pds->withPath('/oauth/authorize');
53 $this->accessTokenURL = (string)$pds->withPath('/oauth/token');
54 $this->apiURL = (string)$pds->withPath('/xrpc');
55 $this->parAuthorizationURL = (string)$pds->withPath('/oauth/par');
56
57 return $this;
58 }
59
60 public function getParRequestUri(array $body):UriInterface{
61 // send the request with the same method and parameters as the token requests
62 // @link https://datatracker.ietf.org/doc/html/rfc9126#name-request
63 $response = $this->sendAccessTokenRequest($this->parAuthorizationURL, $body);
64 $status = $response->getStatusCode();
65 $json = MessageUtil::decodeJSON($response, true);
66
67 // something went horribly wrong
68 if($status !== 201){
69
70 // @link https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
71 if(isset($json['error'], $json['error_description'])){
72 throw new ProviderException(sprintf('PAR error: "%s" (%s)', $json['error'], $json['error_description']));
73 }
74
75 throw new ProviderException(sprintf('PAR request error: (HTTP/%s)', $status)); // @codeCoverageIgnore
76 }
77
78 $url = QueryUtil::merge($this->authorizationURL, $this->getParAuthorizationURLRequestParams($json));
79
80 return $this->uriFactory->createUri($url);
81 }
82
83 protected function getParAuthorizationURLRequestParams(array $response):array{
84
85 if(!isset($response['request_uri'])){
86 throw new ProviderException('PAR response error: "request_uri" missing');
87 }
88
89 return [
90 'client_id' => $this->options->key,
91 'request_uri' => $response['request_uri'],
92 ];
93 }
94}
95?>