diff --git a/docs/customization.md b/docs/customization.md index 10e6a1da7..e7e2e0d65 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -1,17 +1,18 @@ # Customizing Shield -- [Customizing Shield](#customizing-shield) - - [Route Configuration](#route-configuration) - - [Custom Redirect URLs](#custom-redirect-urls) - - [Customize Login Redirect](#customize-login-redirect) - - [Customize Register Redirect](#customize-register-redirect) - - [Customize Logout Redirect](#customize-logout-redirect) - - [Extending the Controllers](#extending-the-controllers) - - [Integrating Custom View Libraries](#integrating-custom-view-libraries) - - [Custom Validation Rules](#custom-validation-rules) - - [Registration](#registration) - - [Login](#login) - - [Custom User Provider](#custom-user-provider) +- [Customizing Shield](#customizing-shield) + - [Route Configuration](#route-configuration) + - [Custom Redirect URLs](#custom-redirect-urls) + - [Customize Login Redirect](#customize-login-redirect) + - [Customize Register Redirect](#customize-register-redirect) + - [Customize Logout Redirect](#customize-logout-redirect) + - [Extending the Controllers](#extending-the-controllers) + - [Integrating Custom View Libraries](#integrating-custom-view-libraries) + - [Custom Validation Rules](#custom-validation-rules) + - [Registration](#registration) + - [Login](#login) + - [Custom User Provider](#custom-user-provider) + - [Custom Login Identifier](#custom-login-identifier) ## Route Configuration @@ -94,11 +95,11 @@ public function logoutRedirect(): string Shield has the following controllers that can be extended to handle various parts of the authentication process: -- **ActionController** handles the after-login and after-registration actions, like Two Factor Authentication and Email Verification. -- **LoginController** handles the login process. -- **RegisterController** handles the registration process. Overriding this class allows you to customize the User Provider, the User Entity, and the validation rules. -- **MagicLinkController** handles the "lost password" process that allows a user to login with a link sent to their email. This allows you to - override the message that is displayed to a user to describe what is happening, if you'd like to provide more information than simply swapping out the view used. +- **ActionController** handles the after-login and after-registration actions, like Two Factor Authentication and Email Verification. +- **LoginController** handles the login process. +- **RegisterController** handles the registration process. Overriding this class allows you to customize the User Provider, the User Entity, and the validation rules. +- **MagicLinkController** handles the "lost password" process that allows a user to login with a link sent to their email. This allows you to + override the message that is displayed to a user to describe what is happening, if you'd like to provide more information than simply swapping out the view used. It is not recommended to copy the entire controller into **app/Controllers** and change its namespace. Instead, you should create a new controller that extends the existing controller and then only override the methods needed. This allows the other methods to stay up to date with any security @@ -236,3 +237,38 @@ After creating the class, set the `$userProvider` property in **app/Config/Auth. ```php public string $userProvider = \App\Models\UserModel::class; ``` + +## Custom Login Identifier + +If your application has a need to use something other than `email` or `username`, you may specify any valid column within the `users` table that you may have added. This allows you to easily use phone numbers, employee or school IDs, etc as the user identifier. You must implement the following steps to set this up: + +This only works with the Session authenticator. + +1. Create a [migration](http://codeigniter.com/user_guide/dbmgmt/migration.html) that adds a new column to the `users` table. +2. Edit `app/Config/Auth.php` so that the new column you just created is within the `$validFields` array. + + ```php + public array $validFields = [ + 'employee_id' + ]; + ``` + + If you have multiple login forms on your site that use different credentials, you must have all of the valid identifying fields in the array. + + ```php + public array $validFields = [ + 'email', + 'employee_id' + ]; + ``` + > **Warning** + > It is very important for security that if you add a new column for identifier you must write a new **Validation Rules** and then set it using the [custom-validation-rules](https://github.com/codeigniter4/shield/blob/develop/docs/customization.md#custom-validation-rules) description. + +3. Edit the login form to change the name of the default `email` input to the new field name. + + ```php + +
+ +
+ ``` diff --git a/src/Authentication/Authenticators/Session.php b/src/Authentication/Authenticators/Session.php index 923d04cac..4f09b784e 100644 --- a/src/Authentication/Authenticators/Session.php +++ b/src/Authentication/Authenticators/Session.php @@ -264,13 +264,28 @@ private function recordLoginAttempt( string $userAgent, $userId = null ): void { - $idType = (! isset($credentials['email']) && isset($credentials['username'])) - ? self::ID_TYPE_USERNAME - : self::ID_TYPE_EMAIL_PASSWORD; + // Determine the type of ID we're using. + // Standard fields would be email, username, + // but any column within config('Auth')->validFields can be used. + $field = array_intersect(config('Auth')->validFields ?? [], array_keys($credentials)); + + if (count($field) !== 1) { + throw new InvalidArgumentException('Invalid credentials passed to recordLoginAttempt.'); + } + + $field = array_pop($field); + + if (! in_array($field, ['email', 'username'], true)) { + $idType = $field; + } else { + $idType = (! isset($credentials['email']) && isset($credentials['username'])) + ? self::ID_TYPE_USERNAME + : self::ID_TYPE_EMAIL_PASSWORD; + } $this->loginModel->recordLoginAttempt( $idType, - $credentials['email'] ?? $credentials['username'], + $credentials[$field], $success, $ipAddress, $userAgent, diff --git a/tests/Authentication/Authenticators/SessionAuthenticatorTest.php b/tests/Authentication/Authenticators/SessionAuthenticatorTest.php index e3d87d623..942c9df7e 100644 --- a/tests/Authentication/Authenticators/SessionAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/SessionAuthenticatorTest.php @@ -4,6 +4,7 @@ namespace Tests\Authentication\Authenticators; +use CodeIgniter\Config\Factories; use CodeIgniter\Shield\Authentication\Authentication; use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\Authenticators\Session; @@ -404,6 +405,12 @@ public function testAttemptCaseInsensitive(): void 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); + /** @var User $user */ $user = fake(UserModel::class, ['username' => 'foorog']); $user->createEmailIdentity([ @@ -433,4 +440,43 @@ public function testAttemptUsernameOnly(): void '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('auth_logins', [ + 'id_type' => 'status', + 'identifier' => '12345', + 'success' => 1, + ]); + } }