mirror of https://github.com/Bubka/2FAuth.git
Compare commits
11 Commits
7e1fdf67bb
...
27fb79413e
Author | SHA1 | Date |
---|---|---|
Bubka | 27fb79413e | |
Bubka | 96978accb3 | |
Bubka | a1ca1ec9bf | |
Bubka | bdfc70732d | |
Bubka | be3aaf319c | |
Bubka | 6458501e51 | |
Bubka | 3fef7d3426 | |
Bubka | 58d97de56d | |
Bubka | de4e35267d | |
Bubka | 091ac41a08 | |
Bubka | 4f5274bfe5 |
|
@ -44,7 +44,7 @@ class WebAuthnRecoveryController extends Controller
|
|||
true,
|
||||
);
|
||||
|
||||
Auth::guard()->setProvider($provider);
|
||||
Auth::setProvider($provider);
|
||||
|
||||
if (Auth::attempt($request->only('email', 'password'))) {
|
||||
if ($this->shouldRevokeAllCredentials($request)) {
|
||||
|
|
|
@ -90,12 +90,14 @@ class User extends Authenticatable implements HasLocalePreference, WebAuthnAuthe
|
|||
];
|
||||
|
||||
/**
|
||||
* User exposed observable events.
|
||||
*
|
||||
* These are extra user-defined events observers may subscribe to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $observables = [
|
||||
'demoting',
|
||||
];
|
||||
|
||||
protected $observables = ['demoting'];
|
||||
|
||||
/**
|
||||
* Get the user's preferred locale.
|
||||
*/
|
||||
|
@ -130,7 +132,9 @@ class User extends Authenticatable implements HasLocalePreference, WebAuthnAuthe
|
|||
*/
|
||||
public function promoteToAdministrator(bool $promote = true) : bool
|
||||
{
|
||||
if ($promote == false && $this->fireModelEvent('demoting') === false) {
|
||||
$eventResult = $promote ? $this->fireModelEvent('promoting') : $this->fireModelEvent('demoting');
|
||||
|
||||
if ($promote == false && $eventResult === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ class FailedLogin extends Notification implements ShouldQueue
|
|||
'account' => $notifiable,
|
||||
'time' => $this->authLog->login_at,
|
||||
'ipAddress' => $this->authLog->ip_address,
|
||||
'browser' => $this->authLog->user_agent,
|
||||
'browser' => $this->agent->browser(),
|
||||
'platform' => $this->agent->platform(),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -107,7 +107,7 @@ class LogoService
|
|||
? Log::info('Fresh tfa.json saved to logos dir')
|
||||
: Log::notice('Cannot save tfa.json to logos dir');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Caching of tfa.json failed');
|
||||
Log::error('Caching of tfa.json failed:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ class GoogleAuthMigrator extends Migrator
|
|||
$parameters['otp_type'] = GAuthValueMapping::OTP_TYPE[OtpType::name($otp_parameters->getType())];
|
||||
$parameters['service'] = $otp_parameters->getIssuer();
|
||||
$parameters['account'] = str_replace($parameters['service'] . ':', '', $otp_parameters->getName());
|
||||
$parameters['secret'] = Base32::encodeUpper($otp_parameters->getSecret());
|
||||
$parameters['secret'] = $this->toBase32($otp_parameters->getSecret());
|
||||
$parameters['algorithm'] = GAuthValueMapping::ALGORITHM[Algorithm::name($otp_parameters->getAlgorithm())];
|
||||
$parameters['digits'] = GAuthValueMapping::DIGIT_COUNT[DigitCount::name($otp_parameters->getDigits())];
|
||||
$parameters['counter'] = $parameters['otp_type'] === TwoFAccount::HOTP ? $otp_parameters->getCounter() : null;
|
||||
|
@ -73,4 +73,11 @@ class GoogleAuthMigrator extends Migrator
|
|||
|
||||
return collect($twofaccounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode into uppercase Base32
|
||||
*/
|
||||
protected function toBase32(string $str) {
|
||||
return Base32::encodeUpper($str);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ class QrCodeService
|
|||
|
||||
Log::info('data encoded to QR code');
|
||||
|
||||
return $qrcode->render($data);
|
||||
return $qrcode->render('stringToEncode');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php": "^8.2",
|
||||
"ext-bcmath": "*",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
|
@ -20,10 +20,10 @@
|
|||
"ext-session": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"ext-xml": "*",
|
||||
"chillerlan/php-qrcode": "^4.3",
|
||||
"chillerlan/php-qrcode": "^5.0",
|
||||
"doctormckay/steam-totp": "^1.0",
|
||||
"doctrine/dbal": "^3.4",
|
||||
"google/protobuf": "^3.21",
|
||||
"google/protobuf": "^4.26",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"jackiedo/dotenv-editor": "^2.1",
|
||||
"jenssegers/agent": "^2.6",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,7 +9,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'version' => '5.1.1',
|
||||
'version' => '5.2.0',
|
||||
'repository' => 'https://github.com/Bubka/2FAuth',
|
||||
'latestReleaseUrl' => 'https://api.github.com/repos/Bubka/2FAuth/releases/latest',
|
||||
'installDocUrl' => 'https://docs.2fauth.app/getting-started/installation/self-hosted-server/',
|
||||
|
@ -123,7 +123,7 @@ return [
|
|||
'lang' => 'browser',
|
||||
'getOtpOnRequest' => true,
|
||||
'notifyOnNewAuthDevice' => true,
|
||||
'notifyOnFailedLogin' => true,
|
||||
'notifyOnFailedLogin' => false,
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
],
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<ini name="memory_limit" value="256M" />
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<!-- following values override .env.testing vars -->
|
||||
</php>
|
||||
|
|
|
@ -161,6 +161,13 @@
|
|||
<FormCheckbox v-model="user.preferences.showOtpAsDot" @update:model-value="val => savePreference('showOtpAsDot', val)" fieldName="showOtpAsDot" label="settings.forms.show_otp_as_dot.label" help="settings.forms.show_otp_as_dot.help" />
|
||||
<!-- reveal dotted OTPs -->
|
||||
<FormCheckbox v-model="user.preferences.revealDottedOTP" @update:model-value="val => savePreference('revealDottedOTP', val)" fieldName="revealDottedOTP" label="settings.forms.reveal_dotted_otp.label" help="settings.forms.reveal_dotted_otp.help" :isDisabled="!user.preferences.showOtpAsDot" :isIndented="true" />
|
||||
|
||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.notifications') }}</h4>
|
||||
<!-- on new device -->
|
||||
<FormCheckbox v-model="user.preferences.notifyOnNewAuthDevice" @update:model-value="val => savePreference('notifyOnNewAuthDevice', val)" fieldName="notifyOnNewAuthDevice" label="settings.forms.notify_on_new_auth_device.label" help="settings.forms.notify_on_new_auth_device.help" />
|
||||
<!-- on failed login -->
|
||||
<FormCheckbox v-model="user.preferences.notifyOnFailedLogin" @update:model-value="val => savePreference('notifyOnFailedLogin', val)" fieldName="notifyOnFailedLogin" label="settings.forms.notify_on_failed_login.label" help="settings.forms.notify_on_failed_login.help" />
|
||||
|
||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.data_input') }}</h4>
|
||||
<!-- basic qrcode -->
|
||||
<FormCheckbox v-model="user.preferences.useBasicQrcodeReader" @update:model-value="val => savePreference('useBasicQrcodeReader', val)" fieldName="useBasicQrcodeReader" label="settings.forms.use_basic_qrcode_reader.label" help="settings.forms.use_basic_qrcode_reader.help" />
|
||||
|
|
|
@ -22,7 +22,7 @@ return [
|
|||
'success' => 'Good news, it works :)'
|
||||
],
|
||||
'new_device' => [
|
||||
'subject' => 'New connection to 2FAuth',
|
||||
'subject' => 'Connection to 2FAuth from a new device',
|
||||
'resume' => 'A new device has just connected to your 2FAuth account.',
|
||||
'connection_details' => 'Here are the details of this connection',
|
||||
'recommandations' => 'If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.'
|
||||
|
|
|
@ -29,6 +29,7 @@ return [
|
|||
'account_linked_to_sso_x_provider' => 'You signed-in via SSO using your :provider account. Your information cannot be changed here but on :provider.',
|
||||
'general' => 'General',
|
||||
'security' => 'Security',
|
||||
'notifications' => 'Notifications',
|
||||
'profile' => 'Profile',
|
||||
'change_password' => 'Change password',
|
||||
'personal_access_tokens' => 'Personal access tokens',
|
||||
|
@ -139,6 +140,14 @@ return [
|
|||
'label' => 'Show Password',
|
||||
'help' => 'Set how and when <abbr title="One-Time Passwords">OTPs</abbr> are displayed.<br/>',
|
||||
],
|
||||
'notify_on_new_auth_device' => [
|
||||
'label' => 'On new device',
|
||||
'help' => 'Get an email when a new device connects to your 2FAuth account for the first time'
|
||||
],
|
||||
'notify_on_failed_login' => [
|
||||
'label' => 'On failed login',
|
||||
'help' => 'Get an email each time an attempt to connect to your 2FAuth account fails'
|
||||
],
|
||||
'otp_generation_on_request' => 'After a click/tap',
|
||||
'otp_generation_on_request_legend' => 'Alone, in its own view',
|
||||
'otp_generation_on_request_title' => 'Click an account to get a password in a dedicated view',
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
@component('mail::message')
|
||||
@lang('notifications.hello_user', ['username' => $account->name])
|
||||
<br/><br/>
|
||||
|
|
|
@ -473,4 +473,20 @@ class GroupControllerTest extends FeatureTestCase
|
|||
$this->assertEquals(0, $this->user->preferences['defaultGroup']);
|
||||
$this->assertEquals(0, $this->user->preferences['activeGroup']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_twofaccount_is_released_on_group_destroy()
|
||||
{
|
||||
$this->actingAs($this->user, 'api-guard')
|
||||
->json('DELETE', '/api/v1/groups/' . $this->userGroupA->id)
|
||||
->assertNoContent();
|
||||
|
||||
$this->twofaccountA->refresh();
|
||||
$this->twofaccountB->refresh();
|
||||
|
||||
$this->assertNull($this->twofaccountA->group_id);
|
||||
$this->assertNull($this->twofaccountB->group_id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ class QrCodeControllerTest extends FeatureTestCase
|
|||
])
|
||||
->assertOk();
|
||||
|
||||
$this->assertStringStartsWith('data:image/png;base64', $response->getData()->qrcode);
|
||||
$this->assertStringStartsWith('data:image/svg+xml;base64', $response->getData()->qrcode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -84,6 +84,9 @@ class LoginTest extends FeatureTestCase
|
|||
{
|
||||
Notification::fake();
|
||||
|
||||
$this->user['preferences->notifyOnNewAuthDevice'] = 1;
|
||||
$this->user->save();
|
||||
|
||||
$this->json('POST', '/user/login', [
|
||||
'email' => $this->user->email,
|
||||
'password' => self::PASSWORD,
|
||||
|
@ -235,6 +238,9 @@ class LoginTest extends FeatureTestCase
|
|||
{
|
||||
Notification::fake();
|
||||
|
||||
$this->user['preferences->notifyOnFailedLogin'] = 1;
|
||||
$this->user->save();
|
||||
|
||||
$this->json('POST', '/user/login', [
|
||||
'email' => $this->user->email,
|
||||
'password' => self::WRONG_PASSWORD,
|
||||
|
|
|
@ -73,11 +73,15 @@ class UserModelTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_promoteToAdministrator_demote_administrator_status()
|
||||
{
|
||||
$user = User::factory()->administrator()->create();
|
||||
$admin = User::factory()->administrator()->create();
|
||||
// We need another admin to prevent demoting event returning false
|
||||
// and blocking the demotion
|
||||
$another_admin = User::factory()->administrator()->create();
|
||||
|
||||
$user->promoteToAdministrator(false);
|
||||
$admin->promoteToAdministrator(false);
|
||||
$admin->save();
|
||||
|
||||
$this->assertEquals($user->isAdministrator(), false);
|
||||
$this->assertFalse($admin->isAdministrator());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4,14 +4,8 @@ namespace Tests\Unit\Listeners;
|
|||
|
||||
use App\Events\GroupDeleting;
|
||||
use App\Listeners\DissociateTwofaccountFromGroup;
|
||||
use App\Models\Group;
|
||||
use App\Models\TwoFAccount;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\PreserveGlobalState;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhp;
|
||||
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
|
@ -20,27 +14,6 @@ use Tests\TestCase;
|
|||
#[CoversClass(DissociateTwofaccountFromGroup::class)]
|
||||
class DissociateTwofaccountFromGroupTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
#[RunInSeparateProcess]
|
||||
#[PreserveGlobalState(false)]
|
||||
#[RequiresPhp('< 8.3.0')]
|
||||
public function test_twofaccount_is_released_on_group_deletion()
|
||||
{
|
||||
$this->mock('alias:' . TwoFAccount::class, function (MockInterface $twoFAccount) {
|
||||
$twoFAccount->shouldReceive('where->update')
|
||||
->once()
|
||||
->andReturn(1);
|
||||
});
|
||||
|
||||
$group = Group::factory()->make();
|
||||
$event = new GroupDeleting($group);
|
||||
$listener = new DissociateTwofaccountFromGroup();
|
||||
|
||||
$this->assertNull($listener->handle($event));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
|
|
@ -18,12 +18,8 @@ use App\Services\SettingService;
|
|||
use Illuminate\Support\Facades\Storage;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use ParagonIE\ConstantTime\Base32;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\PreserveGlobalState;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhp;
|
||||
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\Data\MigrationTestData;
|
||||
use Tests\Data\OtpTestData;
|
||||
|
@ -142,6 +138,20 @@ class MigratorTest extends TestCase
|
|||
$this->fakeTwofaccount->id = TwoFAccount::FAKE_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the testing environment before the next test.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Mockery\Exception\InvalidCountException
|
||||
*/
|
||||
protected function tearDown() : void
|
||||
{
|
||||
$this->forgetMock(SettingService::class);
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
@ -332,17 +342,14 @@ class MigratorTest extends TestCase
|
|||
/**
|
||||
* @test
|
||||
*/
|
||||
#[RunInSeparateProcess]
|
||||
#[PreserveGlobalState(false)]
|
||||
#[RequiresPhp('< 8.3.0')]
|
||||
public function test_migrate_gauth_returns_fake_accounts()
|
||||
{
|
||||
$this->mock('alias:' . Base32::class, function (MockInterface $baseEncoder) {
|
||||
$baseEncoder->shouldReceive('encodeUpper')
|
||||
$migrator = $this->partialMock(GoogleAuthMigrator::class, function (MockInterface $migrator) {
|
||||
$migrator->shouldAllowMockingProtectedMethods()->shouldReceive('toBase32')
|
||||
->andThrow(new \Exception());
|
||||
});
|
||||
|
||||
$migrator = new GoogleAuthMigrator();
|
||||
/** @disregard Undefined function */
|
||||
$accounts = $migrator->migrate(MigrationTestData::GOOGLE_AUTH_MIGRATION_URI);
|
||||
|
||||
$this->assertContainsOnlyInstancesOf(TwoFAccount::class, $accounts);
|
||||
|
@ -352,6 +359,8 @@ class MigratorTest extends TestCase
|
|||
// in the migration payload) so we do not use get() to retrieve items
|
||||
$this->assertEquals($this->fakeTwofaccount->id, $accounts->first()->id);
|
||||
$this->assertEquals($this->fakeTwofaccount->id, $accounts->last()->id);
|
||||
|
||||
$this->forgetMock(GoogleAuthMigrator::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -548,9 +557,4 @@ class MigratorTest extends TestCase
|
|||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function tearDown() : void
|
||||
{
|
||||
Mockery::close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,6 @@ use Illuminate\Support\Facades\Crypt;
|
|||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\PreserveGlobalState;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhp;
|
||||
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
|
||||
use Tests\ModelTestCase;
|
||||
|
||||
/**
|
||||
|
@ -63,6 +60,7 @@ class TwoFAccountModelTest extends ModelTestCase
|
|||
]);
|
||||
|
||||
$this->assertEquals('STRING==', Crypt::decryptString($twofaccount->getAttributes()[$attribute]));
|
||||
$this->forgetMock(SettingService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,6 +96,7 @@ class TwoFAccountModelTest extends ModelTestCase
|
|||
$twofaccount = TwoFAccount::factory()->make();
|
||||
|
||||
$this->assertEquals($twofaccount->getAttributes()[$attribute], $twofaccount->$attribute);
|
||||
$this->forgetMock(SettingService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -118,14 +117,12 @@ class TwoFAccountModelTest extends ModelTestCase
|
|||
$twofaccount = TwoFAccount::factory()->make();
|
||||
|
||||
$this->assertEquals(__('errors.indecipherable'), $twofaccount->$attribute);
|
||||
$this->forgetMock(SettingService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
#[RunInSeparateProcess]
|
||||
#[PreserveGlobalState(false)]
|
||||
#[RequiresPhp('< 8.3.0')]
|
||||
public function test_secret_is_uppercased_and_padded_at_setup()
|
||||
{
|
||||
$settingService = $this->mock(SettingService::class, function (MockInterface $settingService) {
|
||||
|
@ -134,7 +131,7 @@ class TwoFAccountModelTest extends ModelTestCase
|
|||
->andReturn(false);
|
||||
});
|
||||
|
||||
$helpers = $this->mock('alias:' . Helpers::class, function (MockInterface $helpers) {
|
||||
$helpers = $this->mock(Helpers::class, function (MockInterface $helpers) {
|
||||
$helpers->shouldReceive('PadToBase32Format')
|
||||
->andReturn('YYYY====');
|
||||
});
|
||||
|
@ -144,6 +141,7 @@ class TwoFAccountModelTest extends ModelTestCase
|
|||
]);
|
||||
|
||||
$this->assertEquals('YYYY====', $twofaccount->secret);
|
||||
$this->forgetMock(SettingService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue