Testing & TDD

Testing Authentication & Authorization

30 min Lesson 19 of 35

Testing Authentication Systems

Authentication and authorization are critical security components that require thorough testing. Authentication verifies who a user is (login/logout), while authorization determines what they can do (permissions, roles). Properly tested auth systems prevent unauthorized access, data breaches, and privilege escalation attacks.

Key Principle: Never skip testing authentication and authorization logic. These are the gatekeepers of your application's security. A bug in auth can expose sensitive data or allow malicious actions. Test happy paths, edge cases, and security boundaries thoroughly.

Testing User Login

Login tests verify that users can authenticate with valid credentials and are rejected with invalid ones:

Laravel Authentication Testing

<?php namespace Tests\Feature\Auth; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class LoginTest extends TestCase { use RefreshDatabase; public function test_user_can_login_with_valid_credentials() { $user = User::factory()->create([ 'email' => 'john@example.com', 'password' => bcrypt('password123') ]); $response = $this->post('/login', [ 'email' => 'john@example.com', 'password' => 'password123' ]); $response->assertRedirect('/dashboard'); $this->assertAuthenticatedAs($user); } public function test_user_cannot_login_with_invalid_password() { $user = User::factory()->create([ 'email' => 'john@example.com', 'password' => bcrypt('password123') ]); $response = $this->post('/login', [ 'email' => 'john@example.com', 'password' => 'wrongpassword' ]); $response->assertSessionHasErrors('email'); $this->assertGuest(); } public function test_user_cannot_login_with_nonexistent_email() { $response = $this->post('/login', [ 'email' => 'nonexistent@example.com', 'password' => 'password123' ]); $response->assertSessionHasErrors('email'); $this->assertGuest(); } public function test_login_requires_email_and_password() { $response = $this->post('/login', []); $response->assertSessionHasErrors(['email', 'password']); $this->assertGuest(); } public function test_login_validates_email_format() { $response = $this->post('/login', [ 'email' => 'not-an-email', 'password' => 'password123' ]); $response->assertSessionHasErrors('email'); } }
Laravel Auth Assertions:
  • $this->assertAuthenticated() - Verifies user is logged in
  • $this->assertAuthenticatedAs($user) - Verifies specific user is logged in
  • $this->assertGuest() - Verifies user is NOT logged in
  • $this->assertCredentials($credentials) - Verifies credentials match database
  • $this->assertInvalidCredentials($credentials) - Verifies credentials don't match

Testing User Logout

