* * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Tests\Authentication\Authenticators; use CodeIgniter\Config\Factories; use CodeIgniter\Shield\Authentication\Authentication; use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\Authenticators\Session; use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\LogicException; use CodeIgniter\Shield\Models\RememberModel; use CodeIgniter\Shield\Models\UserModel; use CodeIgniter\Shield\Result; use CodeIgniter\Test\Mock\MockEvents; use Config\Services; use Tests\Support\DatabaseTestCase; use Tests\Support\FakeUser; /** * @internal */ final class SessionAuthenticatorTest extends DatabaseTestCase { use FakeUser; private Session $auth; protected function setUp(): void { parent::setUp(); $config = new Auth(); $auth = new Authentication($config); $auth->setProvider(model(UserModel::class)); /** @var Session $authenticator */ $authenticator = $auth->factory('session'); $this->auth = $authenticator; $events = new MockEvents(); Services::injectMock('events', $events); $this->db->table($this->tables['identities'])->truncate(); } public function testLoggedInFalse(): void { $this->assertFalse($this->auth->loggedIn()); } public function testLoggedInTrue(): void { $_SESSION['user']['id'] = $this->user->id; $this->assertTrue($this->auth->loggedIn()); $authUser = $this->auth->getUser(); $this->assertInstanceOf(User::class, $authUser); $this->assertSame($this->user->id, $authUser->id); } public function testLoggedInAsDeletedUserWithSessionData(): void { $_SESSION['user']['id'] = $this->user->id; // Delete the user. $users = model(UserModel::class); $users->delete($this->user->id); $loggedIn = $this->auth->loggedIn(); $this->assertFalse($loggedIn); $this->assertNull(session()->get('user')); } public function testLoggedInWithRememberCookie(): void { unset($_SESSION['user']); // Set Cookie.prefix $cookiePrefix = 'prefix_'; setting('Cookie.prefix', $cookiePrefix); $this->user->createEmailIdentity(['email' => 'foo@example.com', 'password' => 'secret']); // Insert remember-me token. $rememberModel = model(RememberModel::class); $selector = 'selector'; $validator = 'validator'; $expires = date('Y-m-d H:i:s', time() + setting('Auth.sessionConfig')['rememberLength']); $rememberModel->rememberUser($this->user, $selector, hash('sha256', $validator), $expires); // Set Cookie value for remember-me. $token = $selector . ':' . $validator; $cookieName = $cookiePrefix . setting('Auth.sessionConfig')['rememberCookieName']; service('superglobals')->setCookie($cookieName, $token); $this->assertTrue($this->auth->loggedIn()); $authUser = $this->auth->getUser(); $this->assertInstanceOf(User::class, $authUser); $this->assertSame($this->user->id, $authUser->id); // Forget Cookie.prefix setting()->forget('Cookie.prefix'); } public function testLoggedInAsDeletedUserWithRememberCookie(): void { unset($_SESSION['user']); $this->user->createEmailIdentity(['email' => 'foo@example.com', 'password' => 'secret']); // Insert remember-me token. $rememberModel = model(RememberModel::class); $selector = 'selector'; $validator = 'validator'; $expires = date('Y-m-d H:i:s', time() + setting('Auth.sessionConfig')['rememberLength']); $rememberModel->rememberUser($this->user, $selector, hash('sha256', $validator), $expires); // Set Cookie value for remember-me. $token = $selector . ':' . $validator; service('superglobals')->setCookie('remember', $token); // Delete the user. $users = model(UserModel::class); $users->delete($this->user->id); $loggedIn = $this->auth->loggedIn(); $this->assertFalse($loggedIn); } public function testLoginNoRemember(): void { $this->user->createEmailIdentity(['email' => 'foo@example.com', 'password' => 'secret']); $this->auth->login($this->user); $this->assertSame($this->user->id, $_SESSION['user']['id']); $this->dontSeeInDatabase($this->tables['remember_tokens'], [ 'user_id' => $this->user->id, ]); } public function testLoginWithRemember(): void { $this->user->createEmailIdentity(['email' => 'foo@example.com', 'password' => 'secret']); $this->auth->remember()->login($this->user); $this->assertSame($this->user->id, $_SESSION['user']['id']); $this->seeInDatabase($this->tables['remember_tokens'], [ 'user_id' => $this->user->id, ]); // Cookie should have been set $response = service('response'); $this->assertNotNull($response->getCookie('remember')); } public function testLogout(): void { $this->user->createEmailIdentity(['email' => 'foo@example.com', 'password' => 'secret']); $this->auth->remember()->login($this->user); $this->seeInDatabase($this->tables['remember_tokens'], ['user_id' => $this->user->id]); $this->auth->logout(); $this->assertArrayNotHasKey('user', $_SESSION); $this->dontSeeInDatabase($this->tables['remember_tokens'], ['user_id' => $this->user->id]); } public function testLogoutOnlyLogoutCalled(): void { $_SESSION['user']['id'] = $this->user->id; $this->auth->logout(); $this->assertArrayNotHasKey('user', $_SESSION); } public function testLoginByIdBadUser(): void { $this->expectException(AuthenticationException::class); $this->expectExceptionMessage(lang('Auth.invalidUser')); $this->auth->loginById(123); } public function testLoginById(): void { $this->user->createEmailIdentity(['email' => 'foo@example.com', 'password' => 'secret']); $this->auth->loginById($this->user->id); $this->assertSame($this->user->id, $_SESSION['user']['id']); $this->dontSeeInDatabase($this->tables['remember_tokens'], ['user_id' => $this->user->id]); } public function testLoginByIdRemember(): void { $this->user->createEmailIdentity(['email' => 'foo@example.com', 'password' => 'secret']); $this->auth->remember()->loginById($this->user->id); $this->assertSame($this->user->id, $_SESSION['user']['id']); $this->seeInDatabase($this->tables['remember_tokens'], ['user_id' => $this->user->id]); } public function testForgetCurrentUser(): void { $this->user->createEmailIdentity(['email' => 'foo@example.com', 'password' => 'secret']); $this->auth->remember()->loginById($this->user->id); $this->assertSame($this->user->id, $_SESSION['user']['id']); $this->seeInDatabase($this->tables['remember_tokens'], ['user_id' => $this->user->id]); $this->auth->forget(); $this->dontSeeInDatabase($this->tables['remember_tokens'], ['user_id' => $this->user->id]); } public function testForgetAnotherUser(): void { fake(RememberModel::class, ['user_id' => $this->user->id]); $this->seeInDatabase($this->tables['remember_tokens'], ['user_id' => $this->user->id]); $this->auth->forget($this->user); $this->dontSeeInDatabase($this->tables['remember_tokens'], ['user_id' => $this->user->id]); } public function testCheckNoPassword(): void { $result = $this->auth->check([ 'email' => 'johnsmith@example.com', ]); $this->assertInstanceOf(Result::class, $result); $this->assertFalse($result->isOK()); $this->assertSame(lang('Auth.badAttempt'), $result->reason()); } public function testCheckCannotFindUser(): void { $result = $this->auth->check([ 'email' => 'johnsmith@example.com', 'password' => 'secret', ]); $this->assertInstanceOf(Result::class, $result); $this->assertFalse($result->isOK()); $this->assertSame(lang('Auth.badAttempt'), $result->reason()); } public function testCheckBadPassword(): void { $this->user->createEmailIdentity([ 'email' => 'foo@example.com', 'password' => 'secret123', ]); $result = $this->auth->check([ 'email' => $this->user->email, 'password' => 'secret', ]); $this->assertInstanceOf(Result::class, $result); $this->assertFalse($result->isOK()); $this->assertSame(lang('Auth.invalidPassword'), $result->reason()); } public function testCheckSuccess(): void { $this->user->createEmailIdentity([ 'email' => 'foo@example.com', 'password' => 'secret123', ]); $result = $this->auth->check([ 'email' => $this->user->email, 'password' => 'secret123', ]); $this->assertInstanceOf(Result::class, $result); $this->assertTrue($result->isOK()); $foundUser = $result->extraInfo(); $this->assertSame($this->user->id, $foundUser->id); } public function testAttemptCannotFindUser(): void { $result = $this->auth->attempt([ 'email' => 'johnsmith@example.com', 'password' => 'secret', ]); $this->assertInstanceOf(Result::class, $result); $this->assertFalse($result->isOK()); $this->assertSame(lang('Auth.badAttempt'), $result->reason()); // A login attempt should have always been recorded $this->seeInDatabase($this->tables['logins'], [ 'identifier' => 'johnsmith@example.com', 'success' => 0, ]); } public function testAttemptSuccess(): void { $this->user->createEmailIdentity([ 'email' => 'foo@example.com', 'password' => 'secret123', ]); $this->assertArrayNotHasKey('user', $_SESSION); $result = $this->auth->attempt([ 'email' => $this->user->email, 'password' => 'secret123', ]); $this->assertInstanceOf(Result::class, $result); $this->assertTrue($result->isOK()); $foundUser = $result->extraInfo(); $this->assertSame($this->user->id, $foundUser->id); $this->assertArrayHasKey('id', $_SESSION['user']); $this->assertSame($this->user->id, $_SESSION['user']['id']); // A login attempt should have been recorded $this->seeInDatabase($this->tables['logins'], [ 'identifier' => $this->user->email, 'success' => 1, ]); } public function testAttemptUserHavingSessionDataAttemptsAgain(): void { $_SESSION['user']['id'] = '999'; $this->expectException(LogicException::class); $this->expectExceptionMessage( 'The user has User Info in Session, so already logged in or in pending login state.', ); $this->user->createEmailIdentity([ 'email' => 'foo@example.com', 'password' => 'secret123', ]); $this->auth->attempt([ 'email' => $this->user->email, 'password' => 'secret123', ]); } public function testAttemptCaseInsensitive(): void { $this->user->createEmailIdentity([ 'email' => 'FOO@example.com', 'password' => 'secret123', ]); $this->assertArrayNotHasKey('user', $_SESSION); $result = $this->auth->attempt([ 'email' => 'foo@example.COM', 'password' => 'secret123', ]); $this->assertInstanceOf(Result::class, $result); $this->assertTrue($result->isOK()); $foundUser = $result->extraInfo(); $this->assertSame($this->user->id, $foundUser->id); $this->assertArrayHasKey('id', $_SESSION['user']); $this->assertSame($this->user->id, $_SESSION['user']['id']); // A login attempt should have been recorded $this->seeInDatabase($this->tables['logins'], [ 'identifier' => 'foo@example.COM', 'success' => 1, ]); } public function testAttemptUsernameOnly(): void { // Update our auth config to use the username as a valid field for login. // It is commented out by default. $config = config('Auth'); $config->validFields = ['email', 'username']; Factories::injectMock('config', 'Auth', $config); $user = fake(UserModel::class, ['username' => 'foorog']); $user->createEmailIdentity([ 'email' => 'FOO@example.com', 'password' => 'secret123', ]); $this->assertArrayNotHasKey('user', $_SESSION); $result = $this->auth->attempt([ 'username' => 'fooROG', 'password' => 'secret123', ]); $this->assertInstanceOf(Result::class, $result); $this->assertTrue($result->isOK()); $foundUser = $result->extraInfo(); $this->assertSame($user->id, $foundUser->id); $this->assertArrayHasKey('id', $_SESSION['user']); $this->assertSame($user->id, $_SESSION['user']['id']); // A login attempt should have been recorded $this->seeInDatabase($this->tables['logins'], [ 'identifier' => 'fooROG', 'success' => 1, ]); } /** * Test that any field within the user table can be used as the * login identifier. * * @see https://github.com/codeigniter4/shield/issues/334 */ public function testAttemptCustomField(): void { // We don't need email, but do need a password set.... $this->user->createEmailIdentity([ 'email' => $this->db->getPlatform() === 'OCI8' ? ' ' : '', 'password' => 'secret123', ]); // We don't have any custom fields in the User model, so we'll // just use the status field to represent an employoee ID model(UserModel::class)->set('status', '12345')->update($this->user->id); // Update our auth config to use the status field as a valid field for login $config = config('Auth'); $config->validFields = ['email', 'status']; Factories::injectMock('config', 'Auth', $config); // Should block it $result = $this->auth->attempt(['status' => 'abcde', 'password' => 'secret123']); $this->assertFalse($result->isOK()); $result = $this->auth->attempt(['status' => '12345', 'password' => 'secret123']); $this->assertTrue($result->isOK()); $this->seeInDatabase($this->tables['logins'], [ 'id_type' => 'status', 'identifier' => '12345', 'success' => 1, ]); } }