25 KiB
Story 1.2: Authentication & Role System
Status
Done
Epic Reference
Epic 1: Core Foundation & Infrastructure
Story
As an admin, I want a secure authentication system with Admin/Client roles, So that only authorized users can access the platform with appropriate permissions.
Acceptance Criteria
Functional Requirements
- Fortify configured with custom Volt views
- Login page with bilingual support (Arabic/English)
- Session timeout after 2 hours of inactivity
- Rate limiting on login attempts (5 attempts per minute)
- Admin role with full access to all features
- Client role with restricted access (own data only)
- Registration feature DISABLED (admin creates all accounts)
Security Requirements
- CSRF protection enabled on all forms
- Password hashing using bcrypt
- Custom middleware for admin authorization
- Secure session configuration
- Remember me functionality (optional)
Integration Requirements
- Login redirects to appropriate dashboard:
- Admin users →
/admin/dashboard(placeholder until Epic 6) - Client users →
/client/dashboard(placeholder until Epic 7)
- Admin users →
- Logout clears session properly and redirects to login page
- Admin middleware protects admin-only routes (return 403 for unauthorized)
- Failed login attempts logged to
admin_logstable - Deactivated users cannot log in
Quality Requirements
- Login form validates inputs properly
- Error messages are clear and bilingual
- All test scenarios in "Testing" section pass
- No security vulnerabilities
Tasks / Subtasks
-
Task 1: Configure Fortify (AC: 1, 7)
- Update
config/fortify.phpto disable registration feature - Keep
resetPasswordsenabled (admin-triggered only via future Epic) - Keep
emailVerification,updateProfileInformation,updatePasswordsenabled - Keep
twoFactorAuthenticationenabled with confirm options
- Update
-
Task 2: Add User Model Helper Methods (AC: 5, 6)
- Add
isAdmin(): boolmethod toapp/Models/User.php - Add
isClient(): boolmethod toapp/Models/User.php - Add
isIndividual(): boolmethod toapp/Models/User.php - Add
isCompany(): boolmethod toapp/Models/User.php
- Add
-
Task 3: Create Custom Middleware (AC: 10, 15, 17)
- Create
app/Http/Middleware/EnsureUserIsAdmin.php - Create
app/Http/Middleware/EnsureUserIsActive.php - Register middleware aliases in
bootstrap/app.phpasadminandactive - Add
SetLocalemiddleware to web group (for bilingual support)
- Create
-
Task 4: Create Login Volt Component (AC: 1, 2, 8, 12, 18, 19)
- Create
resources/views/livewire/auth/login.blade.phpas class-based Volt component - Implement form with email and password fields using Flux UI components
- Add remember me checkbox
- Add CSRF token via
@csrfor Livewire handling - Display validation errors in current locale
- Add language switcher or respect current locale
- Create
-
Task 5: Configure Login Redirect Logic (AC: 13, 14)
- Update
FortifyServiceProviderto set custom login view - Create
app/Actions/Fortify/RedirectIfAuthenticated.phpor configure inAuthenticatedSessionController - Implement redirect logic: admin →
/admin/dashboard, client →/client/dashboard - Create placeholder routes for dashboards (return simple "Dashboard coming soon" view)
- Implement logout redirect to login page
- Update
-
Task 6: Implement Session and Rate Limiting (AC: 3, 4)
- Set
SESSION_LIFETIME=120in.env(2 hours) - Verify Fortify's built-in rate limiting (5 attempts per minute)
- Ensure rate limit error message uses translation key
- Set
-
Task 7: Implement Login Attempt Logging (AC: 16)
- Create listener for
Illuminate\Auth\Events\Failedevent - Log failed attempts to
admin_logstable with IP address - Register listener in
EventServiceProviderorAppServiceProvider
- Create listener for
-
Task 8: Implement Deactivated User Check (AC: 17)
- Add check in
EnsureUserIsActivemiddleware - Or customize Fortify's
authenticateUsingcallback to reject deactivated users - Return bilingual error message for deactivated accounts
- Add check in
-
Task 9: Write Tests (AC: 20)
- Create
tests/Feature/Auth/LoginTest.phpwith all login flow tests - Create
tests/Feature/Auth/AuthorizationTest.phpfor middleware tests - Create
tests/Unit/Models/UserHelperMethodsTest.phpfor isAdmin/isClient tests - Run all tests and ensure they pass
- Create
-
Task 10: Final Verification (AC: 21)
- Run
vendor/bin/pintto format code - Verify no security vulnerabilities (CSRF, session fixation, etc.)
- Test full login flow in browser for both admin and client users
- Run
Dev Notes
Relevant Source Tree
app/
├── Actions/
│ └── Fortify/ # Existing - authentication logic
│ ├── CreateNewUser.php # Existing
│ ├── ResetUserPassword.php # Existing
│ └── UpdateUserPassword.php # Existing
├── Http/
│ └── Middleware/ # Create these
│ ├── SetLocale.php
│ ├── EnsureUserIsAdmin.php
│ └── EnsureUserIsActive.php
├── Models/
│ └── User.php # Existing - add helper methods
├── Providers/
│ └── FortifyServiceProvider.php # Existing - configure views
bootstrap/
└── app.php # Register middleware aliases
config/
├── fortify.php # Configure features
└── session.php # Session configuration
resources/
└── views/
└── livewire/
└── auth/
└── login.blade.php # Create - Volt component
lang/
├── ar/
│ └── auth.php # Translation strings
└── en/
└── auth.php # Translation strings
tests/
├── Feature/
│ └── Auth/
│ ├── LoginTest.php
│ └── AuthorizationTest.php
└── Unit/
└── Models/
└── UserHelperMethodsTest.php
User Model Helper Methods (Add to User.php)
// app/Models/User.php
use App\Enums\UserType;
use App\Enums\UserStatus;
public function isAdmin(): bool
{
return $this->user_type === UserType::Admin;
}
public function isClient(): bool
{
return in_array($this->user_type, [UserType::Individual, UserType::Company]);
}
public function isIndividual(): bool
{
return $this->user_type === UserType::Individual;
}
public function isCompany(): bool
{
return $this->user_type === UserType::Company;
}
public function isActive(): bool
{
return $this->status === UserStatus::Active;
}
Middleware Implementation (from Architecture Section 7.3)
// app/Http/Middleware/EnsureUserIsAdmin.php
class EnsureUserIsAdmin
{
public function handle(Request $request, Closure $next): Response
{
if (!$request->user()?->isAdmin()) {
abort(403, __('messages.unauthorized'));
}
return $next($request);
}
}
// app/Http/Middleware/EnsureUserIsActive.php
class EnsureUserIsActive
{
public function handle(Request $request, Closure $next): Response
{
if ($request->user()?->status === UserStatus::Deactivated) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login')
->with('error', __('auth.account_deactivated'));
}
return $next($request);
}
}
Middleware Registration (bootstrap/app.php)
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Http\Middleware\SetLocale::class,
]);
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
'active' => \App\Http\Middleware\EnsureUserIsActive::class,
]);
})
Fortify Configuration
// config/fortify.php
'features' => [
// Features::registration(), // DISABLED - admin creates accounts
Features::resetPasswords(),
Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
]),
],
FortifyServiceProvider Setup
// app/Providers/FortifyServiceProvider.php
public function boot(): void
{
// Custom login view
Fortify::loginView(fn () => view('livewire.auth.login'));
// Custom authentication logic (optional - for deactivated check)
Fortify::authenticateUsing(function (Request $request) {
$user = User::where('email', $request->email)->first();
if ($user &&
Hash::check($request->password, $user->password) &&
$user->status === UserStatus::Active) {
return $user;
}
return null;
});
}
Login Redirect Logic
// app/Providers/FortifyServiceProvider.php or custom response
// After successful login, redirect based on role:
if ($user->isAdmin()) {
return redirect('/admin/dashboard');
}
return redirect('/client/dashboard');
Placeholder Dashboard Routes
// routes/web.php
Route::middleware(['auth', 'active'])->group(function () {
// Admin routes
Route::middleware('admin')->prefix('admin')->group(function () {
Route::view('/dashboard', 'livewire.admin.dashboard-placeholder')
->name('admin.dashboard');
});
// Client routes
Route::prefix('client')->group(function () {
Route::view('/dashboard', 'livewire.client.dashboard-placeholder')
->name('client.dashboard');
});
});
Environment Configuration
SESSION_LIFETIME=120 # 2 hours in minutes
SESSION_EXPIRE_ON_CLOSE=false
Translation Keys (lang/en/auth.php and lang/ar/auth.php)
// lang/en/auth.php
return [
'failed' => 'These credentials do not match our records.',
'password' => 'The provided password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
'account_deactivated' => 'Your account has been deactivated. Please contact the administrator.',
];
// lang/ar/auth.php
return [
'failed' => 'بيانات الاعتماد هذه لا تتطابق مع سجلاتنا.',
'password' => 'كلمة المرور المقدمة غير صحيحة.',
'throttle' => 'محاولات تسجيل دخول كثيرة جداً. يرجى المحاولة مرة أخرى بعد :seconds ثانية.',
'account_deactivated' => 'تم تعطيل حسابك. يرجى الاتصال بالمسؤول.',
];
Edge Case Behavior
Rate Limiting (5 attempts per minute):
- Fortify handles this automatically via
RateLimiter - After 5 failed attempts, shows throttle message in current locale
- Lockout duration: 60 seconds
Session Timeout (2 hours inactivity):
- Configured via
SESSION_LIFETIME=120 - When session expires, redirect to login page
- Intended URL preserved for redirect after re-authentication
Deactivated Account Login Attempt:
- Check in
Fortify::authenticateUsing()callback - Reject with
auth.account_deactivatedmessage - User never gets authenticated
Flux UI Components for Login Form
<flux:input type="email" wire:model="email" label="{{ __('Email') }}" required />
<flux:input type="password" wire:model="password" label="{{ __('Password') }}" required />
<flux:checkbox wire:model="remember" label="{{ __('Remember me') }}" />
<flux:button type="submit" variant="primary">{{ __('Login') }}</flux:button>
Testing
Test Location
- Feature tests:
tests/Feature/Auth/ - Unit tests:
tests/Unit/Models/
Testing Framework
Pest 4 with Volt testing support
Feature Tests (tests/Feature/Auth/LoginTest.php)
Login Flow:
test_login_page_renders_correctly- Login page displaystest_user_can_login_with_valid_credentials- Valid credentials authenticatetest_admin_redirects_to_admin_dashboard- Admin →/admin/dashboardtest_client_redirects_to_client_dashboard- Client →/client/dashboardtest_invalid_credentials_show_error- Wrong password shows errortest_nonexistent_user_shows_error- Unknown email shows generic error (no enumeration)test_deactivated_user_cannot_login- Deactivated account rejected
Rate Limiting:
test_rate_limiting_blocks_after_five_attempts- 6th attempt blockedtest_rate_limit_resets_after_cooldown- Can retry after 60 seconds
Session Management:
test_logout_clears_session- Logout destroys sessiontest_authenticated_user_cannot_access_login_page- Redirect to dashboard
Feature Tests (tests/Feature/Auth/AuthorizationTest.php)
Middleware:
test_admin_can_access_admin_routes- Admin passesadminmiddlewaretest_client_cannot_access_admin_routes- Client gets 403test_unauthenticated_user_redirected_to_login- Guest redirectedtest_deactivated_user_logged_out_on_request- Active middleware works
Unit Tests (tests/Unit/Models/UserHelperMethodsTest.php)
Helper Methods:
test_isAdmin_returns_true_for_admin_usertest_isAdmin_returns_false_for_client_usertest_isClient_returns_true_for_individual_usertest_isClient_returns_true_for_company_usertest_isClient_returns_false_for_admin_usertest_isActive_returns_true_for_active_usertest_isActive_returns_false_for_deactivated_user
Test Patterns
// tests/Feature/Auth/LoginTest.php
use App\Models\User;
use App\Enums\UserType;
use App\Enums\UserStatus;
use Livewire\Volt\Volt;
test('admin redirects to admin dashboard after login', function () {
$admin = User::factory()->create([
'user_type' => UserType::Admin,
'status' => UserStatus::Active,
]);
$response = $this->post('/login', [
'email' => $admin->email,
'password' => 'password',
]);
$response->assertRedirect('/admin/dashboard');
$this->assertAuthenticatedAs($admin);
});
test('deactivated user cannot login', function () {
$user = User::factory()->create([
'status' => UserStatus::Deactivated,
]);
$response = $this->post('/login', [
'email' => $user->email,
'password' => 'password',
]);
$this->assertGuest();
});
test('client cannot access admin routes', function () {
$client = User::factory()->create([
'user_type' => UserType::Individual,
]);
$this->actingAs($client)
->get('/admin/dashboard')
->assertForbidden();
});
Definition of Done
- Login page renders correctly in both languages
- Users can log in with valid credentials
- Invalid credentials show proper error (bilingual)
- Deactivated users cannot log in
- Rate limiting prevents brute force (5 attempts/minute)
- Session expires after 2 hours inactivity
- Admin routes protected by
adminmiddleware (403 for clients) - Failed login attempts logged to
admin_logs - All Feature and Unit tests pass
- Code formatted with
vendor/bin/pint
Dependencies
- Story 1.1: Database schema - provides
userstable withuser_typeenum,statusenum, andpreferred_languagefield - Story 1.3: Bilingual infrastructure - provides translation files and locale switching (can be developed in parallel)
Risk Assessment
| Risk | Impact | Likelihood | Mitigation |
|---|---|---|---|
| Security misconfiguration | High | Low | Use Laravel's built-in security features |
| Rate limiting bypass | Medium | Low | Use Fortify's built-in rate limiter |
| Session hijacking | High | Low | Use secure session configuration |
| Middleware registration issues | Low | Medium | Follow architecture Section 7.4 exactly |
Rollback Plan
- Restore default Fortify configuration
- Remove custom middleware
- Revert User model changes
Dev Agent Record
Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101)
File List
New Files:
app/Http/Middleware/EnsureUserIsAdmin.phpapp/Http/Middleware/EnsureUserIsActive.phpapp/Http/Middleware/SetLocale.phpapp/Http/Responses/LoginResponse.phpapp/Http/Responses/VerifyEmailResponse.phpapp/Listeners/LogFailedLoginAttempt.phplang/en/auth.phplang/ar/auth.phplang/en/messages.phplang/ar/messages.phpresources/views/livewire/admin/dashboard-placeholder.blade.phpresources/views/livewire/client/dashboard-placeholder.blade.phptests/Feature/Auth/AuthorizationTest.phpdatabase/migrations/2025_12_26_115421_make_admin_id_nullable_in_admin_logs_table.php
Modified Files:
config/fortify.php- Disabled registration, enabled all other featuresapp/Models/User.php- AddedisActive()methodapp/Providers/FortifyServiceProvider.php- Added custom auth, login response, email verify responseapp/Providers/AppServiceProvider.php- Registered failed login event listenerbootstrap/app.php- Registered middleware aliases (admin, active) and SetLocaleroutes/web.php- Added admin/client dashboard routes with middlewareresources/views/livewire/auth/login.blade.php- Added error message displayresources/views/components/layouts/app/sidebar.blade.php- Fixed dashboard route and user nametests/Feature/Auth/AuthenticationTest.php- Updated for role-based redirectstests/Feature/Auth/RegistrationTest.php- Updated for disabled registrationtests/Feature/Auth/EmailVerificationTest.php- Updated for role-based redirectstests/Feature/DashboardTest.php- Updated for role-based dashboardstests/Unit/Models/UserTest.php- Added isActive() tests
Completion Notes
- All 121 tests pass
- Registration feature disabled per AC 7
- Role-based redirects implemented (admin → /admin/dashboard, client → /client/dashboard)
- Custom middleware for admin authorization and active user checking
- Failed login attempts logged to admin_logs table
- Bilingual support with SetLocale middleware and translation files
- Rate limiting configured (5 attempts per minute via Fortify)
- Session timeout set to 2 hours (SESSION_LIFETIME=120)
- Note: Created migration to make admin_id nullable in admin_logs table for failed login logging
Future Recommendations (from QA Review)
- Add
verifiedmiddleware to dashboard routes: Perarchitecture.mdSection 7.5, routes should use['auth', 'verified', 'active']middleware. Currently using['auth', 'active']. Addverifiedmiddleware inroutes/web.php:11when email verification flow is fully implemented and tested in future epics. This ensures only email-verified users can access protected routes.
Change Log
| Date | Version | Description | Author |
|---|---|---|---|
| Dec 21, 2025 | 1.0 | Initial story draft | SM Agent |
| Dec 21, 2025 | 2.0 | Complete rewrite: Added Status, Tasks/Subtasks, Dev Notes with Source Tree, fixed middleware naming (admin vs can:admin), fixed redirect paths (/client/dashboard), added User helper methods, aligned with architecture Section 7, added Change Log | Validation Task |
| Dec 26, 2025 | 3.0 | Implementation complete: All tasks implemented, 121 tests passing, registration disabled, role-based auth with middleware, failed login logging, bilingual support | Dev Agent (James) |
| Dec 26, 2025 | 3.1 | QA Review PASS: All 21 ACs verified, security review passed, status updated to Done | Quinn (Test Architect) |
QA Results
Review Date: December 26, 2025
Reviewed By: Quinn (Test Architect)
Risk Assessment
Deep review triggered by:
- Auth/security files touched (middleware, login, session handling)
- Story has > 5 acceptance criteria (21 ACs)
- Security-critical implementation
Code Quality Assessment
The implementation demonstrates high-quality, secure, and well-structured code:
-
Middleware Implementation - Clean, focused, single-responsibility middleware classes:
EnsureUserIsAdmin- Properly checks admin role and returns 403 with translated messageEnsureUserIsActive- Correctly logs out deactivated users, invalidates session, regenerates CSRF tokenSetLocale- Appropriately cascades from user preference → session → config default
-
User Model Helpers - Well-implemented helper methods:
isAdmin(),isClient(),isIndividual(),isCompany(),isActive()- All correctly use enum comparisons- Scopes (
scopeAdmins,scopeClients,scopeActive) - Properly implemented for query building
-
Fortify Integration - Properly configured:
- Registration disabled per AC 7
- Custom
authenticateUsingcallback rejects deactivated users at authentication time - Custom
LoginResponseandVerifyEmailResponsehandle role-based redirects - Rate limiting configured at 5 attempts per minute
-
Failed Login Logging -
LogFailedLoginAttemptlistener correctly logs toadmin_logswith IP address -
Bilingual Support - Complete translation files for
auth.phpandmessages.phpin bothenandar
Requirements Traceability (AC → Test Mapping)
| AC# | Requirement | Test Coverage | Status |
|---|---|---|---|
| 1 | Fortify configured with custom Volt views | FortifyServiceProvider.php + manual |
✓ |
| 2 | Login page with bilingual support | login.blade.php with __() helpers |
✓ |
| 3 | Session timeout 2 hours | SESSION_LIFETIME=120 in .env |
✓ |
| 4 | Rate limiting (5 attempts/min) | test_rate_limiting_blocks_after_five_attempts |
✓ |
| 5 | Admin role with full access | test_admin_can_access_admin_routes |
✓ |
| 6 | Client role restricted access | test_client_cannot_access_admin_routes |
✓ |
| 7 | Registration DISABLED | test_registration_is_disabled, test_registration_route_returns_404 |
✓ |
| 8 | CSRF protection | @csrf in login form + Fortify |
✓ |
| 9 | Password hashing bcrypt | Laravel default, 'password' => 'hashed' cast |
✓ |
| 10 | Custom admin middleware | EnsureUserIsAdmin.php |
✓ |
| 11 | Secure session configuration | Laravel defaults + SESSION_LIFETIME | ✓ |
| 12 | Remember me functionality | <flux:checkbox name="remember"> in login |
✓ |
| 13 | Login redirects appropriately | test_admin_redirects_to_admin_dashboard, test_client_redirects_to_client_dashboard |
✓ |
| 14 | Logout clears session | test_logout_clears_session, test_users_can_logout |
✓ |
| 15 | Admin middleware returns 403 | test_client_cannot_access_admin_routes → assertForbidden() |
✓ |
| 16 | Failed login logged to admin_logs | test_failed_login_attempts_are_logged |
✓ |
| 17 | Deactivated users cannot login | test_deactivated_user_cannot_login, test_deactivated_user_logged_out_on_request |
✓ |
| 18 | Login form validates inputs | Fortify built-in validation | ✓ |
| 19 | Error messages bilingual | Translation files in lang/en/auth.php, lang/ar/auth.php |
✓ |
| 20 | All test scenarios pass | 121 tests passing | ✓ |
| 21 | No security vulnerabilities | See Security Review below | ✓ |
Refactoring Performed
No refactoring performed - code quality is already excellent.
Compliance Check
- Coding Standards: ✓ All files pass
vendor/bin/pint --dirty --test - Project Structure: ✓ Follows Laravel 12 conventions (middleware in
app/Http/Middleware/, registration inbootstrap/app.php) - Testing Strategy: ✓ Feature tests for flows, Unit tests for model methods
- All ACs Met: ✓ All 21 acceptance criteria covered
Improvements Checklist
- All implementation complete and tested
- Code formatted with Pint
- Bilingual translations provided
- Rate limiting configured
- Session security configured
- Consider adding
verifiedmiddleware to dashboard routes (per architecture.md Section 7.5) - low priority, can be added when email verification flow is fully tested in future epics
Security Review
PASS - No security vulnerabilities found:
- ✓ CSRF protection enabled (
@csrfin forms) - ✓ Password hashing using bcrypt (Laravel default)
- ✓ Rate limiting prevents brute force attacks (5 attempts/minute)
- ✓ Session regeneration on logout (prevents session fixation)
- ✓ Deactivated users blocked at authentication and via middleware
- ✓ No sensitive data exposure in error messages
- ✓ Generic error message for non-existent users (prevents user enumeration)
- ✓ Admin routes protected with 403 for unauthorized access
- ✓ Failed login attempts logged with IP for audit trail
Performance Considerations
PASS - No performance issues:
- Middleware checks are lightweight (single database field comparisons)
- No N+1 queries in authentication flow
- Session-based authentication (no expensive lookups per request)
Files Modified During Review
None - no modifications made during review.
Gate Status
Gate: PASS → docs/qa/gates/1.2-authentication-role-system.yml
Recommended Status
✓ Ready for Done - All acceptance criteria met, 121 tests passing, no security issues, code quality excellent.