8000 Feat: utopia auth by lohanidamodar · Pull Request #10758 · appwrite/appwrite · GitHub
[go: up one dir, main page]

Skip to content

Feat: utopia auth#10758

Merged
lohanidamodar merged 55 commits into1.8.xfrom
feat-appwrite-auth
Nov 27, 2025
Merged

Feat: utopia auth#10758
lohanidamodar merged 55 commits into1.8.xfrom
feat-appwrite-auth

Conversation

@lohanidamodar
Copy link
Member
@lohanidamodar lohanidamodar commented Nov 4, 2025

What does this PR do?

  • Refactor, remove static Auth methods in favor of utopia-php/auth library

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Screenshots may also be helpful.)

Related PRs and Issues

  • (Related PR or issue)

Checklist

  • Have you read the Contributing Guidelines on issues?
  • If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs?

@coderabbitai
Copy link
Contributor
coderabbitai bot commented Nov 4, 2025
📝 Walkthrough

Walkthrough

Removes the legacy Appwrite\Auth class and many per-algorithm Hash implementations; introduces a new User document class with role, token and session helpers; adds many global auth/session constants; registers User as the document type for "users" in multiple DB initializers; and adds Store and Proofs resources (Password/Token/Code) plus pluggable Hash/Proof classes. Replaces static Auth APIs and constants with User::*, global constants, and injected Proofs/Store across controllers, platform modules, realtime, workers, migrations, init resources, platform tasks, and tests.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Areas to focus review on:

  • app/controllers/api/account.php — large, security-sensitive cryptography and token/session refactor.
  • app/controllers/api/users.php — createUser signature change and pluggable hasher integration.
  • app/init/resources.php and app/init/constants.php — new resources, many global constants, and document-type registrations.
  • src/Appwrite/Auth/Auth.php and src/Appwrite/Auth/Hash/* removals — ensure no residual references and behavior properly replaced.
  • src/Appwrite/Utopia/Database/Documents/User.php — new role/token/session semantics (memberships, labels, expiry).
  • Widespread replacements of Auth::* with User::*, and DI additions — check consistency and dependency injection wiring across modules.
  • Tests: removed Auth unit tests and added User tests — validate coverage and CI expectations.
  • composer.json — confirm new dependency aligns with new Proofs/Store APIs used.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat: utopia auth' is concise and clearly summarizes the main change—adopting the utopia-php/auth library and refactoring Auth methods.
Description check ✅ Passed The description explains the PR's purpose: refactoring to remove static Auth methods in favor of the utopia-php/auth library. It is related to the changeset despite lacking implementation details and test plan specifics.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-appwrite-auth

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef8d7a4 and 06c8769.

📒 Files selected for processing (2)
  • app/controllers/api/teams.php (20 hunks)
  • src/Appwrite/Utopia/Database/Documents/User.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Appwrite/Utopia/Database/Documents/User.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-11T10:32:38.142Z
Learnt from: abnegate
Repo: appwrite/appwrite PR: 10297
File: app/controllers/api/users.php:649-657
Timestamp: 2025-08-11T10:32:38.142Z
Learning: In Appwrite's utopia-php/database (version 0.77.* and later), the database layer internally handles filter separation for count operations. When using `$dbForProject->count()` within a `skipFilters` closure, there's no need to manually separate filter queries using `Query::groupByType($queries)['filters']` as the database handles this automatically.

Applied to files:

  • app/controllers/api/teams.php
🧬 Code graph analysis (1)
app/controllers/api/teams.php (1)
src/Appwrite/Utopia/Database/Documents/User.php (4)
  • User (13-182)
  • isPrivileged (106-117)
  • getRoles (39-85)
  • isApp (126-133)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: E2E Service Test (Webhooks)
  • GitHub Check: E2E Service Test (Sites)
  • GitHub Check: E2E Service Test (Migrations)
  • GitHub Check: E2E Service Test (GraphQL)
  • GitHub Check: E2E Service Test (Messaging)
  • GitHub Check: E2E Service Test (Tokens)
  • GitHub Check: E2E Service Test (Health)
  • GitHub Check: E2E Service Test (Storage)
  • GitHub Check: E2E Service Test (Realtime)
  • GitHub Check: E2E Service Test (Avatars)
  • GitHub Check: E2E Service Test (Databases/TablesDB)
  • GitHub Check: E2E Service Test (FunctionsSchedule)
  • GitHub Check: E2E Service Test (Console)
  • GitHub Check: E2E Service Test (Account)
  • GitHub Check: E2E Service Test (Functions)
  • GitHub Check: E2E Service Test (Site Screenshots)
  • GitHub Check: E2E General Test
  • GitHub Check: Unit Test
  • GitHub Check: E2E Service Test (Dev Keys)
  • GitHub Check: scan
🔇 Additional comments (7)
app/controllers/api/teams.php (7)

20-20: LGTM: Imports updated correctly.

The new imports for User, Password, Token, and Store align with the migration from legacy Auth static methods to the utopia-php/auth library.

Also applies to: 31-33


93-94: LGTM: Auth checks migrated correctly.

The migration from Auth::isPrivilegedUser() and Auth::isAppUser() to User::isPrivileged(Authorization::getRoles()) and User::isApp(Authorization::getRoles()) follows the correct pattern for the new architecture.


577-600: LGTM: Proof-based cryptography correctly implemented.

The migration to proof-based password and token handling is correct:

  • Password proof generates and hashes random passwords for invited users
  • Token proof generates and hashes membership secrets
  • Hash metadata (name and options) is properly stored for password verification

Also applies to: 640-672


938-939: LGTM: Auth checks migrated consistently.

All instances of privilege and app user checks have been correctly migrated to use User::isPrivileged() and User::isApp() with Authorization::getRoles().

Also applies to: 1029-1030, 1125-1126


1292-1320: LGTM: Store-based cookie encoding correctly implemented.

The migration to Store-based cookie encoding is correct:

  • User ID and secret are properly set as Store properties
  • Encoded value is used for both legacy and new cookies
  • Cookie names are derived from $store->getKey()
  • X-Fallback-Cookies header is conditionally set when domain verification is disabled

1232-1234: LGTM: Token verification correctly implemented.

The token verification using $proofForToken->verify($secret, $membership->getAttribute('secret')) correctly compares the plaintext secret from the request against the stored hash.


1266-1280: Constants are properly defined and available.

Both TOKEN_EXPIRATION_LOGIN_LONG (defined as 31536000 in app/init/constants.php:99) and SESSION_PROVIDER_EMAIL (defined as 'email' in app/init/constants.php:131) are correctly defined and used throughout the codebase. No issues found.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
github-actions bot commented Nov 4, 2025

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
imagemagick 7.1.2.3-r0 CVE-2025-62171 HIGH
imagemagick-c++ 7.1.2.3-r0 CVE-2025-62171 HIGH
imagemagick-dev 7.1.2.3-r0 CVE-2025-62171 HIGH
imagemagick-heic 7.1.2.3-r0 CVE-2025-62171 HIGH
imagemagick-jpeg 7.1.2.3-r0 CVE-2025-62171 HIGH
imagemagick-jxl 7.1.2.3-r0 CVE-2025-62171 HIGH
imagemagick-libs 7.1.2.3-r0 CVE-2025-62171 HIGH
imagemagick-openexr 7.1.2.3-r0 CVE-2025-62171 HIGH
imagemagick-webp 7.1.2.3-r0 CVE-2025-62171 HIGH
libecpg 17.6-r0 CVE-2025-12818 HIGH
libecpg-dev 17.6-r0 CVE-2025-12818 HIGH
libpq 17.6-r0 CVE-2025-12818 HIGH
libpq-dev 17.6-r0 CVE-2025-12818 HIGH
libxml2 2.13.8-r0 CVE-2025-49794 CRITICAL
libxml2 2.13.8-r0 CVE-2025-49796 CRITICAL
libxml2 2.13.8-r0 CVE-2025-49795 HIGH
libxml2 2.13.8-r0 CVE-2025-6021 HIGH
postgresql17-dev 17.6-r0 CVE-2025-12818 HIGH
github.com/containerd/containerd/v2 v2.0.2 CVE-2024-25621 HIGH
golang.org/x/crypto v0.31.0 CVE-2025-22869 HIGH
golang.org/x/oauth2 v0.24.0 CVE-2025-22868 HIGH
stdlib 1.22.10 CVE-2025-47907 HIGH
stdlib 1.22.10 CVE-2025-58183 HIGH
stdlib 1.22.10 CVE-2025-58186 HIGH
stdlib 1.22.10 CVE-2025-58187 HIGH
stdlib 1.22.10 CVE-2025-58188 HIGH

Source Code Scan Results

🎉 No vulnerabilities found!

@github-actions
Copy link
github-actions bot commented Nov 4, 2025

✨ Benchmark results

  • Requests per second: 1,196
  • Requests with 200 status code: 215,264
  • P99 latency: 0.162891596

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,196 1,194
200 215,264 214,962
P99 0.162891596 0.17360661

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/controllers/api/account.php (1)

2805-2809: Restore raw OTP in createPhoneToken response.

Lines 2805-2809 encode the OTP using Store before returning it to the client. However, the /v1/account/sessions/token endpoint expects the raw OTP to verify via ProofsCode->verify(). Encoding the secret here will break session creation for phone authentication since clients will send the encoded value instead of the raw OTP.

The response should contain the plain OTP that can be used to create a session.

-        // Encode secret for clients
-        $encoded = $store
-            ->setProperty('id', $user->getId())
-            ->setProperty('secret', $secret)
-            ->encode();
-        $token->setAttribute('secret', $encoded);
+        $token->setAttribute('secret', $secret);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ef6fa9 and c8e6fcc.

📒 Files selected for processing (2)
  • app/controllers/api/account.php (73 hunks)
  • src/Appwrite/Utopia/Database/Documents/User.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Appwrite/Utopia/Database/Documents/User.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-11T10:32:38.142Z
Learnt from: abnegate
Repo: appwrite/appwrite PR: 10297
File: app/controllers/api/users.php:649-657
Timestamp: 2025-08-11T10:32:38.142Z
Learning: In Appwrite's utopia-php/database (version 0.77.* and later), the database layer internally handles filter separation for count operations. When using `$dbForProject->count()` within a `skipFilters` closure, there's no need to manually separate filter queries using `Query::groupByType($queries)['filters']` as the database handles this automatically.

Applied to files:

  • app/controllers/api/account.php
🧬 Code graph analysis (1)
app/controllers/api/account.php (2)
src/Appwrite/Utopia/Database/Documents/User.php (3)
  • User (13-181)
  • tokenVerify (135-152)
  • sessionVerify (162-180)
src/Appwrite/Auth/Validator/PasswordHistory.php (1)
  • PasswordHistory (12-77)

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/controllers/api/account.php (2)

163-166: Wrong PHP date format token.

'YYYY' renders literally. Use 'Y' for 4‑digit year.

-        'year' => (new \DateTime())->format('YYYY'),
+        'year' => (new \DateTime())->format('Y'),

1818-1822: Fatal: wrong constant casing (TYPE::EMAIL).

Use Type::EMAIL; current code will error.

-                'factors' => [TYPE::EMAIL, 'oauth2'], // include a special oauth2 factor to bypass MFA checks
+                'factors' => [Type::EMAIL, 'oauth2'], // include a special oauth2 factor to bypass MFA checks
♻️ Duplicate comments (5)
src/Appwrite/Utopia/Database/Documents/User.php (2)

93-97: Critical: static method uses $this (fatal at runtime).

Make isAnonymous() an instance method; it currently dereferences $this in static context.

-    public static function isAnonymous(): bool
+    public function isAnonymous(): bool
     {
         return is_null($this->getEmail())
             && is_null($this->getPhone());
     }

166-175: Reject sessions missing expire and simplify the date comparison.

Guard for expire before constructing DateTime, and compare using stored value format, not nested format() calls.

-            if (
-                $session->isSet('secret') &&
-                $session->isSet('provider') &&
-                $proofForToken->verify($secret, $session->getAttribute('secret')) &&
-                DateTime::formatTz(DateTime::format(new \DateTime($session->getAttribute('expire')))) >= DateTime::formatTz(DateTime::now())
-            ) {
+            if (
+                $session->isSet('secret') &&
+                $session->isSet('provider') &&
+                $session->isSet('expire') &&
+                $proofForToken->verify($secret, $session->getAttribute('secret')) &&
+                DateTime::formatTz($session->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
+            ) {
                 return $session->getId();
             }
app/controllers/api/account.php (3)

2805-2811: Return raw OTP in createPhoneToken response; don’t replace with encoded blob.

Clients must send the plain OTP to /v1/account/sessions/token. Overwriting secret with $store->encode() will break flows.

-        // Encode secret for clients
-        $encoded = $store
-            ->setProperty('id', $user->getId())
-            ->setProperty('secret', $secret)
-            ->encode();
-        $token->setAttribute('secret', $encoded);
+        // Keep returning the raw OTP for clients
+        // Optionally expose an additional field if needed:
+        // $token->setAttribute('secretEncoded', $store->setProperty('id', $user->getId())->setProperty('secret', $secret)->encode());

2081-2093: Hash algorithm mismatch for magic URL tokens.

Same issue: custom Sha hash at creation, default proof at verification in createSession. Remove custom hash to align.

-        $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
-        $proofForToken->setHash(new Sha());
-
-        $tokenSecret = $proofForToken->generate();
+        $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
+        $tokenSecret = $proofForToken->generate();

2553-2558: Don’t override DI; inject proofForToken into the route and reuse $createSession.

CreateSession already expects a ProofsToken via DI. Inject it here instead of constructing a custom Sha instance.

-    ->inject('store')
-    ->inject('proofForCode')
-    ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode) use ($createSession) {
-        $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
-        $proofForToken->setHash(new Sha());
-        $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode);
-    });
+    ->inject('store')
+    ->inject('proofForToken')
+    ->inject('proofForCode')
+    ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode) use ($createSession) {
+        $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode);
+    });
🧹 Nitpick comments (2)
src/Appwrite/Utopia/Database/Documents/User.php (1)

177-180: Dead code: duplicate return false.

Remove the extra return; it’s unreachable.

-        return false;
-
-        return false;
+        return false;
app/controllers/api/account.php (1)

5368-5375: Simplify count: no manual filter extraction needed.

You can pass $queries directly to count(); DB handles filter separation.

-        $filterQueries = Query::groupByType($queries)['filters'];
         try {
             $results = $dbForProject->find('identities', $queries);
         } catch (OrderException $e) {
             throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
         }
-        $total = $includeTotal ? $dbForProject->count('identities', $filterQueries, APP_LIMIT_COUNT) : 0;
+        $total = $includeTotal ? $dbForProject->count('identities', $queries, APP_LIMIT_COUNT) : 0;

Based on learnings

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8e6fcc and 47b653f.

📒 Files selected for processing (2)
  • app/controllers/api/account.php (73 hunks)
  • src/Appwrite/Utopia/Database/Documents/User.php (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-11T10:32:38.142Z
Learnt from: abnegate
Repo: appwrite/appwrite PR: 10297
File: app/controllers/api/users.php:649-657
Timestamp: 2025-08-11T10:32:38.142Z
Learning: In Appwrite's utopia-php/database (version 0.77.* and later), the database layer internally handles filter separation for count operations. When using `$dbForProject->count()` within a `skipFilters` closure, there's no need to manually separate filter queries using `Query::groupByType($queries)['filters']` as the database handles this automatically.

Applied to files:

  • app/controllers/api/account.php
🧬 Code graph analysis (1)
app/controllers/api/account.php (2)
src/Appwrite/Utopia/Database/Documents/User.php (3)
  • User (13-181)
  • tokenVerify (135-152)
  • sessionVerify (162-180)
src/Appwrite/Auth/Validator/PasswordHistory.php (1)
  • PasswordHistory (12-77)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: Check if utopia-php/database changed
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: Linter
  • GitHub Check: scan
🔇 Additional comments (1)
app/controllers/api/account.php (1)

1767-1772: Review comment is incorrect—no hash algorithm mismatch exists.

Both the injected proofForToken and the locally instantiated $proofForTokenOAuth2 use identical hash algorithms. The DI resource binds proofForToken with new Sha(), and lines 1767–1768 explicitly set $proofForTokenOAuth2 with new Sha() as well. This pattern is consistent throughout the codebase; other token types (MAGIC_URL, etc.) similarly instantiate local ProofsToken instances with Sha().

The OAuth2 tokens created here are stored in the 'tokens' collection with a different type and verification pattern than sessions, so they do not conflict with session verification logic using the injected proof.

Likely an incorrect or invalid review comment.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
app/controllers/api/account.php (4)

2987-2990: Temporary hard‑coded log filter dates should be removed or guarded

The “Temp fix for logs” that hard‑codes a date window (2025-02-26 / 2025-02-13) in the account logs query will silently filter out most logs and will age badly.

Consider either:

  • Moving this workaround behind a config flag or feature toggle, or
  • Removing it before release and addressing the underlying logs issue at the database or query layer.

962-1017: Add defensive defaults to ProofsPassword::createHash() calls or verify migrations guarantee hash/hashOptions are always present

Verification confirms the inconsistency. The code at lines 962–1017 correctly uses defensive defaults:

$userProofForPassword = ProofsPassword::createHash($profile->getAttribute('hash', $proofForPassword->getHash()->getName()), $profile->getAttribute('hashOptions', $proofForPassword->getHash()->getOptions()));

However, multiple other endpoints lack these defaults:

  • Lines 3100, 3109 (password change): ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'))
  • Line 3213 (email change): Same pattern without defaults
  • Line 3317 (phone change): Same pattern without defaults
  • Line 3678 (recovery flow): ProofsPassword::createHash($profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))

While V15.php migration creates a hashOptions attribute with default ['cost' => 8], it does not guarantee legacy users have these set. Apply the same defensive pattern already used at line 962 to all flagged locations, or document that a migration runs before these endpoints are accessible.


1841-1859: OAuth2 token length mismatch breaks verification in /v1/account/sessions/token

OAuth2 tokens are created with ProofsToken(TOKEN_LENGTH_OAUTH2=64) at line 1804, but verified using the injected proofForToken which is Token() with a default length of 256 (app/init/resources.php:417). Since token verification depends on both the hash algorithm and token length matching, OAuth2 tokens created with 64-character length cannot be verified with a 256-character length verifier, causing the hash comparison to fail.

Fix: Either:

  1. Create the DI proofForToken with explicit Token(TOKEN_LENGTH_OAUTH2) and adjust the comment to clarify it's OAuth2-specific, or
  2. Use the injected proofForToken when creating OAuth2 tokens at line 1804 instead of a local instance.

2052-2141: Based on my verification, the review comment is substantiated. I found a clear mismatch:

Confirmed findings:

  1. Magic URL token creation (lines 2129–2130, 2614–2615) uses:

    $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);  // length=64
    $proofForToken->setHash(new Sha());
  2. DI binding (app/init/resources.php:417) provides:

    $token = new Token();  // NO length parameter
    $token->setHash(new Sha());
  3. Token verification path in /v1/account/sessions/token (lines 1220–1258) injects the DI proofForToken without override, passing it to $createSession which calls tokenVerify() at line 212.

  4. Contrast: updateMagicURLSession (lines 2614–2616) correctly creates a local ProofsToken(TOKEN_LENGTH_MAGIC_URL) and explicitly passes it to $createSession, overriding the DI binding.

The issue: Magic URL tokens are created with explicit TOKEN_LENGTH_MAGIC_URL (64), but the generic /v1/account/sessions/token route verifies them using the DI Token() without length. If the Utopia\Auth\Proofs\Token class uses the length parameter in its generation/verification algorithm (which the code pattern strongly suggests), tokens created by createMagicURLToken will fail verification when consumed via /v1/account/sessions/token.

The reviewer's recommendation to align both creation and verification paths is correct.


app/controllers/api/users.php (2)

1326-1385: Empty-password branch in updatePassword falls through and stores a hashed empty password

In the updatePassword action, the empty‑password branch (Lines 1348‑1356) updates the user with password = '' and returns a response, but does not return from the closure. Execution continues into:

  • The passwordValidator hook (Line 1358),
  • New Argon2 hasher (Lines 1360‑1363),
  • Password history logic (Lines 1365‑1377),
  • Final setAttribute('password', $newPassword) (Lines 1379‑1382).

Because $password is an empty string, this effectively:

  • Hashes the empty string with Argon2 (or whatever default), and
  • Persists that hash as the user’s password.

This likely breaks the intended “clear/remove password” semantics and can leave the account authenticatable with an empty password, which is a serious security risk.

You probably want the empty‑password path to short‑circuit before any hashing/history logic. For example:

        if (\strlen($password) === 0) {
            $user
-                ->setAttribute('password', '')
-                ->setAttribute('passwordUpdate', DateTime::now());
-
-            $user = $dbForProject->updateDocument('users', $user->getId(), $user);
-            $queueForEvents->setParam('userId', $user->getId());
-            $response->dynamic($user, Response::MODEL_USER);
-        }
+                ->setAttribute('password', '')
+                ->setAttribute('passwordHistory', [])
+                ->setAttribute('passwordUpdate', DateTime::now());
+
+            $user = $dbForProject->updateDocument('users', $user->getId(), $user);
+            $queueForEvents->setParam('userId', $user->getId());
+            $response->dynamic($user, Response::MODEL_USER);
+            return;
+        }

You may also want to consider whether hash / hashOptions should be reset when an account has no password at all, depending on how downstream verification interprets an empty password.


1365-1377: Based on my investigation of the codebase, I can now provide a definitive assessment:

The review comment's concern is valid and well-founded. Here's what I found:

  1. Password history stores only hashes, no algorithm metadata: The passwordHistory array contains simple hashed password strings without tracking which algorithm was used to hash each entry (confirmed by getAttribute('passwordHistory', [])).

  2. Hash algorithm changes on each password update: Line 1383 in the reviewed code clearly shows that the hash field is updated to the current hasher's name: ->setAttribute('hash', $hasher->getName()). This means if a user's password algorithm ever changes (e.g., bcrypt → Argon2), subsequent password history checks will use only the new algorithm.

  3. Single Hash object verifies all entries: The PasswordHistory validator uses a single Hash object created from the user's current hash and hashOptions to verify all history entries via $this->hash->verify().

  4. No cross-algorithm verification: Different hash algorithms (bcrypt, Argon2, scrypt, etc.) are algorithm-specific and cannot verify each other's outputs. The code has no fallback mechanism to handle this.

  5. No test coverage for algorithm migration: I found no tests covering the scenario where password history spans different hash algorithms.

This pattern repeats identically across all three password update locations (users.php:1370, account.php:3112, account.php:3683).

Rewritten review comment:


Password history enforcement when hash algorithm changes

PasswordHistory is now constructed with a single Hash instance created from the user's current hash and hashOptions:

$hash = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'));
$history = $user->getAttribute('passwordHistory', []);
if ($historyLimit > 0) {
    $validator = new PasswordHistory($history, $hash);
    ...
}

This works well as long as all entries in passwordHistory were generated with the same algorithm/options as the current hash. However, if a user's hash algorithm is migrated (e.g., bcrypt → Argon2), older history entries become unverifiable with the new algorithm's Hash object, and reuse of very old passwords may slip through the check.

The passwordHistory array stores only hashes without per-entry algorithm metadata, so there's no way to retroactively verify entries hashed with different algorithms.

If your policy expects history to span across algorithm migrations, consider:

  • Storing per-entry metadata (hash name/options), or
  • Implementing a multi-algorithm verifier that attempts verification with legacy algorithms.

Otherwise, document that password history enforcement applies only to passwords hashed with the current algorithm.

♻️ Duplicate comments (3)
app/controllers/api/teams.php (2)

482-484: Fix unused array_filter result.

The array_filter result is not assigned back, so invalid roles are not actually excluded. This is a known issue already flagged in previous reviews.

Apply this diff:

         if ($project->getId() === 'console') {
             $roles = array_keys(Config::getParam('roles', []));
-            $roles = array_filter($roles, function ($role) {
+            $roles = array_values(array_filter($roles, function ($role) {
                 return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]);
-            });
+            }));
             return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
         }

1095-1097: Fix unused array_filter result (duplicate issue).

This is the same bug as in lines 482-483. The array_filter call filters the roles array but doesn't assign the result back, so invalid roles are not actually excluded.

Apply this diff:

         if ($project->getId() === 'console') {
             $roles = array_keys(Config::getParam('roles', []));
-            array_filter($roles, function ($role) {
+            $roles = array_values(array_filter($roles, function ($role) {
                 return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]);
-            });
+            }));
             return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
         }
app/controllers/api/account.php (1)

2701-2774: Restore raw OTP in createPhoneToken response; avoid Store‑encoded secret in secret

In /v1/account/tokens/phone you:

  • Generate or reuse an OTP in $secret (Lines 2776–2787).
  • Hash it into the token document with ProofsCode->hash($secret) (Line 2795) – good.
  • Use $secret in the SMS template (Lines 2821–2824) – good.
  • Then:
    • Set $token->setAttribute('secret', $secret);
    • Encode the secret with Store and overwrite it on the token: $token->setAttribute('secret', $encoded); (Lines 2869–2875).

This means the HTTP response’s secret is now the Store‑encoded blob, not the raw OTP. Any client or test that previously reused the secret from the createPhoneToken response when calling /v1/account/sessions/token will now send the wrong value; verification expects the raw 6‑digit code to be checked against the hashed DB value with ProofsCode.

I strongly recommend keeping secret in the response as the raw OTP and, if you need to surface the encoded form, exposing it under a different attribute, e.g.:

- $encoded = $store
-     ->setProperty('id', $user->getId())
-     ->setProperty('secret', $secret)
-     ->encode();
- $token->setAttribute('secret', $encoded);
+ $encoded = $store
+     ->setProperty('id', $user->getId())
+     ->setProperty('secret', $secret)
+     ->encode();
+ $token->setAttribute('secret', $secret);
+ $token->setAttribute('secretEncoded', $encoded); // optional new field

This preserves backward compatibility for existing clients and tests that rely on the raw secret.

Also applies to: 2787-2796, 2812-2868, 2869-2875

🧹 Nitpick comments (7)
app/config/collections/common.php (1)

176-176: Consider caching the Argon2 instance to improve performance.

Creating new Argon2() instances twice within the configuration array causes unnecessary object instantiation each time this file is loaded during database initialization.

Consider instantiating once before the array return:

 <?php
 
 use Utopia\Auth\Hashes\Argon2;
 use Utopia\Database\Database;
 use Utopia\Database\Helpers\ID;
 
+$argon2 = new Argon2();
+
 return [
     'cache' => [

Then use the cached instance:

             [
                 '$id' => 'hash', // Hashing algorithm used to hash the password
                 'type' => Database::VAR_STRING,
                 'format' => '',
                 'size' => 256,
                 'signed' => true,
                 'required' => false,
-                'default' => (new Argon2())->getName(),
+                'default' => $argon2->getName(),
                 'array' => false,
                 'filters' => [],
             ],
             [
                 '$id' => ID::custom('hashOptions'), // Configuration of hashing algorithm
                 'type' => Database::VAR_STRING,
                 'format' => '',
                 'size' => 65535,
                 'signed' => true,
                 'required' => false,
-                'default' => (new Argon2())->getOptions(),
+                'default' => $argon2->getOptions(),
                 'array' => false,
                 'filters' => ['json'],
             ],

Also applies to: 187-187

app/init/constants.php (1)

96-151: Centralizing auth/token constants looks good

The added TOKEN_* and MFA constants are coherent and match existing durations; this should simplify future changes. You may also want to replace hard-coded values like 31536000 elsewhere (e.g., auth duration defaults/ranges) with these constants to avoid drift.

app/controllers/api/projects.php (1)

115-123: Using TOKEN_EXPIRATION_LOGIN_LONG here is appropriate

Switching the default auths['duration'] to TOKEN_EXPIRATION_LOGIN_LONG keeps the 1‑year semantics while decoupling from the old Auth class. Consider also reusing this constant in the /auth/duration route’s default and Range upper bound instead of the hard-coded 31536000 to keep a single source of truth.

app/controllers/api/storage.php (1)

438-445: Consistent use of User::isApp / User::isPrivileged for bucket state bypass

All storage routes now derive:

  • $isAPIKey = User::isApp(Authorization::getRoles());
  • $isPrivilegedUser = User::isPrivileged(Authorization::getRoles());

and use these to allow API keys and privileged users to operate on disabled buckets, while regular users see STORAGE_BUCKET_NOT_FOUND. This mirrors the previous Auth‑based behavior and keeps the “bucket not found” masking for disabled/unauthorized cases, which is desirable from an information‑disclosure standpoint. Based on learnings, this shared error code pattern is intentional.

You could consider extracting this “canBypassBucketEnabled” check into a small helper to reduce repetition, but it’s BE9D not urgent.

Also applies to: 800-807, 901-908, 982-989, 1179-1186, 1339-1346, 1514-1521, 1669-1676, 1783-1790

app/controllers/api/teams.php (1)

576-583: Remove duplicate userId assignment.

The $userId variable is assigned twice on lines 576 and 583, which is redundant.

Apply this diff to remove the first assignment:

             try {
-                $userId = ID::unique();
                 $hash = $proofForPassword->hash($proofForPassword->generate());
                 $emailCanonical = new Email($email);
             } catch (Throwable) {
                 $emailCanonical = null;
             }
 
             $userId = ID::unique();
app/controllers/api/account.php (2)

4856-4876: MFA challenges: consider hashing the code or treating challenges as sensitive

The MFA challenge endpoint now:

  • Uses ProofsCode->generate() for the code (Line 4862) but stores it in plaintext in the challenges collection.
  • Generates a random token via ProofsToken->generate() but does not currently hash it (Line 4867).
  • Sends the raw code via email/SMS and validates against the stored plaintext in the various Challenge\*::challenge methods.

This is functionally correct but leaves both challenge code and token as plaintext in DB. Depending on your threat model, you might want to:

  • Hash the code using ProofsCode->hash() and adjust challenge verifiers, and/or
  • Treat challenge documents as sensitive in events/logs and never expose their raw code.

If MFA challenges are considered security‑sensitive in your environment (similar to OTPs), consider moving to hashed codes here to mirror your email/phone OTP handling.

Also applies to: 4879-5065


5408-5450: Identities listing: existing filter splitting for count can now be simplified

The identities listing endpoint still does:

$filterQueries = Query::groupByType($queries)['filters'];
$results = $dbForProject->find('identities', $queries);
$total = $includeTotal ? $dbForProject->count('identities', $filterQueries, APP_LIMIT_COUNT) : 0;

Given newer utopia-php/database versions handle filter separation internally for count(), you can simplify this to pass the same $queries array to both find and count.

Based on learnings, you can drop groupByType here and just call count('identities', $queries, APP_LIMIT_COUNT) once you’re on the appropriate DB version.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47b653f and 94ba8ee.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • app/config/collections/common.php (3 hunks)
  • app/controllers/api/account.php (73 hunks)
  • app/controllers/api/projects.php (1 hunks)
  • app/controllers/api/storage.php (13 hunks)
  • app/controllers/api/teams.php (20 hunks)
  • app/controllers/api/users.php (19 hunks)
  • app/controllers/general.php (4 hunks)
  • app/controllers/shared/api.php (16 hunks)
  • app/init/constants.php (2 hunks)
  • composer.json (1 hunks)
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php (2 hunks)
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php (2 hunks)
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php (2 hunks)
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php (2 hunks)
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php (2 hunks)
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php
  • composer.json
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-08-11T10:32:38.142Z
Learnt from: abnegate
Repo: appwrite/appwrite PR: 10297
File: app/controllers/api/users.php:649-657
Timestamp: 2025-08-11T10:32:38.142Z
Learning: In Appwrite's utopia-php/database (version 0.77.* and later), the database layer internally handles filter separation for count operations. When using `$dbForProject->count()` within a `skipFilters` closure, there's no need to manually separate filter queries using `Query::groupByType($queries)['filters']` as the database handles this automatically.

Applied to files:

  • app/controllers/api/account.php
  • app/controllers/api/teams.php
📚 Learning: 2025-06-19T09:20:03.312Z
Learnt from: ItzNotABug
Repo: appwrite/appwrite PR: 9693
File: src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Update.php:57-59
Timestamp: 2025-06-19T09:20:03.312Z
Learning: In table-related endpoints (such as `src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Update.php`), parameter descriptions should use "table" and "row" terminology instead of "collection" and "document" for clarity and consistency.

Applied to files:

  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
📚 Learning: 2025-10-02T10:16:48.075Z
Learnt from: abnegate
Repo: appwrite/appwrite PR: 10023
File: src/Appwrite/Databases/TransactionState.php:174-219
Timestamp: 2025-10-02T10:16:48.075Z
Learning: In `src/Appwrite/Databases/TransactionState.php`, the `listDocuments` method intentionally applies queries only to the initial database fetch of committed documents. Transaction state changes (creates, updates, deletes) are then overlaid on top of the filtered committed set without re-evaluating the queries against the final merged result. This is the intended behavior for transaction-aware document listing.

Applied to files:

  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
📚 Learning: 2025-08-29T15:29:23.250Z
Learnt from: ChiragAgg5k
Repo: appwrite/appwrite PR: 10408
File: app/controllers/general.php:0-0
Timestamp: 2025-08-29T15:29:23.250Z
Learning: In the Appwrite codebase, when SDK methods are marked as deprecated (isDeprecated() returns true), the getDeprecated() method is guaranteed to return valid deprecation metadata objects, not null. The deprecation checking logic first verifies isDeprecated() before accessing getDeprecated(), providing implicit null-safety.

Applied to files:

  • src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
📚 Learning: 2025-05-13T12:21:24.622Z
Learnt from: ItzNotABug
Repo: appwrite/appwrite PR: 9750
File: src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php:75-75
Timestamp: 2025-05-13T12:21:24.622Z
Learning: In the Appwrite platform, when updating a token's expiry date, setting the `expire` attribute to `null` is intentional and means the token will never expire (unlimited lifetime). This is by design.

Applied to files:

  • app/init/constants.php
📚 Learning: 2025-04-17T08:07:54.565Z
Learnt from: ItzNotABug
Repo: appwrite/appwrite PR: 9661
File: app/controllers/api/migrations.php:332-337
Timestamp: 2025-04-17T08:07:54.565Z
Learning: In the Appwrite codebase, error handling intentionally uses the same error codes for different failure conditions (like STORAGE_BUCKET_NOT_FOUND for both non-existent buckets and authorization failures) as a security measure to prevent information disclosure about what specifically went wrong.

Applied to files:

  • app/controllers/api/storage.php
🧬 Code graph analysis (7)
app/controllers/general.php (1)
src/Appwrite/Utopia/Database/Documents/User.php (2)
  • isPrivileged (106-117)
  • getRoles (39-85)
app/controllers/api/account.php (3)
src/Appwrite/Utopia/Database/Documents/User.php (3)
  • User (13-181)
  • tokenVerify (135-152)
  • sessionVerify (162-180)
src/Appwrite/Utopia/Request.php (2)
  • Request (13-236)
  • getUserAgent (209-222)
src/Appwrite/Auth/Validator/PasswordHistory.php (1)
  • PasswordHistory (12-77)
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php (1)
src/Appwrite/Utopia/Database/Documents/User.php (3)
  • isApp (126-133)
  • getRoles (39-85)
  • isPrivileged (106-117)
app/controllers/shared/api.php (2)
src/Appwrite/Auth/Key.php (2)
  • Key (14-201)
  • getRole (42-45)
src/Appwrite/Utopia/Database/Documents/User.php (3)
  • getRoles (39-85)
  • isPrivileged (106-117)
  • isApp (126-133)
app/controllers/api/users.php (1)
src/Appwrite/Auth/Validator/PasswordHistory.php (1)
  • PasswordHistory (12-77)
app/controllers/api/teams.php (2)
src/Appwrite/Utopia/Database/Documents/User.php (3)
  • isPrivileged (106-117)
  • getRoles (39-85)
  • isApp (126-133)
src/Appwrite/Utopia/Request.php (1)
  • Request (13-236)
app/controllers/api/storage.php (1)
src/Appwrite/Utopia/Database/Documents/User.php (2)
  • isApp (126-133)
  • isPrivileged (106-117)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: Check if utopia-php/database changed
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: scan
🔇 Additional comments (33)
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php (2)

14-14: LGTM!

The import correctly supports the refactored authorization checks using the new User class.


103-104: LGTM!

The refactored authorization checks correctly replace the legacy Auth static methods with the new User class methods. The functionality is preserved, and this change aligns with the project-wide migration to the User-centric authorization model.

app/config/collections/common.php (2)

3-3: LGTM: Import updated to use utopia-php/auth library.

The import change aligns with the PR's objective to adopt the utopia-php/auth library.


176-187: No action required — backward compatibility is properly implemented.

The schema design supports multiple hashing algorithms coexisting through per-password metadata storage. When verifying a password, the system retrieves the stored hash and hashOptions from the user record and uses those specific values for verification. This means existing passwords retain their original algorithm even as new passwords default to Argon2.

  • Existing passwords are unaffected; each stores its own algorithm metadata
  • New passwords use Argon2 as the default
  • Verification automatically uses the correct algorithm per-password via ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'))
  • Re-hashing occurs transparently on next login (confirmed in test suite)
  • No explicit migration script is needed
app/init/constants.php (1)

357-358: Preview cookie name constant is a solid addition

Defining COOKIE_NAME_PREVIEW here removes the hard-coded cookie name from controllers and keeps preview auth cookie usage consistent across the codebase.

app/controllers/general.php (3)

19-19: DBUser import aligns with new authorization model

Importing Appwrite\Utopia\Database\Documents\User as DBUser for privilege checks is consistent with the new User‑centric auth flow and keeps the dependency out of the legacy Auth namespace.


226-282: Preview auth cookie now consistently uses COOKIE_NAME_PREVIEW

Reading the preview JWT from $request->getCookie(COOKIE_NAME_PREVIEW, '') and setting it via addCookie(COOKIE_NAME_PREVIEW, …, '/', $host, …) keeps the cookie name centralized and avoids magic strings. Domain and secure/HttpOnly flags also look correct for the existing flow.

Also applies to: 1619-1623


1263-1279: Privilege check migration in error metrics

Replacing the old Auth privilege helper with DBUser::isPrivileged(Authorization::getRoles()) keeps the intent of excluding owner/developer/admin traffic from non‑publishable error usage stats while aligning with the new User document implementation.

Please confirm your tests cover both privileged and non‑privileged callers here so that usage metrics aren’t unintentionally suppressed or overcounted after the migration.

app/controllers/api/storage.php (3)

15-15: User document import is correct

Importing Appwrite\Utopia\Database\Documents\User here matches the new storage auth model and enables the static isApp / isPrivileged checks without pulling in the deprecated Auth class.


471-490: Permission-edit guards correctly switched to User helpers

The checks:

$roles = Authorization::getRoles();
if (!User::isApp($roles) && !User::isPrivileged($roles)) {
    // validate each permission against Authorization::isRole(...)
}

preserve the previous “only API keys and privileged users can assign arbitrary roles” rule while routing the logic through the new User helpers. This keeps file permission management semantics intact for end‑users.

Also applies to: 1699-1718


1129-1136: Preview transform timestamp guard still excludes privileged callers

if (!User::isPrivileged(Authorization::getRoles())) before updating transformedAt maintains the idea that console/admin‑side accesses shouldn’t affect the file’s transformation timestamp, now expressed via the new User privilege helper.

app/controllers/api/teams.php (13)

20-20: LGTM!

The new imports align with the PR's goal to adopt the utopia-php/auth library, replacing static Auth methods with User document helpers and proof-based abstractions.

Also applies to: 31-33


93-94: LGTM!

The migration to User::isPrivileged() and User::isApp() is consistent with the refactor objectives and correctly checks authorization roles.


502-504: LGTM!

The injection of Password and Token proof objects enables the replacement of static Auth methods with instance-based proof abstractions, consistent with the PR's refactoring goals.


505-506: LGTM!

The authorization checks correctly use the new User static methods.


598-600: LGTM!

The password hashing correctly uses the Password proof object and stores the hash algorithm details for future verification.


640-640: LGTM!

Token generation and hashing correctly use the Token proof object, replacing static Auth methods.

Also applies to: 660-660, 672-672


938-940: LGTM!

Authorization checks are correctly migrated to use User static methods.


1029-1030: LGTM!

Authorization checks are correctly migrated to use User static methods.


1125-1126: LGTM!

Authorization checks are correctly migrated to use User static methods.


1211-1213: LGTM!

The injection of Store and Token objects enables session encoding and token verification, consistent with the refactoring pattern.


1232-1232: LGTM!

Token verification correctly uses the Token proof object to verify the plain secret against the stored hash.


1292-1298: LGTM!

The session encoding using Store correctly stores user ID and secret, and uses the store key for cookie naming. This pattern is consistent with the refactoring approach across other endpoints.

Also applies to: 1303-1303, 1312-1312


1266-1266: Constants are properly defined.

Verification confirms both TOKEN_EXPIRATION_LOGIN_LONG (defined in app/init/constants.php:99) and SESSION_PROVIDER_EMAIL (defined in app/init/constants.php:131) are correctly defined and used in the code at lines 1266 and 1278. No issues found.

app/controllers/api/account.php (7)

33-47: Auth/document imports and password hashing refactor look consistent

The introduction of User document, Store, and Proofs* imports plus the switch to ProofsPassword for hashing new accounts (including storing hash and hashOptions) is consistent with the utopia-auth design and the rest of the file. No issues spotted here.

Also applies to: 415-442


581-624: Store‑based current session resolution is coherent across endpoints

All updated endpoints that need the “current session” (/sessions list/get/delete/update, JWT creation, status update, push target create) now:

  • Extract the secret via Store::getProperty('secret', '')
  • Call User::sessionVerify($secret, $proofForToken) to resolve the session id
  • Use that id consistently to mark current, clear cookies, or gate JWT/targets

This is a clean, centralized mechanism and appears internally consistent with how sessions are created (DB stores a hash, cookies carry the Store‑encoded raw secret).

Please ensure the store resource is actually decoding the incoming cookie/header before these controllers run (i.e. Store has been decode()ed from the request once per request) so getProperty('secret') is populated.

Also applies to: 626-699, 701-747, 749-835, 836-919, 2881-2924, 3417-3440, 5203-5233


2319-2425: Email OTP tokens correctly use ProofsCode and return raw code

For /v1/account/tokens/email:

  • OTP is generated with ProofsCode->generate() and stored hashed with ProofsCode->hash() in the DB (Lines 2416–2425).
  • The email body uses the raw $tokenSecret.
  • The response and event payloads re‑attach the raw secret but mark it as sensitive (Lines 2558–2562), and clients still receive the actual OTP.

This is consistent with the /v1/account/sessions/token expectation that callers submit the raw OTP for verification using ProofsCode. Looks good.

Also applies to: 2476-2535


3479-3511: Recovery tokens correctly use ProofsToken; ensure verification path uses same proof

Password recovery creation now:

  • Uses injected ProofsToken to generate the secret and hash it into the recovery token with type TOKEN_TYPE_RECOVERY (Lines 3502–3511).
  • Sends the raw secret to the user in the URL (Lines 3527–3529).

In updateRecovery you verify using $profile->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken) (Line 3668), which is consistent.

Please just confirm the DI proofForToken used here is the same as the one bound for all other recovery token consumers (e.g. any background workers) so there’s no hash mismatch.

Also applies to: 3502-3511, 3527-3529


3774-3799: Email/phone verification tokens correctly wired to proofs

For email and phone verifications:

  • Creation routes generate secrets with injected ProofsToken (email) and ProofsCode (phone), hash them into tokens with appropriate TOKEN_TYPE_* and store the raw secrets only in the email/SMS content (Lines 3790–3799, 4087–4096).
  • Confirmation routes call tokenVerify on the User with the matching TOKEN_TYPE_* and the same proof type (Lines 3996–3997, 4215–4216).

This is a clean and consistent flow from creation to verification.

Also applies to: 3987-4022, 4061-4096, 4206-4216


2881-2924: JWT creation now bound to Store‑backed session; behavior change is intentional

/v1/account/jwts now:

  • Resolves a session id via User::sessionVerify($store->getProperty('secret', ''), $proofForToken).
  • Refuses to issue a JWT if that resolution fails.
  • Embeds both userId and sessionId in the JWT payload.

This is a stricter behavior (requires a valid session cookie/Store entry) but is coherent with the new auth model. Just be aware this tightens how server‑side JWT creation works compared to any previous behavior that might have trust the user resource alone.

Please confirm your SDKs and server‑side flows that call createJWT expect to supply a valid session cookie; otherwise they may see more USER_SESSION_NOT_FOUND errors after this change.


409-414: Password validators & hooks are still triggered at the right points

Across account creation, login, and password updates, you consistently:

  • Run hooks->trigger('passwordValidator', ...) with the appropriate isNew flag (true on create/change, false on reuse checks).
  • Only persist new hashes after validations (personal data, history, custom hooks) pass.

This keeps the validator hook contract intact while switching hash providers.

Also applies to: 982-983, 3128-3136

app/controllers/api/users.php (1)

2208-2260: Session and token secret handling with Store/Token proofs

The new session and token flows look solid from a security perspective:

  • Sessions:
    • Secrets are generated via injected Token $proofForToken (Line 2216).
    • The DB document stores only secret as a one‑way hash ($proofForToken->hash($secret), Line 2229).
    • The response replaces secret with an encoded payload from Store containing {id, secret} (Lines 2253‑2256), so the client never sees the DB hash.
  • Tokens:
    • Short‑lived user tokens use a local Token proof with a Sha hash (Lines 2306‑2308).
    • The DB again stores only a hashed secret (Line 2316), while the plain secret is only attached to the response (Line 2325).

From the controller side, this is a good pattern: DB gets a hash, client gets an encoded/opaque value. Please just ensure that:

  • The DI wiring for store and proofForToken for sessions uses the same configuration as the verification side.
  • Any verification path for /v1/users/:userId/tokens re‑constructs Token with matching length/hash, otherwise these codes won’t validate.

If you’d like, this could later be refactored so token proofs are also injected (mirroring sessions) to avoid hard‑coding Sha in the controller.

Also applies to: 2293-2335

app/controllers/shared/api.php (1)

234-402: User-based auth pipeline and role/scope handling

The broader refactor in this file—from static Auth helpers to the new User document and User::isPrivileged / User::isApp—looks consistent:

  • App::init(['api']) now bases Authorization roles on $user->getRoles() (Line 399), which is aligned with src/Appwrite/Utopia/Database/Documents/User::getRoles().
  • Role → scope mapping uses Config::getParam('roles', [])[$role]['scopes'] with $role set to Role::guests()->toString() / Role::users()->toString() or $apiKey->getRole(), which matches the new role constants.
  • Service and API enablement checks correctly exempt privileged/app roles using User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()) (Lines 440‑444 and 510‑515).
  • Abuse and metrics logic now also use User::isPrivileged / User::isApp consistently to bypass rate limits and usage counting for admins and API keys (Lines 543‑547, 644‑647, 666‑673, 701‑707, 951‑963).

From the controller’s perspective this is a clean consolidation around the new User model. Just ensure that:

  • The injected user resource is always an instance of Appwrite\Utopia\Database\Documents\User for the init handler that type‑hints User $user (Line 234).
  • The roles configuration keys match the role:* strings produced by Role helpers and used by User::ROLE_*.

Otherwise, this refactor looks good and keeps the privilege checks centralised.

Also applies to: 440-447, 510-515, 543-547, 644-647, 666-673, 701-707, 951-963

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/controllers/api/users.php (1)

1348-1385: Empty-password update path now conflicts with new Argon2/history logic

For PATCH /v1/users/:userId/password, the empty-password branch:

  • Updates the user (password set to empty string) and calls $response->dynamic($user, ...) (Lines 1348‑1355),
  • But then falls through into the new logic: triggers passwordValidator, hashes the (empty) password with Argon2, runs password history, updates hash/hashOptions, and sends another response at the end of the action (Lines 1358‑1385).

This likely breaks the intended “clear password and exit early” behavior, and can cause double DB updates and inconsistent responses/events.

Consider short‑circuiting on the empty‑password case:

         if (\strlen($password) === 0) {
             $user
                 ->setAttribute('password', '')
                 ->setAttribute('passwordUpdate', DateTime::now());

             $user = $dbForProject->updateDocument('users', $user->getId(), $user);
             $queueForEvents->setParam('userId', $user->getId());
-            $response->dynamic($user, Response::MODEL_USER);
-        }
-
-        $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
+            return $response->dynamic($user, Response::MODEL_USER);
+        }
+
+        $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);

This keeps the new Argon2/password‑history path for non‑empty passwords only and preserves the special handling for “remove password”.

♻️ Duplicate comments (1)
app/controllers/api/users.php (1)

117-127: passwordValidator hook becomes unreachable for plaintext create user flow

Because $hash is overwritten with $defaultHash->getHash() when handling plaintext passwords (Lines 119‑124), the later check if ($hash instanceof Plaintext) (Line 162) is always false, so passwordValidator never fires for /v1/users anymore. This mirrors the concern already raised in the earlier review and still appears unresolved.

Tracking the original “was plaintext” state separately (e.g. $isPlaintext = $hash instanceof Plaintext; before mutation, then using $isPlaintext for both the hashing decision and the hook guard) would restore previous hook behavior while keeping the new hash metadata.

Also applies to: 142-147, 162-164, 252-255

🧹 Nitpick comments (1)
app/controllers/api/users.php (1)

1360-1377: Password history may not cover hashes from previous algorithms after migration

The new history check builds a single Hash instance from the current user hash metadata:

  • $hash = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions')); (Line 1365)
  • new PasswordHistory($history, $hash) (Line 1370), which uses $hash->verify($value, $storedHash) for every entry.

Once a user’s hash / hashOptions are updated to Argon2 (Lines 1383‑1384), any earlier passwordHistory entries created under different algorithms (e.g. Bcrypt, MD5, Scrypt) may no longer be verifiable if Hash::verify expects the stored hash to match the configured algorithm.

If the intent is “cannot reuse any of the last N passwords regardless of algorithm”, you may need either:

  • Per‑entry algorithm/options metadata, or
  • A Hash/ProofsPassword implementation that can infer the algorithm from the stored hash string itself.

Please confirm that ProofsPassword::createHash() and the chosen Hash implementation guarantee correct verification across existing history entries after algorithm migration; otherwise, history enforcement will silently weaken for older passwords.

Also applies to: 1380-1384

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94ba8ee and 149fee5.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • app/controllers/api/users.php (19 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/api/users.php (1)
src/Appwrite/Auth/Validator/PasswordHistory.php (1)
  • PasswordHistory (12-77)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: E2E Service Test (Users)
  • GitHub Check: E2E Service Test (Webhooks)
  • GitHub Check: E2E Service Test (Realtime)
  • GitHub Check: E2E Service Test (VCS)
  • GitHub Check: E2E Service Test (Sites)
  • GitHub Check: E2E Service Test (GraphQL)
  • GitHub Check: E2E Service Test (Proxy)
  • GitHub Check: E2E Service Test (Projects)
  • GitHub Check: E2E Service Test (Locale)
  • GitHub Check: E2E Service Test (Console)
  • GitHub Check: E2E Service Test (FunctionsSchedule)
  • GitHub Check: E2E Service Test (Avatars)
  • GitHub Check: E2E Service Test (Functions)
  • GitHub Check: E2E Service Test (Databases/TablesDB)
  • GitHub Check: E2E Service Test (Account)
  • GitHub Check: E2E Service Test (Site Screenshots)
  • GitHub Check: Unit Test
  • GitHub Check: E2E Service Test (Dev Keys)
  • GitHub Check: E2E General Test
  • GitHub Check: scan
🔇 Additional comments (1)
app/controllers/api/users.php (1)

2208-2217: Session/token secret handling looks sound, but confirm Store/Token semantics and new expiry constraints

For sessions:

  • Token $proofForToken now generates a secret, hashes it for storage, and only later replaces secret in the in‑memory Document with the Store::encode() payload (Lines 2216‑2230, 2253‑2260). That keeps the DB column hashed while returning an encoded blob to clients/events, which is a good pattern.

For generic tokens:

  • Token + Sha are used to generate/hash the secret, with expire constrained to [60, TOKEN_EXPIRATION_LOGIN_LONG] (Lines 2294‑2295, 2306‑2317). The hashed secret stays in DB, and the plaintext secret is only put back on the response document (Line 2325).

Two things worth verifying:

  1. That Store::encode() uses the intended project/instance secret material and TTL, and that decoding on the consumer side matches this new payload shape.
  2. That tightening the expire range to a minimum of 60 seconds is acceptable for existing clients and matches the documented API behavior for /v1/users/:userId/tokens.

Also applies to: 2220-2230, 2253-2260, 2294-2295, 2306-2317, 2325-2335

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/controllers/shared/api.php (1)

859-866: Type mismatch: Guest user created as Document instead of User.

The guest user is instantiated as new Document([...]) but $parseLabel (called at line 912) requires a User parameter. This will cause a TypeError when the guest user is passed to $parseLabel.

Apply this diff to fix the type mismatch:

-            $user = new Document([
+            $user = new User([
                 '$id' => '',
                 'status' => true,
                 'type' => ACTIVITY_TYPE_GUEST,
                 'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
                 'password' => '',
                 'name' => 'Guest',
             ]);

Based on past review comments, this issue has been previously identified and remains unresolved.

🧹 Nitpick comments (1)
app/controllers/shared/api.php (1)

291-291: TODO: Future enhancement for identity-based scopes.

The comment indicates a planned refactor to retrieve scopes from the identity rather than user role configuration. Consider tracking this in an issue if not already done.

Do you want me to open a new issue to track this planned enhancement?

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d539186 and f6b04ae.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (1) 4D23
  • app/controllers/shared/api.php (17 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: Check if utopia-php/database changed
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: scan
🔇 Additional comments (9)
app/controllers/shared/api.php (9)

18-18: LGTM! User class import correctly added.

The addition of the User import supports the type refinement throughout this file.


36-73: Signature tightened to require User instead of Document.

The $parseLabel closure now requires a User parameter. Ensure all call sites (lines 839, 912) pass a User instance. The past review identified that the shutdown handler at lines 859-866 creates a guest Document instead of a User, which will cause a TypeError when passed to this function.


237-280: Comprehensive authentication flow documentation added.

The detailed step-by-step authentication flow documentation is excellent and will help maintainers understand the complex logic.


317-328: Correctly creates User instance for app role.

The app user is properly instantiated as new User([...]) with the appropriate ACTIVITY_TYPE_APP type, which is consistent with the new type system.


399-401: User method correctly replaces static Auth call.

The refactor from Auth::getRoles($user) to $user->getRoles() is part of the migration to instance-based authentication methods.


443-443: User static methods replace Auth checks.

The use of User::isPrivileged() and User::isApp() is consistent with the new authentication model.


544-546: User privilege checks correctly implemented.

The privilege and app checks using User::isPrivileged() and User::isApp() are properly assigned to variables for reuse in the abuse prevention logic.


600-602: User type correctly set for auditing.

The ACTIVITY_TYPE_USER constant is properly used to set the type attribute for audit logging.


952-952: User privilege check correctly implemented.

The use of User::isPrivileged() to conditionally exclude privileged users from network usage metrics is appropriate.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6b04ae and cbc8b2c.

📒 Files selected for processing (1)
  • app/controllers/api/users.php (19 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/api/users.php (1)
src/Appwrite/Auth/Validator/PasswordHistory.php (1)
  • PasswordHistory (12-77)
🪛 GitHub Actions: Linter
app/controllers/api/users.php

[error] 1-1: PSR12: no_whitespace_in_blank_line. The lint tool 'vendor/bin/pint' reported a formatting issue. Command: 'vendor/bin/pint --test --config pint.json'.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: scan
🔇 Additional comments (7)
app/controllers/api/users.php (7)

34-45: LGTM on utopia-php/auth import migration.

The imports correctly bring in the new authentication library classes: Hash interface, algorithm implementations (Argon2, Bcrypt, MD5, PHPass, Plaintext, Scrypt, ScryptModified, Sha), and the proofs/store utilities (ProofsPassword, Token, Store).


117-165: Previous review concern addressed: passwordValidator hook now correctly triggered.

The $isHashed flag at Line 119 captures the original hash type before mutation. The hook at Line 163 now correctly fires only for plaintext password routes, preserving the intended behavior for external password policy hooks.


290-293: Verify bcrypt cost of 8 is intentional.

The comment says "Default cost" but bcrypt cost 8 is below typical security recommendations (10-12). If this is for backward compatibility with existing pre-hashed passwords being imported, it's acceptable. Otherwise, consider increasing the cost for new password imports.


1361-1385: Password update logic correctly handles hash algorithm upgrade.

The implementation properly:

  1. Hashes the new password with Argon2 (Lines 1362-1364)
  2. Uses the user's existing hash algorithm for password history validation (Line 1366, 1371)
  3. Updates the stored hash metadata to Argon2 (Lines 1384-1385)

This ensures seamless algorithm migration while maintaining password history integrity.


143-148: Hash metadata stored correctly on user document.

The hash->getName() and hash->getOptions() methods properly persist algorithm metadata, enabling future password verification with the correct algorithm regardless of which hasher was used at creation time.


2209-2270: Constants verified - session creation implementation is sound.

The verification confirms both TOKEN_EXPIRATION_LOGIN_LONG and SESSION_PROVIDER_SERVER are properly defined in app/init/constants.php:

  • Line 99: TOKEN_EXPIRATION_LOGIN_LONG = 31536000 (1 year in seconds)
  • Line 137: SESSION_PROVIDER_SERVER = 'server'

The session creation code correctly:

  • Injects Store and Token via dependency injection
  • Hashes the secret before database storage
  • Encodes session credentials for the response
  • Uses the constants appropriately throughout

2295-2336: Token constants are properly defined and correctly used.

Verification confirms both constants are defined in app/init/constants.php:

  • TOKEN_EXPIRATION_GENERIC = 60 * 15 (900 seconds / 15 minutes) at line 104
  • TOKEN_TYPE_GENERIC = 8 at line 125

The token creation endpoint at lines 2295-2336 correctly uses these constants. The default expiration period (15 minutes) aligns with the documented comment, and the Range validation (60 to TOKEN_EXPIRATION_LOGIN_LONG) is appropriate.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
app/controllers/api/users.php (2)

76-167: createUser hashing, metadata, and hooks now coherent

The updated createUser(Hash $hash, ...) flow looks solid:

  • $isHashed is captured before $hash is possibly replaced, so passwordValidator is correctly triggered only for the plaintext /v1/users route, fixing the prior “unreachable hook” issue.
  • Plaintext passwords are hashed via ProofsPassword and persisted together with hash / hashOptions; pre‑hashed passwords are passed through with appropriate metadata.
  • Password history initialization (passwordHistory, passwordUpdate) is consistent with $hashedPassword and the configured history limit.

Minor readability nit: !$hash instanceof Plaintext relies on precedence (instanceof > !). Consider making this more explicit, e.g.:

$isPlaintext = $hash instanceof Plaintext;
// ...
$isHashed = !$isPlaintext;

This avoids a double negative and makes intent clearer to future readers.


290-293: Bcrypt import route: metadata vs actual hash cost

Using new Bcrypt() and setCost(8) purely as a metadata carrier into createUser is functionally fine—the password itself is not re‑hashed here, and hashOptions will reflect cost 8.

If in practice these endpoints import hashes created with varying bcrypt costs, consider either:

  • Not forcing a specific cost on the metadata, or
  • Explicitly documenting that the stored hashOptions cost is advisory and may differ from the original hash’s internal cost.

Not a blocker, just something to keep in mind for future migrations.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbc8b2c and ef8d7a4.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • app/controllers/api/users.php (19 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/api/users.php (1)
src/Appwrite/Auth/Validator/PasswordHistory.php (1)
  • PasswordHistory (12-77)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: scan
🔇 Additional comments (11)
app/controllers/api/users.php (11)

34-45: Imports for Utopia\Auth look consistent with new auth design

The new use statements for Hash, concrete hashers, ProofsPassword, Token, and Store match their later usage in this file and align with the utopia-php/auth refactor. No issues here.


254-257: Plaintext /v1/users route correctly wired into createUser

Instantiating Plaintext and passing it into createUser cleanly distinguishes the “password is plaintext” path from the pre‑hashed routes, and works correctly with the $isHashed logic and passwordValidator hook.


328-331: MD5 import route consistent with new Hash interface

The MD5 route correctly passes an MD5 hasher to createUser without re‑hashing the supplied value, ensuring MD5 hashes are preserved while still recording hash / hashOptions. Given this is a legacy/import path, no further changes needed here.


365-367: Argon2 import route wiring is correct

The Argon2 route uses new Argon2() purely to tag the hash algorithm; the supplied password is treated as already hashed, which matches the other per‑algorithm import endpoints. Looks consistent.


403-409: SHA import route: version handling and metadata look good

  • Sha is instantiated once and setVersion($passwordVersion) is only applied when present, matching the passwordVersion param.
  • Passing $sha into createUser ensures both the hash name and chosen version end up in hash / hashOptions.

This should work cleanly with ProofsPassword::createHash and PasswordHistory.


443-445: PHPass import route consistent with other hash-specific endpoints

new PHPass() is created and passed straight into createUser, treating the incoming password as pre‑hashed and recording the algorithm metadata. This mirrors the other import routes and looks fine.


485-493: Scrypt import route correctly maps request params to hasher options

The Scrypt endpoint:

  • Maps passwordSalt, passwordCpu, passwordMemory, passwordParallel, and passwordLength directly into the Scrypt instance.
  • Passes that configured hasher to createUser, which will store these options in hashOptions.

This aligns the stored metadata with how the imported hash was produced, which is important for later verification and history checks.


531-537: Scrypt modified import route preserves full hash configuration

Similarly, the Scrypt‑modified route wires salt, salt separator, and signer key into ScryptModified before handing it to createUser. That should give ProofsPassword::createHash enough information to verify these legacy hashes later.


1361-1385: Remove the suggested refactoring; the premise about ProofsPassword being the source of truth in createUser is incorrect.

The review comment makes a false claim about how createUser works. The function is a generic helper that accepts any Hash parameter—it doesn't inherently use ProofsPassword. In users.php, the /v1/users/argon2 endpoint (line 365) calls createUser by passing new Argon2() directly, matching exactly what updatePassword does at line 1362. Both flows are already consistent.

The actual concern worth noting: updatePassword uses a bare new Argon2() without the tuning parameters (memory cost, time cost, threads) that the injected proofForPassword resource applies in account.php endpoints (lines 403–411 in app/init/resources.php: memory=7168, time=5, threads=1). If consistency in hashing parameters across flows is important, that's the real issue—but it's orthogonal to the ProofsPassword suggestion.

The suggested refactor to use ProofsPassword won't work here because proofForPassword is not injected in the users.php API endpoint, and these endpoints explicitly support multiple hash types (Bcrypt, MD5, SHA, Scrypt, etc.), not just Argon2.

Likely an incorrect or invalid review comment.


2209-2261: Store encoding is safe from cross-request state leakage; within-request patterns use encode() only once per action

The Store class from utopia-php/auth (v0.5.0) maintains an internal $data array that is not cleared after encode(). However, this does not pose a security concern:

  1. Cross-request isolation: Line 398-400 of app/init/resources.php shows new Store() is invoked per DI request cycle, so each HTTP request receives a fresh, independent instance.
  2. Within-request usage: Code patterns across users.php, account.php, and teams.php show encode() is called once per action (following setProperty() chains immediately before), with no evidence of multiple sequential encode() calls on the same instance that could cause data leakage.

The secret handling and DI setup remain correct as originally reviewed.


2295-2318: Review comment verification complete - all claims validated

The code implementation matches all review claims:

  1. TOKEN_EXPIRATION_GENERIC default (900 seconds = 15 minutes)
  2. Range(60, TOKEN_EXPIRATION_LOGIN_LONG) bounds (60 sec minimum, 31,536,000 sec/1 year max BDA5 imum)
  3. Token($length) with setHash(new Sha()) generates and hashes secret
  4. ✓ DB stores only hashed version via $proofForToken->hash($secret)
  5. ✓ Plaintext secret returned to caller via setAttribute('secret', $secret) before response
  6. ✓ utopia-php/auth library documentation confirms Token + Sha hasher as supported approach

The security hardening is correctly implemented and well-described.

@lohanidamodar lohanidamodar self-assigned this Nov 27, 2025
@lohanidamodar lohanidamodar merged commit 2bd308c into 1.8.x Nov 27, 2025
44 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Nov 27, 2025
2 tasks
This was referenced Dec 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0