Compare commits
3 commits
926bd02bfc
...
12d48cce2e
Author | SHA1 | Date | |
---|---|---|---|
12d48cce2e | |||
2ba03885ed | |||
dcb5cf0175 |
22 changed files with 569 additions and 159 deletions
|
@ -28,6 +28,10 @@
|
||||||
background-color: #0a0;
|
background-color: #0a0;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
|
.oauth2-scope-perm-icon-warn {
|
||||||
|
mask: url('/images/circle-regular.svg') no-repeat center, url('/images/exclamation-solid.svg') no-repeat center center / 10px 10px;
|
||||||
|
background-color: #c80;
|
||||||
|
}
|
||||||
.oauth2-scope-perm-text {
|
.oauth2-scope-perm-text {
|
||||||
font-size: .8em;
|
font-size: .8em;
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
const HanyuuOAuth2AppScopeEntry = function(text) {
|
const HanyuuOAuth2AppScopeEntry = function(text, warn) {
|
||||||
|
const icon = <div class="oauth2-scope-perm-icon"/>;
|
||||||
|
if(warn)
|
||||||
|
icon.classList.add('oauth2-scope-perm-icon-warn');
|
||||||
|
|
||||||
const element = <div class="oauth2-scope-perm">
|
const element = <div class="oauth2-scope-perm">
|
||||||
<div class="oauth2-scope-perm-icon"></div>
|
{icon}
|
||||||
<div class="oauth2-scope-perm-text">{text}</div>
|
<div class="oauth2-scope-perm-text">{text}</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
@ -9,14 +13,14 @@ const HanyuuOAuth2AppScopeEntry = function(text) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const HanyuuOAuth2AppScopeList = function() {
|
const HanyuuOAuth2AppScopeList = function(scopes) {
|
||||||
// TODO: actual scope listing
|
const permsElem = <div class="oauth2-scope-perms"/>;
|
||||||
const permsElem = <div class="oauth2-scope-perms">
|
if(Array.isArray(scopes) && scopes.length > 0) {
|
||||||
{new HanyuuOAuth2AppScopeEntry('Do anything because I have not made up scopes yet.')}
|
for(const scope of scopes)
|
||||||
{new HanyuuOAuth2AppScopeEntry('Eat soup.')}
|
if(typeof scope === 'string')
|
||||||
{new HanyuuOAuth2AppScopeEntry('These are placeholders.')}
|
permsElem.appendChild(new HanyuuOAuth2AppScopeEntry(scope).element);
|
||||||
{new HanyuuOAuth2AppScopeEntry('This one is really long because I want to test wrapping and how the chevron icon thing will handle it so there will be a lot of text here, the app will not be gaining anything from it but yeah sometimes you just need to explode seventy times.')}
|
} else
|
||||||
</div>;
|
permsElem.appendChild(new HanyuuOAuth2AppScopeEntry('A limited amount of things. No scope was specified by the developer.', true).element);
|
||||||
|
|
||||||
const element = <div class="oauth2-scope">
|
const element = <div class="oauth2-scope">
|
||||||
<div class="oauth2-scope-header">This application will be able to:</div>
|
<div class="oauth2-scope-header">This application will be able to:</div>
|
||||||
|
|
|
@ -75,7 +75,7 @@ const HanyuuOAuth2Authorise = async () => {
|
||||||
|
|
||||||
window.location.assign(errorUri);
|
window.location.assign(errorUri);
|
||||||
};
|
};
|
||||||
const translateError = serverError => {
|
const translateError = (serverError, detail) => {
|
||||||
if(serverError === 'auth')
|
if(serverError === 'auth')
|
||||||
return displayError('access_denied');
|
return displayError('access_denied');
|
||||||
if(serverError === 'csrf')
|
if(serverError === 'csrf')
|
||||||
|
@ -91,7 +91,7 @@ const HanyuuOAuth2Authorise = async () => {
|
||||||
if(serverError === 'required')
|
if(serverError === 'required')
|
||||||
return displayError('invalid_request', 'A registered redirect URI must be specified.');
|
return displayError('invalid_request', 'A registered redirect URI must be specified.');
|
||||||
if(serverError === 'scope')
|
if(serverError === 'scope')
|
||||||
return displayError('invalid_scope');
|
return displayError('invalid_scope', detail === undefined ? undefined : `Requested scope "${detail.scope}" is ${detail.reason}.`);
|
||||||
if(serverError === 'authorise')
|
if(serverError === 'authorise')
|
||||||
return displayError('server_error', 'Server was unable to complete authorisation.');
|
return displayError('server_error', 'Server was unable to complete authorisation.');
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ const HanyuuOAuth2Authorise = async () => {
|
||||||
if(!resolved)
|
if(!resolved)
|
||||||
throw 'authorisation resolve failed';
|
throw 'authorisation resolve failed';
|
||||||
if(typeof resolved.error === 'string')
|
if(typeof resolved.error === 'string')
|
||||||
return translateError(resolved.error);
|
return translateError(resolved.error, resolved);
|
||||||
|
|
||||||
const userHeader = new HanyuuOAuth2UserHeader(resolved.user);
|
const userHeader = new HanyuuOAuth2UserHeader(resolved.user);
|
||||||
header.setElement(userHeader);
|
header.setElement(userHeader);
|
||||||
|
@ -184,7 +184,7 @@ const HanyuuOAuth2Authorise = async () => {
|
||||||
if(!response)
|
if(!response)
|
||||||
throw 'authorisation failed';
|
throw 'authorisation failed';
|
||||||
if(typeof response.error === 'string')
|
if(typeof response.error === 'string')
|
||||||
return translateError(response.error);
|
return translateError(response.error, resolved);
|
||||||
|
|
||||||
const authoriseUri = new URL(response.redirect);
|
const authoriseUri = new URL(response.redirect);
|
||||||
authoriseUri.searchParams.set('code', response.code);
|
authoriseUri.searchParams.set('code', response.code);
|
||||||
|
@ -206,11 +206,8 @@ const HanyuuOAuth2Authorise = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appElem = new HanyuuOAuth2AppInfo(resolved.app);
|
eAuthsInfo.replaceWith(new HanyuuOAuth2AppInfo(resolved.app).element);
|
||||||
eAuthsInfo.replaceWith(appElem.element);
|
eAuthsScope.replaceWith(new HanyuuOAuth2AppScopeList(resolved.scope).element);
|
||||||
|
|
||||||
const scopeElem = new HanyuuOAuth2AppScopeList();
|
|
||||||
eAuthsScope.replaceWith(scopeElem.element);
|
|
||||||
|
|
||||||
fAuths.onsubmit = ev => {
|
fAuths.onsubmit = ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
|
@ -40,9 +40,16 @@ const HanyuuOAuth2Verify = () => {
|
||||||
alert('This code is not associated with any authorisation request.');
|
alert('This code is not associated with any authorisation request.');
|
||||||
else if(response.error === 'approval')
|
else if(response.error === 'approval')
|
||||||
alert('The authorisation request associated with this code is not pending approval.');
|
alert('The authorisation request associated with this code is not pending approval.');
|
||||||
|
else if(response.error === 'expired')
|
||||||
|
alert('The authorisation request has expired, please restart the process from the application or device.');
|
||||||
else if(response.error === 'invalid')
|
else if(response.error === 'invalid')
|
||||||
alert('Invalid approval state specified.');
|
alert('Invalid approval state specified.');
|
||||||
else
|
else if(response.error === 'scope') {
|
||||||
|
alert(`Requested scope "${response.scope}" is ${response.reason}.`);
|
||||||
|
loading.visible = false;
|
||||||
|
rDenied.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
} else
|
||||||
alert(`An unknown error occurred: ${response.error}`);
|
alert(`An unknown error occurred: ${response.error}`);
|
||||||
|
|
||||||
loading.visible = false;
|
loading.visible = false;
|
||||||
|
@ -83,7 +90,8 @@ const HanyuuOAuth2Verify = () => {
|
||||||
loading.visible = true;
|
loading.visible = true;
|
||||||
fCode.classList.add('hidden');
|
fCode.classList.add('hidden');
|
||||||
|
|
||||||
$x.get(`/oauth2/resolve-verify?csrfp=${encodeURIComponent(HanyuuCSRFP.getToken())}&code=${encodeURIComponent(eUserCode.value)}`, { type: 'json' })
|
userCode= encodeURIComponent(eUserCode.value);
|
||||||
|
$x.get(`/oauth2/resolve-verify?csrfp=${encodeURIComponent(HanyuuCSRFP.getToken())}&code=${userCode}`, { type: 'json' })
|
||||||
.then(result => {
|
.then(result => {
|
||||||
const response = result.body();
|
const response = result.body();
|
||||||
|
|
||||||
|
@ -102,9 +110,16 @@ const HanyuuOAuth2Verify = () => {
|
||||||
alert('Request verification failed, please refresh and try again.');
|
alert('Request verification failed, please refresh and try again.');
|
||||||
else if(response.error === 'code')
|
else if(response.error === 'code')
|
||||||
alert('This code is not associated with any authorisation request.');
|
alert('This code is not associated with any authorisation request.');
|
||||||
|
else if(response.error === 'expired')
|
||||||
|
alert('The authorisation request has expired, please restart the process from the application or device.');
|
||||||
else if(response.error === 'approval')
|
else if(response.error === 'approval')
|
||||||
alert('The authorisation request associated with this code is not pending approval.');
|
alert('The authorisation request associated with this code is not pending approval.');
|
||||||
else
|
else if(response.error === 'scope') {
|
||||||
|
verifyAuthsRequest(false).finally(() => {
|
||||||
|
alert(`Requested scope "${response.scope}" is ${response.reason}.`);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else
|
||||||
alert(`An unknown error occurred: ${response.error}`);
|
alert(`An unknown error occurred: ${response.error}`);
|
||||||
|
|
||||||
loading.visible = false;
|
loading.visible = false;
|
||||||
|
@ -125,11 +140,8 @@ const HanyuuOAuth2Verify = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appElem = new HanyuuOAuth2AppInfo(response.app);
|
eAuthsInfo.replaceWith(new HanyuuOAuth2AppInfo(response.app).element);
|
||||||
eAuthsInfo.replaceWith(appElem.element);
|
eAuthsScope.replaceWith(new HanyuuOAuth2AppScopeList(response.scope).element);
|
||||||
|
|
||||||
const scopeElem = new HanyuuOAuth2AppScopeList();
|
|
||||||
eAuthsScope.replaceWith(scopeElem.element);
|
|
||||||
|
|
||||||
loading.visible = false;
|
loading.visible = false;
|
||||||
fAuths.classList.remove('hidden');
|
fAuths.classList.remove('hidden');
|
||||||
|
|
43
database/2024_09_03_201657_scope_tables.php
Normal file
43
database/2024_09_03_201657_scope_tables.php
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
use Index\Data\IDbConnection;
|
||||||
|
use Index\Data\Migration\IDbMigration;
|
||||||
|
|
||||||
|
final class ScopeTables_20240903_201657 implements IDbMigration {
|
||||||
|
public function migrate(IDbConnection $conn): void {
|
||||||
|
$conn->execute(<<<SQL
|
||||||
|
CREATE TABLE hau_scopes (
|
||||||
|
scope_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
scope_string VARCHAR(50) NOT NULL COLLATE 'ascii_bin',
|
||||||
|
scope_restricted TINYINT(3) UNSIGNED NOT NULL,
|
||||||
|
scope_summary VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8mb4_unicode_520_ci',
|
||||||
|
scope_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
|
||||||
|
scope_deprecated TIMESTAMP NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (scope_id),
|
||||||
|
UNIQUE INDEX hau_scopes_string_unique (scope_string),
|
||||||
|
INDEX hau_scopes_created_index (scope_created),
|
||||||
|
INDEX hau_scopes_deprecated_index (scope_deprecated)
|
||||||
|
) COLLATE=utf8mb4_bin ENGINE=InnoDB;
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
$conn->execute(<<<SQL
|
||||||
|
CREATE TABLE hau_apps_scopes (
|
||||||
|
app_id INT(10) UNSIGNED NOT NULL,
|
||||||
|
scope_id INT(10) UNSIGNED NOT NULL,
|
||||||
|
scope_allowed TINYINT(3) UNSIGNED NOT NULL,
|
||||||
|
PRIMARY KEY (app_id, scope_id),
|
||||||
|
INDEX hau_apps_scopes_app_foreign (app_id),
|
||||||
|
INDEX hau_apps_scopes_scope_foreign (scope_id),
|
||||||
|
CONSTRAINT hau_apps_scopes_app_foreign
|
||||||
|
FOREIGN KEY (app_id)
|
||||||
|
REFERENCES hau_apps (app_id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT hau_apps_scopes_scope_foreign
|
||||||
|
FOREIGN KEY (scope_id)
|
||||||
|
REFERENCES hau_scopes (scope_id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE
|
||||||
|
) COLLATE=utf8mb4_bin ENGINE=InnoDB;
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
1
public/images/circle-regular.svg
Normal file
1
public/images/circle-regular.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg>
|
After Width: | Height: | Size: 328 B |
1
public/images/exclamation-solid.svg
Normal file
1
public/images/exclamation-solid.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M96 64c0-17.7-14.3-32-32-32S32 46.3 32 64l0 256c0 17.7 14.3 32 32 32s32-14.3 32-32L96 64zM64 480a40 40 0 1 0 0-80 40 40 0 1 0 0 80z"/></svg>
|
After Width: | Height: | Size: 362 B |
|
@ -90,7 +90,7 @@ class AppInfo {
|
||||||
public function getClientSecret(): string {
|
public function getClientSecret(): string {
|
||||||
return $this->clientSecret;
|
return $this->clientSecret;
|
||||||
}
|
}
|
||||||
public function verifyClientSecret(string $input): bool {
|
public function verifyClientSecret(#[\SensitiveParameter] string $input): bool {
|
||||||
return $this->isConfidential()
|
return $this->isConfidential()
|
||||||
&& password_verify($input, $this->clientSecret);
|
&& password_verify($input, $this->clientSecret);
|
||||||
}
|
}
|
||||||
|
|
27
src/Apps/AppScopesInfo.php
Normal file
27
src/Apps/AppScopesInfo.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
namespace Hanyuu\Apps;
|
||||||
|
|
||||||
|
class AppScopesInfo {
|
||||||
|
public function __construct(
|
||||||
|
private array $allowed,
|
||||||
|
private array $denied
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getAllowed(): array {
|
||||||
|
return $this->allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDenied(): array {
|
||||||
|
return $this->denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAllowed(string $scope, bool $requiresAllow): bool {
|
||||||
|
if(in_array($scope, $this->denied))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if($requiresAllow && !in_array($scope, $this->allowed))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,60 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Hanyuu\Apps;
|
namespace Hanyuu\Apps;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
use Index\Data\IDbConnection;
|
use Index\Data\IDbConnection;
|
||||||
|
|
||||||
class AppsContext {
|
class AppsContext {
|
||||||
private AppsData $data;
|
private AppsData $apps;
|
||||||
|
private ScopesData $scopes;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn) {
|
public function __construct(IDbConnection $dbConn) {
|
||||||
$this->data = new AppsData($dbConn);
|
$this->apps = new AppsData($dbConn);
|
||||||
|
$this->scopes = new ScopesData($dbConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getData(): AppsData {
|
public function getAppsData(): AppsData {
|
||||||
return $this->data;
|
return $this->apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScopesData(): ScopesData {
|
||||||
|
return $this->scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleScopeString(AppInfo|string $appInfo, string $scope, bool $allowDeprecated = false, bool $sort = true, bool $breakOnFail = true): array {
|
||||||
|
return $this->handleScopes($appInfo, explode(' ', $scope), $allowDeprecated, $sort, $breakOnFail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleScopes(AppInfo|string $appInfo, array $strings, bool $allowDeprecated = false, bool $sort = true, bool $breakOnFail = true): array {
|
||||||
|
if(is_string($appInfo))
|
||||||
|
$appInfo = $this->apps->getAppInfo(appId: $appInfo, deleted: false);
|
||||||
|
|
||||||
|
$infos = [];
|
||||||
|
|
||||||
|
foreach($strings as $string) {
|
||||||
|
try {
|
||||||
|
$scopeInfo = $this->scopes->getScopeInfo($string, ScopesData::BY_STRING);
|
||||||
|
|
||||||
|
if(!$allowDeprecated && $scopeInfo->isDeprecated()) {
|
||||||
|
$infos[$string] = 'deprecated';
|
||||||
|
if($breakOnFail) break; else continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$this->apps->isAppScopeAllowed($appInfo, $scopeInfo)) {
|
||||||
|
$infos[$string] = 'restricted';
|
||||||
|
if($breakOnFail) break; else continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$infos[$string] = $scopeInfo;
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
$infos[$string] = 'unknown';
|
||||||
|
if($breakOnFail) break; else continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($sort)
|
||||||
|
ksort($infos, SORT_STRING);
|
||||||
|
|
||||||
|
return $infos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Hanyuu\Apps;
|
namespace Hanyuu\Apps;
|
||||||
|
|
||||||
|
use stdClass;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\Data\DbStatementCache;
|
use Index\XString;
|
||||||
use Index\Data\IDbConnection;
|
use Index\Data\{DbStatementCache,IDbConnection};
|
||||||
|
|
||||||
class AppsData {
|
class AppsData {
|
||||||
private IDbConnection $dbConn;
|
|
||||||
private DbStatementCache $cache;
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn) {
|
public function __construct(
|
||||||
$this->dbConn = $dbConn;
|
private IDbConnection $dbConn
|
||||||
|
) {
|
||||||
$this->cache = new DbStatementCache($dbConn);
|
$this->cache = new DbStatementCache($dbConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +45,54 @@ class AppsData {
|
||||||
return AppInfo::fromResult($result);
|
return AppInfo::fromResult($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public const CLIENT_ID_LENGTH = 20;
|
||||||
|
public const CLIENT_SECRET_ALGO = PASSWORD_ARGON2ID;
|
||||||
|
|
||||||
|
public const TYPE_PUBLIC = 'public';
|
||||||
|
public const TYPE_CONFIDENTIAL = 'confidential';
|
||||||
|
public const TYPES = [self::TYPE_PUBLIC, self::TYPE_CONFIDENTIAL];
|
||||||
|
|
||||||
|
public function createApp(
|
||||||
|
string $name,
|
||||||
|
string $type,
|
||||||
|
bool $trusted = false,
|
||||||
|
string $summary = '',
|
||||||
|
string $website = '',
|
||||||
|
?string $clientId = null,
|
||||||
|
#[\SensitiveParameter] ?string $clientSecret = null,
|
||||||
|
?int $refreshLifetime = null
|
||||||
|
): AppInfo {
|
||||||
|
if(trim($name) === '')
|
||||||
|
throw new InvalidArgumentException('$name may not be empty');
|
||||||
|
if(!in_array($type, self::TYPES))
|
||||||
|
throw new InvalidArgumentException('$type is not a valid type');
|
||||||
|
|
||||||
|
if($clientId === null)
|
||||||
|
$clientId = XString::random(self::CLIENT_ID_LENGTH);
|
||||||
|
elseif(trim($clientId) === '')
|
||||||
|
throw new InvalidArgumentException('$clientId may not be empty');
|
||||||
|
|
||||||
|
$summary = trim($summary);
|
||||||
|
$website = trim($website);
|
||||||
|
$clientSecret = $clientSecret === null ? '' : password_hash($clientSecret, self::CLIENT_SECRET_ALGO);
|
||||||
|
|
||||||
|
if($type === self::TYPE_CONFIDENTIAL && $clientSecret === '')
|
||||||
|
throw new InvalidArgumentException('$clientSecret must be specified for confidential clients');
|
||||||
|
|
||||||
|
$stmt = $this->cache->get('INSERT INTO hau_apps (app_name, app_summary, app_website, app_trusted, app_type, app_refresh_lifetime, app_client_id, app_client_secret) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
|
||||||
|
$stmt->addParameter(1, $name);
|
||||||
|
$stmt->addParameter(2, $summary);
|
||||||
|
$stmt->addParameter(3, $website);
|
||||||
|
$stmt->addParameter(4, $trusted ? 1 : 0);
|
||||||
|
$stmt->addParameter(5, $type);
|
||||||
|
$stmt->addParameter(6, $refreshLifetime);
|
||||||
|
$stmt->addParameter(7, $clientId);
|
||||||
|
$stmt->addParameter(8, $clientSecret);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $this->getScopeInfo(appId: (string)$this->dbConn->getLastInsertId());
|
||||||
|
}
|
||||||
|
|
||||||
public function countAppUris(AppInfo|string $appInfo): int {
|
public function countAppUris(AppInfo|string $appInfo): int {
|
||||||
$stmt = $this->cache->get('SELECT COUNT(*) FROM hau_apps_uris WHERE app_id = ?');
|
$stmt = $this->cache->get('SELECT COUNT(*) FROM hau_apps_uris WHERE app_id = ?');
|
||||||
$stmt->addParameter(1, $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo);
|
$stmt->addParameter(1, $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo);
|
||||||
|
@ -87,4 +136,53 @@ class AppsData {
|
||||||
$result = $stmt->getResult();
|
$result = $stmt->getResult();
|
||||||
return $result->next() ? $result->getStringOrNull(0) : null;
|
return $result->next() ? $result->getStringOrNull(0) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAppScopes(AppInfo|string $appInfo): array {
|
||||||
|
$allowed = [];
|
||||||
|
$denied = [];
|
||||||
|
|
||||||
|
$stmt = $this->cache->get('SELECT (SELECT scope_string FROM hau_scopes AS _s WHERE _s.scope_id = _as.scope_id), scope_allowed FROM hau_apps_scopes AS _as WHERE app_id = ?');
|
||||||
|
$stmt->addParameter(1, $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$result = $stmt->getResult();
|
||||||
|
while($result->next()) {
|
||||||
|
$scopeStr = $result->getString(0);
|
||||||
|
if($result->getBoolean(1))
|
||||||
|
$allowed[] = $scopeStr;
|
||||||
|
else
|
||||||
|
$denied[] = $scopeStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AppScopesInfo($allowed, $denied);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAppScopeAllow(AppInfo|string $appInfo, ScopeInfo $scopeInfo, ?bool $allowed): void {
|
||||||
|
if($allowed !== $scopeInfo->isRestricted()) {
|
||||||
|
$stmt = $this->cache->get('DELETE FROM hau_apps_scopes WHERE app_id = ? AND scope_id = ?');
|
||||||
|
} else {
|
||||||
|
$stmt = $this->cache->get('REPLACE INTO hau_apps_scopes (app_id, scope_id, scope_allowed) VALUES (?, ?, ?)');
|
||||||
|
$stmt->addParameter(3, $allowed ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->addParameter(1, $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo);
|
||||||
|
$stmt->addParameter(2, $scopeInfo->getId());
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAppScopeAllowed(AppInfo|string $appInfo, ScopeInfo|string $scopeInfo): bool {
|
||||||
|
$stmt = $this->cache->get('SELECT ? AS _scope_id, COALESCE(
|
||||||
|
(SELECT scope_allowed FROM hau_apps_scopes WHERE app_id = ? AND scope_id = _scope_id),
|
||||||
|
(SELECT NOT scope_restricted FROM hau_scopes WHERE scope_id = _scope_id)
|
||||||
|
)');
|
||||||
|
$stmt->addParameter(1, $scopeInfo instanceof ScopeInfo ? $scopeInfo->getId() : $scopeInfo);
|
||||||
|
$stmt->addParameter(2, $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$result = $stmt->getResult();
|
||||||
|
if(!$result->next())
|
||||||
|
throw new RuntimeException('failed to check scope ACL');
|
||||||
|
|
||||||
|
return $result->getBoolean(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
58
src/Apps/ScopeInfo.php
Normal file
58
src/Apps/ScopeInfo.php
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
namespace Hanyuu\Apps;
|
||||||
|
|
||||||
|
use Index\Data\IDbResult;
|
||||||
|
|
||||||
|
class ScopeInfo {
|
||||||
|
public function __construct(
|
||||||
|
private string $id,
|
||||||
|
private string $string,
|
||||||
|
private bool $restricted,
|
||||||
|
private string $summary,
|
||||||
|
private int $created,
|
||||||
|
private ?int $deprecated
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromResult(IDbResult $result): ScopeInfo {
|
||||||
|
return new ScopeInfo(
|
||||||
|
$result->getString(0),
|
||||||
|
$result->getString(1),
|
||||||
|
$result->getBoolean(2),
|
||||||
|
$result->getString(3),
|
||||||
|
$result->getInteger(4),
|
||||||
|
$result->getIntegerOrNull(5)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string {
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getString(): string {
|
||||||
|
return $this->string;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isRestricted(): bool {
|
||||||
|
return $this->restricted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasSummary(): bool {
|
||||||
|
return $this->summary !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSummary(): string {
|
||||||
|
return $this->summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedTime(): int {
|
||||||
|
return $this->created;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDeprecated(): bool {
|
||||||
|
return $this->deprecated !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeprecatedTime(): ?int {
|
||||||
|
return $this->deprecated;
|
||||||
|
}
|
||||||
|
}
|
56
src/Apps/ScopesData.php
Normal file
56
src/Apps/ScopesData.php
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
namespace Hanyuu\Apps;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
use Index\Data\{DbStatementCache,IDbConnection};
|
||||||
|
|
||||||
|
class ScopesData {
|
||||||
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private IDbConnection $dbConn
|
||||||
|
) {
|
||||||
|
$this->cache = new DbStatementCache($dbConn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public const BY_ID = 'id';
|
||||||
|
public const BY_STRING = 'string';
|
||||||
|
|
||||||
|
public function getScopeInfo(string $value, string $mode): ScopeInfo {
|
||||||
|
$stmt = $this->cache->get(sprintf(
|
||||||
|
'SELECT scope_id, scope_string, scope_restricted, scope_summary, UNIX_TIMESTAMP(scope_created), UNIX_TIMESTAMP(scope_deprecated) FROM hau_scopes WHERE %s = ?',
|
||||||
|
match($mode) {
|
||||||
|
self::BY_ID => 'scope_id',
|
||||||
|
self::BY_STRING => 'scope_string',
|
||||||
|
default => throw new InvalidArgumentException('$mode is not a valid mode')
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
$stmt->addParameter(1, $value);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$result = $stmt->getResult();
|
||||||
|
if(!$result->next())
|
||||||
|
throw new RuntimeException('Scope not found.');
|
||||||
|
|
||||||
|
return ScopeInfo::fromResult($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createScope(string $string, bool $restricted, string $summary = ''): ScopeInfo {
|
||||||
|
if(trim($string) === '')
|
||||||
|
throw new InvalidArgumentException('$string may not be empty');
|
||||||
|
if(preg_match('#[^A-Za-z0-9:-]#', $string))
|
||||||
|
throw new InvalidArgumentException('$string contains invalid characters');
|
||||||
|
|
||||||
|
$summary = trim($summary);
|
||||||
|
|
||||||
|
$stmt = $this->cache->get('INSERT INTO hau_scopes (scope_string, scope_restricted, scope_summary) VALUES (?, ?, ?)');
|
||||||
|
$stmt->addParameter(1, $string);
|
||||||
|
$stmt->addParameter(2, $restricted ? 1 : 0);
|
||||||
|
$stmt->addParameter(3, $summary);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $this->getScopeInfo((string)$this->dbConn->getLastInsertId(), self::BY_ID);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,7 +31,8 @@ class HanyuuContext {
|
||||||
$this->appsCtx = new Apps\AppsContext($dbConn);
|
$this->appsCtx = new Apps\AppsContext($dbConn);
|
||||||
$this->oauth2Ctx = new OAuth2\OAuth2Context(
|
$this->oauth2Ctx = new OAuth2\OAuth2Context(
|
||||||
$config->scopeTo('oauth2'),
|
$config->scopeTo('oauth2'),
|
||||||
$dbConn
|
$dbConn,
|
||||||
|
$this->appsCtx
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->templating = new SasaeEnvironment(
|
$this->templating = new SasaeEnvironment(
|
||||||
|
@ -67,6 +68,11 @@ class HanyuuContext {
|
||||||
return new FsDbMigrationRepo(HAU_DIR_MIGRATIONS);
|
return new FsDbMigrationRepo(HAU_DIR_MIGRATIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function pruneExpired($logAction = null): void {
|
||||||
|
$logAction ??= function() {};
|
||||||
|
$this->oauth2Ctx->pruneExpired($logAction);
|
||||||
|
}
|
||||||
|
|
||||||
public function getAuthInfo(): MisuzuAuthInfo {
|
public function getAuthInfo(): MisuzuAuthInfo {
|
||||||
return $this->authInfo;
|
return $this->authInfo;
|
||||||
}
|
}
|
||||||
|
@ -100,13 +106,9 @@ class HanyuuContext {
|
||||||
return 503;
|
return 503;
|
||||||
});
|
});
|
||||||
|
|
||||||
$routingCtx->register(new OAuth2\OAuth2ApiRoutes(
|
$routingCtx->register(new OAuth2\OAuth2ApiRoutes($this->oauth2Ctx));
|
||||||
$this->oauth2Ctx,
|
|
||||||
$this->appsCtx
|
|
||||||
));
|
|
||||||
$routingCtx->register(new OAuth2\OAuth2WebRoutes(
|
$routingCtx->register(new OAuth2\OAuth2WebRoutes(
|
||||||
$this->oauth2Ctx,
|
$this->oauth2Ctx,
|
||||||
$this->appsCtx,
|
|
||||||
$this->templating,
|
$this->templating,
|
||||||
$this->getAuthInfo(...),
|
$this->getAuthInfo(...),
|
||||||
$this->getCSRFPSecret(...)
|
$this->getCSRFPSecret(...)
|
||||||
|
@ -117,10 +119,7 @@ class HanyuuContext {
|
||||||
new HmacVerificationProvider(fn() => $this->config->getString('aleister:secret'))
|
new HmacVerificationProvider(fn() => $this->config->getString('aleister:secret'))
|
||||||
));
|
));
|
||||||
|
|
||||||
$rpcServer->register(new OAuth2\OAuth2RpcActions(
|
$rpcServer->register(new OAuth2\OAuth2RpcActions($this->oauth2Ctx));
|
||||||
$this->oauth2Ctx,
|
|
||||||
$this->appsCtx
|
|
||||||
));
|
|
||||||
|
|
||||||
return $routingCtx;
|
return $routingCtx;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,10 @@ namespace Hanyuu\OAuth2;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,RouteHandler};
|
use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,RouteHandler};
|
||||||
use Syokuhou\IConfig;
|
use Syokuhou\IConfig;
|
||||||
use Hanyuu\Apps\AppsContext;
|
|
||||||
|
|
||||||
final class OAuth2ApiRoutes extends RouteHandler {
|
final class OAuth2ApiRoutes extends RouteHandler {
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private OAuth2Context $oauth2Ctx,
|
private OAuth2Context $oauth2Ctx
|
||||||
private AppsContext $appsCtx
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private static function filter($response, array $result, bool $authzHeader = false): array {
|
private static function filter($response, array $result, bool $authzHeader = false): array {
|
||||||
|
@ -55,7 +53,8 @@ final class OAuth2ApiRoutes extends RouteHandler {
|
||||||
$clientSecret = '';
|
$clientSecret = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$appsData = $this->appsCtx->getData();
|
$appsCtx = $this->oauth2Ctx->getApps();
|
||||||
|
$appsData = $appsCtx->getAppsData();
|
||||||
try {
|
try {
|
||||||
$appInfo = $appsData->getAppInfo(clientId: $clientId, deleted: false);
|
$appInfo = $appsData->getAppInfo(clientId: $clientId, deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
|
@ -132,7 +131,8 @@ final class OAuth2ApiRoutes extends RouteHandler {
|
||||||
$clientSecret = (string)$content->getParam('client_secret');
|
$clientSecret = (string)$content->getParam('client_secret');
|
||||||
}
|
}
|
||||||
|
|
||||||
$appsData = $this->appsCtx->getData();
|
$appsCtx = $this->oauth2Ctx->getApps();
|
||||||
|
$appsData = $appsCtx->getAppsData();
|
||||||
try {
|
try {
|
||||||
$appInfo = $appsData->getAppInfo(clientId: $clientId, deleted: false);
|
$appInfo = $appsData->getAppInfo(clientId: $clientId, deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
|
|
|
@ -98,4 +98,8 @@ class OAuth2AuthorisationData {
|
||||||
$stmt->addParameter(++$args, $value);
|
$stmt->addParameter(++$args, $value);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function pruneExpiredAuthorisations(): int {
|
||||||
|
return (int)$this->dbConn->execute('DELETE FROM hau_oauth2_authorise WHERE auth_expires <= NOW() - INTERVAL 1 HOUR');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
namespace Hanyuu\OAuth2;
|
namespace Hanyuu\OAuth2;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Hanyuu\Apps\AppInfo;
|
use Hanyuu\Apps\{AppsContext,AppInfo};
|
||||||
use Index\Data\IDbConnection;
|
use Index\Data\IDbConnection;
|
||||||
use Syokuhou\IConfig;
|
use Syokuhou\IConfig;
|
||||||
|
|
||||||
|
@ -13,7 +13,8 @@ class OAuth2Context {
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private IConfig $config,
|
private IConfig $config,
|
||||||
IDbConnection $dbConn
|
IDbConnection $dbConn,
|
||||||
|
private AppsContext $appsCtx
|
||||||
) {
|
) {
|
||||||
$this->authorisations = new OAuth2AuthorisationData($dbConn);
|
$this->authorisations = new OAuth2AuthorisationData($dbConn);
|
||||||
$this->tokens = new OAuth2TokensData($dbConn);
|
$this->tokens = new OAuth2TokensData($dbConn);
|
||||||
|
@ -24,6 +25,10 @@ class OAuth2Context {
|
||||||
return $this->config;
|
return $this->config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getApps(): AppsContext {
|
||||||
|
return $this->appsCtx;
|
||||||
|
}
|
||||||
|
|
||||||
public function getAuthorisationData(): OAuth2AuthorisationData {
|
public function getAuthorisationData(): OAuth2AuthorisationData {
|
||||||
return $this->authorisations;
|
return $this->authorisations;
|
||||||
}
|
}
|
||||||
|
@ -36,6 +41,26 @@ class OAuth2Context {
|
||||||
return $this->devices;
|
return $this->devices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function pruneExpired($logAction = null): void {
|
||||||
|
$logAction ??= function() {};
|
||||||
|
|
||||||
|
$logAction('Pruning expired refresh tokens...');
|
||||||
|
$pruned = $this->tokens->pruneExpiredRefresh();
|
||||||
|
$logAction(' Removed %d!', $pruned);
|
||||||
|
|
||||||
|
$logAction('Pruning expired access tokens...');
|
||||||
|
$pruned = $this->tokens->pruneExpiredAccess();
|
||||||
|
$logAction(' Removed %d!', $pruned);
|
||||||
|
|
||||||
|
$logAction('Pruning expired device authorisation requests...');
|
||||||
|
$pruned = $this->devices->pruneExpiredDevices();
|
||||||
|
$logAction(' Removed %d!', $pruned);
|
||||||
|
|
||||||
|
$logAction('Pruning expired authorisation codes...');
|
||||||
|
$pruned = $this->authorisations->pruneExpiredAuthorisations();
|
||||||
|
$logAction(' Removed %d!', $pruned);
|
||||||
|
}
|
||||||
|
|
||||||
public function createRefresh(
|
public function createRefresh(
|
||||||
AppInfo $appInfo,
|
AppInfo $appInfo,
|
||||||
OAuth2AccessInfo $accessInfo
|
OAuth2AccessInfo $accessInfo
|
||||||
|
@ -49,39 +74,34 @@ class OAuth2Context {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validateScopes(AppInfo $appInfo, array $scopes): bool {
|
public function checkAndBuildScopeString(AppInfo $appInfo, ?string $scope = null, bool $skipFail = false): string|array {
|
||||||
foreach($scopes as $scope)
|
if($scope === null)
|
||||||
if(strlen($scope) > 128) // rather than this, actually check if they are defined/supported or smth
|
return '';
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
$scopeInfos = $this->appsCtx->handleScopeString($appInfo, $scope, breakOnFail: !$skipFail);
|
||||||
}
|
$scope = [];
|
||||||
|
|
||||||
public static function filterScopes(string $source): array {
|
foreach($scopeInfos as $scopeName => $scopeInfo) {
|
||||||
$scopes = [];
|
if(is_string($scopeInfo)) {
|
||||||
|
if($skipFail)
|
||||||
|
continue;
|
||||||
|
|
||||||
$source = explode(' ', $source);
|
return [
|
||||||
foreach($source as $scope) {
|
'error' => 'invalid_scope',
|
||||||
$scope = trim($scope);
|
'error_description' => sprintf('Requested scope "%s" is %s.', $scopeName, $scopeInfo),
|
||||||
if($scope !== '' && !in_array($scope, $scopes))
|
];
|
||||||
$scopes[] = $scope;
|
}
|
||||||
|
|
||||||
|
$scope[] = $scopeInfo->getString();
|
||||||
}
|
}
|
||||||
|
|
||||||
sort($scopes);
|
return implode(' ', $scope);
|
||||||
|
|
||||||
return $scopes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createDeviceAuthorisationRequest(AppInfo $appInfo, ?string $scope = null) {
|
public function createDeviceAuthorisationRequest(AppInfo $appInfo, ?string $scope = null) {
|
||||||
$scope ??= ''; // for now
|
$scope = $this->checkAndBuildScopeString($appInfo, $scope);
|
||||||
$scopes = self::filterScopes($scope);
|
if(is_array($scope))
|
||||||
if(!$this->validateScopes($appInfo, $scopes))
|
return $scope;
|
||||||
return [
|
|
||||||
'error' => 'invalid_scope',
|
|
||||||
'error_description' => 'An invalid scope was requested.',
|
|
||||||
];
|
|
||||||
|
|
||||||
$scope = implode(' ', $scopes);
|
|
||||||
|
|
||||||
$deviceInfo = $this->getDevicesData()->createDevice($appInfo, $scope);
|
$deviceInfo = $this->getDevicesData()->createDevice($appInfo, $scope);
|
||||||
|
|
||||||
|
@ -150,14 +170,7 @@ class OAuth2Context {
|
||||||
|
|
||||||
$authsData->deleteAuthorisation($authsInfo);
|
$authsData->deleteAuthorisation($authsInfo);
|
||||||
|
|
||||||
$scopes = $authsInfo->getScopes();
|
$scope = $this->checkAndBuildScopeString($appInfo, $authsInfo->getScope(), true);
|
||||||
if(!$this->validateScopes($appInfo, $scopes))
|
|
||||||
return [
|
|
||||||
'error' => 'invalid_scope',
|
|
||||||
'error_description' => 'Requested scope is no longer valid for this application, please restart authorisation.',
|
|
||||||
];
|
|
||||||
|
|
||||||
$scope = implode(' ', $scopes);
|
|
||||||
|
|
||||||
$tokensData = $this->getTokensData();
|
$tokensData = $this->getTokensData();
|
||||||
$accessInfo = $tokensData->createAccess(
|
$accessInfo = $tokensData->createAccess(
|
||||||
|
@ -166,8 +179,7 @@ class OAuth2Context {
|
||||||
scope: $scope,
|
scope: $scope,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 'scope' only has to be in the response if it differs from what was requested
|
if($authsInfo->getScope() === $scope)
|
||||||
if($scope === $authsInfo->getScope())
|
|
||||||
$scope = null;
|
$scope = null;
|
||||||
|
|
||||||
$refreshInfo = $appInfo->shouldIssueRefreshToken()
|
$refreshInfo = $appInfo->shouldIssueRefreshToken()
|
||||||
|
@ -204,14 +216,16 @@ class OAuth2Context {
|
||||||
if($refreshInfo->hasAccessId())
|
if($refreshInfo->hasAccessId())
|
||||||
$tokensData->deleteAccess(accessInfo: $refreshInfo->getAccessId());
|
$tokensData->deleteAccess(accessInfo: $refreshInfo->getAccessId());
|
||||||
|
|
||||||
$newScopes = self::filterScopes($scope ?? $refreshInfo->getScope());
|
if($scope === null)
|
||||||
if(!$this->validateScopes($appInfo, $newScopes))
|
$scope = $refreshInfo->getScope();
|
||||||
|
elseif(!empty(array_diff(explode(' ', $scope), $refreshInfo->getScopes())))
|
||||||
return [
|
return [
|
||||||
'error' => 'invalid_scope',
|
'error' => 'invalid_scope',
|
||||||
'error_description' => 'Requested scope is no longer valid for this application, please restart authorisation.',
|
'error_description' => 'You cannot request a greater scope than during initial authorisation, please restart authorisation.',
|
||||||
];
|
];
|
||||||
|
|
||||||
$scope = implode(' ', $newScopes);
|
$scope = $this->checkAndBuildScopeString($appInfo, $scope, true);
|
||||||
|
|
||||||
$accessInfo = $tokensData->createAccess(
|
$accessInfo = $tokensData->createAccess(
|
||||||
$appInfo,
|
$appInfo,
|
||||||
$refreshInfo->getUserId(),
|
$refreshInfo->getUserId(),
|
||||||
|
@ -240,17 +254,17 @@ class OAuth2Context {
|
||||||
'error_description' => 'Application must authenticate with client secret in order to use this grant type.',
|
'error_description' => 'Application must authenticate with client secret in order to use this grant type.',
|
||||||
];
|
];
|
||||||
|
|
||||||
$scope ??= ''; // for now
|
$requestedScope = $scope;
|
||||||
$scopes = self::filterScopes($scope);
|
$scope = $this->checkAndBuildScopeString($appInfo, $requestedScope, true);
|
||||||
if(!$this->validateScopes($appInfo, $scopes))
|
|
||||||
return self::error($response, 'invalid_scope', 'Requested scope is no longer valid for this application, please restart authorisation.');
|
|
||||||
|
|
||||||
$origScope = $scope;
|
|
||||||
$scope = implode(' ', $scopes);
|
|
||||||
|
|
||||||
$accessInfo = $this->getTokensData()->createAccess($appInfo, scope: $scope);
|
$accessInfo = $this->getTokensData()->createAccess($appInfo, scope: $scope);
|
||||||
|
|
||||||
if($scope === $origScope)
|
if($requestedScope !== null && $scope === '')
|
||||||
|
return [
|
||||||
|
'error' => 'invalid_scope',
|
||||||
|
'error_description' => 'Requested scope is not valid.',
|
||||||
|
];
|
||||||
|
|
||||||
|
if($requestedScope === null || $scope === $requestedScope)
|
||||||
$scope = null;
|
$scope = null;
|
||||||
|
|
||||||
return $this->packBearerTokenResult($accessInfo, scope: $scope);
|
return $this->packBearerTokenResult($accessInfo, scope: $scope);
|
||||||
|
@ -306,14 +320,7 @@ class OAuth2Context {
|
||||||
'error_description' => 'Device code was approved but has no associated user, please contact the system administrator because something is wrong.',
|
'error_description' => 'Device code was approved but has no associated user, please contact the system administrator because something is wrong.',
|
||||||
];
|
];
|
||||||
|
|
||||||
$scopes = $deviceInfo->getScopes();
|
$scope = $this->checkAndBuildScopeString($appInfo, $deviceInfo->getScope(), true);
|
||||||
if(!$this->validateScopes($appInfo, $scopes))
|
|
||||||
return [
|
|
||||||
'error' => 'invalid_scope',
|
|
||||||
'error_description' => 'Requested scope is no longer valid for this application, please restart authorisation.',
|
|
||||||
];
|
|
||||||
|
|
||||||
$scope = implode(' ', $scopes);
|
|
||||||
|
|
||||||
$tokensData = $this->getTokensData();
|
$tokensData = $this->getTokensData();
|
||||||
$accessInfo = $tokensData->createAccess(
|
$accessInfo = $tokensData->createAccess(
|
||||||
|
@ -323,7 +330,7 @@ class OAuth2Context {
|
||||||
);
|
);
|
||||||
|
|
||||||
// 'scope' only has to be in the response if it differs from what was requested
|
// 'scope' only has to be in the response if it differs from what was requested
|
||||||
if($scope === $deviceInfo->getScope())
|
if($deviceInfo->getScope() === $scope)
|
||||||
$scope = null;
|
$scope = null;
|
||||||
|
|
||||||
$refreshInfo = $appInfo->shouldIssueRefreshToken()
|
$refreshInfo = $appInfo->shouldIssueRefreshToken()
|
||||||
|
|
|
@ -11,11 +11,11 @@ use Hanyuu\Apps\AppInfo;
|
||||||
class OAuth2DevicesData {
|
class OAuth2DevicesData {
|
||||||
private const USER_CODE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
private const USER_CODE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||||
|
|
||||||
private IDbConnection $dbConn;
|
|
||||||
private DbStatementCache $cache;
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn) {
|
public function __construct(
|
||||||
$this->dbConn = $dbConn;
|
private IDbConnection $dbConn
|
||||||
|
) {
|
||||||
$this->cache = new DbStatementCache($dbConn);
|
$this->cache = new DbStatementCache($dbConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,4 +138,8 @@ class OAuth2DevicesData {
|
||||||
$stmt->addParameter(2, $deviceInfo instanceof OAuth2DeviceInfo ? $deviceInfo->getId() : $deviceInfo);
|
$stmt->addParameter(2, $deviceInfo instanceof OAuth2DeviceInfo ? $deviceInfo->getId() : $deviceInfo);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function pruneExpiredDevices(): int {
|
||||||
|
return (int)$this->dbConn->execute('DELETE FROM hau_oauth2_device WHERE dev_expires <= NOW() - INTERVAL 1 HOUR');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,7 @@ use Syokuhou\IConfig;
|
||||||
|
|
||||||
final class OAuth2RpcActions extends RpcActionHandler {
|
final class OAuth2RpcActions extends RpcActionHandler {
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private OAuth2Context $oauth2Ctx,
|
private OAuth2Context $oauth2Ctx
|
||||||
private AppsContext $appsCtx
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private static function filterScopes(string $source): array {
|
private static function filterScopes(string $source): array {
|
||||||
|
@ -30,14 +29,14 @@ final class OAuth2RpcActions extends RpcActionHandler {
|
||||||
#[RpcProcedure('hanyuu:oauth2:attemptAppAuth')]
|
#[RpcProcedure('hanyuu:oauth2:attemptAppAuth')]
|
||||||
public function procAttemptAppAuth(string $remoteAddr, string $clientId, string $clientSecret = ''): array {
|
public function procAttemptAppAuth(string $remoteAddr, string $clientId, string $clientSecret = ''): array {
|
||||||
try {
|
try {
|
||||||
$appInfo = $this->appsCtx->getData()->getAppInfo(clientId: $clientId, deleted: false);
|
$appInfo = $this->oauth2Ctx->getApps()->getAppsData()->getAppInfo(clientId: $clientId, deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
return ['method' => 'basic', 'error' => 'app'];
|
return ['method' => 'basic', 'error' => 'app'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$authed = false;
|
$authed = false;
|
||||||
if($clientSecret !== '') {
|
if($clientSecret !== '') {
|
||||||
// todo: rate limiting
|
// TODO: rate limiting
|
||||||
|
|
||||||
if(!$appInfo->verifyClientSecret($clientSecret))
|
if(!$appInfo->verifyClientSecret($clientSecret))
|
||||||
return ['method' => 'basic', 'error' => 'secret'];
|
return ['method' => 'basic', 'error' => 'secret'];
|
||||||
|
@ -77,7 +76,7 @@ final class OAuth2RpcActions extends RpcActionHandler {
|
||||||
#[RpcProcedure('hanyuu:oauth2:createAuthoriseRequest')]
|
#[RpcProcedure('hanyuu:oauth2:createAuthoriseRequest')]
|
||||||
public function procCreateAuthoriseRequest(string $appId, ?string $scope = null): array {
|
public function procCreateAuthoriseRequest(string $appId, ?string $scope = null): array {
|
||||||
try {
|
try {
|
||||||
$appInfo = $this->appsCtx->getData()->getAppInfo(appId: $appId, deleted: false);
|
$appInfo = $this->oauth2Ctx->getApps()->getAppsData()->getAppInfo(appId: $appId, deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
return [
|
return [
|
||||||
'error' => 'invalid_client',
|
'error' => 'invalid_client',
|
||||||
|
@ -91,7 +90,7 @@ final class OAuth2RpcActions extends RpcActionHandler {
|
||||||
#[RpcProcedure('hanyuu:oauth2:createBearerToken:authorisationCode')]
|
#[RpcProcedure('hanyuu:oauth2:createBearerToken:authorisationCode')]
|
||||||
public function procCreateBearerTokenAuthzCode(string $appId, bool $isAuthed, string $code, string $codeVerifier): array {
|
public function procCreateBearerTokenAuthzCode(string $appId, bool $isAuthed, string $code, string $codeVerifier): array {
|
||||||
try {
|
try {
|
||||||
$appInfo = $this->appsCtx->getData()->getAppInfo(appId: $appId, deleted: false);
|
$appInfo = $this->oauth2Ctx->getApps()->getAppsData()->getAppInfo(appId: $appId, deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
return [
|
return [
|
||||||
'error' => 'invalid_client',
|
'error' => 'invalid_client',
|
||||||
|
@ -105,7 +104,7 @@ final class OAuth2RpcActions extends RpcActionHandler {
|
||||||
#[RpcProcedure('hanyuu:oauth2:createBearerToken:refreshToken')]
|
#[RpcProcedure('hanyuu:oauth2:createBearerToken:refreshToken')]
|
||||||
public function procCreateBearerTokenRefreshToken(string $appId, bool $isAuthed, string $refreshToken, ?string $scope = null): array {
|
public function procCreateBearerTokenRefreshToken(string $appId, bool $isAuthed, string $refreshToken, ?string $scope = null): array {
|
||||||
try {
|
try {
|
||||||
$appInfo = $this->appsCtx->getData()->getAppInfo(appId: $appId, deleted: false);
|
$appInfo = $this->oauth2Ctx->getApps()->getAppsData()->getAppInfo(appId: $appId, deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
return [
|
return [
|
||||||
'error' => 'invalid_client',
|
'error' => 'invalid_client',
|
||||||
|
@ -119,7 +118,7 @@ final class OAuth2RpcActions extends RpcActionHandler {
|
||||||
#[RpcProcedure('hanyuu:oauth2:createBearerToken:clientCredentials')]
|
#[RpcProcedure('hanyuu:oauth2:createBearerToken:clientCredentials')]
|
||||||
public function procCreateBearerTokenClientCreds(string $appId, bool $isAuthed, ?string $scope = null): array {
|
public function procCreateBearerTokenClientCreds(string $appId, bool $isAuthed, ?string $scope = null): array {
|
||||||
try {
|
try {
|
||||||
$appInfo = $this->appsCtx->getData()->getAppInfo(appId: $appId, deleted: false);
|
$appInfo = $this->oauth2Ctx->getApps()->getAppsData()->getAppInfo(appId: $appId, deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
return [
|
return [
|
||||||
'error' => 'invalid_client',
|
'error' => 'invalid_client',
|
||||||
|
@ -133,7 +132,7 @@ final class OAuth2RpcActions extends RpcActionHandler {
|
||||||
#[RpcProcedure('hanyuu:oauth2:createBearerToken:deviceCode')]
|
#[RpcProcedure('hanyuu:oauth2:createBearerToken:deviceCode')]
|
||||||
public function procCreateBearerTokenDeviceCode(string $appId, bool $isAuthed, string $deviceCode): array {
|
public function procCreateBearerTokenDeviceCode(string $appId, bool $isAuthed, string $deviceCode): array {
|
||||||
try {
|
try {
|
||||||
$appInfo = $this->appsCtx->getData()->getAppInfo(appId: $appId, deleted: false);
|
$appInfo = $this->oauth2Ctx->getApps()->getAppsData()->getAppInfo(appId: $appId, deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
return [
|
return [
|
||||||
'error' => 'invalid_client',
|
'error' => 'invalid_client',
|
||||||
|
|
|
@ -9,11 +9,11 @@ use Index\Data\IDbConnection;
|
||||||
use Hanyuu\Apps\AppInfo;
|
use Hanyuu\Apps\AppInfo;
|
||||||
|
|
||||||
class OAuth2TokensData {
|
class OAuth2TokensData {
|
||||||
private IDbConnection $dbConn;
|
|
||||||
private DbStatementCache $cache;
|
private DbStatementCache $cache;
|
||||||
|
|
||||||
public function __construct(IDbConnection $dbConn) {
|
public function __construct(
|
||||||
$this->dbConn = $dbConn;
|
private IDbConnection $dbConn
|
||||||
|
) {
|
||||||
$this->cache = new DbStatementCache($dbConn);
|
$this->cache = new DbStatementCache($dbConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,6 +94,10 @@ class OAuth2TokensData {
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function pruneExpiredAccess(): int {
|
||||||
|
return (int)$this->dbConn->execute('DELETE FROM hau_oauth2_access WHERE acc_expires <= NOW() - INTERVAL 1 DAY');
|
||||||
|
}
|
||||||
|
|
||||||
public const REFRESH_BY_ID = 'id';
|
public const REFRESH_BY_ID = 'id';
|
||||||
public const REFRESH_BY_ACCESS = 'access';
|
public const REFRESH_BY_ACCESS = 'access';
|
||||||
public const REFRESH_BY_TOKEN = 'token';
|
public const REFRESH_BY_TOKEN = 'token';
|
||||||
|
@ -185,4 +189,8 @@ class OAuth2TokensData {
|
||||||
$stmt->addParameter(++$args, $value);
|
$stmt->addParameter(++$args, $value);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function pruneExpiredRefresh(): int {
|
||||||
|
return (int)$this->dbConn->execute('DELETE FROM hau_oauth2_refresh WHERE ref_expires <= NOW()');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,10 @@ use Index\CSRFP;
|
||||||
use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler};
|
use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler};
|
||||||
use Sasae\SasaeEnvironment;
|
use Sasae\SasaeEnvironment;
|
||||||
use Syokuhou\IConfig;
|
use Syokuhou\IConfig;
|
||||||
use Hanyuu\Apps\AppsContext;
|
|
||||||
|
|
||||||
final class OAuth2WebRoutes extends RouteHandler {
|
final class OAuth2WebRoutes extends RouteHandler {
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private OAuth2Context $oauth2Ctx,
|
private OAuth2Context $oauth2Ctx,
|
||||||
private AppsContext $appsCtx,
|
|
||||||
private SasaeEnvironment $templating,
|
private SasaeEnvironment $templating,
|
||||||
private $getAuthInfo,
|
private $getAuthInfo,
|
||||||
private $getCSRFPSecret
|
private $getCSRFPSecret
|
||||||
|
@ -23,21 +21,6 @@ final class OAuth2WebRoutes extends RouteHandler {
|
||||||
throw new InvalidArgumentException('$getCSRFPSecret must be callable');
|
throw new InvalidArgumentException('$getCSRFPSecret must be callable');
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function filterScopes(string $source): array {
|
|
||||||
$scopes = [];
|
|
||||||
|
|
||||||
$source = explode(' ', $source);
|
|
||||||
foreach($source as $scope) {
|
|
||||||
$scope = trim($scope);
|
|
||||||
if($scope !== '' && !in_array($scope, $scopes))
|
|
||||||
$scopes[] = $scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
sort($scopes);
|
|
||||||
|
|
||||||
return $scopes;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[HttpGet('/oauth2/authorise')]
|
#[HttpGet('/oauth2/authorise')]
|
||||||
public function getAuthorise($response, $request) {
|
public function getAuthorise($response, $request) {
|
||||||
$authInfo = ($this->getAuthInfo)();
|
$authInfo = ($this->getAuthInfo)();
|
||||||
|
@ -88,21 +71,27 @@ final class OAuth2WebRoutes extends RouteHandler {
|
||||||
return ['error' => 'length'];
|
return ['error' => 'length'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$appsData = $this->appsCtx->getData();
|
$appsCtx = $this->oauth2Ctx->getApps();
|
||||||
|
$appsData = $appsCtx->getAppsData();
|
||||||
try {
|
try {
|
||||||
$appInfo = $appsData->getAppInfo(clientId: (string)$content->getParam('client'), deleted: false);
|
$appInfo = $appsData->getAppInfo(clientId: (string)$content->getParam('client'), deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
return ['error' => 'client'];
|
return ['error' => 'client'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope = '';
|
|
||||||
if($content->hasParam('scope')) {
|
if($content->hasParam('scope')) {
|
||||||
$scopes = self::filterScopes((string)$content->getParam('scope'));
|
$scope = [];
|
||||||
if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes))
|
$scopeInfos = $this->oauth2Ctx->getApps()->handleScopeString($appInfo, (string)$content->getParam('scope'));
|
||||||
return ['error' => 'scope'];
|
|
||||||
|
|
||||||
$scope = implode(' ', $scopes);
|
foreach($scopeInfos as $scopeName => $scopeInfo) {
|
||||||
}
|
if(is_string($scopeInfo))
|
||||||
|
return ['error' => 'scope', 'scope' => $scopeName, 'reason' => $scopeInfo];
|
||||||
|
|
||||||
|
$scope[] = $scopeInfo->getString();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope = implode(' ', $scope);
|
||||||
|
} else $scope = '';
|
||||||
|
|
||||||
if($content->hasParam('redirect')) {
|
if($content->hasParam('redirect')) {
|
||||||
$redirectUri = (string)$content->getParam('redirect');
|
$redirectUri = (string)$content->getParam('redirect');
|
||||||
|
@ -152,7 +141,7 @@ final class OAuth2WebRoutes extends RouteHandler {
|
||||||
if(!$csrfp->verifyToken((string)$request->getParam('csrfp')))
|
if(!$csrfp->verifyToken((string)$request->getParam('csrfp')))
|
||||||
return ['error' => 'csrf'];
|
return ['error' => 'csrf'];
|
||||||
|
|
||||||
$appsData = $this->appsCtx->getData();
|
$appsData = $this->oauth2Ctx->getApps()->getAppsData();
|
||||||
try {
|
try {
|
||||||
$appInfo = $appsData->getAppInfo(clientId: (string)$request->getParam('client'), deleted: false);
|
$appInfo = $appsData->getAppInfo(clientId: (string)$request->getParam('client'), deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
|
@ -169,10 +158,16 @@ final class OAuth2WebRoutes extends RouteHandler {
|
||||||
return ['error' => 'required'];
|
return ['error' => 'required'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope = [];
|
||||||
if($request->hasParam('scope')) {
|
if($request->hasParam('scope')) {
|
||||||
$scopes = self::filterScopes((string)$request->getParam('scope'));
|
$scopeInfos = $this->oauth2Ctx->getApps()->handleScopeString($appInfo, (string)$request->getParam('scope'));
|
||||||
if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes))
|
|
||||||
return ['error' => 'scope'];
|
foreach($scopeInfos as $scopeName => $scopeInfo) {
|
||||||
|
if(is_string($scopeInfo))
|
||||||
|
return ['error' => 'scope', 'scope' => $scopeName, 'reason' => $scopeInfo];
|
||||||
|
|
||||||
|
$scope[] = $scopeInfo->getSummary();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$userInfo = $authInfo->getUserInfo();
|
$userInfo = $authInfo->getUserInfo();
|
||||||
|
@ -191,6 +186,7 @@ final class OAuth2WebRoutes extends RouteHandler {
|
||||||
'profile_uri' => $userInfo->getProfileUrl(),
|
'profile_uri' => $userInfo->getProfileUrl(),
|
||||||
'avatar_uri' => $userInfo->getAvatar('x120'),
|
'avatar_uri' => $userInfo->getAvatar('x120'),
|
||||||
],
|
],
|
||||||
|
'scope' => $scope,
|
||||||
];
|
];
|
||||||
|
|
||||||
if($authInfo->isGuise()) {
|
if($authInfo->isGuise()) {
|
||||||
|
@ -240,6 +236,10 @@ final class OAuth2WebRoutes extends RouteHandler {
|
||||||
if(!$csrfp->verifyToken((string)$content->getParam('_csrfp')))
|
if(!$csrfp->verifyToken((string)$content->getParam('_csrfp')))
|
||||||
return ['error' => 'csrf'];
|
return ['error' => 'csrf'];
|
||||||
|
|
||||||
|
$approve = (string)$content->getParam('approve');
|
||||||
|
if(!in_array($approve, ['yes', 'no']))
|
||||||
|
return ['error' => 'invalid'];
|
||||||
|
|
||||||
$devicesData = $this->oauth2Ctx->getDevicesData();
|
$devicesData = $this->oauth2Ctx->getDevicesData();
|
||||||
try {
|
try {
|
||||||
$deviceInfo = $devicesData->getDeviceInfo(userCode: (string)$content->getParam('code'));
|
$deviceInfo = $devicesData->getDeviceInfo(userCode: (string)$content->getParam('code'));
|
||||||
|
@ -247,16 +247,40 @@ final class OAuth2WebRoutes extends RouteHandler {
|
||||||
return ['error' => 'code'];
|
return ['error' => 'code'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($deviceInfo->hasExpired())
|
||||||
|
return ['error' => 'expired'];
|
||||||
|
|
||||||
if(!$deviceInfo->isPending())
|
if(!$deviceInfo->isPending())
|
||||||
return ['error' => 'approval'];
|
return ['error' => 'approval'];
|
||||||
|
|
||||||
$approve = (string)$content->getParam('approve');
|
|
||||||
if(!in_array($approve, ['yes', 'no']))
|
|
||||||
return ['error' => 'invalid'];
|
|
||||||
|
|
||||||
$approved = $approve === 'yes';
|
$approved = $approve === 'yes';
|
||||||
|
|
||||||
|
$error = null;
|
||||||
|
if($approved) {
|
||||||
|
$appsData = $this->oauth2Ctx->getApps()->getAppsData();
|
||||||
|
try {
|
||||||
|
$appInfo = $appsData->getAppInfo(appId: $deviceInfo->getAppId(), deleted: false);
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
return ['error' => 'code'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$scopeInfos = $this->oauth2Ctx->getApps()->handleScopeString($appInfo, $deviceInfo->getScope());
|
||||||
|
foreach($scopeInfos as $scopeName => $scopeInfo) {
|
||||||
|
if(is_string($scopeInfo)) {
|
||||||
|
$approved = false;
|
||||||
|
$error = ['error' => 'scope', 'scope' => $scopeName, 'reason' => $scopeInfo];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope[] = $scopeInfo->getSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$devicesData->setDeviceApproval($deviceInfo, $approved, $authInfo->getUserInfo()->getId());
|
$devicesData->setDeviceApproval($deviceInfo, $approved, $authInfo->getUserInfo()->getId());
|
||||||
|
|
||||||
|
if($error !== null)
|
||||||
|
return $error;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'approval' => $approved ? 'approved' : 'denied',
|
'approval' => $approved ? 'approved' : 'denied',
|
||||||
];
|
];
|
||||||
|
@ -281,16 +305,28 @@ final class OAuth2WebRoutes extends RouteHandler {
|
||||||
return ['error' => 'code'];
|
return ['error' => 'code'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($deviceInfo->hasExpired())
|
||||||
|
return ['error' => 'expired'];
|
||||||
|
|
||||||
if(!$deviceInfo->isPending())
|
if(!$deviceInfo->isPending())
|
||||||
return ['error' => 'approval'];
|
return ['error' => 'approval'];
|
||||||
|
|
||||||
$appsData = $this->appsCtx->getData();
|
$appsData = $this->oauth2Ctx->getApps()->getAppsData();
|
||||||
try {
|
try {
|
||||||
$appInfo = $appsData->getAppInfo(appId: $deviceInfo->getAppId(), deleted: false);
|
$appInfo = $appsData->getAppInfo(appId: $deviceInfo->getAppId(), deleted: false);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
return ['error' => 'code'];
|
return ['error' => 'code'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope = [];
|
||||||
|
$scopeInfos = $this->oauth2Ctx->getApps()->handleScopeString($appInfo, $deviceInfo->getScope());
|
||||||
|
foreach($scopeInfos as $scopeName => $scopeInfo) {
|
||||||
|
if(is_string($scopeInfo))
|
||||||
|
return ['error' => 'scope', 'scope' => $scopeName, 'reason' => $scopeInfo];
|
||||||
|
|
||||||
|
$scope[] = $scopeInfo->getSummary();
|
||||||
|
}
|
||||||
|
|
||||||
$userInfo = $authInfo->getUserInfo();
|
$userInfo = $authInfo->getUserInfo();
|
||||||
$result = [
|
$result = [
|
||||||
'req' => [
|
'req' => [
|
||||||
|
@ -304,6 +340,7 @@ final class OAuth2WebRoutes extends RouteHandler {
|
||||||
['title' => 'Website', 'display' => $appInfo->getWebsiteDisplay(), 'uri' => $appInfo->getWebsite()],
|
['title' => 'Website', 'display' => $appInfo->getWebsiteDisplay(), 'uri' => $appInfo->getWebsite()],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'scope' => $scope,
|
||||||
'user' => [
|
'user' => [
|
||||||
'name' => $userInfo->getName(),
|
'name' => $userInfo->getName(),
|
||||||
'colour' => $userInfo->getColour(),
|
'colour' => $userInfo->getColour(),
|
||||||
|
|
7
tools/prune
Executable file
7
tools/prune
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../hanyuu.php';
|
||||||
|
|
||||||
|
$hau->pruneExpired(function(string $format, ...$args) {
|
||||||
|
echo sprintf($format, ...$args) . PHP_EOL;
|
||||||
|
});
|
Loading…
Reference in a new issue