Skip to content

Commit 5b8e7a5

Browse files
chore: Add PR's requested changes
1 parent c6774b3 commit 5b8e7a5

2 files changed

Lines changed: 35 additions & 138 deletions

File tree

app/libs/Auth/AuthService.php

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ final class AuthService extends AbstractService implements IAuthService
9898
* @param IAuthUserService $auth_user_service
9999
* @param ISecurityContextService $security_context_service
100100
* @param ITransactionService $tx_service
101+
* @params ISecurityContextService $security_context_service
101102
*/
102103
public function __construct
103104
(
@@ -394,10 +395,11 @@ public function login(string $username, string $password, bool $remember_me): bo
394395
{
395396
Log::debug("AuthService::login");
396397

398+
$this->last_login_error = "";
397399
if (!Auth::attempt(['username' => $username, 'password' => $password], $remember_me)) {
398400
throw new AuthenticationException
399401
(
400-
"username or password does not match an existing record."
402+
"We are sorry, your username or password does not match an existing record."
401403
);
402404
}
403405
Log::debug("AuthService::login: clearing principal");
@@ -406,7 +408,7 @@ public function login(string $username, string $password, bool $remember_me): bo
406408
if (is_null($current_user) || !$current_user->canLogin())
407409
throw new AuthenticationException
408410
(
409-
"username or password does not match an existing record."
411+
"We are sorry, your username or password does not match an existing record."
410412
);
411413
$this->principal_service->register
412414
(
@@ -420,35 +422,19 @@ public function login(string $username, string $password, bool $remember_me): bo
420422
/**
421423
* @param string $username
422424
* @param string $password
423-
* @return User
425+
* @return User|null
424426
* @throws AuthenticationException
425427
*/
426428
public function validateCredentials(string $username, string $password): User
427429
{
428430
Log::debug("AuthService::validateCredentials");
429431

430-
// retrieveByCredentials swallows AuthenticationLockedUserLoginAttempt and returns null,
431-
// so pre-check lock state here to surface a distinct message for locked accounts.
432-
$existing = $this->user_repository->getByEmailOrName($username);
433-
if (!is_null($existing) && !$existing->isActive()) {
434-
throw new AuthenticationException(
435-
sprintf("User %s is locked.", $username)
436-
);
437-
}
438-
439-
// Known cost: retrieveByCredentials() calls user_repository->getByEmailOrName() internally
440-
// (CustomAuthProvider line ~122), duplicating the query above. Eliminating it would require
441-
// either changing the provider API to accept a pre-fetched User, or moving
442-
// LockUserCounterMeasure checkpoint logic out of the provider — both out of scope here.
443-
$user = Auth::getProvider()->retrieveByCredentials([
444-
'username' => $username,
445-
'password' => $password,
446-
]);
447-
448-
if (is_null($user) || !$user instanceof User || !$user->canLogin()) {
449-
throw new AuthenticationException(
450-
"username or password does not match an existing record."
451-
);
432+
/**
433+
* @var User|null $user
434+
*/
435+
$user = Auth::getProvider()->retrieveByCredentials(['username' => $username, 'password' => $password]);
436+
if (!$user) {
437+
throw new AuthenticationException();
452438
}
453439

454440
return $user;
@@ -462,6 +448,8 @@ public function validateCredentials(string $username, string $password): User
462448
public function loginUser(User $user, bool $remember): void
463449
{
464450
Log::debug("AuthService::loginUser");
451+
if (!$user->canLogin())
452+
throw new AuthenticationException("User is not active or cannot login.");
465453
Auth::login($user, $remember);
466454
}
467455

tests/unit/AuthServiceValidateCredentialsTest.php

Lines changed: 22 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
use App\libs\OAuth2\Repositories\IOAuth2OTPRepository;
1616
use Auth\AuthService;
17+
use Auth\CustomAuthProvider;
1718
use Auth\Exceptions\AuthenticationException;
1819
use Auth\Repositories\IUserRepository;
1920
use Mockery;
@@ -89,16 +90,9 @@ public function testValidCredentials_returnsUser_withoutEstablishingSession(): v
8990
$username = 'jane.doe';
9091
$password = 'Str0ng!Pass';
9192

92-
$this->mock_user_repository
93-
->expects($this->once())
94-
->method('getByEmailOrName')
95-
->with($username)
96-
->willReturn(null);
97-
9893
$resolved_user = Mockery::mock('Auth\User');
99-
$resolved_user->shouldReceive('canLogin')->andReturn(true);
10094

101-
$provider_mock = Mockery::mock('Illuminate\Contracts\Auth\UserProvider');
95+
$provider_mock = Mockery::mock(CustomAuthProvider::class);
10296
$provider_mock->shouldReceive('retrieveByCredentials')
10397
->once()
10498
->with(['username' => $username, 'password' => $password])
@@ -122,13 +116,7 @@ public function testInvalidCredentials_throwsAuthenticationException(): void
122116
$username = 'jane.doe';
123117
$password = 'wrong';
124118

125-
$this->mock_user_repository
126-
->expects($this->once())
127-
->method('getByEmailOrName')
128-
->with($username)
129-
->willReturn(null);
130-
131-
$provider_mock = Mockery::mock('Illuminate\Contracts\Auth\UserProvider');
119+
$provider_mock = Mockery::mock(CustomAuthProvider::class);
132120
$provider_mock->shouldReceive('retrieveByCredentials')
133121
->once()
134122
->with(['username' => $username, 'password' => $password])
@@ -143,110 +131,13 @@ public function testInvalidCredentials_throwsAuthenticationException(): void
143131
$this->service->validateCredentials($username, $password);
144132
}
145133

146-
/**
147-
* Provider returns a User whose canLogin() is false — must throw AuthenticationException.
148-
* This guards against future providers or provider changes that bypass the internal canLogin()
149-
* check inside CustomAuthProvider::retrieveByCredentials().
150-
*/
151-
public function testProviderReturnsUserThatCannotLogin_throwsAuthenticationException(): void
152-
{
153-
$username = 'jane.doe';
154-
$password = 'Str0ng!Pass';
155-
156-
// Pre-check: user not found in repository, so the locked-account short-circuit is not taken.
157-
$this->mock_user_repository
158-
->expects($this->once())
159-
->method('getByEmailOrName')
160-
->with($username)
161-
->willReturn(null);
162-
163-
// Provider returns a valid User instance, but canLogin() is false.
164-
$non_loginable_user = Mockery::mock('Auth\User');
165-
$non_loginable_user->shouldReceive('canLogin')->andReturn(false);
166-
167-
$provider_mock = Mockery::mock('Illuminate\Contracts\Auth\UserProvider');
168-
$provider_mock->shouldReceive('retrieveByCredentials')
169-
->once()
170-
->with(['username' => $username, 'password' => $password])
171-
->andReturn($non_loginable_user);
172-
173-
$this->auth_mock->shouldReceive('getProvider')->once()->andReturn($provider_mock);
174-
$this->auth_mock->shouldNotReceive('login');
175-
$this->auth_mock->shouldNotReceive('attempt');
176-
177-
$this->expectException(AuthenticationException::class);
178-
179-
$this->service->validateCredentials($username, $password);
180-
}
181-
182-
/**
183-
* A user that exists but is inactive (locked) short-circuits the password check
184-
* and throws AuthenticationException with a "is locked" message.
185-
*/
186-
public function testLockedAccount_throwsAuthenticationException_withLockedMessage(): void
187-
{
188-
$username = 'locked.user';
189-
190-
$locked_user = Mockery::mock('Auth\User');
191-
$locked_user->shouldReceive('isActive')->andReturn(false);
192-
193-
$this->mock_user_repository
194-
->expects($this->once())
195-
->method('getByEmailOrName')
196-
->with($username)
197-
->willReturn($locked_user);
198-
199-
// Provider must NOT be consulted when the user is locked.
200-
$this->auth_mock->shouldNotReceive('getProvider');
201-
$this->auth_mock->shouldNotReceive('login');
202-
$this->auth_mock->shouldNotReceive('attempt');
203-
204-
$this->expectException(AuthenticationException::class);
205-
$this->expectExceptionMessageMatches('/is locked/i');
206-
207-
$this->service->validateCredentials($username, 'irrelevant');
208-
}
209-
210-
/**
211-
* When the existing user is active, the locked-path is not taken:
212-
* the provider is consulted and the resolved User is returned.
213-
*/
214-
public function testActiveUser_doesNotTriggerLockedPath(): void
215-
{
216-
$username = 'jane.doe';
217-
$password = 'Str0ng!Pass';
218-
219-
$active_user = Mockery::mock('Auth\User');
220-
$active_user->shouldReceive('isActive')->andReturn(true);
221-
222-
$this->mock_user_repository
223-
->expects($this->once())
224-
->method('getByEmailOrName')
225-
->with($username)
226-
->willReturn($active_user);
227-
228-
$resolved_user = Mockery::mock('Auth\User');
229-
$resolved_user->shouldReceive('canLogin')->andReturn(true);
230-
231-
$provider_mock = Mockery::mock('Illuminate\Contracts\Auth\UserProvider');
232-
$provider_mock->shouldReceive('retrieveByCredentials')
233-
->once()
234-
->andReturn($resolved_user);
235-
236-
$this->auth_mock->shouldReceive('getProvider')->once()->andReturn($provider_mock);
237-
$this->auth_mock->shouldNotReceive('login');
238-
239-
$returned = $this->service->validateCredentials($username, $password);
240-
241-
$this->assertSame($resolved_user, $returned);
242-
}
243-
244134
/**
245135
* loginUser(user, true) delegates to Auth::login with the remember flag set.
246136
*/
247137
public function testLoginUser_callsAuthLogin_withRememberTrue(): void
248138
{
249139
$user = Mockery::mock('Auth\User');
140+
$user->shouldReceive('canLogin')->andReturn(true);
250141

251142
$this->auth_mock
252143
->shouldReceive('login')
@@ -262,6 +153,7 @@ public function testLoginUser_callsAuthLogin_withRememberTrue(): void
262153
public function testLoginUser_callsAuthLogin_withRememberFalse(): void
263154
{
264155
$user = Mockery::mock('Auth\User');
156+
$user->shouldReceive('canLogin')->andReturn(true);
265157

266158
$this->auth_mock
267159
->shouldReceive('login')
@@ -270,4 +162,21 @@ public function testLoginUser_callsAuthLogin_withRememberFalse(): void
270162

271163
$this->service->loginUser($user, false);
272164
}
165+
166+
/**
167+
* loginUser(user, [true|false]) and isActive or canLogin false throws an Exception.
168+
*/
169+
public function testLoginUser_throwsException_whenIsNotActive(): void
170+
{
171+
$user = Mockery::mock('Auth\User');
172+
$user->shouldReceive('canLogin')->andReturn(false);
173+
174+
$this->auth_mock->shouldNotReceive('login');
175+
176+
$this->expectException(AuthenticationException::class);
177+
$this->expectExceptionMessageMatches('/User is not active or cannot login\./');
178+
179+
$this->service->loginUser($user, true);
180+
}
181+
273182
}

0 commit comments

Comments
 (0)