Device code flow beginnings.

This commit is contained in:
flash 2024-07-20 03:25:26 +00:00
parent 3ea982fd43
commit 89a767ec25
7 changed files with 155 additions and 20 deletions

View file

@ -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 {

View file

@ -89,6 +89,7 @@ class HanyuuContext {
});
$routingCtx->register(new OAuth2\OAuth2Routes(
$this->scopeTo('oauth2'),
$this->oauth2Ctx,
$this->appsCtx,
$this->templating,

View file

@ -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;
}

View file

@ -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());

View file

@ -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;
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Hanyuu\OAuth2;
use Index\Data\IDbResult;
class OAuth2DeviceInfo {
public function __construct(
private string $id,
private string $appId,
private ?string $userId,
private string $code,
private string $userCode,
private int $attempts,
private int $interval,
private int $polled,
private string $approval,
private int $created,
private int $expires
) {}
public static function fromResult(IDbResult $result): OAuth2AuthoriseInfo {
return new OAuth2AuthoriseInfo(
id: $result->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;
}
}

View file

@ -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,
];