misuzu/src/Twitter/TwitterAuthorisation.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);
}
}