Skip to content

Commit 63ed7e0

Browse files
committed
feat: enhance authentication logic to throw specific exceptions for login failures
1 parent da32bbe commit 63ed7e0

File tree

8 files changed

+51
-22
lines changed

8 files changed

+51
-22
lines changed

contexts/Authorization/Application/Coordinators/AuthenticationCoordinator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function __construct(
3131

3232
public function login(LoginDTO $dto)
3333
{
34-
$user = $this->userRepository->getByEmail($dto->email);
34+
$user = $this->userRepository->getByEmailOrThrowAuthFailure($dto->email);
3535
$user->authenticate($dto->password);
3636

3737
$token = $this->userRepository->generateLoginToken($user);

contexts/Authorization/Domain/Repositories/UserRepository.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function changePassword(UserIdentity $user): void;
2424

2525
public function existsByEmail(string $email): bool;
2626

27-
public function getByEmail(string $email): UserIdentity;
27+
public function getByEmailOrThrowAuthFailure(string $email): UserIdentity;
2828

2929
public function generateLoginToken(UserIdentity $user): string;
3030
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Contexts\Authorization\Domain\UserIdentity\Exceptions;
6+
7+
use App\Exceptions\BizException;
8+
9+
class AuthenticationFailureException extends BizException
10+
{
11+
public static function make(string $message = ''): self
12+
{
13+
$message = $message ?: 'Invalid login credentials or account access restricted';
14+
return (new static($message))->code(401);
15+
}
16+
}

contexts/Authorization/Domain/UserIdentity/Models/UserIdentity.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
namespace Contexts\Authorization\Domain\UserIdentity\Models;
66

7-
use App\Exceptions\BizException;
87
use Carbon\CarbonImmutable;
98
use Contexts\Authorization\Domain\Role\Models\RoleId;
109
use Contexts\Authorization\Domain\UserIdentity\Events\PasswordChangedEvent;
1110
use Contexts\Authorization\Domain\UserIdentity\Events\RoleAssignedEvent;
1211
use Contexts\Authorization\Domain\UserIdentity\Events\RoleRemovedEvent;
1312
use Contexts\Shared\Domain\BaseDomainModel;
1413
use Contexts\Authorization\Domain\UserIdentity\Events\UserAuthenticatedEvent;
14+
use Contexts\Authorization\Domain\UserIdentity\Exceptions\AuthenticationFailureException;
1515

1616
class UserIdentity extends BaseDomainModel
1717
{
@@ -55,15 +55,13 @@ public function syncRoles(RoleIdCollection $newRoles): void
5555
public function authenticate(string $plainTextPassword): void
5656
{
5757
if ($this->status->isActive() === false) {
58-
throw BizException::make('Invalid login credentials or account access restricted')
59-
->code(401)
58+
throw AuthenticationFailureException::make()
6059
->logMessage('User account is not active')
6160
->logContext($this->getUserSummary());
6261
}
6362

6463
if (! $this->password->verify($plainTextPassword)) {
65-
throw BizException::make('Invalid login credentials or account access restricted')
66-
->code(401)
64+
throw AuthenticationFailureException::make()
6765
->logMessage('Invalid password')
6866
->logContext($this->getUserSummary());
6967
}

contexts/Authorization/Infrastructure/Persistence/UserPersistence.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Contexts\Authorization\Infrastructure\Records\UserRecord;
1414
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
1515
use Illuminate\Database\Eloquent\ModelNotFoundException;
16+
use Contexts\Authorization\Domain\UserIdentity\Exceptions\AuthenticationFailureException;
1617

1718
class UserPersistence implements UserRepository
1819
{
@@ -102,16 +103,28 @@ public function existsByEmail(string $email): bool
102103
return UserRecord::where('email', $email)->exists();
103104
}
104105

105-
public function getByEmail(string $email): UserIdentity
106+
public function getByEmailOrThrowAuthFailure(string $email): UserIdentity
106107
{
107-
$record = UserRecord::where('email', $email)->firstOrFail();
108+
try {
109+
$record = UserRecord::where('email', $email)->firstOrFail();
110+
} catch (ModelNotFoundException $e) {
111+
throw AuthenticationFailureException::make()
112+
->logMessage('User not found')
113+
->logContext(['email' => $email]);
114+
}
108115

109116
return $record->toDomain();
110117
}
111118

112119
public function generateLoginToken(UserIdentity $user): string
113120
{
114-
$record = UserRecord::findOrFail($user->getId()->getValue());
121+
try {
122+
$record = UserRecord::findOrFail($user->getId()->getValue());
123+
} catch (ModelNotFoundException $e) {
124+
throw AuthenticationFailureException::make()
125+
->logMessage('User not found')
126+
->logContext(['user_id' => $user->getId()->getValue()]);
127+
}
115128

116129
return $record->createToken('login', ['*'], now()->addDay())->plainTextToken;
117130
}

contexts/Authorization/Tests/Feature/AuthenticationTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@
7373
'password' => 'password',
7474
]);
7575

76-
$response->assertStatus(404);
77-
// $response->assertJson([
78-
// 'message' => 'Invalid login credentials or account access restricted',
79-
// ]);
76+
$response->assertStatus(401);
77+
$response->assertJson([
78+
'message' => 'Invalid login credentials or account access restricted',
79+
]);
8080
});

