Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pterodactyl
GitHub Repository: pterodactyl/panel
Path: blob/1.0-develop/tests/Integration/Api/Client/TwoFactorControllerTest.php
7459 views
1
<?php
2
3
namespace Pterodactyl\Tests\Integration\Api\Client;
4
5
use Carbon\Carbon;
6
use Pterodactyl\Models\User;
7
use Illuminate\Http\Response;
8
use PragmaRX\Google2FA\Google2FA;
9
use Pterodactyl\Models\RecoveryToken;
10
use PHPUnit\Framework\ExpectationFailedException;
11
12
class TwoFactorControllerTest extends ClientApiIntegrationTestCase
13
{
14
/**
15
* Test that image data for enabling 2FA is returned by the endpoint and that the user
16
* record in the database is updated as expected.
17
*/
18
public function testTwoFactorImageDataIsReturned()
19
{
20
/** @var User $user */
21
$user = User::factory()->create(['use_totp' => false]);
22
23
$this->assertFalse($user->use_totp);
24
$this->assertEmpty($user->totp_secret);
25
$this->assertEmpty($user->totp_authenticated_at);
26
27
$response = $this->actingAs($user)->getJson('/api/client/account/two-factor');
28
29
$response->assertOk();
30
$response->assertJsonStructure(['data' => ['image_url_data']]);
31
32
$user = $user->refresh();
33
34
$this->assertFalse($user->use_totp);
35
$this->assertNotEmpty($user->totp_secret);
36
$this->assertEmpty($user->totp_authenticated_at);
37
}
38
39
/**
40
* Test that an error is returned if the user's account already has 2FA enabled on it.
41
*/
42
public function testErrorIsReturnedWhenTwoFactorIsAlreadyEnabled()
43
{
44
/** @var User $user */
45
$user = User::factory()->create(['use_totp' => true]);
46
47
$response = $this->actingAs($user)->getJson('/api/client/account/two-factor');
48
49
$response->assertStatus(Response::HTTP_BAD_REQUEST);
50
$response->assertJsonPath('errors.0.code', 'BadRequestHttpException');
51
$response->assertJsonPath('errors.0.detail', 'Two-factor authentication is already enabled on this account.');
52
}
53
54
/**
55
* Test that a validation error is thrown if invalid data is passed to the 2FA endpoint.
56
*/
57
public function testValidationErrorIsReturnedIfInvalidDataIsPassedToEnabled2FA()
58
{
59
/** @var User $user */
60
$user = User::factory()->create(['use_totp' => false]);
61
62
$this->actingAs($user)
63
->postJson('/api/client/account/two-factor', ['code' => ''])
64
->assertUnprocessable()
65
->assertJsonPath('errors.0.meta.rule', 'required')
66
->assertJsonPath('errors.0.meta.source_field', 'code')
67
->assertJsonPath('errors.1.meta.rule', 'required')
68
->assertJsonPath('errors.1.meta.source_field', 'password');
69
}
70
71
/**
72
* Tests that 2FA can be enabled on an account for the user.
73
*/
74
public function testTwoFactorCanBeEnabledOnAccount()
75
{
76
/** @var User $user */
77
$user = User::factory()->create(['use_totp' => false]);
78
79
// Make the initial call to get the account setup for 2FA.
80
$this->actingAs($user)->getJson('/api/client/account/two-factor')->assertOk();
81
82
$user = $user->refresh();
83
$this->assertNotNull($user->totp_secret);
84
85
/** @var Google2FA $service */
86
$service = $this->app->make(Google2FA::class);
87
88
$secret = decrypt($user->totp_secret);
89
$token = $service->getCurrentOtp($secret);
90
91
$response = $this->actingAs($user)->postJson('/api/client/account/two-factor', [
92
'code' => $token,
93
'password' => 'password',
94
]);
95
96
$response->assertOk();
97
$response->assertJsonPath('object', 'recovery_tokens');
98
99
$user = $user->refresh();
100
$this->assertTrue($user->use_totp);
101
102
$tokens = RecoveryToken::query()->where('user_id', $user->id)->get();
103
$this->assertCount(10, $tokens);
104
$this->assertStringStartsWith('$2y$10$', $tokens[0]->token);
105
// Ensure the recovery tokens that were created include a "created_at" timestamp
106
// value on them.
107
//
108
// @see https://github.com/pterodactyl/panel/issues/3163
109
$this->assertNotNull($tokens[0]->created_at);
110
111
$tokens = $tokens->pluck('token')->toArray();
112
113
foreach ($response->json('attributes.tokens') as $raw) {
114
foreach ($tokens as $hashed) {
115
if (password_verify($raw, $hashed)) {
116
continue 2;
117
}
118
}
119
120
throw new ExpectationFailedException(sprintf('Failed asserting that token [%s] exists as a hashed value in recovery_tokens table.', $raw));
121
}
122
}
123
124
/**
125
* Test that two-factor authentication can be disabled on an account as long as the password
126
* provided is valid for the account.
127
*/
128
public function testTwoFactorCanBeDisabledOnAccount()
129
{
130
Carbon::setTestNow(Carbon::now());
131
132
/** @var User $user */
133
$user = User::factory()->create(['use_totp' => true]);
134
135
$response = $this->actingAs($user)->postJson('/api/client/account/two-factor/disable', [
136
'password' => 'invalid',
137
]);
138
139
$response->assertStatus(Response::HTTP_BAD_REQUEST);
140
$response->assertJsonPath('errors.0.code', 'BadRequestHttpException');
141
$response->assertJsonPath('errors.0.detail', 'The password provided was not valid.');
142
143
$response = $this->actingAs($user)->postJson('/api/client/account/two-factor/disable', [
144
'password' => 'password',
145
]);
146
147
$response->assertStatus(Response::HTTP_NO_CONTENT);
148
149
$user = $user->refresh();
150
$this->assertFalse($user->use_totp);
151
$this->assertNotNull($user->totp_authenticated_at);
152
$this->assertSame(Carbon::now()->toAtomString(), $user->totp_authenticated_at->toAtomString());
153
}
154
155
/**
156
* Test that no error is returned when trying to disabled two factor on an account where it
157
* was not enabled in the first place.
158
*/
159
public function testNoErrorIsReturnedIfTwoFactorIsNotEnabled()
160
{
161
Carbon::setTestNow(Carbon::now());
162
163
/** @var User $user */
164
$user = User::factory()->create(['use_totp' => false]);
165
166
$response = $this->actingAs($user)->postJson('/api/client/account/two-factor/disable', [
167
'password' => 'password',
168
]);
169
170
$response->assertStatus(Response::HTTP_NO_CONTENT);
171
}
172
173
/**
174
* Test that a valid account password is required when enabling two-factor.
175
*/
176
public function testEnablingTwoFactorRequiresValidPassword()
177
{
178
$user = User::factory()->create(['use_totp' => false]);
179
180
$this->actingAs($user)
181
->postJson('/api/client/account/two-factor', [
182
'code' => '123456',
183
'password' => 'foo',
184
])
185
->assertStatus(Response::HTTP_BAD_REQUEST)
186
->assertJsonPath('errors.0.detail', 'The password provided was not valid.');
187
188
$this->assertFalse($user->refresh()->use_totp);
189
}
190
191
/**
192
* Test that a valid account password is required when disabling two-factor.
193
*/
194
public function testDisablingTwoFactorRequiresValidPassword()
195
{
196
$user = User::factory()->create(['use_totp' => true]);
197
198
$this->actingAs($user)
199
->postJson('/api/client/account/two-factor/disable', [
200
'password' => 'foo',
201
])
202
->assertStatus(Response::HTTP_BAD_REQUEST)
203
->assertJsonPath('errors.0.detail', 'The password provided was not valid.');
204
205
$this->assertTrue($user->refresh()->use_totp);
206
}
207
}
208
209