Refactor subuser permissions and enhance artifact management
This commit is contained in:
@@ -24,6 +24,72 @@ class authRoute
|
||||
{
|
||||
use route_t;
|
||||
|
||||
private function passkeyChallengePrincipalCacheKey(string $challengeToken): string
|
||||
{
|
||||
return 'passkey_challenge_principal:' . $challengeToken;
|
||||
}
|
||||
|
||||
private function setPasskeyChallengePrincipal(string $challengeToken, string $principalType): void
|
||||
{
|
||||
if (!defined('redis')) {
|
||||
return;
|
||||
}
|
||||
constant('redis')->setEx($this->passkeyChallengePrincipalCacheKey($challengeToken), $principalType, 5 * 60);
|
||||
}
|
||||
|
||||
private function getPasskeyChallengePrincipal(string $challengeToken): string
|
||||
{
|
||||
if (!defined('redis')) {
|
||||
return 'discoverable';
|
||||
}
|
||||
$principalType = constant('redis')->get($this->passkeyChallengePrincipalCacheKey($challengeToken));
|
||||
return is_string($principalType) && in_array($principalType, ['user', 'subuser'], true)
|
||||
? $principalType
|
||||
: 'discoverable';
|
||||
}
|
||||
|
||||
private function clearPasskeyChallengePrincipal(string $challengeToken): void
|
||||
{
|
||||
if (!defined('redis')) {
|
||||
return;
|
||||
}
|
||||
constant('redis')->delete($this->passkeyChallengePrincipalCacheKey($challengeToken));
|
||||
}
|
||||
|
||||
private function passkeyAllowCredentials(int $userId, bool $isSubuser): array
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$passkeys = new passkeys_o();
|
||||
$passkeys->setAdditionalWhereClause(
|
||||
'`user_id` = ' . (int)$userId . ' AND `is_subuser` = ' . ($isSubuser ? '1' : '0')
|
||||
);
|
||||
$list = $passkeys->listObjectsWithPaginationIfSet(function ($o) {
|
||||
$transports = null;
|
||||
if (isset($o['transports'])) {
|
||||
$decoded = json_decode($o['transports'], true);
|
||||
$transports = is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
return [
|
||||
'type' => 'public-key',
|
||||
'id' => $o['credential_id'] ?? null,
|
||||
'transports' => $transports,
|
||||
];
|
||||
});
|
||||
|
||||
if (isset($list['items']) && is_array($list['items'])) {
|
||||
$list = $list['items'];
|
||||
}
|
||||
|
||||
return is_array($list)
|
||||
? array_values(array_filter($list, function ($item) {
|
||||
return isset($item['id']) && is_string($item['id']) && strlen($item['id']) > 0;
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->post('/auth/login', function () {
|
||||
@@ -625,11 +691,53 @@ class authRoute
|
||||
global $response;
|
||||
$this->requireRecaptcha();
|
||||
|
||||
$principal_type = strtolower(trim((string)(self::getParameter('principal_type') ?? self::getParameter('user_type') ?? '')));
|
||||
if ($principal_type === '') {
|
||||
$principal_type = self::getParameter('customer_number') !== null ? 'user' : 'discoverable';
|
||||
}
|
||||
if (!in_array($principal_type, ['user', 'subuser', 'discoverable'], true)) {
|
||||
$response->error('Invalid principal_type', 400);
|
||||
}
|
||||
|
||||
$customer_number = self::getParameter('customer_number');
|
||||
$user_id = 0;
|
||||
$allowCredentials = [];
|
||||
|
||||
if ($customer_number !== null) {
|
||||
if ($principal_type === 'subuser') {
|
||||
$subuser = null;
|
||||
if (self::isParametersSet(['subuser_id'])) {
|
||||
$subuser_id = (int)self::getParameter('subuser_id');
|
||||
self::requireType($subuser_id, $this->type_int());
|
||||
self::requireMinValue($subuser_id, 1);
|
||||
$candidate = (new subusers_o())->select($subuser_id);
|
||||
if ($candidate->exists()) {
|
||||
$candidate->getObjectProperties();
|
||||
$subuser = $candidate;
|
||||
}
|
||||
} elseif (self::isParametersSet(['username'])) {
|
||||
$username = (string)self::getParameter('username');
|
||||
self::requireType($username, $this->type_string());
|
||||
self::requireMinLength('username', 3);
|
||||
self::requireMaxLength('username', 255);
|
||||
$subuser = (new subusers_o())->getSubuserByUsername($username);
|
||||
} elseif (self::isParametersSet(['phone_country_code', 'phone'])) {
|
||||
$phone_country_code = (int)self::getParameter('phone_country_code');
|
||||
$phone = (int)self::getParameter('phone');
|
||||
self::requireType($phone_country_code, $this->type_int());
|
||||
self::requireMinLength('phone_country_code', 1);
|
||||
self::requireMaxLength('phone_country_code', 3);
|
||||
self::requireType($phone, $this->type_int());
|
||||
self::requireMinLength('phone', 4);
|
||||
self::requireMaxLength('phone', 15);
|
||||
$subuser = (new subusers_o())->getSubuserByPhone($phone_country_code, $phone);
|
||||
}
|
||||
|
||||
if ($subuser !== null) {
|
||||
$user_id = (int)$subuser->id;
|
||||
$allowCredentials = $this->passkeyAllowCredentials($user_id, true);
|
||||
}
|
||||
} elseif ($customer_number !== null) {
|
||||
$principal_type = 'user';
|
||||
$customer_number = (int)$customer_number;
|
||||
self::requireType($customer_number, $this->type_int());
|
||||
self::requireMinValue($customer_number, 1);
|
||||
@@ -639,33 +747,7 @@ class authRoute
|
||||
|
||||
if ($user->exists()) {
|
||||
$user_id = (int)$user->id;
|
||||
|
||||
// Load passkeys for this user (non-subuser)
|
||||
$passkeys = new passkeys_o();
|
||||
$passkeys->setAdditionalWhereClause('`user_id` = ' . (int)$user_id . ' AND `is_subuser` = 0');
|
||||
$list = $passkeys->listObjectsWithPaginationIfSet(function ($o) {
|
||||
$transports = null;
|
||||
if (isset($o['transports'])) {
|
||||
$decoded = json_decode($o['transports'], true);
|
||||
$transports = is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
return [
|
||||
'type' => 'public-key',
|
||||
'id' => $o['credential_id'] ?? null,
|
||||
'transports' => $transports,
|
||||
];
|
||||
});
|
||||
|
||||
// Ensure we return a simple array of credentials (without pagination wrapper)
|
||||
if (isset($list['items']) && is_array($list['items'])) {
|
||||
$allowCredentials = array_values(array_filter($list['items'], function ($item) {
|
||||
return isset($item['id']) && is_string($item['id']) && strlen($item['id']) > 0;
|
||||
}));
|
||||
} elseif (is_array($list)) {
|
||||
$allowCredentials = array_values(array_filter($list, function ($item) {
|
||||
return isset($item['id']) && is_string($item['id']) && strlen($item['id']) > 0;
|
||||
}));
|
||||
}
|
||||
$allowCredentials = $this->passkeyAllowCredentials($user_id, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,6 +764,10 @@ class authRoute
|
||||
|
||||
// Create an ephemeral token to bind the challenge to the (potential) user
|
||||
(new tokens_o())->create($user_id, $challenge_token, 'PASSKEY_CHALLENGE');
|
||||
$this->setPasskeyChallengePrincipal(
|
||||
$challenge_token,
|
||||
$principal_type === 'subuser' ? 'subuser' : ($principal_type === 'user' ? 'user' : 'discoverable')
|
||||
);
|
||||
|
||||
$logDetails = $customer_number ? 'Issued passkey challenge for customer ' . $customer_number : 'Issued passkey challenge (discoverable)';
|
||||
(new logs_o())->add('auth', 'global', 1, $user_id, 'AUTH_PASSKEY_CHALLENGE', $logDetails);
|
||||
@@ -733,6 +819,7 @@ class authRoute
|
||||
if ($token_type !== 'PASSKEY_CHALLENGE') {
|
||||
$response->error('Invalid token type', 401);
|
||||
}
|
||||
$challengePrincipalType = $this->getPasskeyChallengePrincipal($challenge_token);
|
||||
|
||||
// Determine rpId/host
|
||||
$host = parse_url((string)($_SERVER['HTTP_ORIGIN'] ?? ''), PHP_URL_HOST) ?: ($_SERVER['SERVER_NAME'] ?? 'localhost');
|
||||
@@ -768,7 +855,17 @@ class authRoute
|
||||
// Success → issue session token accordingly and delete the challenge token
|
||||
$issued_to_user_id = (int)$passkey->user_id->value();
|
||||
$is_subuser = (bool)$passkey->is_subuser->value();
|
||||
if (
|
||||
($challengePrincipalType === 'subuser' && !$is_subuser)
|
||||
|| ($challengePrincipalType === 'user' && $is_subuser)
|
||||
) {
|
||||
(new logs_o())->add('auth', 'global', 1, $user_id_hint, 'AUTH_PASSKEY_VERIFY_FAILURE', 'Credential principal mismatch');
|
||||
$token_o->delete($challenge_token);
|
||||
$this->clearPasskeyChallengePrincipal($challenge_token);
|
||||
$response->error('Invalid credential', 401);
|
||||
}
|
||||
$token_o->delete($challenge_token);
|
||||
$this->clearPasskeyChallengePrincipal($challenge_token);
|
||||
|
||||
(new logs_o())->add('auth', 'global', 1, $issued_to_user_id, 'AUTH_PASSKEY_VERIFY_SUCCESS', 'Passkey assertion accepted');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user