fix!: harden audit routes and unify login gate (BREAKING — release as v0.5.0)#14
Merged
Conversation
…footguns Four integration bugs surfaced in a consuming app (vora); this commit prevents the package-preventable ones: **Finding 1 (P1 — access-control bypass)** - Add `AuthUserPolicy::canLogin()` to the contract with a docblock explaining it is the single gate for account state (active, approved, not disabled). The default implementation duck-types `$user->canLogin()` then falls back to `is_disabled`, matching the previous `canPasskeyLogin()` logic. - `DefaultAuthUserPolicy::canPasskeyLogin()` now delegates to `canLogin()` so both code paths share one implementation. - New `RequireActiveUser` middleware calls `policy->canLogin()` and aborts 403 when the user is not allowed in. Applied automatically on all package audit-log routes (previously only `auth` middleware was applied, letting a pending/disabled admin reach the admin endpoint if their Gate ability only checked role). - Config comment on `audit.admin_ability` warns that the gate definition must check both role and account state. **Finding 2 (P1 — lockout-after-migration)** - Package does not own the `approved_at` column (it is app-side), so there is nothing to migrate-fix. Added prominent README guidance with a code example showing how to backfill the column in the migration that adds it, so integrators do not lock out existing users. **Finding 3 (P2 — onboarding redirect divergence)** - Package does not ship an email-verification controller (app-owned), so the hardcoded `/pending` redirect was not in this repo. - Added README guidance: after email verification the app must call `AuthUserPolicy::redirectAfterLogin()` (not a hardcoded path), so an already-approved user who verifies their email is sent to the correct destination instead of the pending page. **Finding 4 (P3 — dropdown outside-click)** - The package ships no navbar/dropdown/menu components; this bug is entirely app-side. No change. Tests: 7 new assertions covering `canLogin()`, the `is_disabled` fallback, the `canPasskeyLogin()` delegation, and the middleware pass/block/unauthenticated cases. All 40 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adding canLogin() to the AuthUserPolicy contract is a BC break for direct implementers. Document that this must ship as v0.5.0 (not a 0.4.x patch, which ^0.4 consumers would auto-pull and fatal on) and how integrators migrate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner
Author
|
This PR adds a required The package versions via git tags (latest Squash-merge per repo convention. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four integration bugs were surfaced in a consuming app (vora). This PR fixes the three findings that the package itself can prevent; the fourth is purely app-side.
Finding 1 (P1) — Access-control bypass on audit routes: FIXED
Root cause: The package's audit routes were protected by
authmiddleware only. If a consuming app's Gate ability foraudit.admin_abilityonly checkedis_adminwithout verifyingapproved_at/is_disabled, a pending or disabled admin could reach the cross-user admin endpoint.Fix:
AuthUserPolicy::canLogin(Authenticatable $user, Request $request): boolto the contract — the single gate for all account-state checks (active, approved, not disabled, etc.).DefaultAuthUserPolicy::canLogin()duck-types$user->canLogin(), falls back tois_disabled, defaults totrue.canPasskeyLogin()now delegates tocanLogin()so both share one implementation.RequireActiveUsermiddleware aborts 403 when the authenticated user failscanLogin(). Applied automatically on all package audit routes (in addition toauth).bherila-auth.phpconfig thatadmin_abilitygate definitions must check account state as well as role.Finding 2 (P1) — Lockout-after-migration of approved_at: DOCUMENTED
Root cause: Apps adding an
approved_at(nullable, null = pending) column to theiruserstable leave all existing rows as null after migration, instantly locking out every current user.Why package can't fully fix this: The
approved_atcolumn is app-owned — the package does not ship a migration for the user table. There is nothing to backfill in this repo.Fix: Added a prominent README section with a copy-paste backfill snippet explaining the requirement and the risk.
Finding 3 (P2) — Post-verification redirect divergence: DOCUMENTED
Root cause: A consuming app hardcoded
/pendingin its email-verification callback instead of callingAuthUserPolicy::redirectAfterLogin(), so an already-approved user who verified their email was falsely shown the pending page.Why package can't fully fix this: The package does not ship an email-verification controller — that is app-owned. The bug was in vora's controller.
Fix: Added README guidance that after email verification the app must call
redirectAfterLogin()(not a hardcoded path), and thatcanLogin()should be checked at every entry point including verification callbacks.Finding 4 (P3) — Dropdown outside-click bug: NOT APPLICABLE
The package ships no navbar, dropdown, or menu components. The bug was in vora's own React component. No change.
Test plan
composer test)RequireActiveUserTestcover:canLogin()default allow,canLogin()via duck-typedcanLogin()method,is_disabledfallback,canPasskeyLogin()delegation tocanLogin(), middleware pass-through for active user, middleware 403 for inactive user, middleware pass-through for unauthenticated (letsauthmiddleware handle it)🤖 Generated with Claude Code