Skip to content

Feature | Add MultiFactor Authentication. Initial migrations and entities#125

Open
matiasperrone-exo wants to merge 6 commits intomainfrom
feat/mfa-phase1---migrations--and--interfaces
Open

Feature | Add MultiFactor Authentication. Initial migrations and entities#125
matiasperrone-exo wants to merge 6 commits intomainfrom
feat/mfa-phase1---migrations--and--interfaces

Conversation

@matiasperrone-exo
Copy link
Copy Markdown
Contributor

@matiasperrone-exo matiasperrone-exo commented Apr 22, 2026

Task:

Ref: https://app.clickup.com/t/86b9e8wm7

Changes

  • Add migration for 2FA schema foundation (Phase I)
  • Add UserTrustedDevice entity
  • Add TwoFactorAuditLog entity
  • Add UserRecoveryCode entity
  • Add repository interfaces for 2FA entities
  • Add Doctrine implementations for 2FA repositories
  • Register 2FA repositories in service container
  • Add repository round-trip tests for 2FA entities

Requested Goal

Current state

The IDP database has no 2FA-related tables or columns. The User entity lacks fields for tracking two-factor enrollment or enforcement status.

Target state

Three new tables (user_trusted_devices, two_factor_audit_log, user_recovery_codes) and three new columns on the users table (two_factor_enabled, two_factor_method, two_factor_enforced_at) exist via a single additive Doctrine migration. Corresponding Doctrine entities and repositories are registered in the ORM mappings.

This migration is the foundation for every other Phase I ticket. It must run with zero downtime since it only adds nullable columns and new tables.

Tasks

  • Create Doctrine migration adding three columns to the users table: two_factor_enabled (boolean, default false), two_factor_method (string/enum, default 'email_otp'), two_factor_enforced_at
    (datetime, nullable)
  • Create UserTrustedDevice Doctrine entity with fields: id, user_id (FK), device_identifier (varchar 255), device_name (varchar 255), ip_address (varchar 45), user_agent (text), trusted_at,
    expires_at, last_seen_at, is_revoked (boolean, default false), created_at, updated_at. Indexes on (user_id, device_identifier), (user_id, is_revoked), (expires_at)
  • Create TwoFactorAuditLog Doctrine entity with fields: id, user_id (FK), event_type (enum: challenge_issued, challenge_succeeded, challenge_failed, enrollment_changed, device_trusted,
    device_revoked, recovery_used, settings_changed), method (enum: email_otp, sms_otp, totp, passkey, recovery), ip_address (varchar 45), user_agent (text), metadata (json, nullable), created_at.
    Indexes on (user_id, event_type, created_at), (created_at)
  • Create UserRecoveryCode Doctrine entity with fields: id, user_id (FK), code_hash (varchar 255, bcrypt hash), used_at (datetime, nullable), created_at. Index on (user_id, used_at)
  • Create IUserTrustedDeviceRepository, IUserRecoveryCodeRepository, ITwoFactorAuditLogRepository interfaces and their Doctrine implementations
  • Register all new entities in the Doctrine ORM entity mappings
  • Verify migration runs cleanly on a fresh database and on an existing database with data

ACCEPTANCE CRITERIA

php artisan doctrine:migrations:migrate runs without errors on both fresh and populated databases
All three new tables exist with correct column types, defaults, and indexes
Users table has the three new columns with correct defaults (two_factor_enabled=false, two_factor_method='email_otp', two_factor_enforced_at=NULL)
Each Doctrine entity can be persisted and retrieved via its repository ( provide unit tests )
Migration is fully additive: no columns dropped, no data modified, no destructive operations
Existing application functionality is unaffected after migration (login, OTP flow, admin panel all work)

DEVELOPMENT NOTES

Key files:

  • New: app/libs/Auth/Models/UserTrustedDevice.php
  • New: app/libs/Auth/Models/TwoFactorAuditLog.php
  • New: app/libs/Auth/Models/UserRecoveryCode.php
  • New: database/migrations/Version*.php (single migration file)
  • Modified: Doctrine entity mapping configuration

