Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions docs/customization.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
<!-- Email -->
<div class="mb-2">
<input type="text" class="form-control" name="employee_id" autocomplete="new-employee-id" placeholder="12345" value="<?= old('employee_id') ?>" required />
</div>
```
23 changes: 19 additions & 4 deletions src/Authentication/Authenticators/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
46 changes: 46 additions & 0 deletions tests/Authentication/Authenticators/SessionAuthenticatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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,
]);
}
}