Skip to content

Commit f75bb28

Browse files
committed
fix(oauth2): scope Native client custom-scheme lock to payloads that touch URI fields
ClientService::create()/update() acquired the TOCTOU lock (NATIVE_CUSTOM_SCHEME_REGISTRATION_LOCK) whenever application_type was Native, regardless of whether the payload touched redirect_uris, allowed_origins, or post_logout_redirect_uris. A plain app_name rename on one Native client therefore contended with an unrelated tenant's scheme registration, producing spurious "please retry" 412s. Extract NATIVE_CUSTOM_SCHEME_URI_FIELDS as the single source of truth for which fields matter, shared by assertNativeCustomSchemesAllowed() and the new payloadTouchesNativeCustomSchemeFields() gate. The lock is now taken only when the payload actually registers a custom scheme - matching what the SHORTCUT comment on the lock constant already claimed. Found during /review-pr-deep review of PR #147.
1 parent 889c57c commit f75bb28

2 files changed

Lines changed: 59 additions & 3 deletions

File tree

app/Services/OAuth2/ClientService.php

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ final class ClientService extends AbstractService implements IClientService
6565
*/
6666
const NATIVE_CUSTOM_SCHEME_REGISTRATION_LOCK_LIFETIME = 30;
6767

68+
/**
69+
* Single source of truth for which payload fields carry Native-client custom URI schemes - shared by
70+
* assertNativeCustomSchemesAllowed() (what to validate) and payloadTouchesNativeCustomSchemeFields()
71+
* (whether the TOCTOU lock is worth taking). Keeping both reads from one list means they can't drift:
72+
* a field that stops needing validation automatically stops needing the lock too.
73+
*/
74+
const NATIVE_CUSTOM_SCHEME_URI_FIELDS = ['redirect_uris', 'allowed_origins', 'post_logout_redirect_uris'];
75+
6876
/**
6977
* @var IAuthService
7078
*/
@@ -154,6 +162,23 @@ private function withNativeCustomSchemeLock(Closure $fn): IEntity
154162
}
155163
}
156164

165+
/**
166+
* Gates the TOCTOU lock to payloads that actually register a custom URI scheme. A Native client
167+
* write that doesn't touch redirect_uris/allowed_origins/post_logout_redirect_uris (e.g. renaming
168+
* app_name) never calls hasCustomSchemeRegisteredOnAnotherClientThan(), so there is no race to close
169+
* and no reason to serialize it behind every other tenant's scheme registration.
170+
*
171+
* @param array $payload
172+
* @return bool
173+
*/
174+
private function payloadTouchesNativeCustomSchemeFields(array $payload): bool
175+
{
176+
foreach (self::NATIVE_CUSTOM_SCHEME_URI_FIELDS as $field) {
177+
if (!empty($payload[$field])) return true;
178+
}
179+
return false;
180+
}
181+
157182

158183
/**
159184
* Clients in possession of a client password MAY use the HTTP Basic
@@ -286,7 +311,7 @@ public function getCurrentClientAuthInfo()
286311
*/
287312
private function assertNativeCustomSchemesAllowed(array $payload, int $exclude_client_id = -1): void
288313
{
289-
foreach (['redirect_uris', 'allowed_origins', 'post_logout_redirect_uris'] as $field) {
314+
foreach (self::NATIVE_CUSTOM_SCHEME_URI_FIELDS as $field) {
290315
if (empty($payload[$field])) continue;
291316
foreach (explode(',', $payload[$field]) as $uri) {
292317
$parts = @parse_url(trim($uri));
@@ -351,7 +376,8 @@ public function create(array $payload):IEntity
351376
});
352377
};
353378

354-
if (($payload['application_type'] ?? null) === IClient::ApplicationType_Native) {
379+
if (($payload['application_type'] ?? null) === IClient::ApplicationType_Native
380+
&& $this->payloadTouchesNativeCustomSchemeFields($payload)) {
355381
return $this->withNativeCustomSchemeLock($do_create);
356382
}
357383

@@ -450,7 +476,8 @@ public function update(int $id, array $payload):IEntity
450476
});
451477
};
452478

453-
if (($payload['application_type'] ?? null) === IClient::ApplicationType_Native) {
479+
if (($payload['application_type'] ?? null) === IClient::ApplicationType_Native
480+
&& $this->payloadTouchesNativeCustomSchemeFields($payload)) {
454481
return $this->withNativeCustomSchemeLock($do_update);
455482
}
456483

tests/ClientApiTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,4 +445,33 @@ public function testUpdateNativeClientRejectsCustomSchemeRegistrationWhenAnother
445445
}
446446
}
447447

448+
public function testUpdateNativeClientNotTouchingUriFieldsIgnoresLockContention(){
449+
450+
// The TOCTOU lock only protects hasCustomSchemeRegisteredOnAnotherClientThan(), which only runs
451+
// when the payload touches redirect_uris/allowed_origins/post_logout_redirect_uris. A Native
452+
// client update that doesn't touch any of those (e.g. renaming app_name) must not contend with
453+
// an unrelated tenant's in-progress scheme registration - unlike the sibling test above, this
454+
// must succeed even while the lock is held.
455+
$lock_manager = App::make(ILockManagerService::class);
456+
$lock_manager->acquireLock(ClientService::NATIVE_CUSTOM_SCHEME_REGISTRATION_LOCK, 5);
457+
458+
try {
459+
$client = EntityManager::getRepository(Client::class)->findOneBy(['app_name' => 'oauth2_native_app']);
460+
461+
$response = $this->action("PUT", "Api\\ClientApiController@update",
462+
array(
463+
'id' => $client->id,
464+
'application_type' => IClient::ApplicationType_Native,
465+
'app_description' => 'updated description while another registration is in progress',
466+
),
467+
[],
468+
[],
469+
[]);
470+
471+
$this->assertResponseStatus(201);
472+
} finally {
473+
$lock_manager->releaseLock(ClientService::NATIVE_CUSTOM_SCHEME_REGISTRATION_LOCK);
474+
}
475+
}
476+
448477
}

0 commit comments

Comments
 (0)