mirror of
https://github.com/Bubka/2FAuth.git
synced 2024-11-25 17:54:57 +01:00
Refactor and finalize the Import feature for G.Auth, Aegis & Plain Text
This commit is contained in:
parent
1610cf3738
commit
e79ae0a3ed
@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Api\v1\Controllers;
|
||||
|
||||
use App\Api\v1\Requests\TwoFAccountImportRequest;
|
||||
use App\Api\v1\Resources\TwoFAccountCollection;
|
||||
use App\Contracts\MigrationService;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class ImportController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var $migrator The Migration service
|
||||
*/
|
||||
protected $migrator;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(MigrationService $migrationService)
|
||||
{
|
||||
$this->migrator = $migrationService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert Google Auth data to a TwoFAccounts collection
|
||||
*
|
||||
* @param \App\Api\v1\Requests\TwoFAccountImportRequest $request
|
||||
* @return \App\Api\v1\Resources\TwoFAccountCollection
|
||||
*/
|
||||
public function googleAuth(TwoFAccountImportRequest $request)
|
||||
{
|
||||
$request->merge(['withSecret' => true]);
|
||||
$twofaccounts = $this->migrator->migrate($request->uri);
|
||||
|
||||
return new TwoFAccountCollection($twofaccounts);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert Aegis data to a TwoFAccounts collection
|
||||
*
|
||||
* @param \App\Api\v1\Requests\TwoFAccountImportRequest $request
|
||||
* @return \App\Api\v1\Resources\TwoFAccountCollection
|
||||
*/
|
||||
public function aegis(TwoFAccountImportRequest $request)
|
||||
{
|
||||
$request->merge(['withSecret' => true]);
|
||||
$twofaccounts = $this->migrator->migrate($request->uri);
|
||||
|
||||
return new TwoFAccountCollection($twofaccounts);
|
||||
}
|
||||
}
|
@ -102,6 +102,30 @@ public function update(TwoFAccountUpdateRequest $request, TwoFAccount $twofaccou
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert a migration resource to a valid TwoFAccounts collection
|
||||
*
|
||||
* @param \App\Api\v1\Requests\TwoFAccountImportRequest $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function migrate(TwoFAccountImportRequest $request)
|
||||
{
|
||||
$request->merge(['withSecret' => true]);
|
||||
$validated = $request->validated();
|
||||
|
||||
if (Arr::has($validated, 'file')) {
|
||||
$migrationResource = $request->file('file');
|
||||
|
||||
return $migrationResource instanceof \Illuminate\Http\UploadedFile
|
||||
? new TwoFAccountCollection(TwoFAccounts::migrate($migrationResource->get()))
|
||||
: response()->json(['message' => __('errors.file_upload_failed')], 500);
|
||||
}
|
||||
else {
|
||||
return new TwoFAccountCollection(TwoFAccounts::migrate($request->payload));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save 2FA accounts order
|
||||
*
|
||||
|
@ -25,7 +25,8 @@ public function authorize()
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'uri' => 'required|string|regex:/^otpauth-migration:\/\/offline\?data=/i',
|
||||
'payload' => 'required_without:file|string',
|
||||
'file' => 'required_without:payload|mimes:txt,json,csv',
|
||||
];
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use \Illuminate\Support\Collection;
|
||||
|
||||
interface MigrationService
|
||||
{
|
||||
/**
|
||||
* Convert migration data to a 2FAccounts collection.
|
||||
*
|
||||
* @param mixed $migrationPayload
|
||||
* @return \Illuminate\Support\Collection The converted accounts
|
||||
*/
|
||||
public function migrate(mixed $migrationPayload) : Collection;
|
||||
}
|
14
app/Exceptions/EncryptedMigrationException.php
Normal file
14
app/Exceptions/EncryptedMigrationException.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class EncryptedMigrationException.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class EncryptedMigrationException extends Exception
|
||||
{
|
||||
}
|
@ -60,9 +60,19 @@ public function register()
|
||||
'message' => $exception->getMessage()], 400);
|
||||
});
|
||||
|
||||
$this->renderable(function (InvalidGoogleAuthMigration $exception, $request) {
|
||||
$this->renderable(function (InvalidMigrationDataException $exception, $request) {
|
||||
return response()->json([
|
||||
'message' => __('errors.invalid_google_auth_migration')], 400);
|
||||
'message' => __('errors.invalid_x_migration', ['appname' => $exception->getMessage()])], 400);
|
||||
});
|
||||
|
||||
$this->renderable(function (UnsupportedMigrationException $exception, $request) {
|
||||
return response()->json([
|
||||
'message' => __('errors.unsupported_migration')], 400);
|
||||
});
|
||||
|
||||
$this->renderable(function (EncryptedMigrationException $exception, $request) {
|
||||
return response()->json([
|
||||
'message' => __('errors.encrypted_migration')], 400);
|
||||
});
|
||||
|
||||
$this->renderable(function (UndecipherableException $exception, $request) {
|
||||
|
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class InvalidGoogleAuthMigration.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class InvalidGoogleAuthMigration extends Exception
|
||||
{
|
||||
}
|
14
app/Exceptions/InvalidMigrationDataException.php
Normal file
14
app/Exceptions/InvalidMigrationDataException.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class InvalidMigrationDataException.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class InvalidMigrationDataException extends Exception
|
||||
{
|
||||
}
|
14
app/Exceptions/UnsupportedMigrationException.php
Normal file
14
app/Exceptions/UnsupportedMigrationException.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class UnsupportedMigrationException.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class UnsupportedMigrationException extends Exception
|
||||
{
|
||||
}
|
114
app/Factories/MigratorFactory.php
Normal file
114
app/Factories/MigratorFactory.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Factories;
|
||||
|
||||
use App\Services\Migrators\GoogleAuthMigrator;
|
||||
use App\Services\Migrators\AegisMigrator;
|
||||
use App\Services\Migrators\Migrator;
|
||||
use App\Services\Migrators\PlainTextMigrator;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use App\Exceptions\UnsupportedMigrationException;
|
||||
use App\Exceptions\EncryptedMigrationException;
|
||||
|
||||
class MigratorFactory implements MigratorFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Infer the type of migrator needed from a payload and create the migrator
|
||||
*
|
||||
* @param string $migrationPayload The migration payload used to infer the migrator type
|
||||
* @return Migrator
|
||||
*/
|
||||
public function create($migrationPayload) : Migrator
|
||||
{
|
||||
if ($this->isAegisJSON($migrationPayload)) {
|
||||
return App::make(AegisMigrator::class);
|
||||
}
|
||||
else if ($this->isGoogleAuth($migrationPayload)) {
|
||||
return App::make(GoogleAuthMigrator::class);
|
||||
}
|
||||
else if ($this->isPlainText($migrationPayload)) {
|
||||
return App::make(PlainTextMigrator::class);
|
||||
}
|
||||
else throw new UnsupportedMigrationException();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function isGoogleAuth($migrationPayload) : bool
|
||||
{
|
||||
// - Google Auth migration URI : a string starting with otpauth-migration://offline?data= on a single line
|
||||
|
||||
$lines = preg_split('~\R~', $migrationPayload, -1 , PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
if (!$lines || count($lines) != 1)
|
||||
return false;
|
||||
|
||||
return preg_match('/^otpauth-migration:\/\/offline\?data=.+$/', $lines[0]) == 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function isPlainText($migrationPayload) : bool
|
||||
{
|
||||
// - Plain text : one or more otpauth URIs (otpauth://[t|h]otp/...), one per line
|
||||
|
||||
return Validator::make(
|
||||
preg_split('~\R~', $migrationPayload, -1 , PREG_SPLIT_NO_EMPTY),
|
||||
[
|
||||
'*' => 'regex:/^otpauth:\/\/[h,t]otp\//i',
|
||||
]
|
||||
)->passes();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function isAegisJSON($migrationPayload) : mixed
|
||||
{
|
||||
// - Aegis JSON : is a JSON object with the key db.entries full of objects like
|
||||
// {
|
||||
// "type": "totp",
|
||||
// "uuid": "5be1c189-240d-5fe1-930b-a78xb669zd86",
|
||||
// "name": "John DOE",
|
||||
// "issuer": "Facebook",
|
||||
// "note": "",
|
||||
// "icon": null,
|
||||
// "info": {
|
||||
// "secret": "A4GRFTVVRBGY7UIW",
|
||||
// "algo": "SHA1",
|
||||
// "digits": 6,
|
||||
// "period": 30
|
||||
// }
|
||||
// }
|
||||
|
||||
$json = json_decode($migrationPayload, true);
|
||||
|
||||
if (Arr::has($json, 'db')) {
|
||||
if (is_string($json['db']) && is_array(Arr::get($json, 'header.slots'))) {
|
||||
throw new EncryptedMigrationException();
|
||||
}
|
||||
else {
|
||||
return count(Validator::validate(
|
||||
$json,
|
||||
[
|
||||
'db.entries.*.type' => 'required',
|
||||
'db.entries.*.name' => 'required',
|
||||
'db.entries.*.issuer' => 'required',
|
||||
'db.entries.*.info' => 'required'
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
16
app/Factories/MigratorFactoryInterface.php
Normal file
16
app/Factories/MigratorFactoryInterface.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Factories;
|
||||
|
||||
use App\Services\Migrators\Migrator;
|
||||
|
||||
interface MigratorFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Infer the type of migrator needed from a payload and create the migrator
|
||||
*
|
||||
* @param string $migrationPayload The migration payload used to infer the migrator type
|
||||
* @return Migrator
|
||||
*/
|
||||
public function create(string $migrationPayload) : Migrator;
|
||||
}
|
@ -2,15 +2,14 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Api\v1\Controllers\ImportController;
|
||||
use App\Contracts\MigrationService;
|
||||
use App\Factories\MigratorFactory;
|
||||
use App\Factories\MigratorFactoryInterface;
|
||||
use App\Services\Migrators\GoogleAuthMigrator;
|
||||
use App\Services\Migrators\AegisMigrator;
|
||||
use App\Services\Migrators\PlainTextMigrator;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Contracts\Support\DeferrableProvider;
|
||||
|
||||
class MigrationServiceProvider extends ServiceProvider implements DeferrableProvider
|
||||
class MigrationServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
@ -19,19 +18,18 @@ class MigrationServiceProvider extends ServiceProvider implements DeferrableProv
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->when(ImportController::class)
|
||||
->needs(MigrationService::class)
|
||||
->give(function () {
|
||||
switch (request()->route()->getName()) {
|
||||
case 'import.googleAuth':
|
||||
return $this->app->get(GoogleAuthMigrator::class);
|
||||
$this->app->bind(MigratorFactoryInterface::class, MigratorFactory::class);
|
||||
|
||||
case 'import.aegis':
|
||||
return $this->app->get(AegisMigrator::class);
|
||||
$this->app->singleton(GoogleAuthMigrator::class, function () {
|
||||
return new GoogleAuthMigrator();
|
||||
});
|
||||
|
||||
default:
|
||||
return $this->app->get(PlainTextMigrator::class);
|
||||
}
|
||||
$this->app->singleton(AegisMigrator::class, function () {
|
||||
return new AegisMigrator();
|
||||
});
|
||||
|
||||
$this->app->singleton(PlainTextMigrator::class, function () {
|
||||
return new PlainTextMigrator();
|
||||
});
|
||||
}
|
||||
|
||||
@ -44,19 +42,4 @@ public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
{
|
||||
return [
|
||||
GoogleAuthMigrator::class,
|
||||
AegisMigrator::class,
|
||||
PlainTextMigrator::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,8 @@
|
||||
use App\Services\LogoService;
|
||||
use App\Services\SettingService;
|
||||
use App\Services\ReleaseRadarService;
|
||||
use App\Services\TwoFAccountService;
|
||||
use App\Factories\MigratorFactoryInterface;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Contracts\Support\DeferrableProvider;
|
||||
|
||||
@ -17,6 +19,10 @@ class TwoFAuthServiceProvider extends ServiceProvider implements DeferrableProvi
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(TwoFAccountService::class, function ($app) {
|
||||
return new TwoFAccountService($app->make(MigratorFactoryInterface::class));
|
||||
});
|
||||
|
||||
$this->app->singleton(SettingService::class, function () {
|
||||
return new SettingService();
|
||||
});
|
||||
|
@ -2,19 +2,85 @@
|
||||
|
||||
namespace App\Services\Migrators;
|
||||
|
||||
use App\Contracts\MigrationService;
|
||||
use \Illuminate\Support\Collection;
|
||||
use App\Services\Migrators\Migrator;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\TwoFAccount;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Exceptions\InvalidMigrationDataException;
|
||||
|
||||
class AegisMigrator implements MigrationService
|
||||
class AegisMigrator extends Migrator
|
||||
{
|
||||
// Typical JSON structure of an Aegis export
|
||||
//
|
||||
// {
|
||||
// "type": "totp",
|
||||
// "uuid": "5be1c189-240d-5fe1-930b-a78xb669zd86",
|
||||
// "name": "John DOE",
|
||||
// "issuer": "Facebook",
|
||||
// "note": "",
|
||||
// "icon": "PHN2ZyB4bWxucz0ia[...]0KPC9zdmc+DQo=",
|
||||
// "icon_mime": "image\/svg+xml",
|
||||
// "info": {
|
||||
// "secret": "A4GRFTVVRBGY7UIW",
|
||||
// "algo": "SHA1",
|
||||
// "digits": 6,
|
||||
// "period": 30,
|
||||
// "counter": 30
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* Convert migration data to a 2FAccounts collection.
|
||||
* Convert migration data to a TwoFAccounts collection.
|
||||
*
|
||||
* @param mixed $migrationPayload
|
||||
* @return \Illuminate\Support\Collection The converted accounts
|
||||
*/
|
||||
public function migrate(mixed $migrationPayload) : Collection
|
||||
{
|
||||
return Collect(['collected from aegisMigrator']);
|
||||
$json = json_decode(htmlspecialchars_decode($migrationPayload), true);
|
||||
|
||||
if (is_null($json) || Arr::has($json, 'db.entries') == false) {
|
||||
Log::error('Aegis JSON migration data cannot be read');
|
||||
throw new InvalidMigrationDataException('Aegis');
|
||||
}
|
||||
|
||||
foreach ($json['db']['entries'] as $key => $otp_parameters) {
|
||||
// Storage::put('file.jpg', $contents);
|
||||
$parameters = array();
|
||||
$parameters['otp_type'] = $otp_parameters['type'] == 'steam' ? TwoFAccount::STEAM_TOTP : $otp_parameters['type'];
|
||||
$parameters['service'] = $otp_parameters['issuer'];
|
||||
$parameters['account'] = $otp_parameters['name'];
|
||||
$parameters['secret'] = $otp_parameters['info']['secret'];
|
||||
$parameters['algorithm'] = $otp_parameters['info']['algo'];
|
||||
$parameters['digits'] = $otp_parameters['info']['digits'];
|
||||
$parameters['counter'] = $otp_parameters['info']['counter'] ?? null;
|
||||
$parameters['period'] = $otp_parameters['info']['period'] ?? null;
|
||||
|
||||
try {
|
||||
$twofaccounts[$key] = new TwoFAccount;
|
||||
$twofaccounts[$key]->fillWithOtpParameters($parameters);
|
||||
}
|
||||
catch (\Exception $exception) {
|
||||
|
||||
Log::error(sprintf('Cannot instanciate a TwoFAccount object with OTP parameters from imported item #%s', $key));
|
||||
Log::error($exception->getMessage());
|
||||
|
||||
// The token failed to generate a valid account so we create a fake account to be returned.
|
||||
$fakeAccount = new TwoFAccount();
|
||||
$fakeAccount->id = -2;
|
||||
$fakeAccount->otp_type = $otp_parameters['type'];
|
||||
// Only basic fields are filled to limit the risk of another exception.
|
||||
$fakeAccount->account = $otp_parameters['name'];
|
||||
$fakeAccount->service = $otp_parameters['issuer'];
|
||||
// The secret field is used to pass the error, not very clean but will do the job for now.
|
||||
$fakeAccount->secret = $exception->getMessage();
|
||||
|
||||
$twofaccounts[$key] = $fakeAccount;
|
||||
}
|
||||
}
|
||||
|
||||
return collect($twofaccounts);
|
||||
}
|
||||
}
|
||||
|
@ -4,19 +4,19 @@
|
||||
|
||||
use Exception;
|
||||
use App\Models\TwoFAccount;
|
||||
use App\Contracts\MigrationService;
|
||||
use \Illuminate\Support\Collection;
|
||||
use App\Services\Migrators\Migrator;
|
||||
use Illuminate\Support\Collection;
|
||||
use ParagonIE\ConstantTime\Base32;
|
||||
use App\Protobuf\GAuthValueMapping;
|
||||
use App\Protobuf\GoogleAuth\Payload;
|
||||
use App\Protobuf\GoogleAuth\Payload\OtpType;
|
||||
use App\Protobuf\GoogleAuth\Payload\Algorithm;
|
||||
use App\Protobuf\GoogleAuth\Payload\DigitCount;
|
||||
use App\Exceptions\InvalidGoogleAuthMigration;
|
||||
use App\Exceptions\InvalidMigrationDataException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class GoogleAuthMigrator extends Migrator implements MigrationService
|
||||
class GoogleAuthMigrator extends Migrator
|
||||
{
|
||||
|
||||
/**
|
||||
@ -38,7 +38,7 @@ public function migrate(mixed $migrationPayload) : Collection
|
||||
Log::error("Protobuf failed to get OTP parameters from provided migration URI");
|
||||
Log::error($ex->getMessage());
|
||||
|
||||
throw new InvalidGoogleAuthMigration();
|
||||
throw new InvalidMigrationDataException('Google Authenticator');
|
||||
}
|
||||
|
||||
$twofaccounts = array();
|
||||
@ -78,6 +78,6 @@ public function migrate(mixed $migrationPayload) : Collection
|
||||
}
|
||||
}
|
||||
|
||||
return self::markAsDuplicate(collect($twofaccounts));
|
||||
return collect($twofaccounts);
|
||||
}
|
||||
}
|
||||
|
@ -2,38 +2,16 @@
|
||||
|
||||
namespace App\Services\Migrators;
|
||||
|
||||
use App\Models\TwoFAccount;
|
||||
use \Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
abstract class Migrator
|
||||
{
|
||||
|
||||
/**
|
||||
* Return the given collection with items marked as Duplicates (using id=-1) if a similar record exists in database
|
||||
* Convert migration data to a 2FAccounts collection.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $twofaccounts
|
||||
* @return \Illuminate\Support\Collection
|
||||
* @param mixed $migrationPayload
|
||||
* @return \Illuminate\Support\Collection The converted accounts
|
||||
*/
|
||||
protected static function markAsDuplicate(Collection $twofaccounts) : Collection
|
||||
{
|
||||
$storage = TwoFAccount::all();
|
||||
|
||||
$twofaccounts = $twofaccounts->map(function ($twofaccount, $key) use ($storage) {
|
||||
if ($storage->contains(function ($value, $key) use ($twofaccount) {
|
||||
return $value->secret == $twofaccount->secret
|
||||
&& $value->service == $twofaccount->service
|
||||
&& $value->account == $twofaccount->account
|
||||
&& $value->otp_type == $twofaccount->otp_type
|
||||
&& $value->digits == $twofaccount->digits
|
||||
&& $value->algorithm == $twofaccount->algorithm;
|
||||
})) {
|
||||
$twofaccount->id = -1;
|
||||
}
|
||||
|
||||
return $twofaccount;
|
||||
});
|
||||
|
||||
return $twofaccounts;
|
||||
}
|
||||
abstract protected function migrate(mixed $migrationPayload) : Collection;
|
||||
|
||||
}
|
||||
|
@ -2,19 +2,60 @@
|
||||
|
||||
namespace App\Services\Migrators;
|
||||
|
||||
use App\Contracts\MigrationService;
|
||||
use \Illuminate\Support\Collection;
|
||||
use App\Services\Migrators\Migrator;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\TwoFAccount;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Exceptions\InvalidMigrationDataException;
|
||||
|
||||
class PlainTextMigrator implements MigrationService
|
||||
class PlainTextMigrator extends Migrator
|
||||
{
|
||||
|
||||
/**
|
||||
* Convert migration data to a 2FAccounts collection.
|
||||
* Convert migration data to a TwoFAccounts collection.
|
||||
*
|
||||
* @param mixed $migrationPayload
|
||||
* @return \Illuminate\Support\Collection The converted accounts
|
||||
*/
|
||||
public function migrate(mixed $migrationPayload) : Collection
|
||||
{
|
||||
return Collect(['collected from plainTextMigrator']);
|
||||
$otpauthURIs = preg_split('~\R~', $migrationPayload);
|
||||
$otpauthURIs = Arr::where($otpauthURIs, function ($value, $key) {
|
||||
return Str::startsWith($value, ['otpauth://totp/', 'otpauth://hotp/']);
|
||||
});
|
||||
|
||||
if (count($otpauthURIs) < 1) {
|
||||
Log::error('No valid OtpAuth URI found in the migration');
|
||||
throw new InvalidMigrationDataException('migration');
|
||||
}
|
||||
|
||||
foreach ($otpauthURIs as $key => $uri) {
|
||||
|
||||
try {
|
||||
$twofaccounts[$key] = new TwoFAccount;
|
||||
$twofaccounts[$key]->fillWithURI($uri);
|
||||
}
|
||||
catch (\Exception $exception) {
|
||||
|
||||
Log::error(sprintf('Cannot instanciate a TwoFAccount object with OTP parameters from imported item #%s', $key));
|
||||
Log::error($exception->getMessage());
|
||||
|
||||
// The token failed to generate a valid account so we create a fake account to be returned.
|
||||
$fakeAccount = new TwoFAccount();
|
||||
$fakeAccount->id = -2;
|
||||
$fakeAccount->otp_type = substr($uri, 10, 4);
|
||||
// Only basic fields are filled to limit the risk of another exception.
|
||||
$fakeAccount->account = '## invalid OTP data ##';
|
||||
$fakeAccount->service = filter_input(INPUT_GET, 'issuer', FILTER_SANITIZE_ENCODED) ?? '-';
|
||||
// The secret field is used to pass the error, not very clean but will do the job for now.
|
||||
$fakeAccount->secret = $exception->getMessage();
|
||||
|
||||
$twofaccounts[$key] = $fakeAccount;
|
||||
}
|
||||
}
|
||||
|
||||
return collect($twofaccounts);
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,26 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\TwoFAccount;
|
||||
use App\Factories\MigratorFactoryInterface;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class TwoFAccountService
|
||||
{
|
||||
/**
|
||||
* @var $migrator The Migration service
|
||||
*/
|
||||
protected $migratorFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(MigratorFactoryInterface $migratorFactory)
|
||||
{
|
||||
$this->migratorFactory = $migratorFactory;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Withdraw one or more twofaccounts from their group
|
||||
@ -31,6 +47,22 @@ public static function withdraw($ids) : void
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert a migration payload to a set of TwoFAccount objects
|
||||
*
|
||||
* @param string $migrationUri migration uri provided by Google Authenticator export feature
|
||||
*
|
||||
* @return \Illuminate\Support\Collection The converted accounts
|
||||
*/
|
||||
public function migrate($migrationPayload) : Collection
|
||||
{
|
||||
$migrator = $this->migratorFactory->create($migrationPayload);
|
||||
$twofaccounts = $migrator->migrate($migrationPayload);
|
||||
|
||||
return self::markAsDuplicate($twofaccounts);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete one or more twofaccounts
|
||||
*
|
||||
@ -50,6 +82,35 @@ public static function delete($ids) : int
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the given collection with items marked as Duplicates (using id=-1) if a similar record exists in database
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $twofaccounts
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
private static function markAsDuplicate(Collection $twofaccounts) : Collection
|
||||
{
|
||||
$storage = TwoFAccount::all();
|
||||
|
||||
$twofaccounts = $twofaccounts->map(function ($twofaccount, $key) use ($storage) {
|
||||
if ($storage->contains(function ($value, $key) use ($twofaccount) {
|
||||
return $value->secret == $twofaccount->secret
|
||||
&& $value->service == $twofaccount->service
|
||||
&& $value->account == $twofaccount->account
|
||||
&& $value->otp_type == $twofaccount->otp_type
|
||||
&& $value->digits == $twofaccount->digits
|
||||
&& $value->algorithm == $twofaccount->algorithm;
|
||||
})) {
|
||||
$twofaccount->id = -1;
|
||||
}
|
||||
|
||||
return $twofaccount;
|
||||
});
|
||||
|
||||
return $twofaccounts;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Explode a comma separated list of IDs to an array of IDs
|
||||
*
|
||||
|
@ -176,7 +176,6 @@
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\TwoFAuthServiceProvider::class,
|
||||
App\Providers\MigrationServiceProvider::class,
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
@ -37,6 +37,12 @@
|
||||
{{ $t('twofaccounts.forms.use_advanced_form') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- link to import view -->
|
||||
<div v-if="showImportButton" class="block has-text-link">
|
||||
<router-link class="button is-link is-outlined is-rounded" :to="{ name: 'importAccounts' }" >
|
||||
{{ $t('twofaccounts.import.import') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
@ -74,6 +80,7 @@
|
||||
return {
|
||||
accountCount: null,
|
||||
form: new Form(),
|
||||
alternativeMethod: null,
|
||||
}
|
||||
},
|
||||
|
||||
@ -82,6 +89,10 @@
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showImportButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
returnToView: {
|
||||
type: String,
|
||||
default: 'accounts'
|
||||
|
@ -5,17 +5,58 @@
|
||||
<h1 class="title has-text-grey-dark">
|
||||
{{ $t('twofaccounts.import.import') }}
|
||||
</h1>
|
||||
<div class="is-size-7-mobile" v-html="$t('twofaccounts.import.import_legend')">
|
||||
</div>
|
||||
<div class="mt-3 mb-6">
|
||||
<router-link class="is-link" :to="{ name: 'start', params: {showAdvancedFormButton: false, returnToView: 'importAccounts'} }">
|
||||
<span class="tag is-black">
|
||||
<font-awesome-icon :icon="['fas', 'qrcode']" size="lg" class="mr-1" />{{ $t('twofaccounts.import.use_the_gauth_qr_code') }}
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="exportedAccounts.length > 0">
|
||||
<div v-if="exportedAccounts.length == 0">
|
||||
<div class="block is-size-7-mobile" v-html="$t('twofaccounts.import.import_legend')"></div>
|
||||
<!-- scan button that launch camera stream -->
|
||||
<div class="block">
|
||||
<button tabindex="0" class="button is-link is-rounded" @click="capture()">
|
||||
{{ $t('twofaccounts.forms.scan_qrcode') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- upload a qr code (with basic file field and backend decoding) -->
|
||||
<div class="block">
|
||||
<label role="button" tabindex="0" class="button is-link is-rounded is-outlined" ref="qrcodeInputLabel" @keyup.enter="$refs.qrcodeInputLabel.click()">
|
||||
<input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
|
||||
{{ $t('twofaccounts.forms.upload_qrcode') }}
|
||||
</label>
|
||||
<field-error :form="form" field="qrcode" />
|
||||
<p class="help">{{ $t('twofaccounts.import.supported_formats_for_qrcode_upload') }}</p>
|
||||
</div>
|
||||
<!-- upload a file -->
|
||||
<div class="block">
|
||||
<label role="button" tabindex="0" class="button is-link is-rounded is-outlined" ref="fileInputLabel" @keyup.enter="$refs.fileInputLabel.click()">
|
||||
<input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="text/plain,application/json,text/csv" v-on:change="submitFile" ref="fileInput">
|
||||
{{ $t('twofaccounts.import.upload_a_file') }}
|
||||
</label>
|
||||
<field-error :form="uploadForm" field="file" />
|
||||
<p class="help">{{ $t('twofaccounts.import.supported_formats_for_file_upload') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Supported migration resources -->
|
||||
<h5 class="title is-5 mb-3">{{ $t('twofaccounts.import.supported_migration_formats') }}</h5>
|
||||
<div class="field is-grouped is-grouped-multiline pt-0">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-dark">Google Auth</span>
|
||||
<span class="tag is-black">{{ $t('twofaccounts.import.qr_code') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-dark">Aegis Auth</span>
|
||||
<span class="tag is-black">JSON</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-dark">Aegis Auth</span>
|
||||
<span class="tag is-black">{{ $t('twofaccounts.import.plain_text') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="(account, index) in exportedAccounts" :key="account.name" class="group-item has-text-light is-size-5 is-size-6-mobile">
|
||||
<div class="is-flex is-justify-content-space-between">
|
||||
<!-- Account name -->
|
||||
@ -108,7 +149,7 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
migrationUri: '',
|
||||
migrationPayload: '',
|
||||
exportedAccounts: [],
|
||||
isFetching: false,
|
||||
form: new Form({
|
||||
@ -125,6 +166,7 @@
|
||||
image: '',
|
||||
qrcode: null,
|
||||
}),
|
||||
uploadForm: new Form(),
|
||||
ShowTwofaccountInModal : false,
|
||||
}
|
||||
},
|
||||
@ -143,18 +185,7 @@
|
||||
// A migration URI is provided as route parameter, we extract the accounts from the URI and
|
||||
// list them in the view
|
||||
if( this.$route.params.migrationUri ) {
|
||||
this.migrationUri = this.$route.params.migrationUri
|
||||
this.isFetching = true
|
||||
|
||||
await this.axios.post('/api/v1/import/google-auth', { uri: this.migrationUri }).then(response => {
|
||||
response.data.forEach((data) => {
|
||||
data.imported = -1;
|
||||
this.exportedAccounts.push(data)
|
||||
})
|
||||
});
|
||||
|
||||
this.$notify({type: 'is-success', text: this.$t('twofaccounts.import.x_valid_accounts_found', { count: this.importableCount }) })
|
||||
this.isFetching = false
|
||||
this.migrate(this.$route.params.migrationUri)
|
||||
}
|
||||
|
||||
this.$on('modalClose', function() {
|
||||
@ -169,6 +200,28 @@
|
||||
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* Post the migration payload
|
||||
*/
|
||||
async migrate(migrationPayload) {
|
||||
this.migrationPayload = migrationPayload
|
||||
this.isFetching = true
|
||||
|
||||
await this.axios.post('/api/v1/twofaccounts/migration', {payload: this.migrationPayload}, {returnError: true}).then(response => {
|
||||
response.data.forEach((data) => {
|
||||
data.imported = -1;
|
||||
this.exportedAccounts.push(data)
|
||||
})
|
||||
|
||||
this.notifyValidAccountFound()
|
||||
})
|
||||
.catch(error => {
|
||||
this.$notify({type: 'is-danger', text: this.$t(error.response.data.message) })
|
||||
});
|
||||
|
||||
this.isFetching = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all duplicates from the accounts list
|
||||
*/
|
||||
@ -258,6 +311,65 @@
|
||||
this.form.counter = twofaccount.otp_type === 'hotp' ? twofaccount.counter : null
|
||||
this.form.period = twofaccount.otp_type === 'totp' ? twofaccount.period : null
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload the submitted file to the backend for parsing
|
||||
*/
|
||||
submitFile() {
|
||||
this.isFetching = true
|
||||
|
||||
let filedata = new FormData();
|
||||
filedata.append('file', this.$refs.fileInput.files[0]);
|
||||
|
||||
this.uploadForm.upload('/api/v1/twofaccounts/migration', filedata, {returnError: true}).then(response => {
|
||||
response.data.forEach((data) => {
|
||||
data.imported = -1;
|
||||
this.exportedAccounts.push(data)
|
||||
})
|
||||
|
||||
this.notifyValidAccountFound()
|
||||
})
|
||||
.catch(error => {
|
||||
if( error.response.status !== 422 ) {
|
||||
this.$notify({type: 'is-danger', text: this.$t(error.response.data.message) })
|
||||
}
|
||||
});
|
||||
|
||||
this.isFetching = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload the submitted QR code file to the backend for decoding
|
||||
*/
|
||||
submitQrCode() {
|
||||
|
||||
let imgdata = new FormData();
|
||||
imgdata.append('qrcode', this.$refs.qrcodeInput.files[0]);
|
||||
|
||||
this.form.upload('/api/v1/qrcode/decode', imgdata, {returnError: true}).then(response => {
|
||||
this.migrate(response.data.data)
|
||||
})
|
||||
.catch(error => {
|
||||
if( error.response.status !== 422 ) {
|
||||
this.$notify({type: 'is-danger', text: this.$t(error.response.data.message) })
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Push user to the dedicated capture view for live scan
|
||||
*/
|
||||
capture() {
|
||||
this.$router.push({ name: 'capture' });
|
||||
},
|
||||
|
||||
/**
|
||||
* Notify that valid account(s) have been found for import
|
||||
*/
|
||||
notifyValidAccountFound() {
|
||||
this.$notify({type: 'is-success', text: this.$t('twofaccounts.import.x_valid_accounts_found', { count: this.importableCount }) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,8 +39,10 @@
|
||||
'user_deletion_failed' => 'User account deletion failed, no data have been deleted',
|
||||
'auth_proxy_failed' => 'Proxy authentication failed',
|
||||
'auth_proxy_failed_legend' => '2Fauth is configured to run behind an authentication proxy but your proxy does not return the expected header. Check your configuration and try again.',
|
||||
'invalid_google_auth_migration' => 'Invalid or unreadable Google Authenticator data',
|
||||
'invalid_x_migration' => 'Invalid or unreadable :appname data',
|
||||
'unsupported_migration' => 'Data do not match any supported format',
|
||||
'unsupported_otp_type' => 'Unsupported OTP type',
|
||||
'encrypted_migration' => 'Unreadable, the data seem encrypted',
|
||||
'no_logo_found_for_x' => 'No logo available for {service}',
|
||||
'file_upload_failed' => 'File upload failed'
|
||||
];
|
@ -134,8 +134,13 @@
|
||||
'import' => [
|
||||
'import' => 'Import',
|
||||
'to_import' => 'Import',
|
||||
'import_legend' => 'Import your Google Authenticator accounts.',
|
||||
'use_the_gauth_qr_code' => 'Load a G-Auth QR code',
|
||||
'import_legend' => '2FAuth can import data from various 2FA apps.<br />Use the Export feature of these apps to get a migration resource (a QR code or a file) and submit it using your preferred method below.',
|
||||
'upload_a_file' => 'Upload a file',
|
||||
'supported_formats_for_qrcode_upload' => 'Accepted: jpg, jpeg, png, bmp, gif, svg, or webp',
|
||||
'supported_formats_for_file_upload' => 'Accepted: Plain text, json, csv',
|
||||
'supported_migration_formats' => 'Supported migration formats',
|
||||
'qr_code' => 'QR code',
|
||||
'plain_text' => 'Plain text',
|
||||
'issuer' => 'Issuer',
|
||||
'imported' => 'Imported',
|
||||
'failure' => 'Failure',
|
||||
|
@ -29,6 +29,7 @@
|
||||
Route::delete('twofaccounts', 'TwoFAccountController@batchDestroy')->name('twofaccounts.batchDestroy');
|
||||
Route::patch('twofaccounts/withdraw', 'TwoFAccountController@withdraw')->name('twofaccounts.withdraw');
|
||||
Route::post('twofaccounts/reorder', 'TwoFAccountController@reorder')->name('twofaccounts.reorder');
|
||||
Route::post('twofaccounts/migration', 'TwoFAccountController@migrate')->name('twofaccounts.migrate');
|
||||
Route::post('twofaccounts/preview', 'TwoFAccountController@preview')->name('twofaccounts.preview');
|
||||
Route::get('twofaccounts/{twofaccount}/qrcode', 'QrCodeController@show')->name('twofaccounts.show.qrcode');
|
||||
Route::get('twofaccounts/count', 'TwoFAccountController@count')->name('twofaccounts.count');
|
||||
@ -36,9 +37,6 @@
|
||||
Route::post('twofaccounts/otp', 'TwoFAccountController@otp')->name('twofaccounts.otp');
|
||||
Route::apiResource('twofaccounts', 'TwoFAccountController');
|
||||
|
||||
Route::post('import/google-auth', 'ImportController@googleAuth')->name('import.googleAuth');
|
||||
Route::post('import/aegis', 'ImportController@aegis')->name('import.aegis');
|
||||
|
||||
Route::get('groups/{group}/twofaccounts', 'GroupController@accounts')->name('groups.show.twofaccounts');
|
||||
Route::post('groups/{group}/assign', 'GroupController@assignAccounts')->name('groups.assign.twofaccounts');
|
||||
Route::apiResource('groups', 'GroupController');
|
||||
|
@ -10,6 +10,7 @@
|
||||
use App\Models\TwoFAccount;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\Classes\LocalFile;
|
||||
|
||||
|
||||
/**
|
||||
@ -457,11 +458,11 @@ public function test_update_twofaccount_with_invalid_data_returns_validation_err
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_import_valid_gauth_data_returns_success_with_consistent_resources()
|
||||
public function test_import_valid_gauth_payload_returns_success_with_consistent_resources()
|
||||
{
|
||||
$response = $this->actingAs($this->user, 'api-guard')
|
||||
->json('POST', '/api/v1/twofaccounts/import', [
|
||||
'uri' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI,
|
||||
->json('POST', '/api/v1/twofaccounts/migration', [
|
||||
'payload' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonCount(2, $key = null)
|
||||
@ -493,10 +494,10 @@ public function test_import_valid_gauth_data_returns_success_with_consistent_res
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_import_with_invalid_uri_returns_validation_error()
|
||||
public function test_import_with_invalid_gauth_payload_returns_validation_error()
|
||||
{
|
||||
$response = $this->actingAs($this->user, 'api-guard')
|
||||
->json('POST', '/api/v1/twofaccounts', [
|
||||
->json('POST', '/api/v1/twofaccounts/migration', [
|
||||
'uri' => OtpTestData::INVALID_GOOGLE_AUTH_MIGRATION_URI,
|
||||
])
|
||||
->assertStatus(422);
|
||||
@ -506,7 +507,7 @@ public function test_import_with_invalid_uri_returns_validation_error()
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_import_gauth_data_with_duplicates_returns_negative_ids()
|
||||
public function test_import_gauth_payload_with_duplicates_returns_negative_ids()
|
||||
{
|
||||
$twofaccount = TwoFAccount::factory()->create([
|
||||
'otp_type' => 'totp',
|
||||
@ -521,8 +522,8 @@ public function test_import_gauth_data_with_duplicates_returns_negative_ids()
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user, 'api-guard')
|
||||
->json('POST', '/api/v1/twofaccounts/import', [
|
||||
'uri' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI,
|
||||
->json('POST', '/api/v1/twofaccounts/migration', [
|
||||
'payload' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonFragment([
|
||||
@ -536,11 +537,11 @@ public function test_import_gauth_data_with_duplicates_returns_negative_ids()
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_import_invalid_gauth_data_returns_bad_request()
|
||||
public function test_import_invalid_gauth_payload_returns_bad_request()
|
||||
{
|
||||
$response = $this->actingAs($this->user, 'api-guard')
|
||||
->json('POST', '/api/v1/twofaccounts/import', [
|
||||
'uri' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA,
|
||||
->json('POST', '/api/v1/twofaccounts/migration', [
|
||||
'payload' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA,
|
||||
])
|
||||
->assertStatus(400)
|
||||
->assertJsonStructure([
|
||||
@ -549,6 +550,215 @@ public function test_import_invalid_gauth_data_returns_bad_request()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_import_valid_aegis_json_file_returns_success()
|
||||
{
|
||||
$file = LocalFile::fake()->validAegisJsonFile();
|
||||
|
||||
$response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
|
||||
->actingAs($this->user, 'api-guard')
|
||||
->json('POST', '/api/v1/twofaccounts/migration', [
|
||||
'file' => $file,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonCount(5, $key = null)
|
||||
->assertJsonFragment([
|
||||
'id' => 0,
|
||||
'service' => OtpTestData::SERVICE . '_totp',
|
||||
'account' => OtpTestData::ACCOUNT . '_totp',
|
||||
'otp_type' => 'totp',
|
||||
'secret' => OtpTestData::SECRET,
|
||||
'digits' => OtpTestData::DIGITS_DEFAULT,
|
||||
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
|
||||
'period' => OtpTestData::PERIOD_DEFAULT,
|
||||
'counter' => null
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'id' => 0,
|
||||
'service' => OtpTestData::SERVICE . '_totp_custom',
|
||||
'account' => OtpTestData::ACCOUNT . '_totp_custom',
|
||||
'otp_type' => 'totp',
|
||||
'secret' => OtpTestData::SECRET,
|
||||
'digits' => OtpTestData::DIGITS_CUSTOM,
|
||||
'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
|
||||
'period' => OtpTestData::PERIOD_CUSTOM,
|
||||
'counter' => null
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'id' => 0,
|
||||
'service' => OtpTestData::SERVICE . '_hotp',
|
||||
'account' => OtpTestData::ACCOUNT . '_hotp',
|
||||
'otp_type' => 'hotp',
|
||||
'secret' => OtpTestData::SECRET,
|
||||
'digits' => OtpTestData::DIGITS_DEFAULT,
|
||||
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
|
||||
'period' => null,
|
||||
'counter' => OtpTestData::COUNTER_DEFAULT
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'id' => 0,
|
||||
'service' => OtpTestData::SERVICE . '_hotp_custom',
|
||||
'account' => OtpTestData::ACCOUNT . '_hotp_custom',
|
||||
'otp_type' => 'totp',
|
||||
'secret' => OtpTestData::SECRET,
|
||||
'digits' => OtpTestData::DIGITS_CUSTOM,
|
||||
'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
|
||||
'period' => null,
|
||||
'counter' => OtpTestData::COUNTER_CUSTOM,
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'id' => 0,
|
||||
'service' => OtpTestData::STEAM,
|
||||
'account' => OtpTestData::ACCOUNT . '_steam',
|
||||
'otp_type' => 'steamtotp',
|
||||
'secret' => OtpTestData::STEAM_SECRET,
|
||||
'digits' => OtpTestData::DIGITS_STEAM,
|
||||
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
|
||||
'period' => OtpTestData::PERIOD_DEFAULT,
|
||||
'counter' => null
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*
|
||||
* @dataProvider invalidAegisJsonFileProvider
|
||||
*/
|
||||
public function test_import_invalid_aegis_json_file_returns_bad_request($file)
|
||||
{
|
||||
$response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
|
||||
->actingAs($this->user, 'api-guard')
|
||||
->json('POST', '/api/v1/twofaccounts/migration', [
|
||||
'file' => $file,
|
||||
])
|
||||
->assertStatus(400);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provide invalid Aegis JSON files for import tests
|
||||
*/
|
||||
public function invalidAegisJsonFileProvider()
|
||||
{
|
||||
return [
|
||||
'validPlainTextFile' => [
|
||||
LocalFile::fake()->encryptedAegisJsonFile()
|
||||
],
|
||||
'validPlainTextFileWithNewLines' => [
|
||||
LocalFile::fake()->invalidAegisJsonFile()
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*
|
||||
* @dataProvider validPlainTextFileProvider
|
||||
*/
|
||||
public function test_import_valid_plain_text_file_returns_success($file)
|
||||
{
|
||||
$response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
|
||||
->actingAs($this->user, 'api-guard')
|
||||
->json('POST', '/api/v1/twofaccounts/migration', [
|
||||
'file' => $file,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonCount(3, $key = null)
|
||||
->assertJsonFragment([
|
||||
'id' => 0,
|
||||
'service' => OtpTestData::SERVICE,
|
||||
'account' => OtpTestData::ACCOUNT,
|
||||
'otp_type' => 'totp',
|
||||
'secret' => OtpTestData::SECRET,
|
||||
'digits' => OtpTestData::DIGITS_CUSTOM,
|
||||
'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
|
||||
'period' => OtpTestData::PERIOD_CUSTOM,
|
||||
'counter' => null
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'id' => 0,
|
||||
'service' => OtpTestData::SERVICE,
|
||||
'account' => OtpTestData::ACCOUNT,
|
||||
'otp_type' => 'hotp',
|
||||
'secret' => OtpTestData::SECRET,
|
||||
'digits' => OtpTestData::DIGITS_CUSTOM,
|
||||
'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
|
||||
'period' => null,
|
||||
'counter' => OtpTestData::COUNTER_CUSTOM
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'id' => 0,
|
||||
'service' => OtpTestData::STEAM,
|
||||
'account' => OtpTestData::ACCOUNT,
|
||||
'otp_type' => 'steamtotp',
|
||||
'secret' => OtpTestData::STEAM_SECRET,
|
||||
'digits' => OtpTestData::DIGITS_STEAM,
|
||||
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
|
||||
'period' => OtpTestData::PERIOD_DEFAULT,
|
||||
'counter' => null
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provide valid Plain Text files for import tests
|
||||
*/
|
||||
public function validPlainTextFileProvider()
|
||||
{
|
||||
return [
|
||||
'validPlainTextFile' => [
|
||||
LocalFile::fake()->validPlainTextFile()
|
||||
],
|
||||
'validPlainTextFileWithNewLines' => [
|
||||
LocalFile::fake()->validPlainTextFileWithNewLines()
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*
|
||||
* @dataProvider invalidPlainTextFileProvider
|
||||
*/
|
||||
public function test_import_invalid_plain_text_file_returns_bad_request($file)
|
||||
{
|
||||
|
||||
$response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
|
||||
->actingAs($this->user, 'api-guard')
|
||||
->json('POST', '/api/v1/twofaccounts/migration', [
|
||||
'file' => $file,
|
||||
])
|
||||
->assertStatus(400);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provide invalid Plain Text files for import tests
|
||||
*/
|
||||
public function invalidPlainTextFileProvider()
|
||||
{
|
||||
return [
|
||||
'validPlainTextFile' => [
|
||||
LocalFile::fake()->invalidPlainTextFileEmpty()
|
||||
],
|
||||
'validPlainTextFileWithNewLines' => [
|
||||
LocalFile::fake()->invalidPlainTextFileNoUri()
|
||||
],
|
||||
'validPlainTextFileWithNewLines' => [
|
||||
LocalFile::fake()->invalidPlainTextFileWithInvalidUri()
|
||||
],
|
||||
'validPlainTextFileWithNewLines' => [
|
||||
LocalFile::fake()->invalidPlainTextFileWithInvalidLine()
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
@ -45,7 +45,7 @@ public function provideValidData() : array
|
||||
{
|
||||
return [
|
||||
[[
|
||||
'uri' => 'otpauth-migration://offline?data=AEoATACEAEYASAA'
|
||||
'payload' => 'otpauth-migration://offline?data=AEoATACEAEYASAA'
|
||||
]],
|
||||
];
|
||||
}
|
||||
@ -68,29 +68,17 @@ public function provideInvalidData() : array
|
||||
{
|
||||
return [
|
||||
[[
|
||||
'uri' => null // required
|
||||
'payload' => null // required
|
||||
]],
|
||||
[[
|
||||
'uri' => '' // required
|
||||
'payload' => '' // required
|
||||
]],
|
||||
[[
|
||||
'uri' => true // string
|
||||
'payload' => true // string
|
||||
]],
|
||||
[[
|
||||
'uri' => 8 // string
|
||||
]],
|
||||
[[
|
||||
'uri' => 'otpXauth-migration://offline?data=fYmlzIAEoATACEAEYASAA' // regex
|
||||
]],
|
||||
[[
|
||||
'uri' => 'otpauth-migration:/offline?data=fYmlzIAEoATACEAEYASAA' // regex
|
||||
]],
|
||||
[[
|
||||
'uri' => 'otpauth-migration://offlinedata=fYmlzIAEoATACEAEYASAA' // regex
|
||||
]],
|
||||
[[
|
||||
'uri' => 'otpauth-migration://offline?dat=fYmlzIAEoATACEAEYASAA' // regex
|
||||
]],
|
||||
'payload' => 8 // string
|
||||
]]
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace Tests\Classes;
|
||||
|
||||
use Illuminate\Http\Testing\File;
|
||||
use Tests\Classes\OtpTestData;
|
||||
|
||||
class LocalFileFactory {
|
||||
|
||||
@ -48,4 +49,181 @@ public function invalidQrcode()
|
||||
fwrite($temp, ob_get_clean());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new local valid Aegis JSON file.
|
||||
*
|
||||
* @return \Illuminate\Http\Testing\File
|
||||
*/
|
||||
public function validAegisJsonFile()
|
||||
{
|
||||
return new File('validAegisMigration.json', tap(tmpfile(), function ($temp) {
|
||||
ob_start();
|
||||
|
||||
echo OtpTestData::AEGIS_JSON_MIGRATION_PAYLOAD;
|
||||
|
||||
fwrite($temp, ob_get_clean());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new local invalid Aegis JSON file.
|
||||
*
|
||||
* @return \Illuminate\Http\Testing\File
|
||||
*/
|
||||
public function invalidAegisJsonFile()
|
||||
{
|
||||
return new File('invalidAegisMigration.json', tap(tmpfile(), function ($temp) {
|
||||
ob_start();
|
||||
|
||||
echo OtpTestData::INVALID_AEGIS_JSON_MIGRATION_PAYLOAD;
|
||||
|
||||
fwrite($temp, ob_get_clean());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new local encrypted Aegis JSON file.
|
||||
*
|
||||
* @return \Illuminate\Http\Testing\File
|
||||
*/
|
||||
public function encryptedAegisJsonFile()
|
||||
{
|
||||
return new File('encryptedAegisJsonFile.txt', tap(tmpfile(), function ($temp) {
|
||||
ob_start();
|
||||
|
||||
echo OtpTestData::ENCRYPTED_AEGIS_JSON_MIGRATION_PAYLOAD;
|
||||
|
||||
fwrite($temp, ob_get_clean());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new local valid Plain Text file.
|
||||
*
|
||||
* @return \Illuminate\Http\Testing\File
|
||||
*/
|
||||
public function validPlainTextFile()
|
||||
{
|
||||
return new File('validPlainTextFile.txt', tap(tmpfile(), function ($temp) {
|
||||
ob_start();
|
||||
|
||||
echo OtpTestData::TOTP_FULL_CUSTOM_URI;
|
||||
echo PHP_EOL;
|
||||
echo OtpTestData::HOTP_FULL_CUSTOM_URI;
|
||||
echo PHP_EOL;
|
||||
echo OtpTestData::STEAM_TOTP_URI;
|
||||
|
||||
fwrite($temp, ob_get_clean());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new local valid Plain Text file with new lines.
|
||||
*
|
||||
* @return \Illuminate\Http\Testing\File
|
||||
*/
|
||||
public function validPlainTextFileWithNewLines()
|
||||
{
|
||||
return new File('validPlainTextFileWithNewLines.txt', tap(tmpfile(), function ($temp) {
|
||||
ob_start();
|
||||
|
||||
echo PHP_EOL;
|
||||
echo OtpTestData::TOTP_FULL_CUSTOM_URI;
|
||||
echo PHP_EOL;
|
||||
echo PHP_EOL;
|
||||
echo OtpTestData::HOTP_FULL_CUSTOM_URI;
|
||||
echo PHP_EOL;
|
||||
echo PHP_EOL;
|
||||
echo OtpTestData::STEAM_TOTP_URI;
|
||||
echo PHP_EOL;
|
||||
|
||||
fwrite($temp, ob_get_clean());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new local invalid Plain Text file with no URI.
|
||||
*
|
||||
* @return \Illuminate\Http\Testing\File
|
||||
*/
|
||||
public function invalidPlainTextFileNoUri()
|
||||
{
|
||||
return new File('invalidPlainTextFileNoUri.txt', tap(tmpfile(), function ($temp) {
|
||||
ob_start();
|
||||
|
||||
echo 'lorem ipsum';
|
||||
echo PHP_EOL;
|
||||
echo 'lorem ipsum';
|
||||
|
||||
fwrite($temp, ob_get_clean());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new local invalid Plain Text file with invalid line.
|
||||
*
|
||||
* @return \Illuminate\Http\Testing\File
|
||||
*/
|
||||
public function invalidPlainTextFileWithInvalidLine()
|
||||
{
|
||||
return new File('invalidPlainTextFileWithInvalidLine.txt', tap(tmpfile(), function ($temp) {
|
||||
ob_start();
|
||||
|
||||
echo OtpTestData::TOTP_FULL_CUSTOM_URI;
|
||||
echo PHP_EOL;
|
||||
echo 'lorem ipsum';
|
||||
echo PHP_EOL;
|
||||
echo OtpTestData::HOTP_FULL_CUSTOM_URI;
|
||||
|
||||
fwrite($temp, ob_get_clean());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new local invalid Plain Text file with invalid URI.
|
||||
*
|
||||
* @return \Illuminate\Http\Testing\File
|
||||
*/
|
||||
public function invalidPlainTextFileWithInvalidUri()
|
||||
{
|
||||
return new File('invalidPlainTextFileWithInvalidUri.txt', tap(tmpfile(), function ($temp) {
|
||||
ob_start();
|
||||
|
||||
echo OtpTestData::TOTP_FULL_CUSTOM_URI;
|
||||
echo PHP_EOL;
|
||||
echo OtpTestData::INVALID_OTPAUTH_URI;
|
||||
echo PHP_EOL;
|
||||
echo OtpTestData::HOTP_FULL_CUSTOM_URI;
|
||||
|
||||
fwrite($temp, ob_get_clean());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new local empty Plain Text file.
|
||||
*
|
||||
* @return \Illuminate\Http\Testing\File
|
||||
*/
|
||||
public function invalidPlainTextFileEmpty()
|
||||
{
|
||||
return new File('invalidPlainTextFileEmpty.txt', tap(tmpfile(), function ($temp) {
|
||||
ob_start();
|
||||
|
||||
echo '';
|
||||
|
||||
fwrite($temp, ob_get_clean());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -84,4 +84,145 @@ class OtpTestData
|
||||
const GOOGLE_AUTH_MIGRATION_URI = 'otpauth-migration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY2UgASgBMAIKLAoKBw0SnrWITY/RFhILYWNjb3VudF9iaXMaC3NlcnZpY2VfYmlzIAEoATACEAEYASAA';
|
||||
const INVALID_GOOGLE_AUTH_MIGRATION_URI = 'otpauthmigration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY2UgASgBMAIKLAoKBw0SnrWITY/RFhILYWNjb3VudF9iaXMaC3NlcnZpY2VfYmlzIAEoATACEAEYASAA';
|
||||
const GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA = 'otpauth-migration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY';
|
||||
|
||||
const AEGIS_JSON_MIGRATION_PAYLOAD = '
|
||||
{
|
||||
"version": 1,
|
||||
"header": {
|
||||
"slots": null,
|
||||
"params": null
|
||||
},
|
||||
"db": {
|
||||
"version": 2,
|
||||
"entries": [
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "5be1b189-260d-4fe1-930a-a78cb669dd86",
|
||||
"name": "'.self::ACCOUNT.'_totp",
|
||||
"issuer": "'.self::SERVICE.'_totp",
|
||||
"note": "",
|
||||
"icon": null,
|
||||
"info": {
|
||||
"secret": "'.self::SECRET.'",
|
||||
"algo": "'.self::ALGORITHM_DEFAULT.'",
|
||||
"digits": '.self::DIGITS_DEFAULT.',
|
||||
"period": '.self::PERIOD_DEFAULT.'
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "fb2ebd05-9d71-4b2e-9d4e-b7f8d2942bfb",
|
||||
"name": "'.self::ACCOUNT.'_totp_custom",
|
||||
"issuer": "'.self::SERVICE.'_totp_custom",
|
||||
"note": "",
|
||||
"icon": null,
|
||||
"info": {
|
||||
"secret": "'.self::SECRET.'",
|
||||
"algo": "'.self::ALGORITHM_CUSTOM.'",
|
||||
"digits": '.self::DIGITS_CUSTOM.',
|
||||
"period": '.self::PERIOD_CUSTOM.'
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "hotp",
|
||||
"uuid": "90a2af2e-2857-4515-bb18-52c4fa823f6f",
|
||||
"name": "'.self::ACCOUNT.'_hotp",
|
||||
"issuer": "'.self::SERVICE.'_hotp",
|
||||
"note": "",
|
||||
"icon": null,
|
||||
"info": {
|
||||
"secret": "'.self::SECRET.'",
|
||||
"algo": "'.self::ALGORITHM_DEFAULT.'",
|
||||
"digits": '.self::DIGITS_DEFAULT.',
|
||||
"counter": '.self::COUNTER_DEFAULT.'
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "hotp",
|
||||
"uuid": "e1b3f683-d5fe-4126-b616-8c8abd8ad97c",
|
||||
"name": "'.self::ACCOUNT.'_hotp_custom",
|
||||
"issuer": "'.self::SERVICE.'_hotp_custom",
|
||||
"note": "",
|
||||
"icon": null,
|
||||
"info": {
|
||||
"secret": "'.self::SECRET.'",
|
||||
"algo": "'.self::ALGORITHM_CUSTOM.'",
|
||||
"digits": '.self::DIGITS_CUSTOM.',
|
||||
"counter": '.self::COUNTER_CUSTOM.'
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "steamtotp",
|
||||
"uuid": "9fb06143-421d-46e1-a7e9-4aafe44b0e72",
|
||||
"name": "'.self::ACCOUNT.'_steam",
|
||||
"issuer": "'.self::STEAM.'",
|
||||
"note": "",
|
||||
"icon": "null",
|
||||
"info": {
|
||||
"secret": "'.self::STEAM_SECRET.'",
|
||||
"algo": "'.self::ALGORITHM_DEFAULT.'",
|
||||
"digits": '.self::DIGITS_STEAM.',
|
||||
"period": '.self::PERIOD_DEFAULT.'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}';
|
||||
|
||||
const INVALID_AEGIS_JSON_MIGRATION_PAYLOAD = '
|
||||
{
|
||||
"version": 1,
|
||||
"header": {
|
||||
"slots": null,
|
||||
"params": null
|
||||
},
|
||||
"db": {
|
||||
"version": 2,
|
||||
"thisIsNotTheCorrectKeyName": [
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "5be1b189-260d-4fe1-930a-a78cb669dd86",
|
||||
"name": "'.self::ACCOUNT.'",
|
||||
"issuer": "'.self::SERVICE.'",
|
||||
"note": "",
|
||||
"icon": null,
|
||||
"info": {
|
||||
"secret": "'.self::SECRET.'",
|
||||
"algo": "'.self::ALGORITHM_DEFAULT.'",
|
||||
"digits": '.self::DIGITS_DEFAULT.',
|
||||
"period": '.self::PERIOD_DEFAULT.'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}';
|
||||
|
||||
const ENCRYPTED_AEGIS_JSON_MIGRATION_PAYLOAD = '
|
||||
{
|
||||
"version": 1,
|
||||
"header": {
|
||||
"slots": [
|
||||
{
|
||||
"type": 1,
|
||||
"uuid": "1f447956-c71c-4be4-8192-97197dc67df7",
|
||||
"key": "d742967686cae462c5732023a72d59245d8q7c5c93a66aeb2q2a350bb8b6a7ae",
|
||||
"key_params": {
|
||||
"nonce": "77a8ff6d84265efd2a3ed9b7",
|
||||
"tag": "cc13fb4a5baz3fd27bc97f5eacaa00d0"
|
||||
},
|
||||
"n": 32768,
|
||||
"r": 8,
|
||||
"p": 1,
|
||||
"salt": "1c245b2696b948dt040c30c538aeb6f9620b054d9ff182f33dd4b285b67bed51",
|
||||
"repaired": true
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"nonce": "f31675d9966d2z588bd07788",
|
||||
"tag": "ad5729fa135dc6d6sw87e0c955932661"
|
||||
}
|
||||
},
|
||||
"db": "1rX0ajzsxNbhN2hvnNCMBNooLlzqwz\/LMT3bNEIJjPH+zIvIbA6GVVPHLpna+yvjxLPKVkt1OQig=="
|
||||
}';
|
||||
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ public function test_delete_single_id()
|
||||
*/
|
||||
public function test_convert_migration_from_gauth_returns_correct_accounts()
|
||||
{
|
||||
$twofaccounts = TwoFAccounts::convertMigrationFromGA(OtpTestData::GOOGLE_AUTH_MIGRATION_URI);
|
||||
$twofaccounts = TwoFAccounts::migrate(OtpTestData::GOOGLE_AUTH_MIGRATION_URI);
|
||||
|
||||
$this->assertCount(2, $twofaccounts);
|
||||
|
||||
@ -240,7 +240,7 @@ public function test_convert_migration_from_gauth_returns_flagged_duplicates()
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithOtpParameters($parameters)->save();
|
||||
|
||||
$twofaccounts = TwoFAccounts::convertMigrationFromGA(OtpTestData::GOOGLE_AUTH_MIGRATION_URI);
|
||||
$twofaccounts = TwoFAccounts::migrate(OtpTestData::GOOGLE_AUTH_MIGRATION_URI);
|
||||
|
||||
$this->assertEquals(-1, $twofaccounts->first()->id);
|
||||
$this->assertEquals(-1, $twofaccounts->last()->id);
|
||||
@ -250,10 +250,10 @@ public function test_convert_migration_from_gauth_returns_flagged_duplicates()
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_convert_invalid_migration_from_gauth_returns_InvalidGoogleAuthMigration_excpetion()
|
||||
public function test_convert_invalid_migration_from_gauth_returns_InvalidMigrationData_exception()
|
||||
{
|
||||
$this->expectException(\App\Exceptions\InvalidGoogleAuthMigration::class);
|
||||
$twofaccounts = TwoFAccounts::convertMigrationFromGA(OtpTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA);
|
||||
$this->expectException(\App\Exceptions\InvalidMigrationDataException::class);
|
||||
$twofaccounts = TwoFAccounts::migrate(OtpTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA);
|
||||
}
|
||||
|
||||
}
|
@ -59,11 +59,14 @@ public function provideExceptionsforBadRequest() : array
|
||||
'\App\Exceptions\DbEncryptionException'
|
||||
],
|
||||
[
|
||||
'\App\Exceptions\InvalidGoogleAuthMigration'
|
||||
'\App\Exceptions\InvalidMigrationDataException'
|
||||
],
|
||||
[
|
||||
'\App\Exceptions\UndecipherableException'
|
||||
],
|
||||
[
|
||||
'\App\Exceptions\UnsupportedMigrationException'
|
||||
],
|
||||
[
|
||||
'\App\Exceptions\UnsupportedOtpTypeException'
|
||||
],
|
||||
|
Loading…
Reference in New Issue
Block a user