<?php public function test_authenticated_user_can_logout() { $user = User::factory()->create(); $response = $this->actingAs($user)->post('/logout'); $response->assertRedirect('/'); $this->assertGuest(); } public function test_guest_cannot_logout() { $response = $this->post('/logout'); $response->assertRedirect('/login'); } public function test_logout_invalidates_session() { $user = User::factory()->create(); // Login $this->actingAs($user); $sessionId = session()->getId(); // Logout $this->post('/logout'); // Try to use old session $response = $this->withSession(['_token' => $sessionId]) ->get('/dashboard'); $response->assertRedirect('/login'); }

Testing User Registration

<?php public function test_new_users_can_register() { $response = $this->post('/register', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password123', 'password_confirmation' => 'password123' ]); $this->assertDatabaseHas('users', [ 'email' => 'john@example.com' ]); $response->assertRedirect('/dashboard'); $this->assertAuthenticated(); } public function test_registration_requires_password_confirmation() { $response = $this->post('/register', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password123', 'password_confirmation' => 'different' ]); $response->assertSessionHasErrors('password'); $this->assertDatabaseMissing('users', [ 'email' => 'john@example.com' ]); } public function test_registration_enforces_unique_email() { User::factory()->create(['email' => 'john@example.com']); $response = $this->post('/register', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password123', 'password_confirmation' => 'password123' ]); $response->assertSessionHasErrors('email'); } public function test_registration_requires_minimum_password_length() { $response = $this->post('/register', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'short', 'password_confirmation' => 'short' ]); $response->assertSessionHasErrors('password'); }

Testing Middleware Authentication

Middleware tests verify that protected routes require authentication:

<?php public function test_dashboard_requires_authentication() { $response = $this->get('/dashboard'); $response->assertRedirect('/login'); } public function test_authenticated_user_can_access_dashboard() { $user = User::factory()->create(); $response = $this->actingAs($user)->get('/dashboard'); $response->assertOk(); } public function test_api_routes_require_token_authentication() { $response = $this->getJson('/api/user'); $response->assertUnauthorized(); } public function test_verified_middleware_blocks_unverified_users() { $user = User::factory()->unverified()->create(); $response = $this->actingAs($user)->get('/verified-only'); $response->assertRedirect('/verify-email'); } public function test_verified_user_can_access_verified_routes() { $user = User::factory()->create([ 'email_verified_at' => now() ]); $response = $this->actingAs($user)->get('/verified-only'); $response->assertOk(); }

Testing Role-Based Authorization

Role tests verify that users can only perform actions appropriate to their role:

<?php public function test_admin_can_delete_any_post() { $admin = User::factory()->admin()->create(); $post = Post::factory()->create(); $response = $this->actingAs($admin) ->delete("/posts/{$post->id}"); $response->assertStatus(204); $this->assertModelMissing($post); } public function test_regular_user_cannot_delete_other_users_posts() { $user = User::factory()->create(); $otherUser = User::factory()->create(); $post = Post::factory()->for($otherUser)->create(); $response = $this->actingAs($user) ->delete("/posts/{$post->id}"); $response->assertForbidden(); $this->assertModelExists($post); } public function test_user_can_delete_own_posts() { $user = User::factory()->create(); $post = Post::factory()->for($user)->create(); $response = $this->actingAs($user) ->delete("/posts/{$post->id}"); $response->assertStatus(204); $this->assertModelMissing($post); } public function test_moderator_can_edit_any_post() { $moderator = User::factory()->moderator()->create(); $post = Post::factory()->create(); $response = $this->actingAs($moderator) ->put("/posts/{$post->id}", [ 'title' => 'Updated Title', 'content' => 'Updated content' ]); $response->assertOk(); $this->assertDatabaseHas('posts', [ 'id' => $post->id, 'title' => 'Updated Title' ]); }
Security Testing Reminder: Always test both positive and negative cases:
  • Positive: Authorized users CAN perform allowed actions
  • Negative: Unauthorized users CANNOT perform restricted actions
  • Edge cases: Deleted users, suspended accounts, expired permissions
A single missing negative test can create a security vulnerability.

Testing Laravel Policies

Laravel policies centralize authorization logic. Test them thoroughly:

<?php namespace Tests\Unit\Policies; use App\Models\Post; use App\Models\User; use App\Policies\PostPolicy; use Tests\TestCase; class PostPolicyTest extends TestCase { private PostPolicy $policy; protected function setUp(): void { parent::setUp(); $this->policy = new PostPolicy(); } public function test_user_can_view_published_post() { $user = User::factory()->create(); $post = Post::factory()->published()->create(); $this->assertTrue($this->policy->view($user, $post)); } public function test_user_can_view_own_draft() { $user = User::factory()->create(); $post = Post::factory()->draft()->for($user)->create(); $this->assertTrue($this->policy->view($user, $post)); } public function test_user_cannot_view_other_users_draft() { $user = User::factory()->create(); $otherUser = User::factory()->create(); $post = Post::factory()->draft()->for($otherUser)->create(); $this->assertFalse($this->policy->view($user, $post)); } public function test_admin_can_view_any_draft() { $admin = User::factory()->admin()->create(); $post = Post::factory()->draft()->create(); $this->assertTrue($this->policy->view($admin, $post)); } public function test_user_can_update_own_post() { $user = User::factory()->create(); $post = Post::factory()->for($user)->create(); $this->assertTrue($this->policy->update($user, $post)); } public function test_user_cannot_update_other_users_post() { $user = User::factory()->create(); $post = Post::factory()->create(); $this->assertFalse($this->policy->update($user, $post)); } }

Testing API Token Authentication

Test token-based authentication for APIs (Laravel Sanctum, Passport, JWT):

Laravel Sanctum Testing

<?php use Laravel\Sanctum\Sanctum; public function test_authenticated_user_can_access_api() { $user = User::factory()->create(); Sanctum::actingAs($user); $response = $this->getJson('/api/user'); $response->assertOk() ->assertJson([ 'id' => $user->id, 'email' => $user->email ]); } public function test_api_requires_valid_token() { $response = $this->getJson('/api/user'); $response->assertUnauthorized(); } public function test_user_can_create_token() { $user = User::factory()->create(); $response = $this->actingAs($user) ->postJson('/api/tokens', [ 'name' => 'mobile-app' ]); $response->assertOk() ->assertJsonStructure(['token']); $this->assertDatabaseHas('personal_access_tokens', [ 'tokenable_id' => $user->id, 'name' => 'mobile-app' ]); } public function test_token_can_be_revoked() { $user = User::factory()->create(); $token = $user->createToken('test-token'); $response = $this->actingAs($user) ->deleteJson("/api/tokens/{$token->accessToken->id}"); $response->assertNoContent(); $this->assertDatabaseMissing('personal_access_tokens', [ 'id' => $token->accessToken->id ]); } public function test_token_with_abilities_restricts_access() { $user = User::factory()->create(); Sanctum::actingAs($user, ['read']); // Only read ability $response = $this->postJson('/api/posts', [ 'title' => 'Test Post' ]); $response->assertForbidden(); }

Testing Permission Systems

Test granular permission systems (e.g., spatie/laravel-permission):

<?php use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Role; public function test_user_with_permission_can_perform_action() { $user = User::factory()->create(); $permission = Permission::create(['name' => 'delete posts']); $user->givePermissionTo($permission); $post = Post::factory()->create(); $response = $this->actingAs($user) ->delete("/posts/{$post->id}"); $response->assertNoContent(); } public function test_user_without_permission_cannot_perform_action() { $user = User::factory()->create(); $post = Post::factory()->create(); $response = $this->actingAs($user) ->delete("/posts/{$post->id}"); $response->assertForbidden(); } public function test_role_grants_permissions_to_user() { $role = Role::create(['name' => 'editor']); $role->givePermissionTo(['create posts', 'edit posts']); $user = User::factory()->create(); $user->assignRole($role); $this->assertTrue($user->can('create posts')); $this->assertTrue($user->can('edit posts')); $this->assertFalse($user->can('delete posts')); } public function test_direct_permission_overrides_role() { $role = Role::create(['name' => 'viewer']); // Role has no permissions $user = User::factory()->create(); $user->assignRole($role); $user->givePermissionTo('delete posts'); $this->assertTrue($user->can('delete posts')); }

JavaScript Authentication Testing

Test authentication in JavaScript applications:

// tests/auth/login.test.js import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { LoginForm } from '../LoginForm'; import { authService } from '../../services/auth'; jest.mock('../../services/auth'); describe('LoginForm', () => { test('successful login redirects to dashboard', async () => { authService.login.mockResolvedValue({ user: { id: 1, email: 'john@example.com' }, token: 'fake-token' }); render(<LoginForm />); fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'john@example.com' } }); fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'password123' } }); fireEvent.click(screen.getByRole('button', { name: 'Login' })); await waitFor(() => { expect(authService.login).toHaveBeenCalledWith( 'john@example.com', 'password123' ); expect(window.location.pathname).toBe('/dashboard'); }); }); test('invalid credentials show error message', async () => { authService.login.mockRejectedValue({ message: 'Invalid credentials' }); render(<LoginForm />); fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'john@example.com' } }); fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'wrongpassword' } }); fireEvent.click(screen.getByRole('button', { name: 'Login' })); await waitFor(() => { expect(screen.getByText('Invalid credentials')).toBeInTheDocument(); }); }); }); // tests/auth/protected-route.test.js import { render } from '@testing-library/react'; import { ProtectedRoute } from '../ProtectedRoute'; import { useAuth } from '../../hooks/useAuth'; jest.mock('../../hooks/useAuth'); test('redirects to login when not authenticated', () => { useAuth.mockReturnValue({ isAuthenticated: false }); render(<ProtectedRoute component={DashboardPage} />); expect(window.location.pathname).toBe('/login'); }); test('renders component when authenticated', () => { useAuth.mockReturnValue({ isAuthenticated: true, user: { id: 1, email: 'john@example.com' } }); const { getByText } = render( <ProtectedRoute component={() => <div>Dashboard</div>} /> ); expect(getByText('Dashboard')).toBeInTheDocument(); });

Practical Exercise

Exercise 1: Test a complete authentication flow

<?php // Create tests for: // 1. User registration (valid/invalid inputs) // 2. Email verification requirement // 3. Login with verified email // 4. Login blocked for unverified users // 5. Password reset request // 6. Password reset with valid token // 7. Password reset with expired token // Hint: Use Laravel\Fortify or Breeze as reference

Exercise 2: Test a multi-role authorization system

// Roles: Admin, Editor, Author, Subscriber // Test matrix: // - Admin: Can do everything // - Editor: Can edit any post, publish any post // - Author: Can create/edit own posts, cannot publish // - Subscriber: Can only read published posts // Write tests verifying each role's permissions

Exercise 3: Test API authentication with rate limiting

// Test: // 1. API endpoint requires authentication // 2. Valid token grants access // 3. Invalid token returns 401 // 4. Expired token returns 401 // 5. Rate limit blocks after N requests // 6. Rate limit resets after time window