Gotchas

  • The User entity uses Doctrine ORM, not Eloquent. All mappings follow the existing annotation/attribute pattern in the codebase.
  • device_identifier stores a SHA-256 hash, not the raw cookie token. Size varchar(255) is sufficient for hex-encoded SHA-256 (64 chars).
  • The OTP table (oauth2_otp) already has phone_number and connection='sms' columns from existing infrastructure. Do NOT modify this table.
  • two_factor_method should be stored as a string column, not a database-level ENUM, to allow adding values in Phase II/III without migration.

Out of scope:

User entity PHP methods (separate ticket), service layer code, UI changes.

Summary by CodeRabbit

  • New Features

    • Added two-factor authentication with trusted device management, recovery codes, and security audit logging.
  • Tests

    • Added comprehensive repository tests covering trusted devices, audit logs, recovery codes, expiry/revocation behavior, ordering, and deletion.
  • Chores

    • Updated database schema to support two-factor features and enforce unique device identifiers.

feat(2fa): add migration for 2FA schema foundation (Phase I)
feat(2fa): add UserTrustedDevice entity
feat(2fa): add TwoFactorAuditLog entity
feat(2fa): add UserRecoveryCode entity
feat(2fa): add repository interfaces for 2FA entities
feat(2fa): add Doctrine implementations for 2FA repositories
feat(2fa): register 2FA repositories in service container
test(2fa): add repository round-trip tests for 2FA entities
@matiasperrone-exo matiasperrone-exo self-assigned this Apr 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Warning

Rate limit exceeded

@matiasperrone-exo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 23 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9725039a-f123-4451-8ba3-1f130d9f47da

📥 Commits

Reviewing files that changed from the base of the PR and between 16e6ebb and f406fbf.

📒 Files selected for processing (4)
  • app/Repositories/DoctrineUserTrustedDeviceRepository.php
  • app/libs/Auth/Models/TwoFactorAuditLog.php
  • app/libs/Auth/Models/UserTrustedDevice.php
  • database/migrations/Version20260424120000.php
📝 Walkthrough

Walkthrough

Adds a Phase I two-factor authentication stack: three Doctrine entities (trusted devices, audit logs, recovery codes), repository interfaces and Doctrine implementations, service bindings, migrations to create/alter schema, and integration tests exercising persistence and repository behavior.

Changes

Cohort / File(s) Summary
Entities
app/libs/Auth/Models/TwoFactorAuditLog.php, app/libs/Auth/Models/UserRecoveryCode.php, app/libs/Auth/Models/UserTrustedDevice.php
New Doctrine entities mapped to two_factor_audit_log, user_recovery_codes, and user_trusted_devices with fields, relationships to users, lifecycle init, getters/setters, and repositoryClass hints.
Repository Interfaces
app/libs/Auth/Repositories/ITwoFactorAuditLogRepository.php, app/libs/Auth/Repositories/IUserRecoveryCodeRepository.php, app/libs/Auth/Repositories/IUserTrustedDeviceRepository.php
Added interfaces declaring methods: getRecentByUser(User,int), getUnusedByUser(User), deleteAllForUser(User), getActiveByUserAndIdentifier(User,string), and getActiveByUser(User).
Repository Implementations
app/Repositories/DoctrineTwoFactorAuditLogRepository.php, app/Repositories/DoctrineUserRecoveryCodeRepository.php, app/Repositories/DoctrineUserTrustedDeviceRepository.php
Doctrine-backed repositories implementing the above interfaces: recent-audit query, unused-recovery retrieval, deletion of recovery codes via DQL, and active trusted-device queries (respecting expiry and revocation).
Service Provider
app/Repositories/RepositoriesProvider.php
Registered three new repository services in the container (user trusted device, two-factor audit log, user recovery code) and added them to provides() for deferred loading.
Migrations
database/migrations/Version20260416194357.php, database/migrations/Version20260424120000.php
Migration to add 2FA columns to users and create user_trusted_devices, two_factor_audit_log, user_recovery_codes; separate migration to convert user/device index to unique on (user_id, device_identifier).
Tests
tests/TwoFactorRepositoriesTest.php
Comprehensive integration tests persisting and asserting repository behaviors: trusted device lifecycle (active/expired/revoked/uniqueness), audit log retrieval and ordering, recovery code usage and deletion.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(100,150,240,0.5)
    participant Client
    end
    rect rgba(120,200,160,0.5)
    participant Repository
    end
    rect rgba(200,100,140,0.5)
    participant EntityManager
    end
    rect rgba(240,200,100,0.5)
    participant Database
    end

    Client->>Repository: saveTrustedDevice(data)
    Repository->>EntityManager: persist(UserTrustedDevice)
    EntityManager->>Database: INSERT user_trusted_devices
    Database-->>EntityManager: OK
    EntityManager-->>Repository: persisted entity

    Client->>Repository: getActiveByUserAndIdentifier(user, id)
    Repository->>EntityManager: createQuery(filter active conditions)
    EntityManager->>Database: SELECT ... WHERE user_id=... AND device_identifier=... AND is_revoked=0 AND (expires_at IS NULL OR expires_at > now)
    Database-->>EntityManager: rows
    EntityManager-->>Repository: hyditated entity/NULL
    Repository-->>Client: entity/NULL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • smarcet

