From 89a767ec252e39a7be9853d018a01c27c83b077b Mon Sep 17 00:00:00 2001 From: flashwave Date: Sat, 20 Jul 2024 03:25:26 +0000 Subject: [PATCH] Device code flow beginnings. --- src/Apps/AppInfo.php | 3 +- src/HanyuuContext.php | 1 + src/OAuth2/OAuth2AccessInfo.php | 2 - src/OAuth2/OAuth2AuthoriseData.php | 2 +- src/OAuth2/OAuth2AuthoriseInfo.php | 15 +++-- src/OAuth2/OAuth2DeviceInfo.php | 98 ++++++++++++++++++++++++++++++ src/OAuth2/OAuth2Routes.php | 54 ++++++++++++---- 7 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 src/OAuth2/OAuth2DeviceInfo.php diff --git a/src/Apps/AppInfo.php b/src/Apps/AppInfo.php index f9a70a7..d2b7197 100644 --- a/src/Apps/AppInfo.php +++ b/src/Apps/AppInfo.php @@ -72,7 +72,8 @@ class AppInfo { return $this->clientSecret; } public function verifyClientSecret(string $input): bool { - return password_verify($input, $this->clientSecret); + return $this->isConfidential() + && password_verify($input, $this->clientSecret); } public function getCreatedTime(): int { diff --git a/src/HanyuuContext.php b/src/HanyuuContext.php index d62f8bb..3af9052 100644 --- a/src/HanyuuContext.php +++ b/src/HanyuuContext.php @@ -89,6 +89,7 @@ class HanyuuContext { }); $routingCtx->register(new OAuth2\OAuth2Routes( + $this->scopeTo('oauth2'), $this->oauth2Ctx, $this->appsCtx, $this->templating, diff --git a/src/OAuth2/OAuth2AccessInfo.php b/src/OAuth2/OAuth2AccessInfo.php index c8f2c0b..33cdf9a 100644 --- a/src/OAuth2/OAuth2AccessInfo.php +++ b/src/OAuth2/OAuth2AccessInfo.php @@ -59,11 +59,9 @@ class OAuth2AccessInfo { public function getExpiresTime(): int { return $this->expires; } - public function getRemainingLifetime(): int { return max(0, $this->expires - time()); } - public function hasExpired(): bool { return time() > $this->expires; } diff --git a/src/OAuth2/OAuth2AuthoriseData.php b/src/OAuth2/OAuth2AuthoriseData.php index 978de77..deb5d87 100644 --- a/src/OAuth2/OAuth2AuthoriseData.php +++ b/src/OAuth2/OAuth2AuthoriseData.php @@ -101,7 +101,7 @@ class OAuth2AuthoriseData { $stmt->addParameter(5, $challengeCode); $stmt->addParameter(6, $challengeMethod); $stmt->addParameter(7, $scope); - $stmt->addParameter(8, XString::random(100)); + $stmt->addParameter(8, XString::random(60)); $stmt->execute(); return $this->getAuthoriseInfo(authoriseId: (string)$this->dbConn->getLastInsertId()); diff --git a/src/OAuth2/OAuth2AuthoriseInfo.php b/src/OAuth2/OAuth2AuthoriseInfo.php index dbd7086..77c7644 100644 --- a/src/OAuth2/OAuth2AuthoriseInfo.php +++ b/src/OAuth2/OAuth2AuthoriseInfo.php @@ -5,8 +5,6 @@ use Index\Data\IDbResult; use Index\Serialisation\UriBase64; class OAuth2AuthoriseInfo { - private const EXPIRES_TIME = 10 * 60; - public function __construct( private string $id, private string $appId, @@ -18,7 +16,8 @@ class OAuth2AuthoriseInfo { private string $scope, private string $code, private string $approval, - private int $created + private int $created, + private int $expires ) {} public static function fromResult(IDbResult $result): OAuth2AuthoriseInfo { @@ -34,6 +33,7 @@ class OAuth2AuthoriseInfo { code: $result->getString(8), approval: $result->getString(9), created: $result->getInteger(10), + expires: $result->getInteger(11), ); } @@ -105,7 +105,14 @@ class OAuth2AuthoriseInfo { public function getCreatedTime(): int { return $this->created; } + + public function getExpiresTime(): int { + return $this->expires; + } + public function getRemainingLifetime(): int { + return max(0, $this->expires - time()); + } public function hasExpired(): bool { - return time() > ($this->created + self::EXPIRES_TIME); + return time() > $this->expires; } } diff --git a/src/OAuth2/OAuth2DeviceInfo.php b/src/OAuth2/OAuth2DeviceInfo.php new file mode 100644 index 0000000..7b7b80f --- /dev/null +++ b/src/OAuth2/OAuth2DeviceInfo.php @@ -0,0 +1,98 @@ +getString(0), + appId: $result->getString(1), + userId: $result->getStringOrNull(2), + code: $result->getString(3), + userCode: $result->getString(4), + attempts: $result->getInteger(5), + interval: $result->getInteger(6), + polled: $result->getInteger(7), + approval: $result->getString(8), + created: $result->getInteger(9), + expires: $result->getInteger(10), + ); + } + + public function getId(): string { + return $this->id; + } + + public function getAppId(): string { + return $this->appId; + } + + public function hasUserId(): bool { + return $this->userId !== null; + } + public function getUserId(): ?string { + return $this->userId; + } + + public function getCode(): string { + return $this->code; + } + + public function getUserCode(): string { + return $this->userCode; + } + + public function getRemainingAttempts(): int { + return $this->attempts; + } + + public function getPollInterval(): int { + return $this->interval; + } + + public function getPolledTime(): int { + return $this->polled; + } + + public function getApproval(): string { + return $this->approval; + } + public function isPending(): bool { + return strcasecmp($this->approval, 'pending') === 0; + } + public function isApproved(): bool { + return strcasecmp($this->approval, 'approved') === 0; + } + public function isDenied(): bool { + return strcasecmp($this->approval, 'denied') === 0; + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getExpiresTime(): int { + return $this->expires; + } + public function getRemainingLifetime(): int { + return max(0, $this->expires - time()); + } + public function hasExpired(): bool { + return time() > $this->expires; + } +} diff --git a/src/OAuth2/OAuth2Routes.php b/src/OAuth2/OAuth2Routes.php index fa58a08..95fdb3e 100644 --- a/src/OAuth2/OAuth2Routes.php +++ b/src/OAuth2/OAuth2Routes.php @@ -6,10 +6,12 @@ use RuntimeException; use Index\XString; use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,RouteHandler}; use Sasae\SasaeEnvironment; +use Syokuhou\IConfig; use Hanyuu\Apps\AppsContext; final class OAuth2Routes extends RouteHandler { public function __construct( + private IConfig $config, private OAuth2Context $oauth2Ctx, private AppsContext $appsCtx, private SasaeEnvironment $templating, @@ -202,15 +204,11 @@ final class OAuth2Routes extends RouteHandler { if(!in_array($approve, ['yes', 'no'])) return 400; - $code = (string)$content->getParam('code'); - if(strlen($code) !== 100) - return 400; - $authoriseData = $this->oauth2Ctx->getAuthoriseData(); try { $authoriseInfo = $authoriseData->getAuthoriseInfo( userId: $authInfo->user->id, - code: $code, + code: (string)$content->getParam('code'), ); } catch(RuntimeException $ex) { return 404; @@ -253,6 +251,8 @@ final class OAuth2Routes extends RouteHandler { #[HttpPost('/oauth2/authorise-device')] public function postAuthoriseDevice($response, $request) { + $response->setHeader('Cache-Control', 'no-store'); + if(!$request->isFormContent()) { $response->setStatusCode(400); return self::error('invalid_request', 'Your request must use content type application/x-www-form-urlencoded.'); @@ -260,23 +260,53 @@ final class OAuth2Routes extends RouteHandler { $content = $request->getContent(); - $clientId = (string)$content->getParam('client_id'); - if($clientId === 'wrong') { // verify and look up + $authzHeader = explode(' ', (string)$request->getHeaderLine('Authorization')); + if($authzHeader[0] === 'Basic') { + $authzHeader = explode(':', base64_decode($authzHeader[1] ?? '')); + $clientId = $authzHeader[0]; + $clientSecret = $authzHeader[1] ?? ''; + } elseif($authzHeader[0] !== '') { + $response->setStatusCode(401); + $message = 'You must use the Basic method for Authorization parameters.'; + $response->setHeader('WWW-Authenticate', "Basic realm=\"{$message}\""); + return self::error('invalid_client', $message); + } else { + $clientId = (string)$content->getParam('client_id'); + $clientSecret = ''; + } + + $appsData = $this->appsCtx->getData(); + + try { + $appInfo = $appsData->getAppInfo(clientId: $clientId, deleted: false); + } catch(RuntimeException $ex) { $response->setStatusCode(400); return self::error('invalid_client', 'No application has been registered with this client id.'); } - $scope = self::filterScopes((string)$content->getParam('scope')); - if(in_array('invalid', $scope)) { + $appAuthenticated = false; + if($clientSecret !== '') { + // TODO: rate limiting + if(!$appInfo->verifyClientSecret($clientSecret)) { + $response->setStatusCode(400); + return self::error('invalid_client', 'Provided client secret is not correct for this application.'); + } + } + + $scopes = self::filterScopes((string)$content->getParam('scope')); + if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes)) { $response->setStatusCode(400); return self::error('invalid_scope', 'An invalid scope was requested.'); } + $scope = implode(' ', $scopes); + + $userCode = 'SOAP-504P'; $result = [ 'device_code' => 'soapsoapsoapsoapsoapsoapsoapsoap', - 'user_code' => 'SOAP-504P', - 'verification_uri' => 'https://fii.moe/auth', - 'verification_uri_complete' => 'https://fii.moe/auth?code=SOAP-504P', + 'user_code' => $userCode, + 'verification_uri' => $this->config->getString('device:verification_uri'), + 'verification_uri_complete' => sprintf($this->config->getString('device:verification_uri_complete'), $userCode), 'expires_in' => 1800, ];