libra/docs/stories/story-1.2-authentication-ro...

16 KiB

Story 1.2: Authentication & Role System

Status

Draft

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

  1. Fortify configured with custom Volt views
  2. Login page with bilingual support (Arabic/English)
  3. Session timeout after 2 hours of inactivity
  4. Rate limiting on login attempts (5 attempts per minute)
  5. Admin role with full access to all features
  6. Client role with restricted access (own data only)
  7. Registration feature DISABLED (admin creates all accounts)

Security Requirements

  1. CSRF protection enabled on all forms
  2. Password hashing using bcrypt
  3. Custom middleware for admin authorization
  4. Secure session configuration
  5. Remember me functionality (optional)

Integration Requirements

  1. Login redirects to appropriate dashboard:
    • Admin users → /admin/dashboard (placeholder until Epic 6)
    • Client users → /client/dashboard (placeholder until Epic 7)
  2. Logout clears session properly and redirects to login page
  3. Admin middleware protects admin-only routes (return 403 for unauthorized)
  4. Failed login attempts logged to admin_logs table
  5. Deactivated users cannot log in

Quality Requirements

  1. Login form validates inputs properly
  2. Error messages are clear and bilingual
  3. All test scenarios in "Testing" section pass
  4. No security vulnerabilities

Tasks / Subtasks

  • Task 1: Configure Fortify (AC: 1, 7)

    • Update config/fortify.php to disable registration feature
    • Keep resetPasswords enabled (admin-triggered only via future Epic)
    • Keep emailVerification, updateProfileInformation, updatePasswords enabled
    • Keep twoFactorAuthentication enabled with confirm options
  • Task 2: Add User Model Helper Methods (AC: 5, 6)

    • Add isAdmin(): bool method to app/Models/User.php
    • Add isClient(): bool method to app/Models/User.php
    • Add isIndividual(): bool method to app/Models/User.php
    • Add isCompany(): bool method to app/Models/User.php
  • 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.php as admin and active
    • Add SetLocale middleware to web group (for bilingual support)
  • Task 4: Create Login Volt Component (AC: 1, 2, 8, 12, 18, 19)

    • Create resources/views/livewire/auth/login.blade.php as class-based Volt component
    • Implement form with email and password fields using Flux UI components
    • Add remember me checkbox
    • Add CSRF token via @csrf or Livewire handling
    • Display validation errors in current locale
    • Add language switcher or respect current locale
  • Task 5: Configure Login Redirect Logic (AC: 13, 14)

    • Update FortifyServiceProvider to set custom login view
    • Create app/Actions/Fortify/RedirectIfAuthenticated.php or configure in AuthenticatedSessionController
    • 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
  • Task 6: Implement Session and Rate Limiting (AC: 3, 4)

    • Set SESSION_LIFETIME=120 in .env (2 hours)
    • Verify Fortify's built-in rate limiting (5 attempts per minute)
    • Ensure rate limit error message uses translation key
  • Task 7: Implement Login Attempt Logging (AC: 16)

    • Create listener for Illuminate\Auth\Events\Failed event
    • Log failed attempts to admin_logs table with IP address
    • Register listener in EventServiceProvider or AppServiceProvider
  • Task 8: Implement Deactivated User Check (AC: 17)

    • Add check in EnsureUserIsActive middleware
    • Or customize Fortify's authenticateUsing callback to reject deactivated users
    • Return bilingual error message for deactivated accounts
  • Task 9: Write Tests (AC: 20)

    • Create tests/Feature/Auth/LoginTest.php with all login flow tests
    • Create tests/Feature/Auth/AuthorizationTest.php for middleware tests
    • Create tests/Unit/Models/UserHelperMethodsTest.php for isAdmin/isClient tests
    • Run all tests and ensure they pass
  • Task 10: Final Verification (AC: 21)

    • Run vendor/bin/pint to format code
    • Verify no security vulnerabilities (CSRF, session fixation, etc.)
    • Test full login flow in browser for both admin and client users

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_deactivated message
  • 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 displays
  • test_user_can_login_with_valid_credentials - Valid credentials authenticate
  • test_admin_redirects_to_admin_dashboard - Admin → /admin/dashboard
  • test_client_redirects_to_client_dashboard - Client → /client/dashboard
  • test_invalid_credentials_show_error - Wrong password shows error
  • test_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 blocked
  • test_rate_limit_resets_after_cooldown - Can retry after 60 seconds

Session Management:

  • test_logout_clears_session - Logout destroys session
  • test_authenticated_user_cannot_access_login_page - Redirect to dashboard

Feature Tests (tests/Feature/Auth/AuthorizationTest.php)

Middleware:

  • test_admin_can_access_admin_routes - Admin passes admin middleware
  • test_client_cannot_access_admin_routes - Client gets 403
  • test_unauthenticated_user_redirected_to_login - Guest redirected
  • test_deactivated_user_logged_out_on_request - Active middleware works

Unit Tests (tests/Unit/Models/UserHelperMethodsTest.php)

Helper Methods:

  • test_isAdmin_returns_true_for_admin_user
  • test_isAdmin_returns_false_for_client_user
  • test_isClient_returns_true_for_individual_user
  • test_isClient_returns_true_for_company_user
  • test_isClient_returns_false_for_admin_user
  • test_isActive_returns_true_for_active_user
  • test_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 admin middleware (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 users table with user_type enum, status enum, and preferred_language field
  • 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

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