Skip to content

Commit cb7e0c0

Browse files
authored
Merge pull request #181 from kenjis/add-id-type-logins-table
feat: add `id_type` in logins table
2 parents 2dc9fd5 + f32c7b9 commit cb7e0c0

18 files changed

+157
-48
lines changed

src/Authentication/Actions/Email2FA.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717
class Email2FA implements ActionInterface
1818
{
19-
private string $type = 'email_2fa';
19+
private string $type = Session::ID_TYPE_EMAIL_2FA;
2020

2121
/**
2222
* Displays the "Hey we're going to send you a number to your email"

src/Authentication/Actions/EmailActivator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
class EmailActivator implements ActionInterface
1515
{
16-
private string $type = 'email_activate';
16+
private string $type = Session::ID_TYPE_EMAIL_ACTIVATE;
1717

1818
/**
1919
* Shows the initial screen to the user telling them

src/Authentication/Authenticators/AccessTokens.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
class AccessTokens implements AuthenticatorInterface
1717
{
18+
public const ID_TYPE_ACCESS_TOKEN = 'access_token';
19+
1820
/**
1921
* The persistence engine
2022
*/
@@ -51,7 +53,8 @@ public function attempt(array $credentials): Result
5153
if (! $result->isOK()) {
5254
// Always record a login attempt, whether success or not.
5355
$this->loginModel->recordLoginAttempt(
54-
'token: ' . ($credentials['token'] ?? ''),
56+
self::ID_TYPE_ACCESS_TOKEN,
57+
$credentials['token'] ?? '',
5558
false,
5659
$ipAddress,
5760
$userAgent
@@ -69,7 +72,8 @@ public function attempt(array $credentials): Result
6972
$this->login($user);
7073

7174
$this->loginModel->recordLoginAttempt(
72-
'token: ' . ($credentials['token'] ?? ''),
75+
self::ID_TYPE_ACCESS_TOKEN,
76+
$credentials['token'] ?? '',
7377
true,
7478
$ipAddress,
7579
$userAgent,

src/Authentication/Authenticators/Session.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,20 @@
2525

2626
class Session implements AuthenticatorInterface
2727
{
28-
private const STATE_UNKNOWN = 0;
29-
private const STATE_ANONYMOUS = 1;
30-
private const STATE_PENDING = 2;
31-
private const STATE_LOGGED_IN = 3;
28+
/**
29+
* @var string Special ID Type.
30+
* `username` is stored in `users` table, so no `auth_identities` record.
31+
*/
32+
public const ID_TYPE_USERNAME = 'username';
33+
34+
public const ID_TYPE_EMAIL_PASSWORD = 'email_password';
35+
public const ID_TYPE_MAGIC_LINK = 'magic-link';
36+
public const ID_TYPE_EMAIL_2FA = 'email_2fa';
37+
public const ID_TYPE_EMAIL_ACTIVATE = 'email_activate';
38+
private const STATE_UNKNOWN = 0;
39+
private const STATE_ANONYMOUS = 1;
40+
private const STATE_PENDING = 2;
41+
private const STATE_LOGGED_IN = 3;
3242

3343
/**
3444
* The persistence engine
@@ -239,7 +249,12 @@ private function recordLoginAttempt(
239249
string $userAgent,
240250
$userId = null
241251
): void {
252+
$idType = (! isset($credentials['email']) && isset($credentials['username']))
253+
? self::ID_TYPE_USERNAME
254+
: self::ID_TYPE_EMAIL_PASSWORD;
255+
242256
$this->loginModel->recordLoginAttempt(
257+
$idType,
243258
$credentials['email'] ?? $credentials['username'],
244259
$success,
245260
$ipAddress,

src/Controllers/MagicLinkController.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,15 @@ public function loginAction()
6262
$identityModel = model(UserIdentityModel::class);
6363

6464
// Delete any previous magic-link identities
65-
$identityModel->deleteIdentitiesByType($user, 'magic-link');
65+
$identityModel->deleteIdentitiesByType($user, Session::ID_TYPE_MAGIC_LINK);
6666

6767
// Generate the code and save it as an identity
6868
helper('text');
6969
$token = random_string('crypto', 20);
7070

7171
$identityModel->insert([
7272
'user_id' => $user->id,
73-
'type' => 'magic-link',
73+
'type' => Session::ID_TYPE_MAGIC_LINK,
7474
'secret' => $token,
7575
'expires' => Time::now()->addSeconds(setting('Auth.magicLinkLifetime'))->toDateTimeString(),
7676
]);
@@ -105,9 +105,9 @@ public function verify(): RedirectResponse
105105
/** @var UserIdentityModel $identityModel */
106106
$identityModel = model(UserIdentityModel::class);
107107

108-
$identity = $identityModel->getIdentityBySecret('magic-link', $token);
108+
$identity = $identityModel->getIdentityBySecret(Session::ID_TYPE_MAGIC_LINK, $token);
109109

110-
$identifier = 'magic-link: ' . $token;
110+
$identifier = $token ?? '';
111111

112112
// No token found?
113113
if ($identity === null) {
@@ -158,6 +158,7 @@ private function recordLoginAttempt(
158158
$loginModel = model(LoginModel::class);
159159

160160
$loginModel->recordLoginAttempt(
161+
Session::ID_TYPE_MAGIC_LINK,
161162
$identifier,
162163
$success,
163164
$this->request->getIPAddress(),

src/Database/Migrations/2020-12-28-223112_create_auth_tables.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,14 @@ public function up(): void
5757
'id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
5858
'ip_address' => ['type' => 'varchar', 'constraint' => 255],
5959
'user_agent' => ['type' => 'varchar', 'constraint' => 255, 'null' => true],
60+
'id_type' => ['type' => 'varchar', 'constraint' => 255],
6061
'identifier' => ['type' => 'varchar', 'constraint' => 255],
6162
'user_id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'null' => true], // Only for successful logins
6263
'date' => ['type' => 'datetime'],
6364
'success' => ['type' => 'tinyint', 'constraint' => 1],
6465
]);
6566
$this->forge->addPrimaryKey('id');
66-
$this->forge->addKey('identifier');
67+
$this->forge->addKey(['id_type', 'identifier']);
6768
$this->forge->addKey('user_id');
6869
// NOTE: Do NOT delete the user_id or identifier when the user is deleted for security audits
6970
$this->forge->createTable('auth_logins', true);
@@ -76,13 +77,14 @@ public function up(): void
7677
'id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
7778
'ip_address' => ['type' => 'varchar', 'constraint' => 255],
7879
'user_agent' => ['type' => 'varchar', 'constraint' => 255, 'null' => true],
80+
'id_type' => ['type' => 'varchar', 'constraint' => 255],
7981
'identifier' => ['type' => 'varchar', 'constraint' => 255],
8082
'user_id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'null' => true], // Only for successful logins
8183
'date' => ['type' => 'datetime'],
8284
'success' => ['type' => 'tinyint', 'constraint' => 1],
8385
]);
8486
$this->forge->addPrimaryKey('id');
85-
$this->forge->addKey('identifier');
87+
$this->forge->addKey(['id_type', 'identifier']);
8688
$this->forge->addKey('user_id');
8789
// NOTE: Do NOT delete the user_id or identifier when the user is deleted for security audits
8890
$this->forge->createTable('auth_token_logins', true);

src/Entities/User.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace CodeIgniter\Shield\Entities;
44

55
use CodeIgniter\Entity\Entity;
6+
use CodeIgniter\Shield\Authentication\Authenticators\Session;
67
use CodeIgniter\Shield\Authentication\Traits\HasAccessTokens;
78
use CodeIgniter\Shield\Authorization\Traits\Authorizable;
89
use CodeIgniter\Shield\Models\LoginModel;
@@ -47,7 +48,8 @@ class User extends Entity
4748
/**
4849
* Returns the first identity of the given $type for this user.
4950
*
50-
* @param string $type 'email_2fa'|'email_activate'|'email_password'|'magic-link'
51+
* @param string $type See const ID_TYPE_* in Authenticator.
52+
* 'email_2fa'|'email_activate'|'email_password'|'magic-link'|'access_token'
5153
*/
5254
public function getIdentity(string $type): ?UserIdentity
5355
{
@@ -116,7 +118,7 @@ public function createEmailIdentity(array $credentials): void
116118
*/
117119
public function getEmailIdentity(): ?UserIdentity
118120
{
119-
return $this->getIdentity('email_password');
121+
return $this->getIdentity(Session::ID_TYPE_EMAIL_PASSWORD);
120122
}
121123

122124
/**

src/Models/CheckQueryReturnTrait.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace CodeIgniter\Shield\Models;
44

5+
use CodeIgniter\Shield\Exceptions\RuntimeException;
56
use ReflectionObject;
67
use ReflectionProperty;
78

@@ -13,6 +14,8 @@ private function checkQueryReturn(bool $return): void
1314
{
1415
$this->restoreDBDebug();
1516

17+
$this->checkValidationError();
18+
1619
if ($return === false) {
1720
$error = $this->db->error();
1821
$message = 'Query error: ' . $error['code'] . ', '
@@ -22,6 +25,21 @@ private function checkQueryReturn(bool $return): void
2225
}
2326
}
2427

28+
private function checkValidationError(): void
29+
{
30+
$validationErrors = $this->validation->getErrors();
31+
32+
if ($validationErrors !== []) {
33+
$message = 'Validation error:';
34+
35+
foreach ($validationErrors as $field => $error) {
36+
$message .= ' [' . $field . '] ' . $error;
37+
}
38+
39+
throw new RuntimeException($message);
40+
}
41+
}
42+
2543
private function disableDBDebug(): void
2644
{
2745
if (! $this->db->DBDebug) {

src/Models/LoginModel.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use CodeIgniter\I18n\Time;
66
use CodeIgniter\Model;
7+
use CodeIgniter\Shield\Authentication\Authenticators\Session;
78
use CodeIgniter\Shield\Entities\Login;
89
use CodeIgniter\Shield\Entities\User;
910
use Exception;
@@ -20,6 +21,7 @@ class LoginModel extends Model
2021
protected $allowedFields = [
2122
'ip_address',
2223
'user_agent',
24+
'id_type',
2325
'identifier',
2426
'user_id',
2527
'date',
@@ -28,7 +30,8 @@ class LoginModel extends Model
2830
protected $useTimestamps = false;
2931
protected $validationRules = [
3032
'ip_address' => 'required',
31-
'identifier' => 'required',
33+
'id_type' => 'required',
34+
'identifier' => 'permit_empty|string',
3235
'user_agent' => 'permit_empty|string',
3336
'user_id' => 'permit_empty|integer',
3437
'date' => 'required|valid_date',
@@ -37,9 +40,15 @@ class LoginModel extends Model
3740
protected $skipValidation = false;
3841

3942
/**
43+
* Records login attempt.
44+
*
45+
* @param string $idType Identifier type. See const ID_YPE_* in Authenticator.
46+
* auth_logins: 'email_password'|'username'|'magic-link'
47+
* auth_token_logins: 'access-token'
4048
* @param int|string|null $userId
4149
*/
4250
public function recordLoginAttempt(
51+
string $idType,
4352
string $identifier,
4453
bool $success,
4554
?string $ipAddress = null,
@@ -49,6 +58,7 @@ public function recordLoginAttempt(
4958
$return = $this->insert([
5059
'ip_address' => $ipAddress,
5160
'user_agent' => $userAgent,
61+
'id_type' => $idType,
5262
'identifier' => $identifier,
5363
'user_id' => $userId,
5464
'date' => date('Y-m-d H:i:s'),
@@ -78,6 +88,7 @@ public function fake(Generator &$faker): Login
7888
{
7989
return new Login([
8090
'ip_address' => $faker->ipv4,
91+
'id_type' => Session::ID_TYPE_EMAIL_PASSWORD,
8192
'identifier' => $faker->email,
8293
'user_id' => null,
8394
'date' => Time::parse('-1 day')->toDateTimeString(),

src/Models/UserIdentityModel.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace CodeIgniter\Shield\Models;
44

55
use CodeIgniter\Model;
6+
use CodeIgniter\Shield\Authentication\Authenticators\AccessTokens;
7+
use CodeIgniter\Shield\Authentication\Authenticators\Session;
68
use CodeIgniter\Shield\Authentication\Passwords;
79
use CodeIgniter\Shield\Entities\AccessToken;
810
use CodeIgniter\Shield\Entities\User;
@@ -59,7 +61,7 @@ public function createEmailIdentity(User $user, array $credentials): void
5961

6062
$return = $this->insert([
6163
'user_id' => $user->id,
62-
'type' => 'email_password',
64+
'type' => Session::ID_TYPE_EMAIL_PASSWORD,
6365
'secret' => $credentials['email'],
6466
'secret2' => $passwords->hash($credentials['password']),
6567
]);
@@ -119,7 +121,7 @@ public function generateAccessToken(User $user, string $name, array $scopes = ['
119121
helper('text');
120122

121123
$return = $this->insert([
122-
'type' => 'access_token',
124+
'type' => AccessTokens::ID_TYPE_ACCESS_TOKEN,
123125
'user_id' => $user->id,
124126
'name' => $name,
125127
'secret' => hash('sha256', $rawToken = random_string('crypto', 64)),
@@ -141,7 +143,7 @@ public function generateAccessToken(User $user, string $name, array $scopes = ['
141143
public function getAccessTokenByRawToken(string $rawToken): ?AccessToken
142144
{
143145
return $this
144-
->where('type', 'access_token')
146+
->where('type', AccessTokens::ID_TYPE_ACCESS_TOKEN)
145147
->where('secret', hash('sha256', $rawToken))
146148
->asObject(AccessToken::class)
147149
->first();
@@ -150,7 +152,7 @@ public function getAccessTokenByRawToken(string $rawToken): ?AccessToken
150152
public function getAccessToken(User $user, string $rawToken): ?AccessToken
151153
{
152154
return $this->where('user_id', $user->id)
153-
->where('type', 'access_token')
155+
->where('type', AccessTokens::ID_TYPE_ACCESS_TOKEN)
154156
->where('secret', hash('sha256', $rawToken))
155157
->asObject(AccessToken::class)
156158
->first();
@@ -164,7 +166,7 @@ public function getAccessToken(User $user, string $rawToken): ?AccessToken
164166
public function getAccessTokenById($id, User $user): ?AccessToken
165167
{
166168
return $this->where('user_id', $user->id)
167-
->where('type', 'access_token')
169+
->where('type', AccessTokens::ID_TYPE_ACCESS_TOKEN)
168170
->where('id', $id)
169171
->asObject(AccessToken::class)
170172
->first();
@@ -177,7 +179,7 @@ public function getAllAccessTokens(User $user): array
177179
{
178180
return $this
179181
->where('user_id', $user->id)
180-
->where('type', 'access_token')
182+
->where('type', AccessTokens::ID_TYPE_ACCESS_TOKEN)
181183
->orderBy($this->primaryKey)
182184
->asObject(AccessToken::class)
183185
->findAll();
@@ -267,7 +269,7 @@ public function deleteIdentitiesByType(User $user, string $type): void
267269
public function revokeAccessToken(User $user, string $rawToken): void
268270
{
269271
$return = $this->where('user_id', $user->id)
270-
->where('type', 'access_token')
272+
->where('type', AccessTokens::ID_TYPE_ACCESS_TOKEN)
271273
->where('secret', hash('sha256', $rawToken))
272274
->delete();
273275

@@ -280,7 +282,7 @@ public function revokeAccessToken(User $user, string $rawToken): void
280282
public function revokeAllAccessTokens(User $user): void
281283
{
282284
$return = $this->where('user_id', $user->id)
283-
->where('type', 'access_token')
285+
->where('type', AccessTokens::ID_TYPE_ACCESS_TOKEN)
284286
->delete();
285287

286288
$this->checkQueryReturn($return);
@@ -290,7 +292,7 @@ public function fake(Generator &$faker): UserIdentity
290292
{
291293
return new UserIdentity([
292294
'user_id' => fake(UserModel::class)->id,
293-
'type' => 'email_password',
295+
'type' => Session::ID_TYPE_EMAIL_PASSWORD,
294296
'name' => null,
295297
'secret' => 'info@example.com',
296298
'secret2' => password_hash('secret', PASSWORD_DEFAULT),

0 commit comments

Comments
 (0)