Poem

🐰
Hops of Audit, codes in tow,
Trusted paws mark where devices go,
Logs that whisper when challenges pass,
Recovery seeds tucked in the grass,
A rabbit cheers — two-factor grow! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and accurately reflects the main objective: adding the initial foundation for multi-factor authentication including migrations and entities.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mfa-phase1---migrations--and--interfaces

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
Review rate limit: 0/1 reviews remaining, refill in 54 minutes and 23 seconds.

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

@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

Copy link
Copy Markdown

@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

🧹 Nitpick comments (6)
app/libs/Auth/Repositories/IUserRecoveryCodeRepository.php (1)

25-28: Consider documenting transaction/atomicity expectations for deleteAllForUser.

Since this is typically invoked during recovery-code regeneration (delete-then-insert), callers likely need the operation wrapped in a transaction with the subsequent re-insert to avoid leaving a user with zero recovery codes on partial failure. Document whether callers must manage the transaction themselves, or have the implementation wrap it. A one-line docblock addition is sufficient.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/libs/Auth/Repositories/IUserRecoveryCodeRepository.php` around lines 25 -
28, Add a one-line sentence to the deleteAllForUser(User $user): int docblock
clarifying transaction/atomicity expectations: state whether implementations
will wrap this operation in a database transaction or whether callers must
perform a surrounding transaction when doing delete-then-insert (e.g., "This
method does NOT start a transaction; callers should wrap delete-and-reinsert
operations in a transaction to ensure atomicity." or the converse if
implementations guarantee transactional semantics). Reference the method name
deleteAllForUser in the docblock so callers know the requirement.
app/Repositories/DoctrineUserRecoveryCodeRepository.php (1)

34-42: Consider flushing pending UoW changes or documenting caller responsibility.

Query::execute() on a DQL DELETE bypasses the UnitOfWork and runs directly against the DB. If a caller has pending in-memory changes on UserRecoveryCode entities for this user (e.g. newly persisted but not flushed, or a markUsed() pending flush), those will not be considered by the bulk delete and may re-surface on a later flush(), or conflict. Either document this (bulk operation; caller must flush/clear beforehand) or call $em->flush()/$em->clear(UserRecoveryCode::class) around the delete. Given the "used when regenerating" use case, a docblock note is likely sufficient.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Repositories/DoctrineUserRecoveryCodeRepository.php` around lines 34 -
42, The bulk DQL delete in deleteAllForUser bypasses the UnitOfWork and can
conflict with pending in-memory UserRecoveryCode changes; update the docblock
for DoctrineUserRecoveryCodeRepository::deleteAllForUser to state this is a bulk
operation and callers must flush or clear pending UserRecoveryCode changes
before calling, or alternatively call $em->flush() and/or
$em->clear(UserRecoveryCode::class) around the delete inside deleteAllForUser to
ensure UoW consistency (refer to getEntityManager(), UserRecoveryCode and the
Query::execute() bulk delete in your change).
app/libs/Auth/Models/UserRecoveryCode.php (1)

27-29: Minor: attribute order — JoinColumn is placed before ManyToOne.

Doctrine accepts either order, but the conventional pairing is #[ManyToOne] first, then #[JoinColumn], which matches Doctrine docs/examples and the surrounding codebase's style. Purely stylistic; no behavior change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/libs/Auth/Models/UserRecoveryCode.php` around lines 27 - 29, Swap the
attribute order on the $user property so #[ORM\ManyToOne(...)] appears before
#[ORM\JoinColumn(...)]; specifically update the attributes on the private $user
property (currently using #[ORM\JoinColumn(...)] then #[ORM\ManyToOne(...)] ) to
#[ORM\ManyToOne(targetEntity: \Auth\User::class)] followed by
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', onDelete:
'CASCADE')], preserving the same arguments.
app/Repositories/DoctrineUserTrustedDeviceRepository.php (1)

