116 lines
3.5 KiB
PHP
116 lines
3.5 KiB
PHP
|
<?php
|
||
|
namespace Misuzu\Twitter;
|
||
|
|
||
|
use Index\XString;
|
||
|
use Index\Serialisation\Serialiser;
|
||
|
|
||
|
class TwitterAuthorisation {
|
||
|
private const AUTHORIZE = 'https://twitter.com/i/oauth2/authorize';
|
||
|
|
||
|
private const STATE_RNG_LENGTH = 16;
|
||
|
private const STATE_EPOCH = 1661126400;
|
||
|
private const STATE_TOLERANCE = 5 * 60;
|
||
|
private const VERIFIER_LENGTH = 48;
|
||
|
|
||
|
private TwitterClientId $clientId;
|
||
|
private array $scope;
|
||
|
private string $redirect;
|
||
|
private string $state;
|
||
|
private string $verifier;
|
||
|
private string $verifierHash;
|
||
|
|
||
|
public function __construct(TwitterClientId $clientId, array $scope, string $redirect) {
|
||
|
$this->clientId = $clientId;
|
||
|
$this->scope = $scope;
|
||
|
$this->redirect = $redirect;
|
||
|
|
||
|
$this->state = self::generateState($clientId);
|
||
|
[$this->verifier, $this->verifierHash] = self::generateVerifier();
|
||
|
}
|
||
|
|
||
|
public function getClientId(): TwitterClientId {
|
||
|
return $this->clientId;
|
||
|
}
|
||
|
|
||
|
public function getScope(): array {
|
||
|
return $this->scope;
|
||
|
}
|
||
|
|
||
|
public function getRedirectUri(): string {
|
||
|
return $this->redirect;
|
||
|
}
|
||
|
|
||
|
public function getState(): string {
|
||
|
return $this->state;
|
||
|
}
|
||
|
|
||
|
public function getVerifier(): string {
|
||
|
return $this->verifier;
|
||
|
}
|
||
|
|
||
|
public function getVerifierHash(): string {
|
||
|
return $this->verifierHash;
|
||
|
}
|
||
|
|
||
|
public function getUri(): string {
|
||
|
return self::AUTHORIZE . '?' . http_build_query([
|
||
|
'response_type' => 'code',
|
||
|
'client_id' => $this->clientId->getClientId(),
|
||
|
'redirect_uri' => $this->redirect,
|
||
|
'scope' => implode(' ', $this->scope),
|
||
|
'state' => $this->state,
|
||
|
'code_challenge' => $this->verifierHash,
|
||
|
'code_challenge_method' => 'S256',
|
||
|
], '', null, PHP_QUERY_RFC3986);
|
||
|
}
|
||
|
|
||
|
public static function generateVerifier(): array {
|
||
|
$verifier = XString::random(self::VERIFIER_LENGTH);
|
||
|
return [
|
||
|
$verifier,
|
||
|
Serialiser::uriBase64()->serialise(hash('sha256', $verifier, true)),
|
||
|
];
|
||
|
}
|
||
|
|
||
|
private static function currentStateTime(): int {
|
||
|
return time() - self::STATE_EPOCH;
|
||
|
}
|
||
|
|
||
|
public static function generateState(TwitterClientId $clientId): string {
|
||
|
$rng = XString::random(self::STATE_RNG_LENGTH);
|
||
|
$time = self::currentStateTime();
|
||
|
|
||
|
$string = $rng . ':' . (string)$time;
|
||
|
$hash = hash_hmac('sha256', $string, $clientId->getClientSecret(), true);
|
||
|
|
||
|
$time = Serialiser::base62()->serialise($time);
|
||
|
$hash = Serialiser::uriBase64()->serialise($hash);
|
||
|
|
||
|
return $rng . '.' . $time . '.' . $hash;
|
||
|
}
|
||
|
|
||
|
public static function verifyState(TwitterClientId $clientId, string $state): bool {
|
||
|
$parts = explode('.', $state, 4);
|
||
|
if(count($parts) !== 3)
|
||
|
return false;
|
||
|
|
||
|
$rng = $parts[0];
|
||
|
if(strlen($rng) !== self::STATE_RNG_LENGTH)
|
||
|
return false;
|
||
|
|
||
|
$currentTime = self::currentStateTime();
|
||
|
$time = Serialiser::base62()->deserialise($parts[1]);
|
||
|
if($currentTime < $time || $currentTime >= ($time + self::STATE_TOLERANCE))
|
||
|
return false;
|
||
|
|
||
|
$hash = Serialiser::uriBase64()->deserialise($parts[2]);
|
||
|
if(strlen($hash) !== 32)
|
||
|
return false;
|
||
|
|
||
|
$string = $rng . ':' . (string)$time;
|
||
|
$realHash = hash_hmac('sha256', $string, $clientId->getClientSecret(), true);
|
||
|
|
||
|
return hash_equals($realHash, $hash);
|
||
|
}
|
||
|
}
|