Made OAuth2 IDs snowflakes and also removed the client_id field.

This commit is contained in:
flash 2025-04-24 00:17:35 +00:00
parent c91fa69362
commit af476491b2
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
12 changed files with 237 additions and 58 deletions

View file

@ -1 +1 @@
20250422.3
20250423

View file

@ -0,0 +1,169 @@
<?php
use Index\Db\DbConnection;
use Index\Db\Migration\DbMigration;
final class MakeOauthStuffBigint_20250423_224653 implements DbMigration {
public function migrate(DbConnection $conn): void {
$conn->execute(<<<SQL
ALTER TABLE msz_apps_scopes
DROP FOREIGN KEY apps_scopes_app_foreign;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_apps_scopes
CHANGE COLUMN app_id app_id BIGINT UNSIGNED NOT NULL FIRST;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_apps_uris
DROP FOREIGN KEY apps_uris_app_foreign;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_apps_uris
CHANGE COLUMN app_id app_id BIGINT UNSIGNED NOT NULL AFTER uri_id;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_access
DROP FOREIGN KEY oauth2_access_app_foreign;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_access
CHANGE COLUMN app_id app_id BIGINT UNSIGNED NULL DEFAULT NULL AFTER acc_id;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_authorise
DROP FOREIGN KEY oauth2_authorise_app_foreign;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_authorise
DROP FOREIGN KEY oauth2_authorise_uri_foreign;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_authorise
CHANGE COLUMN app_id app_id BIGINT UNSIGNED NOT NULL AFTER auth_id,
CHANGE COLUMN uri_id uri_id BIGINT UNSIGNED NOT NULL AFTER user_id;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_device
DROP FOREIGN KEY oauth2_device_app_foreign;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_device
CHANGE COLUMN app_id app_id BIGINT UNSIGNED NOT NULL AFTER dev_id;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_refresh
DROP FOREIGN KEY oauth2_refresh_app_foreign;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_refresh
DROP FOREIGN KEY oauth2_refresh_access_foreign;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_refresh
CHANGE COLUMN app_id app_id BIGINT UNSIGNED NULL DEFAULT NULL AFTER ref_id,
CHANGE COLUMN acc_id acc_id BIGINT UNSIGNED NULL DEFAULT NULL AFTER user_id;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_apps
CHANGE COLUMN app_id app_id BIGINT UNSIGNED NOT NULL FIRST,
DROP COLUMN app_client_id,
DROP INDEX apps_client_id_unique;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_apps_uris
CHANGE COLUMN uri_id uri_id BIGINT UNSIGNED NOT NULL FIRST;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_access
CHANGE COLUMN acc_id acc_id BIGINT UNSIGNED NOT NULL FIRST;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_authorise
CHANGE COLUMN auth_id auth_id BIGINT UNSIGNED NOT NULL FIRST;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_device
CHANGE COLUMN dev_id dev_id BIGINT UNSIGNED NOT NULL FIRST;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_refresh
CHANGE COLUMN ref_id ref_id BIGINT UNSIGNED NOT NULL FIRST;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_apps_scopes
ADD CONSTRAINT apps_scopes_app_foreign
FOREIGN KEY (app_id)
REFERENCES msz_apps (app_id)
ON UPDATE CASCADE
ON DELETE CASCADE;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_apps_uris
ADD CONSTRAINT apps_uris_app_foreign
FOREIGN KEY (app_id)
REFERENCES msz_apps (app_id)
ON UPDATE CASCADE
ON DELETE CASCADE;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_access
ADD CONSTRAINT oauth2_access_app_foreign
FOREIGN KEY (app_id)
REFERENCES msz_apps (app_id)
ON UPDATE CASCADE
ON DELETE CASCADE;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_authorise
ADD CONSTRAINT oauth2_authorise_app_foreign
FOREIGN KEY (app_id)
REFERENCES msz_apps (app_id)
ON UPDATE CASCADE
ON DELETE CASCADE;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_authorise
ADD CONSTRAINT oauth2_authorise_uri_foreign
FOREIGN KEY (uri_id)
REFERENCES msz_apps_uris (uri_id)
ON UPDATE CASCADE
ON DELETE CASCADE;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_device
ADD CONSTRAINT oauth2_device_app_foreign
FOREIGN KEY (app_id)
REFERENCES msz_apps (app_id)
ON UPDATE CASCADE
ON DELETE CASCADE;
SQL);
$conn->execute(<<<SQL
ALTER TABLE msz_oauth2_refresh
ADD CONSTRAINT oauth2_refresh_app_foreign
FOREIGN KEY (app_id)
REFERENCES msz_apps (app_id)
ON UPDATE CASCADE
ON DELETE CASCADE;
SQL);
$snowflake = new \Index\Snowflake\RandomSnowflake;
$stmt = $conn->prepare('UPDATE msz_apps SET app_id = ? WHERE app_id = ?');
$result = $conn->query('SELECT app_id, UNIX_TIMESTAMP(app_created) FROM msz_apps');
while($result->next()) {
$stmt->addParameter(1, (string)$snowflake->next(at: $result->getInteger(1) * 1000));
$stmt->addParameter(2, $result->getString(0));
$stmt->execute();
}
}
}