35-41: Consider deterministic ordering for getActiveByUser.

No orderBy is specified, so results come back in storage order. For a user-facing "trusted devices" listing, ordering by last_seen_at DESC (or trusted_at DESC) produces a more predictable/useful listing and avoids flaky tests if ever asserting order.

♻️ Suggested change
     public function getActiveByUser(User $user): array
     {
         return $this->findBy([
             'user'       => $user,
             'is_revoked' => false,
-        ]);
+        ], ['last_seen_at' => 'DESC']);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Repositories/DoctrineUserTrustedDeviceRepository.php` around lines 35 -
41, getActiveByUser currently returns results with no deterministic ordering;
update the repository method (getActiveByUser) to return active devices ordered
by most-recent activity (e.g. order by last_seen_at DESC or trusted_at DESC
depending on your entity field name) so the user-facing list and tests are
stable—use the Doctrine findBy third parameter for orderBy or switch to the
query builder in DoctrineUserTrustedDeviceRepository::getActiveByUser to apply
the DESC ordering on the appropriate timestamp field.
database/migrations/Version20260416194357.php (2)

33-39: Partial-state guard only checks one column.

The up() guard checks only two_factor_enabled. If a prior run failed after adding two_factor_enabled but before adding the other two columns (or vice versa in a manual intervention), subsequent runs will silently skip the missing columns. Consider guarding each column addition independently, e.g.:

-        if ($schema->hasTable("users") && !$builder->hasColumn("users", "two_factor_enabled")) {
-            $builder->table('users', function (Table $table) {
-                $table->boolean('two_factor_enabled')->setNotnull(true)->setDefault(false);
-                $table->string('two_factor_method', 32)->setNotnull(true)->setDefault('email_otp');
-                $table->dateTime('two_factor_enforced_at')->setNotnull(false)->setDefault(null);
-            });
-        }
+        if ($schema->hasTable("users")) {
+            $builder->table('users', function (Table $table) use ($builder) {
+                if (!$builder->hasColumn("users", "two_factor_enabled")) {
+                    $table->boolean('two_factor_enabled')->setNotnull(true)->setDefault(false);
+                }
+                if (!$builder->hasColumn("users", "two_factor_method")) {
+                    $table->string('two_factor_method', 32)->setNotnull(true)->setDefault('email_otp');
+                }
+                if (!$builder->hasColumn("users", "two_factor_enforced_at")) {
+                    $table->dateTime('two_factor_enforced_at')->setNotnull(false)->setDefault(null);
+                }
+            });
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database/migrations/Version20260416194357.php` around lines 33 - 39, The
migration's up() currently gates adding all three columns behind a single check
for two_factor_enabled, so if a prior run added one column and failed the rest
will be skipped; change the logic to first verify $schema->hasTable("users") and
then individually check $builder->hasColumn("users", "<column_name>") for each
of the three columns (two_factor_enabled, two_factor_method,
two_factor_enforced_at) and add each missing column (using the existing
builder->table('users', function (Table $table) { ... }) block or separate
blocks) so each column is created idempotently even if previous runs partially
applied the migration.

55-58: Consider a UNIQUE index on (user_id, device_identifier).

