Merge pull request #11276 from appwrite/fix-rule-oauth

Fix: rule oauth flow
This commit is contained in:
Matej Bačo
2026-02-10 10:27:25 +01:00
committed by GitHub
4 changed files with 134 additions and 1 deletions
+21 -1
View File
@@ -1469,13 +1469,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('devKey')
->inject('user')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('geodb')
->inject('queueForEvents')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('authorization')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
@@ -1512,6 +1513,25 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$state = $defaultState;
}
// Allow redirect to rule URL if related to project
//Check if $redirectValidator is instance of Redirect class
if ($redirectValidator instanceof Redirect) {
$rules = $authorization->skip(fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [
parse_url($state['success'], PHP_URL_HOST),
parse_url($state['failure'], PHP_URL_HOST)
]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::limit(2)
]));
foreach ($rules as $rule) {
$allowedHostnames = $redirectValidator->getAllowedHostnames();
$allowedHostnames[] = $rule->getAttribute('domain', '');
$redirectValidator->setAllowedHostnames($allowedHostnames);
}
}
if ($devKey->isEmpty() && !$redirectValidator->isValid($state['success'])) {
throw new Exception(Exception::PROJECT_INVALID_SUCCESS_URL);
}
+21
View File
@@ -22,6 +22,27 @@ class Origin extends Validator
{
}
public function setAllowedHostnames(array $allowedHostnames): self
{
$this->allowedHostnames = $allowedHostnames;
return $this;
}
public function setAllowedSchemes(array $allowedSchemes): self
{
$this->allowedSchemes = $allowedSchemes;
return $this;
}
public function getAllowedHostnames(): array
{
return $this->allowedHostnames;
}
public function getAllowedSchemes(): array
{
return $this->allowedSchemes;
}
/**
* Check if Origin is valid.
@@ -5191,6 +5191,24 @@ class ProjectsConsoleClientTest extends Scope
], followRedirects: false);
$this->assertEquals(400, $response['headers']['status-code']);
// Also ensure final step blocks unknown redirect URL
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider . '/redirect', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'origin' => '',
'referer' => 'https://mockserver.com',
], [
'code' => 'any-code',
'state' => \json_encode([
'success' => 'https://domain-without-rule.com',
'failure' => 'https://domain-without-rule.com'
]),
'error' => '',
'error_description' => '',
], followRedirects: false);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertStringContainsString('project_invalid_success_url', $response['body']);
// Ensure rule's domain can be redirect URL
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
'content-type' => 'application/json',
@@ -5203,6 +5221,24 @@ class ProjectsConsoleClientTest extends Scope
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
// Also ensure final step allows redirect URL
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider . '/redirect', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'origin' => '',
'referer' => 'https://mockserver.com',
], [
'code' => 'any-code',
'state' => \json_encode([
'success' => 'https://' . $domain,
'failure' => 'https://' . $domain
]),
'error' => '',
'error_deescription' => '',
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringContainsString('https://' . $domain, $response['headers']['location']);
// Ensure unknown domain cannot be redirect URL
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [
'content-type' => 'application/json',
@@ -74,4 +74,60 @@ class OriginTest extends TestCase
$this->assertEquals(false, $validator->isValid('random-scheme://localhost'));
$this->assertEquals('Invalid Scheme. The scheme used (random-scheme) in the Origin (random-scheme://localhost) is not supported. If you are using a custom scheme, please change it to `appwrite-callback-<PROJECT_ID>`', $validator->getDescription());
}
public function testGetAllowedHostnames(): void
{
$validator = new Origin(
allowedHostnames: ['appwrite.io', 'localhost'],
allowedSchemes: ['exp']
);
$this->assertEquals(['appwrite.io', 'localhost'], $validator->getAllowedHostnames());
}
public function testGetAllowedSchemes(): void
{
$validator = new Origin(
allowedHostnames: ['appwrite.io'],
allowedSchemes: ['exp', 'appwrite-callback-123']
);
$this->assertEquals(['exp', 'appwrite-callback-123'], $validator->getAllowedSchemes());
}
public function testSetAllowedHostnames(): void
{
$validator = new Origin(
allowedHostnames: ['appwrite.io'],
allowedSchemes: ['exp']
);
$this->assertEquals(true, $validator->isValid('https://appwrite.io'));
$this->assertEquals(false, $validator->isValid('https://example.com'));
$result = $validator->setAllowedHostnames(['example.com']);
$this->assertSame($validator, $result);
$this->assertEquals(['example.com'], $validator->getAllowedHostnames());
$this->assertEquals(true, $validator->isValid('https://example.com'));
$this->assertEquals(false, $validator->isValid('https://appwrite.io'));
}
public function testSetAllowedSchemes(): void
{
$validator = new Origin(
allowedHostnames: ['appwrite.io'],
allowedSchemes: ['exp']
);
$this->assertEquals(true, $validator->isValid('exp://'));
$this->assertEquals(false, $validator->isValid('appwrite-callback-456://'));
$result = $validator->setAllowedSchemes(['appwrite-callback-456']);
$this->assertSame($validator, $result);
$this->assertEquals(['appwrite-callback-456'], $validator->getAllowedSchemes());
$this->assertEquals(true, $validator->isValid('appwrite-callback-456://'));
$this->assertEquals(false, $validator->isValid('exp://'));
}
}