contexts/Authorization/Tests/Feature/Infrastructure/Persistence/UserPersistenceTest.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Contexts\Authorization\Infrastructure\Records\RoleRecord;
1717
use Contexts\Authorization\Infrastructure\Records\UserRecord;
1818
use Illuminate\Database\Eloquent\ModelNotFoundException;
19+
use Contexts\Authorization\Domain\UserIdentity\Exceptions\AuthenticationFailureException;
1920

2021
beforeEach(function () {
2122
$this->userLabelUniquenessService = mock(UserEmailUniquenessService::class);
@@ -463,7 +464,7 @@
463464
$savedUser = $userPersistence->create($createdUser);
464465

465466
// Retrieve the user using getByEmail
466-
$retrievedUser = $userPersistence->getByEmail('retrieve-by-email@example.com');
467+
$retrievedUser = $userPersistence->getByEmailOrThrowAuthFailure('retrieve-by-email@example.com');
467468

468469
// Assert the retrieved user matches the created one
469470
expect($retrievedUser->getId()->getValue())->toBe($savedUser->getId()->getValue());
@@ -472,12 +473,12 @@
472473
expect($retrievedUser->getPassword()->verify('password123'))->toBeTrue();
473474
});
474475

475-
it('throws an exception when retrieving a user with non-existent email', function () {
476+
it('throws an auth failure exception when retrieving a user with non-existent email', function () {
476477
$userPersistence = new UserPersistence();
477478

478479
// Attempt to retrieve a non-existent user by email
479-
$userPersistence->getByEmail('nonexistent-email@example.com');
480-
})->throws(ModelNotFoundException::class);
480+
$userPersistence->getByEmailOrThrowAuthFailure('nonexistent-email@example.com');
481+
})->throws(AuthenticationFailureException::class);
481482

482483
it('can generate a login token for a user', function () {
483484
// Create a test user
@@ -511,4 +512,4 @@
511512

512513
// Attempt to generate a token for a non-existent user
513514
$userPersistence->generateLoginToken($nonExistentUser);
514-
})->throws(ModelNotFoundException::class);
515+
})->throws(AuthenticationFailureException::class);

contexts/Authorization/Tests/Unit/Domain/UserIdentity/Models/UserIdentityTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Contexts\Authorization\Domain\UserIdentity\Models\UserId;
1717
use Contexts\Authorization\Domain\UserIdentity\Models\UserIdentity;
1818
use Contexts\Authorization\Domain\UserIdentity\Models\UserStatus;
19+
use Contexts\Authorization\Domain\UserIdentity\Exceptions\AuthenticationFailureException;
1920

2021
beforeEach(function () {
2122
$this->email = new Email('test@example.com');
@@ -576,11 +577,11 @@
576577
$exception = null;
577578
try {
578579
$user->authenticate($this->plainPassword);
579-
} catch (BizException $e) {
580+
} catch (AuthenticationFailureException $e) {
580581
$exception = $e;
581582
}
582583

583-
expect($exception)->toBeInstanceOf(BizException::class);
584+
expect($exception)->toBeInstanceOf(AuthenticationFailureException::class);
584585
expect($exception->getMessage())->toBe('Invalid login credentials or account access restricted');
585586
});
586587

@@ -599,7 +600,7 @@
599600
$exception = $e;
600601
}
601602

602-
expect($exception)->toBeInstanceOf(BizException::class);
603+
expect($exception)->toBeInstanceOf(AuthenticationFailureException::class);
603604
expect($exception->getMessage())->toBe('Invalid login credentials or account access restricted');
604605
});
605606

0 commit comments

Comments
 (0)