DoctrineUserTrustedDeviceRepository::getActiveByUserAndIdentifier() is expected to return a single row for a given (user, device_identifier) pair, and device_identifier is a SHA-256 hex string (globally unique per device/token). A plain index allows duplicate rows to accumulate (e.g., re-trusting the same device on retries) and makes the "single active device" contract unenforceable at the DB layer. A unique constraint would prevent silent duplication and align the schema with the repository's semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database/migrations/Version20260416194357.php` around lines 55 - 58, Replace
the non-unique index on (user_id, device_identifier) with a UNIQUE constraint so
the DB enforces one row per (user_id, device_identifier) pair; update the
migration where $table->index(["user_id", "device_identifier"],
"utd_user_device_idx") is declared in Version20260416194357.php to create a
unique index/constraint (e.g. using $table->unique(...) or the DBAL equivalent)
with a clear name like "utd_user_device_unique", and remove or replace the
existing plain index to avoid duplicate-key conflicts; this aligns the schema
with DoctrineUserTrustedDeviceRepository::getActiveByUserAndIdentifier()
expectations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/libs/Auth/Models/UserRecoveryCode.php`:
- Line 66: The magic __get method on class UserRecoveryCode exposes
private/protected properties (notably code_hash) and bypasses the explicit
getters; replace it with a safe approach by removing the passthrough or
implementing an allow-list: in UserRecoveryCode::__get only return values for an
explicit whitelist of non-sensitive keys (e.g., id, user_id, created_at) or
throw/return null for anything else, so sensitive fields like code_hash are not
accessible via property access while preserving legacy compatibility for
approved fields.

In `@app/libs/Auth/Models/UserTrustedDevice.php`:
- Line 85: Remove the magic __get() passthrough from the UserTrustedDevice
entity: delete the public function __get($name) { return $this->{$name}; } so
callers no longer bypass explicit accessors (e.g. getIsRevoked(), isRevoked(),
getUser()) and Doctrine proxies fire their lazy-load hooks; update any call
sites that relied on property-style access to use the declared getters instead
to preserve type casting and avoid silent nulls for missing properties.

In `@tests/TwoFactorRepositoriesTest.php`:
- Around line 37-41: The test currently grabs an arbitrary real user via
IUserRepository->findOneBy([]) in setUp and testRecoveryCodeRoundTrip calls
RecoveryCodeRepository->deleteAllForUser($this->user), risking deletion of real
users' data; change setUp to create (and persist) a dedicated test user or
select a user explicitly marked for testing, then use that user object for the
test, and in tearDown remove that test user and assert deleteAllForUser removed
exactly the number of recovery codes inserted by the test (or call delete only
for the specific created entities) so that testRecoveryCodeRoundTrip no longer
wipes production/seeding data. Ensure you update any references to findOneBy,
testRecoveryCodeRoundTrip, deleteAllForUser, setUp and tearDown accordingly.

---

Nitpick comments:
In `@app/libs/Auth/Models/UserRecoveryCode.php`:
- Around line 27-29: Swap the attribute order on the $user property so
#[ORM\ManyToOne(...)] appears before #[ORM\JoinColumn(...)]; specifically update
the attributes on the private $user property (currently using
#[ORM\JoinColumn(...)] then #[ORM\ManyToOne(...)] ) to
#[ORM\ManyToOne(targetEntity: \Auth\User::class)] followed by
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', onDelete:
'CASCADE')], preserving the same arguments.

In `@app/libs/Auth/Repositories/IUserRecoveryCodeRepository.php`:
- Around line 25-28: Add a one-line sentence to the deleteAllForUser(User
$user): int docblock clarifying transaction/atomicity expectations: state
whether implementations will wrap this operation in a database transaction or
whether callers must perform a surrounding transaction when doing
delete-then-insert (e.g., "This method does NOT start a transaction; callers
should wrap delete-and-reinsert operations in a transaction to ensure
atomicity." or the converse if implementations guarantee transactional
semantics). Reference the method name deleteAllForUser in the docblock so
callers know the requirement.

In `@app/Repositories/DoctrineUserRecoveryCodeRepository.php`:
- Around line 34-42: The bulk DQL delete in deleteAllForUser bypasses the
UnitOfWork and can conflict with pending in-memory UserRecoveryCode changes;
update the docblock for DoctrineUserRecoveryCodeRepository::deleteAllForUser to
state this is a bulk operation and callers must flush or clear pending
UserRecoveryCode changes before calling, or alternatively call $em->flush()
and/or $em->clear(UserRecoveryCode::class) around the delete inside
deleteAllForUser to ensure UoW consistency (refer to getEntityManager(),
UserRecoveryCode and the Query::execute() bulk delete in your change).

In `@app/Repositories/DoctrineUserTrustedDeviceRepository.php`:
- Around line 35-41: getActiveByUser currently returns results with no
deterministic ordering; update the repository method (getActiveByUser) to return
active devices ordered by most-recent activity (e.g. order by last_seen_at DESC
or trusted_at DESC depending on your entity field name) so the user-facing list
and tests are stable—use the Doctrine findBy third parameter for orderBy or
switch to the query builder in
DoctrineUserTrustedDeviceRepository::getActiveByUser to apply the DESC ordering
on the appropriate timestamp field.

In `@database/migrations/Version20260416194357.php`:
- Around line 33-39: The migration's up() currently gates adding all three
columns behind a single check for two_factor_enabled, so if a prior run added
one column and failed the rest will be skipped; change the logic to first verify
$schema->hasTable("users") and then individually check
$builder->hasColumn("users", "<column_name>") for each of the three columns
(two_factor_enabled, two_factor_method, two_factor_enforced_at) and add each
missing column (using the existing builder->table('users', function (Table
$table) { ... }) block or separate blocks) so each column is created
idempotently even if previous runs partially applied the migration.
- Around line 55-58: Replace the non-unique index on (user_id,
device_identifier) with a UNIQUE constraint so the DB enforces one row per
(user_id, device_identifier) pair; update the migration where
$table->index(["user_id", "device_identifier"], "utd_user_device_idx") is
declared in Version20260416194357.php to create a unique index/constraint (e.g.
using $table->unique(...) or the DBAL equivalent) with a clear name like
"utd_user_device_unique", and remove or replace the existing plain index to
avoid duplicate-key conflicts; this aligns the schema with
DoctrineUserTrustedDeviceRepository::getActiveByUserAndIdentifier()
expectations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 672e540a-70f5-47fd-8667-ab3e07d97782

📥 Commits

Reviewing files that changed from the base of the PR and between db9f777 and 6a2bd34.

📒 Files selected for processing (12)
  • app/Repositories/DoctrineTwoFactorAuditLogRepository.php
  • app/Repositories/DoctrineUserRecoveryCodeRepository.php
  • app/Repositories/DoctrineUserTrustedDeviceRepository.php
  • app/Repositories/RepositoriesProvider.php
  • app/libs/Auth/Models/TwoFactorAuditLog.php
  • app/libs/Auth/Models/UserRecoveryCode.php
  • app/libs/Auth/Models/UserTrustedDevice.php
  • app/libs/Auth/Repositories/ITwoFactorAuditLogRepository.php
  • app/libs/Auth/Repositories/IUserRecoveryCodeRepository.php
  • app/libs/Auth/Repositories/IUserTrustedDeviceRepository.php
  • database/migrations/Version20260416194357.php
  • tests/TwoFactorRepositoriesTest.php

Comment thread app/libs/Auth/Models/UserRecoveryCode.php Outdated
Comment thread app/libs/Auth/Models/UserTrustedDevice.php Outdated
Comment thread tests/TwoFactorRepositoriesTest.php Outdated
Copy link
Copy Markdown

@martinquiroga-exo martinquiroga-exo left a comment

Choose a reason for hiding this comment

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

@matiasperrone-exo please see comments. Thanks

Comment thread app/Repositories/DoctrineUserTrustedDeviceRepository.php
Comment thread database/migrations/Version20260416194357.php
Comment thread tests/TwoFactorRepositoriesTest.php
@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

@matiasperrone-exo matiasperrone-exo force-pushed the feat/mfa-phase1---migrations--and--interfaces branch from bd6e93f to f75c63d Compare April 24, 2026 20:59
@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

@matiasperrone-exo matiasperrone-exo force-pushed the feat/mfa-phase1---migrations--and--interfaces branch from f75c63d to 4624ff5 Compare April 24, 2026 21:04
@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

@matiasperrone-exo
Copy link
Copy Markdown
Contributor Author

@martinquiroga-exo please review again

Copy link
Copy Markdown

@martinquiroga-exo martinquiroga-exo left a comment

Choose a reason for hiding this comment

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

@matiasperrone-exo please see comments

Comment thread app/libs/Auth/Models/TwoFactorAuditLog.php Outdated
Comment thread app/Repositories/DoctrineUserTrustedDeviceRepository.php Outdated
@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
app/libs/Auth/Models/UserTrustedDevice.php (1)

19-20: Add UniqueConstraint annotation to mirror the database migration.

The migration enforces uniqueness on (user_id, device_identifier) via UNIQUE INDEX utd_user_device_uniq, but the entity metadata declares no corresponding UniqueConstraint. This drift causes schema validation mismatches and lets metadata-driven tools miss the constraint. Add a #[ORM\UniqueConstraint(name: 'utd_user_device_uniq', columns: ['user_id', 'device_identifier'])] attribute to the #[ORM\Table] declaration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/libs/Auth/Models/UserTrustedDevice.php` around lines 19 - 20, The ORM
entity for UserTrustedDevice is missing the UniqueConstraint that mirrors the DB
migration; update the #[ORM\Table(...)] attribute on the UserTrustedDevice class
to include #[ORM\UniqueConstraint(name: 'utd_user_device_uniq', columns:
['user_id', 'device_identifier'])] so the entity metadata matches the migration
(ensure the attribute is added alongside the existing name and repositoryClass
in the class declaration for UserTrustedDevice).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/libs/Auth/Models/TwoFactorAuditLog.php`:
- Around line 22-35: The setters for audit event/method must validate input
against the allowed constants to prevent typos from being persisted; update the
TwoFactorAuditLog class to add validation in the relevant setters (e.g., the
methods that set event and method values) by building arrays of allowed values
from the constants (Event* and Method*) and checking inputs with an explicit
membership test (throw a ValidationException/InvalidArgumentException or return
an error instead of accepting unknown strings), and apply the same guard to the
other setters referenced around lines 75-79 so only the defined constants can be
stored (also consider adding a DB constraint/migration as a follow-up).

In `@database/migrations/Version20260424120000.php`:
- Around line 27-33: In Version20260424120000.php inside the up(Schema $schema)
method, guard the ALTER TABLE that replaces utd_user_device_idx with the UNIQUE
utd_user_device_uniq by first running a preflight duplicate check on
user_trusted_devices for (user_id, device_identifier) and aborting/messaging the
migration if any duplicates exist; if duplicates are found implement a
deterministic dedupe/backfill step (e.g. keep the most recent row per (user_id,
device_identifier) or copy conflicting rows to a retention table and delete
extras) before executing the ALTER TABLE, and then proceed to DROP INDEX
utd_user_device_idx and ADD UNIQUE INDEX utd_user_device_uniq only after
deduplication completes successfully.

---

Nitpick comments:
In `@app/libs/Auth/Models/UserTrustedDevice.php`:
- Around line 19-20: The ORM entity for UserTrustedDevice is missing the
UniqueConstraint that mirrors the DB migration; update the #[ORM\Table(...)]
attribute on the UserTrustedDevice class to include #[ORM\UniqueConstraint(name:
'utd_user_device_uniq', columns: ['user_id', 'device_identifier'])] so the
entity metadata matches the migration (ensure the attribute is added alongside
the existing name and repositoryClass in the class declaration for
UserTrustedDevice).
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8e15f9c4-68e3-4df9-a2d3-f1e2af5e201f

📥 Commits

Reviewing files that changed from the base of the PR and between 6a2bd34 and 16e6ebb.

📒 Files selected for processing (6)
  • app/Repositories/DoctrineUserTrustedDeviceRepository.php
  • app/libs/Auth/Models/TwoFactorAuditLog.php
  • app/libs/Auth/Models/UserRecoveryCode.php
  • app/libs/Auth/Models/UserTrustedDevice.php
  • database/migrations/Version20260424120000.php
  • tests/TwoFactorRepositoriesTest.php

Comment thread app/libs/Auth/Models/TwoFactorAuditLog.php Outdated
Comment thread database/migrations/Version20260424120000.php
@matiasperrone-exo matiasperrone-exo force-pushed the feat/mfa-phase1---migrations--and--interfaces branch from 16e6ebb to 8fab15c Compare April 29, 2026 19:19
@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

@matiasperrone-exo matiasperrone-exo force-pushed the feat/mfa-phase1---migrations--and--interfaces branch from 8fab15c to 5b8c829 Compare April 29, 2026 19:22
@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

@matiasperrone-exo matiasperrone-exo force-pushed the feat/mfa-phase1---migrations--and--interfaces branch from 5b8c829 to a5c0cff Compare April 29, 2026 19:54
@matiasperrone-exo matiasperrone-exo force-pushed the feat/mfa-phase1---migrations--and--interfaces branch from a5c0cff to f21b6fb Compare April 29, 2026 19:55
@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

1 similar comment
@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

@matiasperrone-exo
Copy link
Copy Markdown
Contributor Author

Add UniqueConstraint annotation to mirror the database migration.

done

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Autofix skipped. No unresolved CodeRabbit review comments with fix instructions found.

@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-125/

This page is automatically updated on each push to this PR.

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