Skip to content

Commit 72809c4

Browse files
committed
feat(FOUR-28542): Authenticator app option still available even the user has already configured it.
1 parent 520e403 commit 72809c4

11 files changed

Lines changed: 242 additions & 7 deletions

File tree

ProcessMaker/Http/Controllers/Api/UserController.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,4 +1150,28 @@ public function updateLanguage(Request $request)
11501150

11511151
return response([], 204);
11521152
}
1153+
1154+
public function resetAuthApp(User $user)
1155+
{
1156+
if (!Auth::user()->can('edit', $user)) {
1157+
throw new AuthorizationException(__('Not authorized to update this user.'));
1158+
}
1159+
1160+
if (!$user->hasAuthAppConfigured()) {
1161+
return response([
1162+
'message' => __('Authenticator app is not configured for this user.'),
1163+
], 422);
1164+
}
1165+
1166+
$original = $user->getOriginal();
1167+
$user->auth_app_configured_at = null;
1168+
$user->saveOrFail();
1169+
1170+
UserUpdated::dispatch($user, $user->getChanges(), $original);
1171+
1172+
return response([
1173+
'message' => __('Authenticator app reset successfully.'),
1174+
'auth_app_configured_at' => null,
1175+
]);
1176+
}
11531177
}

ProcessMaker/Http/Controllers/Auth/TwoFactorAuthController.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ public function displayTwoFactorAuthForm(Request $request)
5959
}
6060

6161
// Display view
62-
return view('auth.2fa.otp');
62+
return view('auth.2fa.otp', [
63+
'showAuthAppSetup' => $this->twoFactorAuthentication->userCanSetUpAuthApp($user),
64+
]);
6365
}
6466

6567
public function validateTwoFactorAuthCode(Request $request)
@@ -89,6 +91,10 @@ public function validateTwoFactorAuthCode(Request $request)
8991
session()->put(self::TFA_VALIDATED, $validated);
9092

9193
if ($validated) {
94+
if ($this->twoFactorAuthentication->isAuthAppCode($code)) {
95+
$this->twoFactorAuthentication->markAuthAppConfigured($user);
96+
}
97+
9298
// Remove 2fa values in session
9399
session()->remove(self::TFA_MESSAGE);
94100
session()->remove(self::TFA_ERROR);
@@ -133,6 +139,10 @@ public function displayAuthAppQr(Request $request)
133139
return redirect()->route('login');
134140
}
135141

142+
if (!$this->twoFactorAuthentication->userCanSetUpAuthApp($user)) {
143+
return redirect()->route('2fa');
144+
}
145+
136146
// Generate QR code
137147
$qrCode = $this->twoFactorAuthentication->generateQr($user);
138148

ProcessMaker/Models/User.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class User extends Authenticatable implements HasMedia
130130
'password_changed_at',
131131
'connected_accounts',
132132
'preferences_2fa',
133+
'auth_app_configured_at',
133134
'email_task_notification',
134135
];
135136

@@ -144,6 +145,7 @@ class User extends Authenticatable implements HasMedia
144145
'loggedin_at' => 'datetime',
145146
'schedule' => 'array',
146147
'preferences_2fa' => 'array',
148+
'auth_app_configured_at' => 'datetime',
147149
];
148150

