Skip to content

Commit f13ce6d

Browse files
fix: Custom login fields should work for logging in. (#629)
* fix: Custom login fields should work for logging in. Fixes #334 * Apply suggestions from code review Co-authored-by: Pooya Parsa Dadashi <pooya_parsa_dadashi@yahoo.com> * fix test for OCI8 --------- Co-authored-by: Pooya Parsa Dadashi <pooya_parsa_dadashi@yahoo.com>
1 parent 5c7e0f8 commit f13ce6d

3 files changed

Lines changed: 118 additions & 21 deletions

File tree

docs/customization.md

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
# Customizing Shield
22

3-
- [Customizing Shield](#customizing-shield)
4-
- [Route Configuration](#route-configuration)
5-
- [Custom Redirect URLs](#custom-redirect-urls)
6-
- [Customize Login Redirect](#customize-login-redirect)
7-
- [Customize Register Redirect](#customize-register-redirect)
8-
- [Customize Logout Redirect](#customize-logout-redirect)
9-
- [Extending the Controllers](#extending-the-controllers)
10-
- [Integrating Custom View Libraries](#integrating-custom-view-libraries)
11-
- [Custom Validation Rules](#custom-validation-rules)
12-
- [Registration](#registration)
13-
- [Login](#login)
14-
- [Custom User Provider](#custom-user-provider)
3+
- [Customizing Shield](#customizing-shield)
4+
- [Route Configuration](#route-configuration)
5+
- [Custom Redirect URLs](#custom-redirect-urls)
6+
- [Customize Login Redirect](#customize-login-redirect)
7+
- [Customize Register Redirect](#customize-register-redirect)
8+
- [Customize Logout Redirect](#customize-logout-redirect)
9+
- [Extending the Controllers](#extending-the-controllers)
10+
- [Integrating Custom View Libraries](#integrating-custom-view-libraries)
11+
- [Custom Validation Rules](#custom-validation-rules)
12+
- [Registration](#registration)
13+
- [Login](#login)
14+
- [Custom User Provider](#custom-user-provider)
15+
- [Custom Login Identifier](#custom-login-identifier)
1516

1617
## Route Configuration
1718

@@ -94,11 +95,11 @@ public function logoutRedirect(): string
9495
Shield has the following controllers that can be extended to handle
9596
various parts of the authentication process:
9697

97-
- **ActionController** handles the after-login and after-registration actions, like Two Factor Authentication and Email Verification.
98-
- **LoginController** handles the login process.
99-
- **RegisterController** handles the registration process. Overriding this class allows you to customize the User Provider, the User Entity, and the validation rules.
100-
- **MagicLinkController** handles the "lost password" process that allows a user to login with a link sent to their email. This allows you to
101-
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.
98+
- **ActionController** handles the after-login and after-registration actions, like Two Factor Authentication and Email Verification.
99+
- **LoginController** handles the login process.
100+
- **RegisterController** handles the registration process. Overriding this class allows you to customize the User Provider, the User Entity, and the validation rules.
101+
- **MagicLinkController** handles the "lost password" process that allows a user to login with a link sent to their email. This allows you to
102+
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.
102103

103104
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
104105
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.
236237
```php
237238
public string $userProvider = \App\Models\UserModel::class;
238239
```
240+
241+
## Custom Login Identifier
242+
243+
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:
244+
245+
This only works with the Session authenticator.
246+
247+
1. Create a [migration](http://codeigniter.com/user_guide/dbmgmt/migration.html) that adds a new column to the `users` table.
248+
2. Edit `app/Config/Auth.php` so that the new column you just created is within the `$validFields` array.
249+
250+
```php
251+
public array $validFields = [
252+
'employee_id'
253+
];
254+
```
255+
256+
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.
257+
258+
```php
259+
public array $validFields = [
260+
'email',
261+
'employee_id'
262+
];
263+
```
264+
> **Warning**
265+
> 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.
266+
267+
3. Edit the login form to change the name of the default `email` input to the new field name.
268+
269+
```php
270+
<!-- Email -->
271+
<div class="mb-2">
272+
<input type="text" class="form-control" name="employee_id" autocomplete="new-employee-id" placeholder="12345" value="<?= old('employee_id') ?>" required />
273+
</div>
274+
```

src/Authentication/Authenticators/Session.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,28 @@ private function recordLoginAttempt(
264264
string $userAgent,
265265
$userId = null
266266
): void {
267-
$idType = (! isset($credentials['email']) && isset($credentials['username']))
268-
? self::ID_TYPE_USERNAME
269-
: self::ID_TYPE_EMAIL_PASSWORD;
267+
// Determine the type of ID we're using.
268+
// Standard fields would be email, username,
269+
// but any column within config('Auth')->validFields can be used.
270+
$field = array_intersect(config('Auth')->validFields ?? [], array_keys($credentials));
271+
272+
if (count($field) !== 1) {
273+
throw new InvalidArgumentException('Invalid credentials passed to recordLoginAttempt.');
274+
}
275+
276+
$field = array_pop($field);
277+
278+
if (! in_array($field, ['email', 'username'], true)) {
279+
$idType = $field;
280+
} else {
281+
$idType = (! isset($credentials['email']) && isset($credentials['username']))
282+
? self::ID_TYPE_USERNAME
283+
: self::ID_TYPE_EMAIL_PASSWORD;
284+
}
270285

271286
$this->loginModel->recordLoginAttempt(
272287
$idType,
273-
$credentials['email'] ?? $credentials['username'],
288+
$credentials[$field],
274289
$success,
275290
$ipAddress,
276291
$userAgent,

tests/Authentication/Authenticators/SessionAuthenticatorTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tests\Authentication\Authenticators;
66

7+
use CodeIgniter\Config\Factories;
78
use CodeIgniter\Shield\Authentication\Authentication;
89
use CodeIgniter\Shield\Authentication\AuthenticationException;
910
use CodeIgniter\Shield\Authentication\Authenticators\Session;
@@ -404,6 +405,12 @@ public function testAttemptCaseInsensitive(): void
404405

405406
public function testAttemptUsernameOnly(): void
406407
{
408+
// Update our auth config to use the username as a valid field for login.
409+
// It is commented out by default.
410+
$config = config('Auth');
411+
$config->validFields = ['email', 'username'];
412+
Factories::injectMock('config', 'Auth', $config);
413+
407414
/** @var User $user */
408415
$user = fake(UserModel::class, ['username' => 'foorog']);
409416
$user->createEmailIdentity([
@@ -433,4 +440,43 @@ public function testAttemptUsernameOnly(): void
433440
'success' => 1,
434441
]);
435442
}
443+
444+
/**
445+
* Test that any field within the user table can be used as the
446+
* login identifier.
447+
*
448+
* @see https://github.com/codeigniter4/shield/issues/334
449+
*/
450+
public function testAttemptCustomField(): void
451+
{
452+
// We don't need email, but do need a password set....
453+
$this->user->createEmailIdentity([
454+
'email' => $this->db->getPlatform() === 'OCI8' ? ' ' : '',
455+
'password' => 'secret123',
456+
]);
457+
458+
// We don't have any custom fields in the User model, so we'll
459+
// just use the status field to represent an employoee ID
460+
model(UserModel::class)->set('status', '12345')->update($this->user->id);
461+
462+
// Update our auth config to use the status field as a valid field for login
463+
$config = config('Auth');
464+
$config->validFields = ['email', 'status'];
465+
Factories::injectMock('config', 'Auth', $config);
466+
467+
// Should block it
468+
$result = $this->auth->attempt(['status' => 'abcde', 'password' => 'secret123']);
469+
470+
$this->assertFalse($result->isOK());
471+
472+
$result = $this->auth->attempt(['status' => '12345', 'password' => 'secret123']);
473+
474+
$this->assertTrue($result->isOK());
475+
476+
$this->seeInDatabase('auth_logins', [
477+
'id_type' => 'status',
478+
'identifier' => '12345',
479+
'success' => 1,
480+
]);
481+
}
436482
}

0 commit comments

Comments
 (0)