Skip to content

Commit c155324

Browse files
authored
Merge pull request #132 from kenjis/refactor-pending-login
Add User State in Authenticators\Session
2 parents c15241e + 44f1deb commit c155324

File tree

13 files changed

+402
-150
lines changed

13 files changed

+402
-150
lines changed

src/Authentication/Actions/ActionInterface.php

+6
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,10 @@ public function handle(IncomingRequest $request);
3939
* @return Response|string
4040
*/
4141
public function verify(IncomingRequest $request);
42+
43+
/**
44+
* Returns the string type of the action class.
45+
* E.g., 'email_2fa', 'email_activate'.
46+
*/
47+
public function getType(): string;
4248
}

src/Authentication/Actions/Email2FA.php

+59-25
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use CodeIgniter\HTTP\IncomingRequest;
66
use CodeIgniter\HTTP\RedirectResponse;
77
use CodeIgniter\Shield\Authentication\Authenticators\Session;
8+
use CodeIgniter\Shield\Entities\User;
89
use CodeIgniter\Shield\Exceptions\RuntimeException;
910
use CodeIgniter\Shield\Models\UserIdentityModel;
1011

@@ -15,33 +16,25 @@
1516
*/
1617
class Email2FA implements ActionInterface
1718
{
19+
private string $type = 'email_2fa';
20+
1821
/**
1922
* Displays the "Hey we're going to send you an number to your email"
2023
* message to the user with a prompt to continue.
2124
*/
2225
public function show(): string
2326
{
24-
$user = auth()->user();
25-
26-
/** @var UserIdentityModel $identityModel */
27-
$identityModel = model(UserIdentityModel::class);
27+
/** @var Session $authenticator */
28+
$authenticator = auth('session')->getAuthenticator();
2829

29-
// Delete any previous activation identities
30-
$identityModel->deleteIdentitiesByType($user->getAuthId(), 'email_2fa');
30+
$user = $authenticator->getPendingUser();
31+
if ($user === null) {
32+
throw new RuntimeException('Cannot get the pending login User.');
33+
}
3134

32-
// Create an identity for our 2fa hash
33-
helper('text');
34-
$code = random_string('nozero', 6);
35+
$this->createIdentity($user);
3536

36-
$identityModel->insert([
37-
'user_id' => $user->getAuthId(),
38-
'type' => 'email_2fa',
39-
'secret' => $code,
40-
'name' => 'login',
41-
'extra' => lang('Auth.need2FA'),
42-
]);
43-
44-
return view(setting('Auth.views')['action_email_2fa']);
37+
return view(setting('Auth.views')['action_email_2fa'], ['user' => $user]);
4538
}
4639

4740
/**
@@ -54,20 +47,23 @@ public function show(): string
5447
public function handle(IncomingRequest $request)
5548
{
5649
$email = $request->getPost('email');
57-
$user = auth()->user();
5850

59-
if (empty($email) || $email !== $user->getAuthEmail()) {
60-
return redirect()->route('auth-action-show')->with('error', lang('Auth.invalidEmail'));
61-
}
51+
/** @var Session $authenticator */
52+
$authenticator = auth('session')->getAuthenticator();
6253

54+
$user = $authenticator->getPendingUser();
6355
if ($user === null) {
64-
throw new RuntimeException('Cannot get the User.');
56+
throw new RuntimeException('Cannot get the pending login User.');
57+
}
58+
59+
if (empty($email) || $email !== $user->getAuthEmail()) {
60+
return redirect()->route('auth-action-show')->with('error', lang('Auth.invalidEmail'));
6561
}
6662

6763
/** @var UserIdentityModel $identityModel */
6864
$identityModel = model(UserIdentityModel::class);
6965

70-
$identity = $identityModel->getIdentityByType($user->getAuthId(), 'email_2fa');
66+
$identity = $identityModel->getIdentityByType($user->getAuthId(), $this->type);
7167

7268
if (empty($identity)) {
7369
return redirect()->route('auth-action-show')->with('error', lang('Auth.need2FA'));
@@ -101,7 +97,7 @@ public function verify(IncomingRequest $request)
10197
$authenticator = auth('session')->getAuthenticator();
10298

10399
// Token mismatch? Let them try again...
104-
if (! $authenticator->checkAction('email_2fa', $token)) {
100+
if (! $authenticator->checkAction($this->type, $token)) {
105101
session()->setFlashdata('error', lang('Auth.invalid2FAToken'));
106102

107103
return view(setting('Auth.views')['action_email_2fa_verify']);
@@ -110,4 +106,42 @@ public function verify(IncomingRequest $request)
110106
// Get our login redirect url
111107
return redirect()->to(config('Auth')->loginRedirect());
112108
}
109+
110+
/**
111+
* Called from `Session::attempt()`.
112+
*/
113+
public function afterAttempt(User $user): void
114+
{
115+
$this->createIdentity($user);
116+
}
117+
118+
/**
119+
* Create an identity for Email 2FA
120+
*/
121+
private function createIdentity(User $user): void
122+
{
123+
helper('text');
124+
125+
/** @var UserIdentityModel $userIdentityModel */
126+
$userIdentityModel = model(UserIdentityModel::class);
127+
128+
// Delete any previous activation identities
129+
$userIdentityModel->deleteIdentitiesByType($user->getAuthId(), $this->type);
130+
131+
// Create an identity for our 2fa hash
132+
$code = random_string('nozero', 6);
133+
134+
$userIdentityModel->insert([
135+
'user_id' => $user->getAuthId(),
136+
'type' => $this->type,
137+
'secret' => $code,
138+
'name' => 'login',
139+
'extra' => lang('Auth.need2FA'),
140+
]);
141+
}
142+
143+
public function getType(): string
144+
{
145+
return $this->type;
146+
}
113147
}

src/Authentication/Actions/EmailActivator.php

+52-26
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,21 @@
1313

1414
class EmailActivator implements ActionInterface
1515
{
16+
private string $type = 'email_activate';
17+
1618
/**
1719
* Shows the initial screen to the user telling them
1820
* that an email was just sent to them with a link
1921
* to confirm their email address.
2022
*/
2123
public function show(): string
2224
{
23-
$user = auth()->user();
25+
/** @var Session $authenticator */
26+
$authenticator = auth('session')->getAuthenticator();
2427

28+
$user = $authenticator->getPendingUser();
2529
if ($user === null) {
26-
throw new RuntimeException('Cannot get the User.');
30+
throw new RuntimeException('Cannot get the pending login User.');
2731
}
2832

2933
$userEmail = $user->getAuthEmail();
@@ -33,23 +37,7 @@ public function show(): string
3337
);
3438
}
3539

36-
/** @var UserIdentityModel $identityModel */
37-
$identityModel = model(UserIdentityModel::class);
38-
39-
// Delete any previous activation identities
40-
$identityModel->deleteIdentitiesByType($user->getAuthId(), 'email_activate');
41-
42-
// Create an identity for our activation hash
43-
helper('text');
44-
$code = random_string('nozero', 6);
45-
46-
$identityModel->insert([
47-
'user_id' => $user->getAuthId(),
48-
'type' => 'email_activate',
49-
'secret' => $code,
50-
'name' => 'register',
51-
'extra' => lang('Auth.needVerification'),
52-
]);
40+
$code = $this->createIdentity($user);
5341

5442
// Send the email
5543
helper('email');
@@ -85,25 +73,63 @@ public function verify(IncomingRequest $request)
8573
{
8674
$token = $request->getVar('token');
8775

88-
$auth = auth('session');
89-
9076
/** @var Session $authenticator */
91-
$authenticator = $auth->getAuthenticator();
77+
$authenticator = auth('session')->getAuthenticator();
9278

9379
// No match - let them try again.
94-
if (! $authenticator->checkAction('email_activate', $token)) {
80+
if (! $authenticator->checkAction($this->type, $token)) {
9581
session()->setFlashdata('error', lang('Auth.invalidActivateToken'));
9682

9783
return view(setting('Auth.views')['action_email_activate_show']);
9884
}
9985

100-
/** @var User $user */
101-
$user = $auth->user();
86+
$user = $authenticator->getUser();
10287

10388
// Set the user active now
104-
$auth->activateUser($user);
89+
$authenticator->activateUser($user);
10590

10691
// Get our login redirect url
10792
return redirect()->to(config('Auth')->loginRedirect());
10893
}
94+
95+
/**
96+
* Called from `RegisterController::registerAction()`
97+
*/
98+
public function afterRegister(User $user): void
99+
{
100+
$this->createIdentity($user);
101+
}
102+
103+
/**
104+
* Create an identity for Email Activation
105+
*
106+
* @return string The secret code
107+
*/
108+
private function createIdentity(User $user): string
109+
{
110+
helper('text');
111+
112+
/** @var UserIdentityModel $userIdentityModel */
113+
$userIdentityModel = model(UserIdentityModel::class);
114+
115+
$userIdentityModel->deleteIdentitiesByType($user->getAuthId(), $this->type);
116+
117+
// Create an identity for our activation hash
118+
$code = random_string('nozero', 6);
119+
120+
$userIdentityModel->insert([
121+
'user_id' => $user->getAuthId(),
122+
'type' => $this->type,
123+
'secret' => $code,
124+
'name' => 'register',
125+
'extra' => lang('Auth.needVerification'),
126+
]);
127+
128+
return $code;
129+
}
130+
131+
public function getType(): string
132+
{
133+
return $this->type;
134+
}
109135
}

src/Authentication/Authenticators/AccessTokens.php

+21-5
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ class AccessTokens implements AuthenticatorInterface
2626
public function __construct(UserModel $provider)
2727
{
2828
helper('session');
29-
$this->provider = $provider;
29+
30+
$this->provider = $provider;
31+
3032
$this->loginModel = model(LoginModel::class); // @phpstan-ignore-line
3133
}
3234

@@ -48,7 +50,12 @@ public function attempt(array $credentials): Result
4850

4951
if (! $result->isOK()) {
5052
// Always record a login attempt, whether success or not.
51-
$this->loginModel->recordLoginAttempt('token: ' . ($credentials['token'] ?? ''), false, $ipAddress, $userAgent);
53+
$this->loginModel->recordLoginAttempt(
54+
'token: ' . ($credentials['token'] ?? ''),
55+
false,
56+
$ipAddress,
57+
$userAgent
58+
);
5259

5360
return $result;
5461
}
@@ -61,7 +68,13 @@ public function attempt(array $credentials): Result
6168

6269
$this->login($user);
6370

64-
$this->loginModel->recordLoginAttempt('token: ' . ($credentials['token'] ?? ''), true, $ipAddress, $userAgent, $this->user->getAuthId());
71+
$this->loginModel->recordLoginAttempt(
72+
'token: ' . ($credentials['token'] ?? ''),
73+
true,
74+
$ipAddress,
75+
$userAgent,
76+
$this->user->getAuthId()
77+
);
6578

6679
return $result;
6780
}
@@ -99,7 +112,10 @@ public function check(array $credentials): Result
99112
}
100113

101114
// Hasn't been used in a long time
102-
if ($token->last_used_at && $token->last_used_at->isBefore(Time::now()->subSeconds(config('Auth')->unusedTokenLifetime))) {
115+
if (
116+
$token->last_used_at
117+
&& $token->last_used_at->isBefore(Time::now()->subSeconds(config('Auth')->unusedTokenLifetime))
118+
) {
103119
return new Result([
104120
'success' => false,
105121
'reason' => lang('Auth.oldToken'),
@@ -218,7 +234,7 @@ public function recordActiveDate(): void
218234
{
219235
if (! $this->user instanceof User) {
220236
throw new InvalidArgumentException(
221-
self::class . '::recordActiveDate() requires logged in user before calling.'
237+
__METHOD__ . '() requires logged in user before calling.'
222238
);
223239
}
224240

0 commit comments

Comments
 (0)