Device code flow beginnings.
This commit is contained in:
parent
3ea982fd43
commit
89a767ec25
7 changed files with 155 additions and 20 deletions
|
@ -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 {
|
||||
|
|
|
@ -89,6 +89,7 @@ class HanyuuContext {
|
|||
});
|
||||
|
||||
$routingCtx->register(new OAuth2\OAuth2Routes(
|
||||
$this->scopeTo('oauth2'),
|
||||
$this->oauth2Ctx,
|
||||
$this->appsCtx,
|
||||
$this->templating,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
98
src/OAuth2/OAuth2DeviceInfo.php
Normal file
98
src/OAuth2/OAuth2DeviceInfo.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
|
||||
|
|
Loading…
Reference in a new issue