View file

@ -128,7 +128,7 @@ if(isset($_POST['action']) && is_string($_POST['action'])) {
$tmpFiles = [];
try {
$tmpFiles[] = db_to_zip($archive, $userInfo, 'apps', ['app_id:s', 'user_id:s:n', 'app_name:s', 'app_summary:s', 'app_website:s', 'app_type:s', 'app_access_lifetime:i:n', 'app_refresh_lifetime:i:n', 'app_client_id:s', 'app_client_secret:n', 'app_created:t', 'app_updated:t', 'app_deleted:t:n']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'apps', ['app_id:s', 'user_id:s:n', 'app_name:s', 'app_summary:s', 'app_website:s', 'app_type:s', 'app_access_lifetime:i:n', 'app_refresh_lifetime:i:n', 'app_client_secret:n', 'app_created:t', 'app_updated:t', 'app_deleted:t:n']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'audit_log', ['log_id:i', 'user_id:s:n', 'log_action:s', 'log_params:j', 'log_created:t', 'log_remote_addr:a:n', 'log_country:s']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'auth_tfa', ['user_id:s', 'tfa_token:n', 'tfa_created:t']);
$tmpFiles[] = db_to_zip($archive, $userInfo, 'changelog_changes', ['change_id:s', 'user_id:s:n', 'change_action:s:n', 'change_created:t', 'change_log:s', 'change_text:s:n']);

View file

@ -14,7 +14,6 @@ class AppInfo {
public private(set) AppType $type,
public private(set) ?int $accessTokenLifetime,
public private(set) ?int $refreshTokenLifetime,
public private(set) string $clientId,
#[\SensitiveParameter] private string $clientSecret,
public private(set) int $createdTime,
public private(set) int $updatedTime,
@ -31,11 +30,10 @@ class AppInfo {
type: AppType::tryFrom($result->getString(5)) ?? AppType::Public,
accessTokenLifetime: $result->getIntegerOrNull(6),
refreshTokenLifetime: $result->getIntegerOrNull(7),
clientId: $result->getString(8),
clientSecret: $result->getString(9),
createdTime: $result->getInteger(10),
updatedTime: $result->getInteger(11),
deletedTime: $result->getIntegerOrNull(12),
clientSecret: $result->getString(8),
createdTime: $result->getInteger(9),
updatedTime: $result->getInteger(10),
deletedTime: $result->getIntegerOrNull(11),
);
}

View file

@ -3,13 +3,17 @@ namespace Misuzu\Apps;
use RuntimeException;
use Index\Db\DbConnection;
use Index\Snowflake\RandomSnowflake;
class AppsContext {
public private(set) AppsData $apps;
public private(set) ScopesData $scopes;
public function __construct(DbConnection $dbConn) {
$this->apps = new AppsData($dbConn);
public function __construct(
DbConnection $dbConn,
RandomSnowflake $snowflake,
) {
$this->apps = new AppsData($dbConn, $snowflake);
$this->scopes = new ScopesData($dbConn);
}
@ -38,7 +42,7 @@ class AppsContext {
bool $breakOnFail = true
): array {
if(is_string($appInfo))
$appInfo = $this->apps->getAppInfo(appId: $appInfo, deleted: false);
$appInfo = $this->apps->getAppInfo($appInfo, deleted: false);
$infos = [];

View file

@ -6,40 +6,38 @@ use InvalidArgumentException;
use RuntimeException;
use Index\XString;
use Index\Db\{DbConnection,DbStatementCache};
use Index\Snowflake\RandomSnowflake;
use Misuzu\Users\UserInfo;
class AppsData {
private DbStatementCache $cache;
public function __construct(DbConnection $dbConn) {
public function __construct(
DbConnection $dbConn,
private RandomSnowflake $snowflake,
) {
$this->cache = new DbStatementCache($dbConn);
}
public function getAppInfo(
?string $appId = null,
?string $clientId = null,
?bool $deleted = null
string $appId,
?bool $deleted = null,
): AppInfo {
$hasAppId = $appId !== null;
$hasClientId = $clientId !== null;
$hasDeleted = $deleted !== null;
if($hasAppId === $hasClientId)
throw new InvalidArgumentException('you must specify either $appId or $clientId');
$values = [];
$query = <<<SQL
SELECT app_id, user_id, app_name, app_summary, app_website, app_type,
app_access_lifetime, app_refresh_lifetime, app_client_id, app_client_secret,
app_access_lifetime, app_refresh_lifetime, app_client_secret,
UNIX_TIMESTAMP(app_created), UNIX_TIMESTAMP(app_updated), UNIX_TIMESTAMP(app_deleted)
FROM msz_apps
WHERE app_id = ?
SQL;
$query .= sprintf(' WHERE %s = ?', $hasAppId ? 'app_id' : 'app_client_id');
if($hasDeleted)
$query .= sprintf(' AND app_deleted %s NULL', $deleted ? 'IS NOT' : 'IS');
$stmt = $this->cache->get($query);
$stmt->nextParameter($hasAppId ? $appId : $clientId);
$stmt->nextParameter($appId);
$stmt->execute();
$result = $stmt->getResult();
@ -58,7 +56,6 @@ class AppsData {
UserInfo|string|null $userInfo = null,
string $summary = '',
string $website = '',
?string $clientId = null,
#[\SensitiveParameter] ?string $clientSecret = null,
?int $accessLifetime = null,
?int $refreshLifetime = null
@ -66,11 +63,6 @@ class AppsData {
if(trim($name) === '')
throw new InvalidArgumentException('$name may not be empty');
if($clientId === null)
$clientId = XString::random(self::CLIENT_ID_LENGTH);
elseif(trim($clientId) === '')
throw new InvalidArgumentException('$clientId may not be empty');
if($accessLifetime !== null && $accessLifetime < 1)
throw new InvalidArgumentException('$accessLifetime must be null or greater than zero');
if($refreshLifetime !== null && $refreshLifetime < 0)
@ -83,13 +75,14 @@ class AppsData {
if($type !== AppType::Public && $clientSecret === '')
throw new InvalidArgumentException('$clientSecret must be specified for confidential clients');
$appId = (string)$this->snowflake->next();
$stmt = $this->cache->get(<<<SQL
INSERT INTO msz_apps (
user_id, app_name, app_summary, app_website, app_type,
app_access_lifetime, app_refresh_lifetime,
app_client_id, app_client_secret
app_id, user_id, app_name, app_summary, app_website, app_type,
app_access_lifetime, app_refresh_lifetime, app_client_secret
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
SQL);
$stmt->nextParameter($appId);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->nextParameter($name);
$stmt->nextParameter($summary);
@ -97,11 +90,10 @@ class AppsData {
$stmt->nextParameter($type);
$stmt->nextParameter($accessLifetime);
$stmt->nextParameter($refreshLifetime);
$stmt->nextParameter($clientId);
$stmt->nextParameter($clientSecret);
$stmt->execute();
return $this->getAppInfo(appId: (string)$stmt->lastInsertId);
return $this->getAppInfo($appId);
}
public function countAppUris(AppInfo|string $appInfo): int {

View file

@ -358,7 +358,7 @@ final class AuthProcessors implements RouteHandler {
} else return false;
try {
$appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(clientId: $clientId, deleted: false);
$appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo($clientId, deleted: false);
} catch(RuntimeException $ex) {
return false;
}

View file

@ -5,6 +5,7 @@ use InvalidArgumentException;
use RuntimeException;
use Index\XString;
use Index\Db\{DbConnection,DbStatementCache};
use Index\Snowflake\RandomSnowflake;
use Misuzu\Apps\{AppInfo,AppUriInfo};
use Misuzu\Users\UserInfo;
@ -12,7 +13,8 @@ class OAuth2AuthorisationData {
private DbStatementCache $cache;
public function __construct(
private DbConnection $dbConn
private DbConnection $dbConn,
private RandomSnowflake $snowflake,
) {
$this->cache = new DbStatementCache($dbConn);
}
@ -68,13 +70,15 @@ class OAuth2AuthorisationData {
string $scope,
?string $code = null
): OAuth2AuthorisationInfo {
$authId = (string)$this->snowflake->next();
$code ??= XString::random(60);
$stmt = $this->cache->get(<<<SQL
INSERT INTO msz_oauth2_authorise (
app_id, user_id, uri_id, auth_challenge_code, auth_challenge_method, auth_scope, auth_code
) VALUES (?, ?, ?, ?, ?, ?, ?)
auth_id, app_id, user_id, uri_id, auth_challenge_code, auth_challenge_method, auth_scope, auth_code
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
SQL);
$stmt->nextParameter($authId);
$stmt->nextParameter($appInfo instanceof AppInfo ? $appInfo->id : $appInfo);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->nextParameter($appUriInfo instanceof AppUriInfo ? $appUriInfo->id : $appUriInfo);
@ -84,7 +88,7 @@ class OAuth2AuthorisationData {
$stmt->nextParameter($code);
$stmt->execute();
return $this->getAuthorisationInfo(authsId: (string)$stmt->lastInsertId);
return $this->getAuthorisationInfo(authsId: $authId);
}
public function deleteAuthorisation(

View file

@ -4,6 +4,7 @@ namespace Misuzu\OAuth2;
use RuntimeException;
use Index\Config\Config;
use Index\Db\DbConnection;
use Index\Snowflake\RandomSnowflake;
use Misuzu\SiteInfo;
use Misuzu\Apps\{AppsContext,AppInfo};
use Misuzu\JWT\JWT;
@ -46,12 +47,13 @@ class OAuth2Context {
public function __construct(
private Config $config,
DbConnection $dbConn,
RandomSnowflake $snowflake,
public private(set) AppsContext $appsCtx,
private SiteInfo $siteInfo,
) {
$this->authorisations = new OAuth2AuthorisationData($dbConn);
$this->tokens = new OAuth2TokensData($dbConn);
$this->devices = new OAuth2DevicesData($dbConn);
$this->authorisations = new OAuth2AuthorisationData($dbConn, $snowflake);
$this->tokens = new OAuth2TokensData($dbConn, $snowflake);
$this->devices = new OAuth2DevicesData($dbConn, $snowflake);
$this->keys = new OAuth2Keys($config->getArray('keys'));
}
@ -147,7 +149,7 @@ class OAuth2Context {
$token = [
'iss' => $siteInfo->url,
'sub' => $accessOrAuthzInfo->userId,
'aud' => $appInfo->clientId,
'aud' => $appInfo->id,
'exp' => $accessOrAuthzInfo->expiresTime,
'iat' => $issuedAt ?? time(),
'auth_time' => $accessOrAuthzInfo->createdTime,

View file

@ -5,6 +5,7 @@ use InvalidArgumentException;
use RuntimeException;
use Index\XString;
use Index\Db\{DbConnection,DbStatementCache};
use Index\Snowflake\RandomSnowflake;
use Misuzu\Apps\AppInfo;
use Misuzu\Users\UserInfo;
@ -14,7 +15,8 @@ class OAuth2DevicesData {
private DbStatementCache $cache;
public function __construct(
private DbConnection $dbConn
private DbConnection $dbConn,
private RandomSnowflake $snowflake,
) {
$this->cache = new DbStatementCache($dbConn);
}
@ -82,14 +84,16 @@ class OAuth2DevicesData {
?string $code = null,
?string $userCode = null
): OAuth2DeviceInfo {
$devId = (string)$this->snowflake->next();
$code ??= XString::random(60);
$userCode ??= XString::random(9, self::USER_CODE_CHARS);
$stmt = $this->cache->get(<<<SQL
INSERT INTO msz_oauth2_device (
app_id, user_id, dev_code, dev_user_code, dev_scope
) VALUES (?, ?, ?, ?, ?)
dev_id, app_id, user_id, dev_code, dev_user_code, dev_scope
) VALUES (?, ?, ?, ?, ?, ?)
SQL);
$stmt->nextParameter($devId);
$stmt->nextParameter($appInfo instanceof AppInfo ? $appInfo->id : $appInfo);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->nextParameter($code);
@ -97,7 +101,7 @@ class OAuth2DevicesData {
$stmt->nextParameter($scope);
$stmt->execute();
return $this->getDeviceInfo(deviceId: (string)$stmt->lastInsertId);
return $this->getDeviceInfo(deviceId: $devId);
}
public function deleteDevice(

View file

@ -5,6 +5,7 @@ use InvalidArgumentException;
use RuntimeException;
use Index\XString;
use Index\Db\{DbConnection,DbStatementCache};
use Index\Snowflake\RandomSnowflake;
use Misuzu\Apps\AppInfo;
use Misuzu\Users\UserInfo;
@ -12,7 +13,8 @@ class OAuth2TokensData {
private DbStatementCache $cache;
public function __construct(
private DbConnection $dbConn
private DbConnection $dbConn,
private RandomSnowflake $snowflake,
) {
$this->cache = new DbStatementCache($dbConn);
}
@ -48,17 +50,19 @@ class OAuth2TokensData {
string $scope = '',
?int $lifetime = null
): OAuth2AccessInfo {
$accId = (string)$this->snowflake->next();
$token ??= XString::random(80);
$stmt = $this->cache->get(<<<SQL
INSERT INTO msz_oauth2_access (
app_id, user_id, acc_token, acc_scope,
acc_id, app_id, user_id, acc_token, acc_scope,
acc_expires
) VALUES (
?, ?, ?, ?,
?, ?, ?, ?, ?,
IF(?, NOW() + INTERVAL ? SECOND, DEFAULT(acc_expires))
)
SQL);
$stmt->nextParameter($accId);
$stmt->nextParameter($appInfo instanceof AppInfo ? $appInfo->id : $appInfo);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->nextParameter($token);
@ -67,7 +71,7 @@ class OAuth2TokensData {
$stmt->nextParameter($lifetime);
$stmt->execute();
return $this->getAccessInfo((string)$stmt->lastInsertId, OAuth2AccessInfoGetField::Id);
return $this->getAccessInfo($accId, OAuth2AccessInfoGetField::Id);
}
public function deleteAccess(
@ -149,17 +153,19 @@ class OAuth2TokensData {
string $scope = '',
?int $lifetime = null
): OAuth2RefreshInfo {
$refId = (string)$this->snowflake->next();
$token ??= XString::random(120);
$stmt = $this->cache->get(<<<SQL
INSERT INTO msz_oauth2_refresh (
app_id, user_id, acc_id, ref_token, ref_scope,
ref_id, app_id, user_id, acc_id, ref_token, ref_scope,
ref_expires
) VALUES (
?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?,
IF(?, NOW() + INTERVAL ? SECOND, DEFAULT(ref_expires))
)
SQL);
$stmt->nextParameter($refId);
$stmt->nextParameter($appInfo instanceof AppInfo ? $appInfo->id : $appInfo);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->nextParameter($accessInfo instanceof OAuth2AccessInfo ? $accessInfo->id : $accessInfo);
@ -169,7 +175,7 @@ class OAuth2TokensData {
$stmt->nextParameter($lifetime);
$stmt->execute();
return $this->getRefreshInfo((string)$stmt->lastInsertId, OAuth2RefreshInfoGetField::Id);
return $this->getRefreshInfo($refId, OAuth2RefreshInfoGetField::Id);
}
public function deleteRefresh(

View file

@ -85,7 +85,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
try {
$appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(
clientId: (string)$content->getParam('client'),
(string)$content->getParam('client'),
deleted: false
);
} catch(RuntimeException $ex) {
@ -189,7 +189,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
try {
$appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(
clientId: (string)$request->getParam('client'),
(string)$request->getParam('client'),
deleted: false
);
} catch(RuntimeException $ex) {
@ -302,7 +302,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
if($approved) {
try {
$appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(
appId: $deviceInfo->appId,
$deviceInfo->appId,
deleted: false
);
} catch(RuntimeException $ex) {
@ -390,7 +390,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
try {
$appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(
appId: $deviceInfo->appId,
$deviceInfo->appId,
deleted: false
);
} catch(RuntimeException $ex) {