149151
/**
@@ -550,6 +552,11 @@ public function sessions(): HasMany
550552
return $this->hasMany(UserSession::class);
551553
}
552554

555+
public function hasAuthAppConfigured(): bool
556+
{
557+
return $this->auth_app_configured_at !== null;
558+
}
559+
553560
public function getValid2FAPreferences(): array
554561
{
555562
// Get global and user values

ProcessMaker/TwoFactorAuthentication.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,36 @@ private function getCodeForEmailSms(User $user): string
8080
return $otp->now();
8181
}
8282

83-
public function validateCode(User $user, string $code)
83+
public function isAuthAppCode(string $code): bool
8484
{
85-
// The code is for Google Authenticator app?
86-
$forGoogleAuthApp = strlen($code) === 6;
85+
return strlen($code) === 6;
86+
}
8787

88+
public function validateCode(User $user, string $code)
89+
{
8890
// Create OTP instance
89-
$otp = $this->createOtpInstance($user, $forGoogleAuthApp);
91+
$otp = $this->createOtpInstance($user, $this->isAuthAppCode($code));
9092

9193
// Validate code
9294
return $otp->verify($code);
9395
}
9496

97+
public function markAuthAppConfigured(User $user): void
98+
{
99+
if ($user->hasAuthAppConfigured()) {
100+
return;
101+
}
102+
103+
$user->auth_app_configured_at = now();
104+
$user->save();
105+
}
106+
107+
public function userCanSetUpAuthApp(User $user): bool
108+
{
109+
return in_array(self::AUTH_APP, $user->getValid2FAPreferences(), true)
110+
&& !$user->hasAuthAppConfigured();
111+
}
112+
95113
/**
96114
* @param User $user
97115
* @param string $code
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::table('users', function (Blueprint $table) {
12+
$table->timestamp('auth_app_configured_at')->nullable()->after('preferences_2fa');
13+
});
14+
}
15+
16+
public function down(): void
17+
{
18+
Schema::table('users', function (Blueprint $table) {
19+
$table->dropColumn('auth_app_configured_at');
20+
});
21+
}
22+
};

resources/views/admin/users/edit.blade.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@
287287
originalEmail: '',
288288
emailHasChanged: false,
289289
canCreateTokens: @json($canCreateTokens),
290+
resettingAuthApp: false,
290291
}
291292
},
292293
created() {
@@ -555,6 +556,27 @@
555556
this.errors = error.response.data.errors;
556557
});
557558
},
559+
resetAuthApp() {
560+
if (!confirm(this.$t('Reset the authenticator app for this user?'))) {
561+
return;
562+
}
563+
564+
this.resettingAuthApp = true;
565+
566+
ProcessMaker.apiClient.put(`users/${this.formData.id}/reset_auth_app`)
567+
.then(() => {
568+
this.formData.auth_app_configured_at = null;
569+
ProcessMaker.alert(this.$t('Authenticator app reset successfully.'), 'success');
570+
})
571+
.catch(error => {
572+
const message = error.response?.data?.message
573+
|| this.$t('Unable to reset authenticator app.');
574+
ProcessMaker.alert(message, 'danger');
575+
})
576+
.finally(() => {
577+
this.resettingAuthApp = false;
578+
});
579+
},
558580
loadGroups(filter) {
559581
filter = typeof filter === 'string' ? '?filter=' + filter + '&' : '?';
560582
ProcessMaker.apiClient

resources/views/auth/2fa/otp.blade.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ class="form-control{{ $errors->has('code') ? ' is-invalid' : '' }}"
6969
{{ __('Send Again') }}
7070
</a>
7171
</div>
72-
@if (in_array(\ProcessMaker\TwoFactorAuthentication::AUTH_APP,
73-
config('password-policies.2fa_method', [])))
72+
@if ($showAuthAppSetup ?? false)
7473
<div class="form-group">
7574
<a href="{{ route('2fa.auth_app_qr') }}">
7675
{{ __('Authenticator app') }}

resources/views/shared/users/sidebar.blade.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@
9292
>
9393
</b-form-checkbox-group>
9494
</div>
95+
@if (!\Request::is('profile/edit') && in_array(\ProcessMaker\TwoFactorAuthentication::AUTH_APP, $global2FAEnabled))
96+
<div class="form-group mb-0" v-if="formData.auth_app_configured_at">
97+
<button
98+
type="button"
99+
class="btn btn-outline-danger btn-sm w-100"
100+
@click="resetAuthApp"
101+
:disabled="resettingAuthApp"
102+
>
103+
{{ __('Reset Authenticator App') }}
104+
</button>
105+
<small class="form-text text-muted">
106+
{{ __('The user will configure a new authenticator on next login.') }}
107+
</small>
108+
</div>
109+
@endif
95110
@endif
96111
</div>
97112

routes/api.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666

6767
// User Groups
6868
Route::put('users/{user}/groups', [UserController::class, 'updateGroups'])->name('users.groups.update')->middleware('can:edit-users');
69+
Route::put('users/{user}/reset_auth_app', [UserController::class, 'resetAuthApp'])->name('users.reset_auth_app')->middleware('can:edit-users');
6970
// User personal access tokens
7071
Route::get('users/{user}/tokens', [UserTokenController::class, 'index'])->name('users.tokens.index'); // Permissions handled in the controller
7172
Route::get('users/{user}/tokens/{tokenId}', [UserTokenController::class, 'show'])->name('users.tokens.show'); // Permissions handled in the controller
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Feature\Api;
6+
7+
use ProcessMaker\Models\User;
8+
use Tests\Feature\Shared\RequestHelper;
9+
use Tests\TestCase;
10+
11+
class ResetAuthAppTest extends TestCase
12+
{
13+
use RequestHelper;
14+
15+
public function test_admin_can_reset_authenticator_app_for_a_user(): void
16+
{
17+
$targetUser = User::factory()->create([
18+
'auth_app_configured_at' => now(),
19+
]);
20+
21+
$response = $this->apiCall('PUT', route('api.users.reset_auth_app', $targetUser));
22+
23+
$response->assertStatus(200);
24+
$this->assertNull($targetUser->fresh()->auth_app_configured_at);
25+
}
26+
27+
public function test_reset_returns_error_when_authenticator_is_not_configured(): void
28+
{
29+
$targetUser = User::factory()->create([
30+
'auth_app_configured_at' => null,
31+
]);
32+
33+
$response = $this->apiCall('PUT', route('api.users.reset_auth_app', $targetUser));
34+
35+
$response->assertStatus(422);
36+
}
37+
}

0 commit comments

Comments
 (0)