-
-
Notifications
You must be signed in to change notification settings - Fork 190
Expand file tree
/
Copy pathEmailChallenge.php
More file actions
168 lines (149 loc) · 3.6 KB
/
Copy pathEmailChallenge.php
File metadata and controls
168 lines (149 loc) · 3.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<?php
namespace Kirby\Auth\Challenge;
use Kirby\Auth\Challenge;
use Kirby\Auth\Pending;
use Kirby\Cms\User;
use Kirby\Panel\Ui\Button;
use Kirby\Panel\Ui\Component;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str;
use SensitiveParameter;
/**
* Creates and verifies one-time auth codes
* that are sent via email
*
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 6.0.0
*/
class EmailChallenge extends Challenge
{
/**
* Generates a random one-time auth code and
* returns that code for later verification
*/
public function create(): Pending
{
$code = Str::random(6, 'num');
$this->send(
// insert a space in the middle for easier readability
code: substr($code, 0, 3) . ' ' . substr($code, 3, 3)
);
return new Pending(
secret: User::hashPassword($code)
);
}
public function form(Pending $pending): Component
{
return new Component(
component: 'k-login-email-challenge-form',
submit: $this->submit(),
user: $this->user->email(),
);
}
public static function icon(): string
{
return 'email-unread';
}
/**
* As a second factor, the email challenge is opt-in per user,
* so that users are not forced into it just by having an email
* address. For every other purpose it stays always available.
*/
public static function isAvailable(User $user, string $mode): bool
{
if ($mode !== '2fa') {
return true;
}
if ($user->secret('email') === true) {
return true;
}
// enforced 2FA needs a factor for every user, so email stays
// the baseline for those who have not set up anything else
return $user->kirby()->auth()->methods()->hasAnyRequiring2FA();
}
/**
* Sends the email with the code to the user
*/
protected function send(string $code): void
{
// use the login templates for 2FA
$mode = match ($this->mode) {
'2fa' => 'login',
default => $this->mode
};
$this->kirby->email([
'from' => $this->senderFrom(),
'fromName' => $this->senderFromName(),
'to' => $this->user,
'subject' => $this->subject($mode),
'template' => 'auth/' . $mode,
'data' => [
'user' => $this->user,
'site' => $this->kirby->system()->title(),
'code' => $code,
'timeout' => round($this->timeout / 60)
]
]);
}
/**
* Returns sender email address for the email
*/
protected function senderFrom(): string
{
return $this->kirby->option(
'auth.challenge.email.from',
'noreply@' . $this->kirby->url('index', true)->host()
);
}
/**
* Returns sender name for the email
*/
protected function senderFromName(): string
{
return $this->kirby->option(
'auth.challenge.email.fromName',
$this->kirby->site()->title()
);
}
public static function settings(User $user): array
{
return [
new Button(
icon: static::icon(),
text: static::i18n('login.challenge.email.label'),
drawer: $user->panel()->url(true) . '/security/challenge/email'
)
];
}
/**
* Returns subject for the email
*/
protected function subject(string $mode): string
{
return $this->kirby->option(
'auth.challenge.email.subject',
I18n::translate(
key: 'login.email.' . $mode . '.subject',
locale: $this->user->language()
)
);
}
/**
* Verifies the provided input against the code
* that was returned from the `create()` method
*/
public function verify(
#[SensitiveParameter]
mixed $input,
Pending $data
): bool {
// normalize the formatting in the user-provided code
$input = str_replace(' ', '', $input);
$hash = $data->secret();
if (is_string($hash) !== true) {
return false;
}
return password_verify($input, $hash);
}
}