Refactoring - Move OTPHP logic to TwoFAccount model

This commit is contained in:
Bubka 2022-07-05 10:10:24 +02:00
parent 1d99c27675
commit 720eb16750
24 changed files with 1297 additions and 1268 deletions

View File

@ -44,7 +44,7 @@ public function __construct(QrCodeService $qrcodeService, TwoFAccountService $tw
*/
public function show(TwoFAccount $twofaccount)
{
$uri = $this->twofaccountService->getURI($twofaccount);
$uri = $twofaccount->getURI();
return response()->json(['qrcode' => $this->qrcodeService->encode($uri)], 200);
}

View File

@ -87,10 +87,15 @@ public function store(TwoFAccountDynamicRequest $request)
// -> We use the parameters array to define the account
$validated = $request->validated();
$twofaccount = new TwoFAccount;
$twofaccount = Arr::has($validated, 'uri')
? $this->twofaccountService->createFromUri($validated['uri'])
: $this->twofaccountService->createFromParameters($validated);
if (Arr::has($validated, 'uri')) {
$twofaccount->fillWithURI($validated['uri'], Arr::get($validated, 'custom_otp') === TwoFAccount::STEAM_TOTP);
}
else {
$twofaccount->fillWithOtpParameters($validated);
}
$twofaccount->save();
// Possible group association
$this->groupService->assign($twofaccount->id);
@ -113,7 +118,8 @@ public function update(TwoFAccountUpdateRequest $request, TwoFAccount $twofaccou
{
$validated = $request->validated();
$this->twofaccountService->update($twofaccount, $validated);
$twofaccount->fillWithOtpParameters($validated);
$twofaccount->save();
return (new TwoFAccountReadResource($twofaccount))
->response()
@ -161,7 +167,8 @@ public function reorder(TwoFAccountReorderRequest $request)
*/
public function preview(TwoFAccountUriRequest $request)
{
$twofaccount = $this->twofaccountService->createFromUri($request->uri, false);
$twofaccount = new TwoFAccount;
$twofaccount->fillWithURI($request->uri, $request->custom_otp === TwoFAccount::STEAM_TOTP);
return new TwoFAccountStoreResource($twofaccount);
}
@ -179,38 +186,34 @@ public function otp(Request $request, $id = null)
$inputs = $request->all();
// The request input is the ID of an existing account
if ( $id ) {
try {
$otp = $this->twofaccountService->getOTP((int) $id);
}
catch (UndecipherableException $ex) {
return response()->json([
'message' => __('errors.cannot_decipher_secret')
], 400);
}
if ($id) {
$twofaccount = TwoFAccount::findOrFail((int) $id);
}
// The request input is an uri
else if ( count($inputs) === 1 && $request->has('uri') ) {
$validatedData = $request->validate((new TwoFAccountUriRequest)->rules());
$otp = $this->twofaccountService->getOTP($validatedData['uri']);
}
// return bad request if uri is provided with any other input
else if ( count($inputs) > 1 && $request->has('uri')) {
return response()->json([
'message' => 'bad request',
'reason' => ['uri' => __('validation.single', ['attribute' => 'uri'])]
], 400);
else if ( $request->has('uri') ) {
// return 404 if uri is provided with any parameter other than otp_type
if ((count($inputs) == 2 && $request->missing('custom_otp')) || count($inputs) > 2) {
return response()->json([
'message' => 'bad request',
'reason' => ['uri' => __('validation.onlyCustomOtpWithUri')]
], 400);
}
else {
$validatedData = $request->validate((new TwoFAccountUriRequest)->rules());
$twofaccount = new TwoFAccount;
$twofaccount->fillWithURI($validatedData['uri'], Arr::get($validatedData, 'custom_otp') === TwoFAccount::STEAM_TOTP);
}
}
// The request inputs should define an account
else {
$validatedData = $request->validate((new TwoFAccountStoreRequest)->rules());
$otp = $this->twofaccountService->getOTP($validatedData);
$twofaccount = new TwoFAccount();
$twofaccount->fillWithOtpParameters($validatedData);
}
return response()->json($otp, 200);
return response()->json($twofaccount->getOTP(), 200);
}

View File

@ -25,7 +25,8 @@ public function authorize()
public function rules()
{
return [
'uri' => 'required|string|regex:/^otpauth:\/\/[h,t]otp\//i',
'uri' => 'required|string|regex:/^otpauth:\/\/[h,t]otp\//i',
'custom_otp' => 'string|in:steamtotp',
];
}
}

View File

@ -65,6 +65,16 @@ public function register()
'message' => __('errors.invalid_google_auth_migration')], 400);
});
$this->renderable(function (UndecipherableException $exception, $request) {
return response()->json([
'message' => __('errors.cannot_decipher_secret')], 400);
});
$this->renderable(function (UnsupportedOtpTypeException $exception, $request) {
return response()->json([
'message' => __('errors.unsupported_otp_type')], 400);
});
$this->renderable(function (\Illuminate\Auth\AuthenticationException $exception, $request) {
if ($exception->guards() === ['reverse-proxy-guard']) {
return response()->json([

View File

@ -5,7 +5,7 @@
use Exception;
/**
* Class UndecipherableException.
* Class InvalidGoogleAuthMigration.
*
* @codeCoverageIgnore
*/

View File

@ -0,0 +1,14 @@
<?php
namespace App\Exceptions;
use Exception;
/**
* Class NotImplementedException.
*
* @codeCoverageIgnore
*/
class UnsupportedOtpTypeException extends Exception
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Models\Dto;
class HotpDto extends OtpDto
{
/* @var integer */
public int $counter;
}

12
app/Models/Dto/OtpDto.php Normal file
View File

@ -0,0 +1,12 @@
<?php
namespace App\Models\Dto;
class OtpDto
{
/* @var integer */
public string $password;
/* @var integer */
public string $otp_type;
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Models\Dto;
class TotpDto extends OtpDto
{
/* @var integer */
public int $generated_at;
/* @var integer */
public int $period;
}

View File

@ -3,27 +3,77 @@
namespace App\Models;
use Exception;
use App\Models\Dto\TotpDto;
use App\Models\Dto\HotpDto;
use App\Events\TwoFAccountDeleted;
use App\Exceptions\InvalidSecretException;
use App\Exceptions\InvalidOtpParameterException;
use App\Exceptions\UnsupportedOtpTypeException;
use App\Exceptions\UndecipherableException;
use Illuminate\Validation\ValidationException;
use Facades\App\Services\SettingService;
use Spatie\EloquentSortable\Sortable;
use Spatie\EloquentSortable\SortableTrait;
use OTPHP\TOTP;
use OTPHP\HOTP;
use OTPHP\Factory;
use SteamTotp\SteamTotp;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use ParagonIE\ConstantTime\Base32;
class TwoFAccount extends Model implements Sortable
{
use SortableTrait, HasFactory;
const TOTP = 'totp';
const HOTP = 'hotp';
const STEAM_TOTP = 'steamtotp';
const SHA1 = 'sha1';
const MD5 = 'md5';
const SHA256 = 'sha256';
const SHA512 = 'sha512';
const DEFAULT_PERIOD = 30;
const DEFAULT_COUNTER = 0;
const DEFAULT_DIGITS = 6;
const DEFAULT_ALGORITHM = self::SHA1;
private const IMAGELINK_STORAGE_PATH = 'imagesLink/';
private const ICON_STORAGE_PATH = 'public/icons/';
/**
* List of OTP types supported by 2FAuth
*/
private array $generatorClassMap = [
'OTPHP\TOTP' => self::TOTP,
'OTPHP\HOTP' => self::HOTP,
];
/**
* model's array form.
*
* @var array
*/
protected $fillable = [];
protected $fillable = [
// 'service',
// 'account',
// 'otp_type',
// 'digits',
// 'secret',
// 'algorithm',
// 'counter',
// 'period',
// 'icon'
];
/**
@ -40,6 +90,17 @@ class TwoFAccount extends Model implements Sortable
* @var array
*/
public $appends = [];
/**
* The model's default values for attributes.
*
* @var array
*/
protected $attributes = [
'digits' => 6,
'algorithm' => self::SHA1,
];
/**
@ -77,11 +138,35 @@ protected static function boot()
{
parent::boot();
static::saving(function ($twofaccount) {
if (!$twofaccount->legacy_uri) $twofaccount->legacy_uri = $twofaccount->getURI();
if ($twofaccount->otp_type == TwoFAccount::TOTP && !$twofaccount->period) $twofaccount->period = TwoFAccount::DEFAULT_PERIOD;
if ($twofaccount->otp_type == TwoFAccount::HOTP && !$twofaccount->counter) $twofaccount->counter = TwoFAccount::DEFAULT_COUNTER;
});
// static::deleted(function ($model) {
// Log::info(sprintf('TwoFAccount #%d deleted', $model->id));
// });
}
/**
* Fill the model with an array of attributes.
*
* @param array $attributes
* @return $this
*
* @throws \Illuminate\Database\Eloquent\MassAssignmentException
*/
// public function fill(array $attributes)
// {
// parent::fill($attributes);
// if ($this->otp_type == self::TOTP && !$this->period) $this->period = self::DEFAULT_PERIOD;
// if ($this->otp_type == self::HOTP && !$this->counter) $this->counter = self::DEFAULT_COUNTER;
// return $this;
// }
/**
* Settings for @spatie/eloquent-sortable package
@ -94,6 +179,15 @@ protected static function boot()
];
/**
* The OTP generator.
* Instanciated as null to keep the model light
*
* @var
*/
protected $generator = null;
/**
* Get legacy_uri attribute
*
@ -166,6 +260,309 @@ public function setSecretAttribute($value)
}
/**
* Set digits attribute
*
* @param string $value
* @return void
*/
public function setDigitsAttribute($value)
{
$this->attributes['digits'] = !$value ? 6 : $value;
}
/**
* Set algorithm attribute
*
* @param string $value
* @return void
*/
public function setAlgorithmAttribute($value)
{
$this->attributes['algorithm'] = !$value ? self::SHA1 : $value;
}
/**
* Set period attribute
*
* @param string $value
* @return void
*/
public function setPeriodAttribute($value)
{
$this->attributes['period'] = !$value && $this->otp_type === self::TOTP ? self::DEFAULT_PERIOD : $value;
}
/**
* Set counter attribute
*
* @param string $value
* @return void
*/
public function setCounterAttribute($value)
{
$this->attributes['counter'] = is_null($value) && $this->otp_type === self::HOTP ? self::DEFAULT_COUNTER : $value;
}
/**
* Returns a One-Time Password with its parameters
*
* @throws InvalidSecretException The secret is not a valid base32 encoded string
* @throws UndecipherableException The secret cannot be deciphered
*/
public function getOTP() : TotpDto|HotpDto
{
Log::info(sprintf('OTP requested for TwoFAccount #%s', $this->id));
// Early exit if the model has an undecipherable secret
if (strtolower($this->secret) === __('errors.indecipherable')) {
Log::error('Secret cannot be deciphered, OTP generation aborted');
throw new UndecipherableException();
}
$this->initGenerator();
try {
if ( $this->otp_type === self::TOTP || $this->otp_type === self::STEAM_TOTP ) {
$OtpDto = new TotpDto();
$OtpDto->otp_type = $this->otp_type;
$OtpDto->generated_at = time();
$OtpDto->password = $this->otp_type === self::TOTP
? $this->generator->at($OtpDto->generated_at)
: SteamTotp::getAuthCode(base64_encode(Base32::decodeUpper($this->secret)));
$OtpDto->period = $this->period;
}
else if ( $this->otp_type === self::HOTP ) {
$OtpDto = new HotpDto();
$OtpDto->otp_type = $this->otp_type;
$counter = $this->generator->getCounter();
$OtpDto->password = $this->generator->at($counter);
$OtpDto->counter = $this->counter = $counter + 1;
}
else throw new UnsupportedOtpTypeException();
Log::info(sprintf('New OTP generated for TwoFAccount #%s', $this->id));
return $OtpDto;
}
catch (\Exception|\Throwable $ex) {
Log::error('An error occured, OTP generation aborted');
// Currently a secret issue is the only possible exception thrown by OTPHP for this stack
// so it is Ok to send the corresponding 2FAuth exception.
// If the generator package change it could be necessary to throw a more generic exception.
throw new InvalidSecretException($ex->getMessage());
}
}
/**
* Fill the model using an array of OTP parameters.
* Missing parameters will be set with default values
*
* @return $this
*/
public function fillWithOtpParameters(array $parameters, bool $isSteamTotp = false)
{
$this->otp_type = Arr::get($parameters, 'otp_type');
$this->account = Arr::get($parameters, 'account');
$this->service = Arr::get($parameters, 'service');
$this->icon = Arr::get($parameters, 'icon');
$this->secret = Arr::get($parameters, 'secret');
$this->algorithm = Arr::get($parameters, 'algorithm', self::SHA1);
$this->digits = Arr::get($parameters, 'digits', self::DEFAULT_DIGITS);
$this->period = Arr::get($parameters, 'period', $this->otp_type == self::TOTP ? self::DEFAULT_PERIOD : null);
$this->counter = Arr::get($parameters, 'counter', $this->otp_type == self::HOTP ? self::DEFAULT_COUNTER : null);
$this->initGenerator();
if ($isSteamTotp) {
$this->enforceAsSteam();
}
Log::info(sprintf('TwoFAccount filled with OTP parameters'));
return $this;
}
/**
* Fill the model by parsing an otpauth URI
*
* @return $this
*/
public function fillWithURI(string $uri, bool $isSteamTotp = false)
{
// First we instanciate the OTP generator
try {
$this->generator = Factory::loadFromProvisioningUri($uri);
}
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
throw ValidationException::withMessages([
'uri' => __('validation.custom.uri.regex', ['attribute' => 'uri'])
]);
}
// As loadFromProvisioningUri() accept URI without label (nor account nor service) we check
// that the account is set
if ( ! $this->generator->getLabel() ) {
Log::error('URI passed to fillWithURI() must contain a label');
throw ValidationException::withMessages([
'label' => __('validation.custom.label.required')
]);
}
$this->otp_type = $this->getGeneratorOtpType();
$this->account = $this->generator->getLabel();
$this->secret = $this->generator->getSecret();
$this->service = $this->generator->getIssuer();
$this->algorithm = $this->generator->getDigest();
$this->digits = $this->generator->getDigits();
$this->period = $this->generator->hasParameter('period') ? $this->generator->getParameter('period') : null;
$this->counter = $this->generator->hasParameter('counter') ? $this->generator->getParameter('counter') : null;
$this->legacy_uri = $uri;
if ($isSteamTotp) {
$this->enforceAsSteam();
}
if ( $this->generator->hasParameter('image') ) {
$this->icon = $this->storeTokenImageAsIcon();
}
Log::info(sprintf('TwoFAccount filled with an URI'));
return $this;
}
/**
* Sets model attributes to STEAM values
*/
private function enforceAsSteam()
{
$this->otp_type = self::STEAM_TOTP;
$this->digits = 5;
$this->algorithm = self::SHA1;
$this->period = 30;
}
/**
* Returns the OTP type of the instanciated OTP generator
*/
private function getGeneratorOtpType()
{
return Arr::get($this->generatorClassMap, $this->generator::class);
}
/**
* Returns an otpauth URI built with model attribute values
*/
public function getURI() : string
{
$this->initGenerator();
return $this->generator->getProvisioningUri();
}
/**
* Instanciates the OTP generator with model attribute values
*/
private function initGenerator()
{
try {
switch ($this->otp_type) {
case self::TOTP:
$this->generator = TOTP::create(
$this->secret,
$this->period ?: self::DEFAULT_PERIOD,
$this->algorithm ?: self::DEFAULT_ALGORITHM,
$this->digits ?: self::DEFAULT_DIGITS
);
break;
case self::STEAM_TOTP:
$this->generator = TOTP::create($this->secret, 30, self::SHA1, 5);
break;
case self::HOTP:
$this->generator = HOTP::create(
$this->secret,
$this->counter ?: self::DEFAULT_COUNTER,
$this->algorithm ?: self::DEFAULT_ALGORITHM,
$this->digits ?: self::DEFAULT_DIGITS
);
break;
default:
throw new UnsupportedOtpTypeException();
break;
}
if ($this->service) $this->generator->setIssuer($this->service);
if ($this->account) $this->generator->setLabel($this->account);
}
catch (UnsupportedOtpTypeException $exception) {
Log::error(sprintf('%s is not an OTP type supported by the current generator', $this->otp_type));
throw $exception;
}
catch (\Exception|\Throwable $exception) {
throw new InvalidOtpParameterException($exception->getMessage());
}
}
/**
* Gets the image resource pointed by the generator image parameter and store it as an icon
*
* @return string|null The filename of the stored icon or null if the operation fails
*/
private function storeTokenImageAsIcon()
{
try {
$remoteImageURL = $this->generator->getParameter('image');
$path_parts = pathinfo($remoteImageURL);
$newFilename = Str::random(40) . '.' . $path_parts['extension'];
$imageFile = self::IMAGELINK_STORAGE_PATH . $newFilename;
$iconFile = self::ICON_STORAGE_PATH . $newFilename;
Storage::disk('local')->put($imageFile, file_get_contents($remoteImageURL));
if ( in_array(Storage::mimeType($imageFile), ['image/png', 'image/jpeg', 'image/webp', 'image/bmp'])
&& getimagesize(storage_path() . '/app/' . $imageFile) )
{
// Should be a valid image
Storage::move($imageFile, $iconFile);
Log::info(sprintf('Icon file %s stored', $newFilename));
}
else {
// @codeCoverageIgnoreStart
Storage::delete($imageFile);
throw new \Exception;
// @codeCoverageIgnoreEnd
}
return $newFilename;
}
// @codeCoverageIgnoreStart
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
return null;
}
// @codeCoverageIgnoreEnd
}
/**
* Returns an acceptable value
*/

View File

@ -4,14 +4,16 @@
namespace App\Protobuf;
use App\Models\TwoFAccount;
class GAuthValueMapping
{
const ALGORITHM = [
'ALGORITHM_UNSPECIFIED' => '',
'ALGORITHM_SHA1' => 'sha1',
'ALGORITHM_SHA256' => 'sha256',
'ALGORITHM_SHA512' => 'sha512',
'ALGORITHM_MD5' => 'md5'
'ALGORITHM_SHA1' => TwoFAccount::SHA1,
'ALGORITHM_SHA256' => TwoFAccount::SHA256,
'ALGORITHM_SHA512' => TwoFAccount::SHA512,
'ALGORITHM_MD5' => TwoFAccount::MD5
];
const DIGIT_COUNT = [
@ -22,8 +24,8 @@ class GAuthValueMapping
const OTP_TYPE = [
'OTP_TYPE_UNSPECIFIED' => '',
'OTP_TYPE_HOTP' => 'hotp',
'OTP_TYPE_TOTP' => 'totp'
'OTP_TYPE_HOTP' => TwoFAccount::HOTP,
'OTP_TYPE_TOTP' => TwoFAccount::TOTP
];
private function __construct() {}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Services\Dto;
class OtpDto
{
/* @var integer */
public string $password;
/* @var integer */
public string $otp_type;
/* @var integer */
public ?int $generated_at;
/* @var integer */
public ?int $period;
/* @var integer */
public ?int $counter;
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Services\Dto;
class TwoFAccountDto
{
/* @var string */
public string $otp_type;
/* @var string */
public string $account = '';
/* @var string */
public ?string $service = null;
/* @var string */
public ?string $icon = null;
/* @var string */
public ?string $secret = null;
/* @var string */
public ?string $algorithm = 'sha1';
/* @var integer */
public ?int $digits = 6;
/* @var integer */
public ?int $period = 30;
/* @var integer */
public ?int $counter = 0;
}

View File

@ -3,22 +3,11 @@
namespace App\Services;
use App\Models\TwoFAccount;
use App\Exceptions\InvalidSecretException;
use App\Exceptions\InvalidOtpParameterException;
use App\Exceptions\UndecipherableException;
use App\Exceptions\InvalidGoogleAuthMigration;
use App\Services\Dto\OtpDto;
use App\Services\Dto\TwoFAccountDto;
use Exception;
use OTPHP\TOTP;
use OTPHP\HOTP;
use OTPHP\Factory;
use Illuminate\Support\Str;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use ParagonIE\ConstantTime\Base32;
use App\Protobuf\GAuthValueMapping;
use App\Protobuf\GoogleAuth\Payload;
@ -28,172 +17,6 @@
class TwoFAccountService
{
/**
*
*/
private $token;
/**
*
*/
private array $supportedOtpTypes = [
"OTPHP\TOTP" => "totp",
"OTPHP\HOTP" => "hotp"
];
private const IMAGELINK_STORAGE_PATH = 'imagesLink/';
private const ICON_STORAGE_PATH = 'public/icons/';
public function __construct()
{
//$this->token = $otpType === TOTP::create($secret) : HOTP::create($secret);
}
/**
* Creates an account using an otpauth URI
*
* @param string $uri
* @param bool $saveToDB Whether or not the created account should be saved to DB
*
* @return \App\Models\TwoFAccount The created account
*/
public function createFromUri(string $uri, bool $saveToDB = true ) : TwoFAccount
{
// Instanciate the token
$this->initTokenWith($uri);
// Create the account
$twofaccount = new TwoFAccount;
$twofaccount->legacy_uri = $uri;
$this->fillWithToken($twofaccount);
if ( $saveToDB ) {
$twofaccount->save();
Log::info(sprintf('TwoFAccount #%d created (from URI)', $twofaccount->id));
}
return $twofaccount;
}
/**
* Creates an account using a list of parameters
*
* @param array $data
* @param bool $saveToDB Whether or not the created account should be saved to DB
*
* @return \App\Models\TwoFAccount The created account
*/
public function createFromParameters(array $data, bool $saveToDB = true) : TwoFAccount
{
// Instanciate the token
$this->initTokenWith($data);
// Create and fill the account
$twofaccount = new TwoFAccount;
$twofaccount->legacy_uri = $this->token->getProvisioningUri();
$twofaccount->icon = Arr::get($data, 'icon', null);
$this->fillWithToken($twofaccount);
if ( $saveToDB ) {
$twofaccount->save();
Log::info(sprintf('TwoFAccount #%d created (from parameters)', $twofaccount->id));
}
return $twofaccount;
}
/**
* Updates an account using a list of parameters
*
* @param \App\Models\TwoFAccount $twofaccount The account
* @param array $data The parameters
*
* @return \App\Models\TwoFAccount The updated account
*/
public function update(TwoFAccount $twofaccount, array $data) : TwoFAccount
{
// Instanciate the token
$this->initTokenWith($data);
$this->fillWithToken($twofaccount);
$twofaccount->icon = Arr::get($data, 'icon', null);
$twofaccount->save();
Log::info(sprintf('TwoFAccount #%d updated', $twofaccount->id));
return $twofaccount;
}
/**
* Returns a One-Time Password (with its parameters) for the specified account
*
* @param \App\Models\TwoFAccount|TwoFAccountDto|int|string $data Data defining an account
*
* @return OtpDto an OTP DTO
*
* @throws InvalidSecretException The secret is not a valid base32 encoded string
* @throws UndecipherableException The secret cannot be deciphered
*/
public function getOTP($data) : OtpDto
{
$this->initTokenWith($data);
$OtpDto = new OtpDto();
// Early exit if the model returned an undecipherable secret
if (strtolower($this->token->getSecret()) === __('errors.indecipherable')) {
Log::error('Secret cannot be deciphered, OTP generation aborted');
throw new UndecipherableException();
}
try {
if ( $this->tokenOtpType() === 'totp' ) {
$OtpDto->generated_at = time();
$OtpDto->otp_type = 'totp';
$OtpDto->password = $this->token->at($OtpDto->generated_at);
$OtpDto->period = $this->token->getParameter('period');
}
else if ( $this->tokenOtpType() === 'hotp' ) {
$counter = $this->token->getCounter();
$OtpDto->otp_type = 'hotp';
$OtpDto->password = $this->token->at($counter);
$OtpDto->counter = $counter + 1;
}
}
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
// Currently a secret issue is the only possible exception thrown by OTPHP for this stack
// so it is Ok to send the corresponding 2FAuth exception.
// If the token package change it could be necessary to throw a more generic exception.
throw new InvalidSecretException($ex->getMessage());
}
Log::info(sprintf('New %s generated', $OtpDto->otp_type));
return $OtpDto;
}
/**
* Returns a generated otpauth URI for the specified account
*
* @param \App\Models\TwoFAccount|TwoFAccountDto|int $data Data defining an account
*/
public function getURI($data) : string
{
$this->initTokenWith($data);
return $this->token->getProvisioningUri();
}
/**
* Withdraw one or more twofaccounts from their group
@ -268,10 +91,11 @@ public function convertMigrationFromGA($migrationUri) : Collection
$parameters['secret'] = Base32::encodeUpper($otp_parameters->getSecret());
$parameters['algorithm'] = GAuthValueMapping::ALGORITHM[Algorithm::name($otp_parameters->getAlgorithm())];
$parameters['digits'] = GAuthValueMapping::DIGIT_COUNT[DigitCount::name($otp_parameters->getDigits())];
$parameters['counter'] = $otp_parameters->getCounter();
// $parameters['period'] = $otp_parameters->getPeriod();
$parameters['counter'] = $parameters['otp_type'] === TwoFAccount::HOTP ? $otp_parameters->getCounter() : null;
$parameters['period'] = $parameters['otp_type'] === TwoFAccount::TOTP ? $otp_parameters->getPeriod() : null;
$twofaccounts[$key] = $this->createFromParameters($parameters, false);
$twofaccounts[$key] = new TwoFAccount;
$twofaccounts[$key]->fillWithOtpParameters($parameters);
}
catch (Exception $exception) {
@ -281,7 +105,7 @@ public function convertMigrationFromGA($migrationUri) : Collection
// 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 = 'totp';
$fakeAccount->otp_type = $fakeAccount::TOTP;
// Only basic fields are filled to limit the risk of another exception.
$fakeAccount->account = $otp_parameters->getName();
$fakeAccount->service = $otp_parameters->getIssuer();
@ -297,11 +121,6 @@ public function convertMigrationFromGA($migrationUri) : Collection
}
// ########################################################################################################################
// ########################################################################################################################
// ########################################################################################################################
// ########################################################################################################################
/**
*
*/
@ -318,231 +137,6 @@ private function commaSeparatedToArray($ids)
return $ids;
}
/**
* Inits the Token
*/
private function initTokenWith($data) : void
{
// init with a TwoFAccount instance
if ( is_object($data) && get_class($data) === 'App\Models\TwoFAccount' ) {
$this->initTokenWithTwoFAccount($data);
}
// init with a TwoFAccountDto instance
else if ( is_object($data) && get_class($data) === 'App\Services\Dto\TwoFAccountDto' ) {
$this->initTokenWithParameters($data);
}
// init with an account ID
else if ( is_integer($data) ) {
// we should have an ID
$twofaccount = TwoFAccount::findOrFail($data);
$this->initTokenWithTwoFAccount($twofaccount);
}
// init with an array of property
else if( is_array($data) ) {
$dto = $this->mapArrayToDto($data);
$this->initTokenWithParameters($dto);
}
// or with a string that should be an otpauth URI
else {
$this->initTokenWithUri($data);
}
}
/**
* Maps array items to a TwoFAccountDto instance
*
* @param array $array The array to map
*
* @returns TwoFAccountDto
*/
private function mapArrayToDto($array) : TwoFAccountDto
{
$dto = new TwoFAccountDto();
try {
foreach ($array as $key => $value) {
$dto->$key = ! Arr::has($array, $key) ?: $value;
}
}
catch (\TypeError $ex) {
throw new InvalidOtpParameterException($ex->getMessage());
}
return $dto;
}
/**
* Instanciates the token with a TwoFAccount
*
* @param \App\Models\TwoFAccount $twofaccount
*
* @param bool $usingUri Whether or not the token should be fed with the account uri
*/
private function initTokenWithTwoFAccount(TwoFAccount $twofaccount) : void
{
$dto = new TwoFAccountDto();
$dto->otp_type = $twofaccount->otp_type;
$dto->account = $twofaccount->account;
$dto->service = $twofaccount->service;
$dto->icon = $twofaccount->icon;
$dto->secret = $twofaccount->secret;
$dto->algorithm = $twofaccount->algorithm;
$dto->digits = $twofaccount->digits;
if ( $twofaccount->period ) $dto->period = $twofaccount->period;
if ( $twofaccount->counter ) $dto->counter = $twofaccount->counter;
$this->initTokenWithParameters($dto);
}
/**
* Instanciates the token object by parsing an otpauth URI
*
* @throws ValidationException The URI is not a valid otpauth URI
*/
private function initTokenWithUri(string $uri) : void
{
try {
$this->token = Factory::loadFromProvisioningUri($uri);
}
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
throw ValidationException::withMessages([
'uri' => __('validation.custom.uri.regex', ['attribute' => 'uri'])
]);
}
// As loadFromProvisioningUri() accept URI without label (nor account nor service) we check
// that the account is set
if ( ! $this->token->getLabel() ) {
Log::error('URI passed to initTokenWithUri() must contain a label');
throw ValidationException::withMessages([
'label' => __('validation.custom.label.required')
]);
}
}
/**
* Instanciates the token object by passing a list of parameters
*
* @throws ValidationException otp type not supported
* @throws InvalidOtpParameterException invalid otp parameters
*/
private function initTokenWithParameters(TwoFAccountDto $dto) : void
{
// Check OTP type again to ensure the upcoming OTPHP instanciation
if ( ! in_array($dto->otp_type, $this->supportedOtpTypes, true) ) {
Log::error(sprintf('%s is not an OTP type supported by the current token', $dto->otp_type));
throw ValidationException::withMessages([
'otp_type' => __('validation.custom.otp_type.in', ['attribute' => 'otp type'])
]);
}
try {
if ( $dto->otp_type === 'totp' ) {
$this->token = TOTP::create(
$dto->secret
);
if ($dto->period) $this->token->setParameter('period', $dto->period);
}
else if ( $dto->otp_type === 'hotp' ) {
$this->token = HOTP::create(
$dto->secret
);
if ($dto->counter) $this->token->setParameter('counter', $dto->counter);
}
if ($dto->algorithm) $this->token->setParameter('algorithm', $dto->algorithm);
if ($dto->digits) $this->token->setParameter('digits', $dto->digits);
// if ($dto->epoch) $this->token->setParameter('epoch', $dto->epoch);
if ($dto->service) $this->token->setIssuer($dto->service);
if ($dto->account) $this->token->setLabel($dto->account);
}
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
throw new InvalidOtpParameterException($ex->getMessage());
}
}
/**
* Fills a TwoFAccount with token's parameters
*/
private function fillWithToken(TwoFAccount &$twofaccount) : void
{
$twofaccount->otp_type = $this->tokenOtpType();
$twofaccount->account = $this->token->getLabel();
$twofaccount->secret = $this->token->getSecret();
$twofaccount->service = $this->token->getIssuer();
$twofaccount->algorithm = $this->token->getDigest();
$twofaccount->digits = $this->token->getDigits();
$twofaccount->period = $this->token->hasParameter('period') ? $this->token->getParameter('period') : null;
$twofaccount->counter = $this->token->hasParameter('counter') ? $this->token->getParameter('counter') : null;
if ( $this->token->hasParameter('image') ) {
$twofaccount->icon = $this->storeTokenImageAsIcon();
}
}
/**
* Returns the otp_type that matchs the token instance class
*/
private function tokenOtpType() : string
{
return $this->supportedOtpTypes[get_class($this->token)];
}
/**
* Gets the image resource pointed by the token image parameter and store it as an icon
*
* @return string|null The filename of the stored icon or null if the operation fails
*/
private function storeTokenImageAsIcon()
{
try {
$remoteImageURL = $this->token->getParameter('image');
$path_parts = pathinfo($remoteImageURL);
$newFilename = Str::random(40) . '.' . $path_parts['extension'];
$imageFile = self::IMAGELINK_STORAGE_PATH . $newFilename;
$iconFile = self::ICON_STORAGE_PATH . $newFilename;
Storage::disk('local')->put($imageFile, file_get_contents($remoteImageURL));
if ( in_array(Storage::mimeType($imageFile), ['image/png', 'image/jpeg', 'image/webp', 'image/bmp'])
&& getimagesize(storage_path() . '/app/' . $imageFile) )
{
// Should be a valid image
Storage::move($imageFile, $iconFile);
Log::info(sprintf('Icon file %s stored', $newFilename));
}
else {
// @codeCoverageIgnoreStart
Storage::delete($imageFile);
throw new \Exception;
// @codeCoverageIgnoreEnd
}
return $newFilename;
}
// @codeCoverageIgnoreStart
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
return null;
}
// @codeCoverageIgnoreEnd
}
/**
* Return the given collection with items marked as Duplicates (using id=-1) if a similar record exists in database

View File

@ -22,6 +22,7 @@
"ext-xml": "*",
"chillerlan/php-qrcode": "^4.3",
"darkghosthunter/larapass": "^3.0.2",
"doctormckay/steam-totp": "^1.0",
"doctrine/dbal": "^3.2",
"fruitcake/laravel-cors": "^2.0",
"google/protobuf": "^3.21",

View File

@ -40,4 +40,5 @@
'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',
'unsupported_otp_type' => 'Unsupported OTP type',
];

View File

@ -128,6 +128,7 @@
'uuid' => 'The :attribute must be a valid UUID.',
'single' => 'When using :attribute it must be the only parameter in this request body',
'onlyCustomOtpWithUri' => 'The uri parameter must be provided alone or only in combination with the \'custom_otp\' parameter',
/*
|--------------------------------------------------------------------------

View File

@ -5,6 +5,7 @@
use App\Models\User;
use App\Models\Group;
use Tests\FeatureTestCase;
use Tests\Classes\OtpTestData;
use App\Models\TwoFAccount;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
@ -28,25 +29,8 @@ class TwoFAccountControllerTest extends FeatureTestCase
*/
protected $group;
private const ACCOUNT = 'account';
private const SERVICE = 'service';
private const SECRET = 'A4GRFHVVRBGY7UIW';
private const ALGORITHM_DEFAULT = 'sha1';
private const ALGORITHM_CUSTOM = 'sha256';
private const DIGITS_DEFAULT = 6;
private const DIGITS_CUSTOM = 7;
private const PERIOD_DEFAULT = 30;
private const PERIOD_CUSTOM = 40;
private const COUNTER_DEFAULT = 0;
private const COUNTER_CUSTOM = 5;
private const IMAGE = 'https%3A%2F%2Fen.opensuse.org%2Fimages%2F4%2F44%2FButton-filled-colour.png';
private const ICON = 'test.png';
private const TOTP_FULL_CUSTOM_URI = 'otpauth://totp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&period='.self::PERIOD_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
private const HOTP_FULL_CUSTOM_URI = 'otpauth://hotp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&counter='.self::COUNTER_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
private const TOTP_SHORT_URI = 'otpauth://totp/'.self::ACCOUNT.'?secret='.self::SECRET;
private const HOTP_SHORT_URI = 'otpauth://hotp/'.self::ACCOUNT.'?secret='.self::SECRET;
private const TOTP_URI_WITH_UNREACHABLE_IMAGE = 'otpauth://totp/service:account?secret=A4GRFHVVRBGY7UIW&image=https%3A%2F%2Fen.opensuse.org%2Fimage.png';
private const INVALID_OTPAUTH_URI = 'otpauth://Xotp/'.self::ACCOUNT.'?secret='.self::SECRET;
private const VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET = [
'id',
'group_id',
@ -83,86 +67,52 @@ class TwoFAccountControllerTest extends FeatureTestCase
'password',
'counter',
];
private const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP = [
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'icon' => self::ICON,
'otp_type' => 'totp',
'secret' => self::SECRET,
'digits' => self::DIGITS_CUSTOM,
'algorithm' => self::ALGORITHM_CUSTOM,
'period' => self::PERIOD_CUSTOM,
'counter' => null,
];
private const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP = [
'account' => self::ACCOUNT,
'otp_type' => 'totp',
'secret' => self::SECRET,
];
private const JSON_FRAGMENTS_FOR_CUSTOM_TOTP = [
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'service' => OtpTestData::SERVICE,
'account' => OtpTestData::ACCOUNT,
'otp_type' => 'totp',
'secret' => self::SECRET,
'digits' => self::DIGITS_CUSTOM,
'algorithm' => self::ALGORITHM_CUSTOM,
'period' => self::PERIOD_CUSTOM,
'secret' => OtpTestData::SECRET,
'digits' => OtpTestData::DIGITS_CUSTOM,
'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
'period' => OtpTestData::PERIOD_CUSTOM,
'counter' => null,
];
private const JSON_FRAGMENTS_FOR_DEFAULT_TOTP = [
'service' => null,
'account' => self::ACCOUNT,
'account' => OtpTestData::ACCOUNT,
'otp_type' => 'totp',
'secret' => self::SECRET,
'digits' => self::DIGITS_DEFAULT,
'algorithm' => self::ALGORITHM_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'secret' => OtpTestData::SECRET,
'digits' => OtpTestData::DIGITS_DEFAULT,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'period' => OtpTestData::PERIOD_DEFAULT,
'counter' => null,
];
private const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP = [
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'icon' => self::ICON,
'otp_type' => 'hotp',
'secret' => self::SECRET,
'digits' => self::DIGITS_CUSTOM,
'algorithm' => self::ALGORITHM_CUSTOM,
'period' => null,
'counter' => self::COUNTER_CUSTOM,
];
private const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP = [
'account' => self::ACCOUNT,
'otp_type' => 'hotp',
'secret' => self::SECRET,
];
private const JSON_FRAGMENTS_FOR_CUSTOM_HOTP = [
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'service' => OtpTestData::SERVICE,
'account' => OtpTestData::ACCOUNT,
'otp_type' => 'hotp',
'secret' => self::SECRET,
'digits' => self::DIGITS_CUSTOM,
'algorithm' => self::ALGORITHM_CUSTOM,
'secret' => OtpTestData::SECRET,
'digits' => OtpTestData::DIGITS_CUSTOM,
'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
'period' => null,
'counter' => self::COUNTER_CUSTOM,
'counter' => OtpTestData::COUNTER_CUSTOM,
];
private const JSON_FRAGMENTS_FOR_DEFAULT_HOTP = [
'service' => null,
'account' => self::ACCOUNT,
'account' => OtpTestData::ACCOUNT,
'otp_type' => 'hotp',
'secret' => self::SECRET,
'digits' => self::DIGITS_DEFAULT,
'algorithm' => self::ALGORITHM_DEFAULT,
'secret' => OtpTestData::SECRET,
'digits' => OtpTestData::DIGITS_DEFAULT,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'period' => null,
'counter' => self::COUNTER_DEFAULT,
'counter' => OtpTestData::COUNTER_DEFAULT,
];
private const ARRAY_OF_INVALID_PARAMETERS = [
'account' => null,
'otp_type' => 'totp',
'secret' => self::SECRET,
'secret' => OtpTestData::SECRET,
];
private const GOOGLE_AUTH_MIGRATION_URI = 'otpauth-migration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY2UgASgBMAIKLAoKBw0SnrWITY/RFhILYWNjb3VudF9iaXMaC3NlcnZpY2VfYmlzIAEoATACEAEYASAA';
private const INVALID_GOOGLE_AUTH_MIGRATION_URI = 'otpauthmigration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY2UgASgBMAIKLAoKBw0SnrWITY/RFhILYWNjb3VudF9iaXMaC3NlcnZpY2VfYmlzIAEoATACEAEYASAA';
private const GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA = 'otpauth-migration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY';
/**
@ -301,28 +251,28 @@ public function provideDataForTestStoreStructure() : array
{
return [
[[
'uri' => self::TOTP_FULL_CUSTOM_URI,
'uri' => OtpTestData::TOTP_FULL_CUSTOM_URI,
]],
[[
'uri' => self::TOTP_SHORT_URI,
'uri' => OtpTestData::TOTP_SHORT_URI,
]],
[
self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP
],
[
self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP
OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP
],
[[
'uri' => self::HOTP_FULL_CUSTOM_URI,
'uri' => OtpTestData::HOTP_FULL_CUSTOM_URI,
]],
[[
'uri' => self::HOTP_SHORT_URI,
'uri' => OtpTestData::HOTP_SHORT_URI,
]],
[
self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP
OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP
],
[
self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP
OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP
],
];
}
@ -335,7 +285,7 @@ public function test_store_totp_using_fully_custom_uri_returns_consistent_resour
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', [
'uri' => self::TOTP_FULL_CUSTOM_URI,
'uri' => OtpTestData::TOTP_FULL_CUSTOM_URI,
])
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
}
@ -348,7 +298,7 @@ public function test_store_totp_using_short_uri_returns_resource_with_default_ot
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', [
'uri' => self::TOTP_SHORT_URI,
'uri' => OtpTestData::TOTP_SHORT_URI,
])
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
}
@ -360,7 +310,7 @@ public function test_store_totp_using_short_uri_returns_resource_with_default_ot
public function test_store_totp_using_fully_custom_parameters_returns_consistent_resource()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
->json('POST', '/api/v1/twofaccounts', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
}
@ -371,7 +321,7 @@ public function test_store_totp_using_fully_custom_parameters_returns_consistent
public function test_store_totp_using_minimum_parameters_returns_consistent_resource()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP)
->json('POST', '/api/v1/twofaccounts', OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP)
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
}
@ -383,7 +333,7 @@ public function test_store_hotp_using_fully_custom_uri_returns_consistent_resour
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', [
'uri' => self::HOTP_FULL_CUSTOM_URI,
'uri' => OtpTestData::HOTP_FULL_CUSTOM_URI,
])
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP);
}
@ -396,7 +346,7 @@ public function test_store_hotp_using_short_uri_returns_resource_with_default_ot
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', [
'uri' => self::HOTP_SHORT_URI,
'uri' => OtpTestData::HOTP_SHORT_URI,
])
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
}
@ -408,7 +358,7 @@ public function test_store_hotp_using_short_uri_returns_resource_with_default_ot
public function test_store_hotp_using_fully_custom_parameters_returns_consistent_resource()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
->json('POST', '/api/v1/twofaccounts', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP);
}
@ -419,7 +369,7 @@ public function test_store_hotp_using_fully_custom_parameters_returns_consistent
public function test_store_hotp_using_minimum_parameters_returns_consistent_resource()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP)
->json('POST', '/api/v1/twofaccounts', OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP)
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
}
@ -431,7 +381,7 @@ public function test_store_with_invalid_uri_returns_validation_error()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', [
'uri' => self::INVALID_OTPAUTH_URI,
'uri' => OtpTestData::INVALID_OTPAUTH_URI,
])
->assertStatus(422);
}
@ -448,7 +398,7 @@ public function test_store_assigns_created_account_when_default_group_is_a_speci
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', [
'uri' => self::TOTP_SHORT_URI,
'uri' => OtpTestData::TOTP_SHORT_URI,
])
->assertJsonFragment([
'group_id' => $this->group->id
@ -470,7 +420,7 @@ public function test_store_assigns_created_account_when_default_group_is_the_act
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', [
'uri' => self::TOTP_SHORT_URI,
'uri' => OtpTestData::TOTP_SHORT_URI,
])
->assertJsonFragment([
'group_id' => $this->group->id
@ -490,7 +440,7 @@ public function test_store_assigns_created_account_when_default_group_is_no_grou
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', [
'uri' => self::TOTP_SHORT_URI,
'uri' => OtpTestData::TOTP_SHORT_URI,
])
->assertJsonFragment([
'group_id' => null
@ -510,7 +460,7 @@ public function test_store_assigns_created_account_when_default_group_does_not_e
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', [
'uri' => self::TOTP_SHORT_URI,
'uri' => OtpTestData::TOTP_SHORT_URI,
])
->assertJsonFragment([
'group_id' => null
@ -526,7 +476,7 @@ public function test_update_totp_returns_success_with_updated_resource()
$twofaccount = TwoFAccount::factory()->create();
$response = $this->actingAs($this->user, 'api-guard')
->json('PUT', '/api/v1/twofaccounts/' . $twofaccount->id, self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
->json('PUT', '/api/v1/twofaccounts/' . $twofaccount->id, OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
->assertOk()
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
}
@ -540,7 +490,7 @@ public function test_update_hotp_returns_success_with_updated_resource()
$twofaccount = TwoFAccount::factory()->create();
$response = $this->actingAs($this->user, 'api-guard')
->json('PUT', '/api/v1/twofaccounts/' . $twofaccount->id, self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
->json('PUT', '/api/v1/twofaccounts/' . $twofaccount->id, OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
->assertOk()
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP);
}
@ -552,7 +502,7 @@ public function test_update_hotp_returns_success_with_updated_resource()
public function test_update_missing_twofaccount_returns_not_found()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('PUT', '/api/v1/twofaccounts/1000', self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
->json('PUT', '/api/v1/twofaccounts/1000', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
->assertNotFound();
}
@ -577,30 +527,30 @@ public function test_import_valid_gauth_data_returns_success_with_consistent_res
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/import', [
'uri' => self::GOOGLE_AUTH_MIGRATION_URI,
'uri' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI,
])
->assertOk()
->assertJsonCount(2, $key = null)
->assertJsonFragment([
'id' => 0,
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'service' => OtpTestData::SERVICE,
'account' => OtpTestData::ACCOUNT,
'otp_type' => 'totp',
'secret' => self::SECRET,
'digits' => self::DIGITS_DEFAULT,
'algorithm' => self::ALGORITHM_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'secret' => OtpTestData::SECRET,
'digits' => OtpTestData::DIGITS_DEFAULT,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'period' => OtpTestData::PERIOD_DEFAULT,
'counter' => null
])
->assertJsonFragment([
'id' => 0,
'service' => self::SERVICE . '_bis',
'account' => self::ACCOUNT . '_bis',
'service' => OtpTestData::SERVICE . '_bis',
'account' => OtpTestData::ACCOUNT . '_bis',
'otp_type' => 'totp',
'secret' => self::SECRET,
'digits' => self::DIGITS_DEFAULT,
'algorithm' => self::ALGORITHM_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'secret' => OtpTestData::SECRET,
'digits' => OtpTestData::DIGITS_DEFAULT,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'period' => OtpTestData::PERIOD_DEFAULT,
'counter' => null
]);
}
@ -613,7 +563,7 @@ public function test_import_with_invalid_uri_returns_validation_error()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts', [
'uri' => self::INVALID_GOOGLE_AUTH_MIGRATION_URI,
'uri' => OtpTestData::INVALID_GOOGLE_AUTH_MIGRATION_URI,
])
->assertStatus(422);
}
@ -626,25 +576,25 @@ public function test_import_gauth_data_with_duplicates_returns_negative_ids()
{
$twofaccount = TwoFAccount::factory()->create([
'otp_type' => 'totp',
'account' => self::ACCOUNT,
'service' => self::SERVICE,
'secret' => self::SECRET,
'algorithm' => self::ALGORITHM_DEFAULT,
'digits' => self::DIGITS_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'legacy_uri' => self::TOTP_SHORT_URI,
'account' => OtpTestData::ACCOUNT,
'service' => OtpTestData::SERVICE,
'secret' => OtpTestData::SECRET,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'digits' => OtpTestData::DIGITS_DEFAULT,
'period' => OtpTestData::PERIOD_DEFAULT,
'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
'icon' => '',
]);
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/import', [
'uri' => self::GOOGLE_AUTH_MIGRATION_URI,
'uri' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI,
])
->assertOk()
->assertJsonFragment([
'id' => -1,
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'service' => OtpTestData::SERVICE,
'account' => OtpTestData::ACCOUNT,
]);
}
@ -656,7 +606,7 @@ public function test_import_invalid_gauth_data_returns_bad_request()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/import', [
'uri' => self::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA,
'uri' => OtpTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA,
])
->assertStatus(400)
->assertJsonStructure([
@ -703,7 +653,7 @@ public function test_preview_returns_success_with_resource()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/preview', [
'uri' => self::TOTP_FULL_CUSTOM_URI,
'uri' => OtpTestData::TOTP_FULL_CUSTOM_URI,
])
->assertOk()
->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
@ -717,7 +667,7 @@ public function test_preview_with_invalid_data_returns_validation_error()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/preview', [
'uri' => self::INVALID_OTPAUTH_URI,
'uri' => OtpTestData::INVALID_OTPAUTH_URI,
])
->assertStatus(422);
}
@ -730,7 +680,7 @@ public function test_preview_with_unreachable_image_returns_success()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/preview', [
'uri' => self::TOTP_URI_WITH_UNREACHABLE_IMAGE,
'uri' => OtpTestData::TOTP_URI_WITH_UNREACHABLE_IMAGE,
])
->assertOk()
->assertJsonFragment([
@ -746,13 +696,13 @@ public function test_get_otp_using_totp_twofaccount_id_returns_consistent_resour
{
$twofaccount = TwoFAccount::factory()->create([
'otp_type' => 'totp',
'account' => self::ACCOUNT,
'service' => self::SERVICE,
'secret' => self::SECRET,
'algorithm' => self::ALGORITHM_DEFAULT,
'digits' => self::DIGITS_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'legacy_uri' => self::TOTP_SHORT_URI,
'account' => OtpTestData::ACCOUNT,
'service' => OtpTestData::SERVICE,
'secret' => OtpTestData::SECRET,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'digits' => OtpTestData::DIGITS_DEFAULT,
'period' => OtpTestData::PERIOD_DEFAULT,
'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
'icon' => '',
]);
@ -762,7 +712,7 @@ public function test_get_otp_using_totp_twofaccount_id_returns_consistent_resour
->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
->assertJsonFragment([
'otp_type' => 'totp',
'period' => self::PERIOD_DEFAULT,
'period' => OtpTestData::PERIOD_DEFAULT,
]);
}
@ -774,13 +724,13 @@ public function test_get_otp_by_posting_totp_uri_returns_consistent_resource()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/otp', [
'uri' => self::TOTP_FULL_CUSTOM_URI,
'uri' => OtpTestData::TOTP_FULL_CUSTOM_URI,
])
->assertOk()
->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
->assertJsonFragment([
'otp_type' => 'totp',
'period' => self::PERIOD_CUSTOM,
'period' => OtpTestData::PERIOD_CUSTOM,
]);
}
@ -791,12 +741,12 @@ public function test_get_otp_by_posting_totp_uri_returns_consistent_resource()
public function test_get_otp_by_posting_totp_parameters_returns_consistent_resource()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/otp', self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
->json('POST', '/api/v1/twofaccounts/otp', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
->assertOk()
->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
->assertJsonFragment([
'otp_type' => 'totp',
'period' => self::PERIOD_CUSTOM,
'period' => OtpTestData::PERIOD_CUSTOM,
]);
}
@ -808,13 +758,13 @@ public function test_get_otp_using_hotp_twofaccount_id_returns_consistent_resour
{
$twofaccount = TwoFAccount::factory()->create([
'otp_type' => 'hotp',
'account' => self::ACCOUNT,
'service' => self::SERVICE,
'secret' => self::SECRET,
'algorithm' => self::ALGORITHM_DEFAULT,
'digits' => self::DIGITS_DEFAULT,
'account' => OtpTestData::ACCOUNT,
'service' => OtpTestData::SERVICE,
'secret' => OtpTestData::SECRET,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'digits' => OtpTestData::DIGITS_DEFAULT,
'period' => null,
'legacy_uri' => self::HOTP_SHORT_URI,
'legacy_uri' => OtpTestData::HOTP_SHORT_URI,
'icon' => '',
]);
@ -824,7 +774,7 @@ public function test_get_otp_using_hotp_twofaccount_id_returns_consistent_resour
->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
->assertJsonFragment([
'otp_type' => 'hotp',
'counter' => self::COUNTER_DEFAULT + 1,
'counter' => OtpTestData::COUNTER_DEFAULT + 1,
]);
}
@ -836,13 +786,13 @@ public function test_get_otp_by_posting_hotp_uri_returns_consistent_resource()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/otp', [
'uri' => self::HOTP_FULL_CUSTOM_URI,
'uri' => OtpTestData::HOTP_FULL_CUSTOM_URI,
])
->assertOk()
->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
->assertJsonFragment([
'otp_type' => 'hotp',
'counter' => self::COUNTER_CUSTOM + 1,
'counter' => OtpTestData::COUNTER_CUSTOM + 1,
]);
}
@ -853,12 +803,12 @@ public function test_get_otp_by_posting_hotp_uri_returns_consistent_resource()
public function test_get_otp_by_posting_hotp_parameters_returns_consistent_resource()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/otp', self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
->json('POST', '/api/v1/twofaccounts/otp', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
->assertOk()
->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
->assertJsonFragment([
'otp_type' => 'hotp',
'counter' => self::COUNTER_CUSTOM + 1,
'counter' => OtpTestData::COUNTER_CUSTOM + 1,
]);
}
@ -870,7 +820,7 @@ public function test_get_otp_by_posting_multiple_inputs_returns_bad_request()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/otp', [
'uri' => self::HOTP_FULL_CUSTOM_URI,
'uri' => OtpTestData::HOTP_FULL_CUSTOM_URI,
'key' => 'value',
])
->assertStatus(400)
@ -924,7 +874,7 @@ public function test_get_otp_by_posting_invalid_uri_returns_validation_error()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/twofaccounts/otp', [
'uri' => self::INVALID_OTPAUTH_URI,
'uri' => OtpTestData::INVALID_OTPAUTH_URI,
])
->assertStatus(422);
}

View File

@ -50,6 +50,10 @@ public function provideValidData() : array
[[
'uri' => 'otpauth://hotp/test@test.com?secret=A4GRFHZVRBGY7UIW&issuer=test'
]],
[[
'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHZVRBGY7UIW&issuer=test',
'custom_otp' => 'steamtotp'
]],
];
}
@ -85,6 +89,18 @@ public function provideInvalidData() : array
[[
'uri' => 'otpXauth://totp/test@test.com?secret=A4GRFHZVRBGY7UIW&issuer=test' // regex
]],
[[
'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHZVRBGY7UIW&issuer=test',
'custom_otp' => 'notSteam' // not in
]],
[[
'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHZVRBGY7UIW&issuer=test',
'custom_otp' => 0 // string
]],
[[
'uri' => 'otpauth://totp/test@test.com?secret=A4GRFHZVRBGY7UIW&issuer=test',
'custom_otp' => true // string
]],
];
}

View File

@ -0,0 +1,73 @@
<?php
namespace Tests\Classes;
class OtpTestData
{
const ACCOUNT = 'account';
const SERVICE = 'service';
const SECRET = 'A4GRFHVVRBGY7UIW';
const ALGORITHM_DEFAULT = 'sha1';
const ALGORITHM_CUSTOM = 'sha256';
const DIGITS_DEFAULT = 6;
const DIGITS_CUSTOM = 7;
const PERIOD_DEFAULT = 30;
const PERIOD_CUSTOM = 40;
const COUNTER_DEFAULT = 0;
const COUNTER_CUSTOM = 5;
const IMAGE = 'https%3A%2F%2Fen.opensuse.org%2Fimages%2F4%2F44%2FButton-filled-colour.png';
const ICON = 'test.png';
const TOTP_FULL_CUSTOM_URI = 'otpauth://totp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&period='.self::PERIOD_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
const HOTP_FULL_CUSTOM_URI = 'otpauth://hotp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&counter='.self::COUNTER_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
const TOTP_SHORT_URI = 'otpauth://totp/'.self::ACCOUNT.'?secret='.self::SECRET;
const HOTP_SHORT_URI = 'otpauth://hotp/'.self::ACCOUNT.'?secret='.self::SECRET;
const TOTP_URI_WITH_UNREACHABLE_IMAGE = 'otpauth://totp/service:account?secret=A4GRFHVVRBGY7UIW&image=https%3A%2F%2Fen.opensuse.org%2Fimage.png';
const INVALID_OTPAUTH_URI = 'otpauth://Xotp/'.self::ACCOUNT.'?secret='.self::SECRET;
const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP = [
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'icon' => self::ICON,
'otp_type' => 'totp',
'secret' => self::SECRET,
'digits' => self::DIGITS_CUSTOM,
'algorithm' => self::ALGORITHM_CUSTOM,
'period' => self::PERIOD_CUSTOM,
'counter' => null,
];
const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP = [
'account' => self::ACCOUNT,
'otp_type' => 'totp',
'secret' => self::SECRET,
];
const ARRAY_OF_PARAMETERS_FOR_UNSUPPORTED_OTP_TYPE = [
'account' => self::ACCOUNT,
'otp_type' => 'Xotp',
'secret' => self::SECRET,
];
const ARRAY_OF_INVALID_PARAMETERS_FOR_TOTP = [
'account' => self::ACCOUNT,
'otp_type' => 'totp',
'secret' => 0,
];
const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP = [
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'icon' => self::ICON,
'otp_type' => 'hotp',
'secret' => self::SECRET,
'digits' => self::DIGITS_CUSTOM,
'algorithm' => self::ALGORITHM_CUSTOM,
'period' => null,
'counter' => self::COUNTER_CUSTOM,
];
const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP = [
'account' => self::ACCOUNT,
'otp_type' => 'hotp',
'secret' => self::SECRET,
];
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';
}

View File

@ -0,0 +1,510 @@
<?php
namespace Tests\Feature\Models;
use App\Models\TwoFAccount;
use Tests\FeatureTestCase;
use Tests\Classes\OtpTestData;
/**
* @covers \App\Models\TwoFAccount
*/
class TwoFAccountModelTest extends FeatureTestCase
{
/**
* App\Models\TwoFAccount $customTotpTwofaccount
*/
protected $customTotpTwofaccount;
/**
* App\Models\TwoFAccount $customTotpTwofaccount
*/
protected $customHotpTwofaccount;
/**
* @test
*/
public function setUp() : void
{
parent::setUp();
// $this->twofaccountService = $this->app->make('App\Services\TwoFAccountService');
$this->customTotpTwofaccount = new TwoFAccount;
$this->customTotpTwofaccount->legacy_uri = OtpTestData::TOTP_FULL_CUSTOM_URI;
$this->customTotpTwofaccount->service = OtpTestData::SERVICE;
$this->customTotpTwofaccount->account = OtpTestData::ACCOUNT;
$this->customTotpTwofaccount->icon = OtpTestData::ICON;
$this->customTotpTwofaccount->otp_type = 'totp';
$this->customTotpTwofaccount->secret = OtpTestData::SECRET;
$this->customTotpTwofaccount->digits = OtpTestData::DIGITS_CUSTOM;
$this->customTotpTwofaccount->algorithm = OtpTestData::ALGORITHM_CUSTOM;
$this->customTotpTwofaccount->period = OtpTestData::PERIOD_CUSTOM;
$this->customTotpTwofaccount->counter = null;
$this->customTotpTwofaccount->save();
$this->customHotpTwofaccount = new TwoFAccount;
$this->customHotpTwofaccount->legacy_uri = OtpTestData::HOTP_FULL_CUSTOM_URI;
$this->customHotpTwofaccount->service = OtpTestData::SERVICE;
$this->customHotpTwofaccount->account = OtpTestData::ACCOUNT;
$this->customHotpTwofaccount->icon = OtpTestData::ICON;
$this->customHotpTwofaccount->otp_type = 'hotp';
$this->customHotpTwofaccount->secret = OtpTestData::SECRET;
$this->customHotpTwofaccount->digits = OtpTestData::DIGITS_CUSTOM;
$this->customHotpTwofaccount->algorithm = OtpTestData::ALGORITHM_CUSTOM;
$this->customHotpTwofaccount->period = null;
$this->customHotpTwofaccount->counter = OtpTestData::COUNTER_CUSTOM;
$this->customHotpTwofaccount->save();
// $this->group = new Group;
// $this->group->name = 'MyGroup';
// $this->group->save();
}
/**
* @test
*/
public function test_fill_with_custom_totp_uri_returns_correct_value()
{
$twofaccount = new TwoFAccount;
$twofaccount->fillWithURI(OtpTestData::TOTP_FULL_CUSTOM_URI);
$this->assertEquals('totp', $twofaccount->otp_type);
$this->assertEquals(OtpTestData::TOTP_FULL_CUSTOM_URI, $twofaccount->legacy_uri);
$this->assertEquals(OtpTestData::SERVICE, $twofaccount->service);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
$this->assertEquals(OtpTestData::DIGITS_CUSTOM, $twofaccount->digits);
$this->assertEquals(OtpTestData::PERIOD_CUSTOM, $twofaccount->period);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(OtpTestData::ALGORITHM_CUSTOM, $twofaccount->algorithm);
$this->assertStringEndsWith('.png',$twofaccount->icon);
}
/**
* @test
*/
public function test_fill_with_basic_totp_uri_returns_default_value()
{
$twofaccount = new TwoFAccount;
$twofaccount->fillWithURI(OtpTestData::TOTP_SHORT_URI);
$this->assertEquals('totp', $twofaccount->otp_type);
$this->assertEquals(OtpTestData::TOTP_SHORT_URI, $twofaccount->legacy_uri);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(OtpTestData::PERIOD_DEFAULT, $twofaccount->period);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_fill_with_custom_hotp_uri_returns_correct_value()
{
$twofaccount = new TwoFAccount;
$twofaccount->fillWithURI(OtpTestData::HOTP_FULL_CUSTOM_URI);
$this->assertEquals('hotp', $twofaccount->otp_type);
$this->assertEquals(OtpTestData::HOTP_FULL_CUSTOM_URI, $twofaccount->legacy_uri);
$this->assertEquals(OtpTestData::SERVICE, $twofaccount->service);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
$this->assertEquals(OtpTestData::DIGITS_CUSTOM, $twofaccount->digits);
$this->assertEquals(null, $twofaccount->period);
$this->assertEquals(OtpTestData::COUNTER_CUSTOM, $twofaccount->counter);
$this->assertEquals(OtpTestData::ALGORITHM_CUSTOM, $twofaccount->algorithm);
$this->assertStringEndsWith('.png',$twofaccount->icon);
}
/**
* @test
*/
public function test_fill_with_basic_hotp_uri_returns_default_value()
{
$twofaccount = new TwoFAccount;
$twofaccount->fillWithURI(OtpTestData::HOTP_SHORT_URI);
$this->assertEquals('hotp', $twofaccount->otp_type);
$this->assertEquals(OtpTestData::HOTP_SHORT_URI, $twofaccount->legacy_uri);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(null, $twofaccount->period);
$this->assertEquals(OtpTestData::COUNTER_DEFAULT, $twofaccount->counter);
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_filled_with_uri_persists_correct_values_to_db()
{
$twofaccount = new TwoFAccount;
$twofaccount->fillWithURI(OtpTestData::TOTP_SHORT_URI);
$twofaccount->save();
$this->assertDatabaseHas('twofaccounts', [
'otp_type' => 'totp',
'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
'service' => null,
'account' => OtpTestData::ACCOUNT,
'secret' => OtpTestData::SECRET,
'digits' => OtpTestData::DIGITS_DEFAULT,
'period' => OtpTestData::PERIOD_DEFAULT,
'counter' => null,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'icon' => null,
]);
}
/**
* @test
*/
public function test_fill_with_invalid_uri_returns_ValidationException()
{
$this->expectException(\Illuminate\Validation\ValidationException::class);
$twofaccount = new TwoFAccount;
$twofaccount->fillWithURI(OtpTestData::INVALID_OTPAUTH_URI);
}
/**
* @test
*/
public function test_fill_with_uri_without_label_returns_ValidationException()
{
$this->expectException(\Illuminate\Validation\ValidationException::class);
$twofaccount = new TwoFAccount;
$twofaccount->fillWithURI('otpauth://totp/?secret='.OtpTestData::SECRET);
}
/**
* @test
*/
public function test_create_custom_totp_from_parameters_returns_correct_value()
{
$twofaccount = new TwoFAccount;
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP);
$this->assertEquals('totp', $twofaccount->otp_type);
$this->assertEquals(OtpTestData::SERVICE, $twofaccount->service);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
$this->assertEquals(OtpTestData::DIGITS_CUSTOM, $twofaccount->digits);
$this->assertEquals(OtpTestData::PERIOD_CUSTOM, $twofaccount->period);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(OtpTestData::ALGORITHM_CUSTOM, $twofaccount->algorithm);
$this->assertStringEndsWith('.png',$twofaccount->icon);
}
/**
* @test
*/
public function test_create_basic_totp_from_parameters_returns_correct_value()
{
$twofaccount = new TwoFAccount;
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
$this->assertEquals('totp', $twofaccount->otp_type);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(OtpTestData::PERIOD_DEFAULT, $twofaccount->period);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_create_custom_hotp_from_parameters_returns_correct_value()
{
$twofaccount = new TwoFAccount;
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP);
$this->assertEquals('hotp', $twofaccount->otp_type);
$this->assertEquals(OtpTestData::SERVICE, $twofaccount->service);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
$this->assertEquals(OtpTestData::DIGITS_CUSTOM, $twofaccount->digits);
$this->assertEquals(null, $twofaccount->period);
$this->assertEquals(OtpTestData::COUNTER_CUSTOM, $twofaccount->counter);
$this->assertEquals(OtpTestData::ALGORITHM_CUSTOM, $twofaccount->algorithm);
$this->assertStringEndsWith('.png',$twofaccount->icon);
}
/**
* @test
*/
public function test_create_basic_hotp_from_parameters_returns_correct_value()
{
$twofaccount = new TwoFAccount;
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP);
$this->assertEquals('hotp', $twofaccount->otp_type);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(null, $twofaccount->period);
$this->assertEquals(OtpTestData::COUNTER_DEFAULT, $twofaccount->counter);
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_create_from_parameters_persists_correct_values_to_db()
{
$twofaccount = new TwoFAccount;
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
$twofaccount->save();
$this->assertDatabaseHas('twofaccounts', [
'otp_type' => 'totp',
'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
'service' => null,
'account' => OtpTestData::ACCOUNT,
'secret' => OtpTestData::SECRET,
'digits' => OtpTestData::DIGITS_DEFAULT,
'period' => OtpTestData::PERIOD_DEFAULT,
'counter' => null,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'icon' => null,
]);
}
/**
* @test
*/
public function test_create_from_unsupported_parameters_returns_unsupportedOtpTypeException()
{
$this->expectException(\App\Exceptions\UnsupportedOtpTypeException::class);
$twofaccount = new TwoFAccount;
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_PARAMETERS_FOR_UNSUPPORTED_OTP_TYPE);
}
/**
* @test
*/
public function test_create_from_invalid_parameters_type_returns_InvalidOtpParameterException()
{
$this->expectException(\App\Exceptions\InvalidOtpParameterException::class);
$twofaccount = new TwoFAccount;
$twofaccount->fillWithOtpParameters([
'account' => OtpTestData::ACCOUNT,
'otp_type' => 'totp',
'digits' => 'notsupported',
]);
}
/**
* @test
*/
public function test_create_from_invalid_parameters_returns_InvalidOtpParameterException()
{
$this->expectException(\App\Exceptions\InvalidOtpParameterException::class);
$twofaccount = new TwoFAccount;
$twofaccount->fillWithOtpParameters([
'account' => OtpTestData::ACCOUNT,
'otp_type' => 'totp',
'algorithm' => 'notsupported',
]);
}
/**
* @test
*/
public function test_update_totp_returns_updated_model()
{
$twofaccount = $this->customTotpTwofaccount;
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
$this->assertEquals('totp', $twofaccount->otp_type);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(OtpTestData::PERIOD_DEFAULT, $twofaccount->period);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_update_hotp_returns_updated_model()
{
$twofaccount = $this->customTotpTwofaccount;
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP);
$this->assertEquals('hotp', $twofaccount->otp_type);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(null, $twofaccount->period);
$this->assertEquals(OtpTestData::COUNTER_DEFAULT, $twofaccount->counter);
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_update_totp_persists_updated_model()
{
$twofaccount = $this->customTotpTwofaccount;
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
$twofaccount->save();
$this->assertDatabaseHas('twofaccounts', [
'otp_type' => 'totp',
'service' => null,
'account' => OtpTestData::ACCOUNT,
'secret' => OtpTestData::SECRET,
'digits' => OtpTestData::DIGITS_DEFAULT,
'period' => OtpTestData::PERIOD_DEFAULT,
'counter' => null,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'icon' => null,
]);
}
/**
* @test
*/
public function test_getOTP_for_totp_returns_the_same_password()
{
$twofaccount = new TwoFAccount;
$otp_from_model = $this->customTotpTwofaccount->getOTP();
$otp_from_uri = $twofaccount->fillWithURI(OtpTestData::TOTP_FULL_CUSTOM_URI)->getOTP();
if ($otp_from_model->generated_at === $otp_from_uri->generated_at) {
$this->assertEquals($otp_from_model, $otp_from_uri);
}
$otp_from_model = $this->customTotpTwofaccount->getOTP();
$otp_from_parameters = $twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)->getOTP();
if ($otp_from_model->generated_at === $otp_from_parameters->generated_at) {
$this->assertEquals($otp_from_model, $otp_from_parameters);
}
}
/**
* @test
*/
public function test_getOTP_for_hotp_returns_the_same_password()
{
$twofaccount = new TwoFAccount;
$otp_from_model = $this->customHotpTwofaccount->getOTP();
$otp_from_uri = $twofaccount->fillWithURI(OtpTestData::HOTP_FULL_CUSTOM_URI)->getOTP();
$this->assertEquals($otp_from_model, $otp_from_uri);
$otp_from_parameters = $twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)->getOTP();
$this->assertEquals($otp_from_model, $otp_from_parameters);
}
/**
* @test
*/
public function test_getOTP_for_totp_with_invalid_secret_returns_InvalidSecretException()
{
$twofaccount = new TwoFAccount;
$this->expectException(\App\Exceptions\InvalidSecretException::class);
$otp_from_uri = $twofaccount->fillWithURI('otpauth://totp/'.OtpTestData::ACCOUNT.'?secret=0')->getOTP();
}
/**
* @test
*/
public function test_getOTP_for_totp_with_undecipherable_secret_returns_UndecipherableException()
{
$twofaccount = new TwoFAccount;
$this->expectException(\App\Exceptions\UndecipherableException::class);
$otp_from_uri = $twofaccount->fillWithOtpParameters([
'account' => OtpTestData::ACCOUNT,
'otp_type' => 'totp',
'secret' => __('errors.indecipherable'),
])->getOTP();
}
/**
* @test
*/
public function test_getURI_for_custom_totp_model_returns_uri()
{
$uri = $this->customTotpTwofaccount->getURI();
$this->assertStringContainsString('otpauth://totp/', $uri);
$this->assertStringContainsString(OtpTestData::SERVICE, $uri);
$this->assertStringContainsString(OtpTestData::ACCOUNT, $uri);
$this->assertStringContainsString('secret='.OtpTestData::SECRET, $uri);
$this->assertStringContainsString('digits='.OtpTestData::DIGITS_CUSTOM, $uri);
$this->assertStringContainsString('period='.OtpTestData::PERIOD_CUSTOM, $uri);
$this->assertStringContainsString('algorithm='.OtpTestData::ALGORITHM_CUSTOM, $uri);
}
/**
* @test
*/
public function test_getURI_for_custom_hotp_model_returns_uri()
{
$uri = $this->customHotpTwofaccount->getURI();
$this->assertStringContainsString('otpauth://hotp/', $uri);
$this->assertStringContainsString(OtpTestData::SERVICE, $uri);
$this->assertStringContainsString(OtpTestData::ACCOUNT, $uri);
$this->assertStringContainsString('secret='.OtpTestData::SECRET, $uri);
$this->assertStringContainsString('digits='.OtpTestData::DIGITS_CUSTOM, $uri);
$this->assertStringContainsString('counter='.OtpTestData::COUNTER_CUSTOM, $uri);
$this->assertStringContainsString('algorithm='.OtpTestData::ALGORITHM_CUSTOM, $uri);
}
}

View File

@ -5,7 +5,7 @@
use App\Models\Group;
use App\Models\TwoFAccount;
use Tests\FeatureTestCase;
use Illuminate\Support\Facades\DB;
use Tests\Classes\OtpTestData;
/**
@ -36,70 +36,6 @@ class TwoFAccountServiceTest extends FeatureTestCase
*/
protected $customHotpTwofaccount;
private const ACCOUNT = 'account';
private const SERVICE = 'service';
private const SECRET = 'A4GRFHVVRBGY7UIW';
private const ALGORITHM_DEFAULT = 'sha1';
private const ALGORITHM_CUSTOM = 'sha256';
private const DIGITS_DEFAULT = 6;
private const DIGITS_CUSTOM = 7;
private const PERIOD_DEFAULT = 30;
private const PERIOD_CUSTOM = 40;
private const COUNTER_DEFAULT = 0;
private const COUNTER_CUSTOM = 5;
private const IMAGE = 'https%3A%2F%2Fen.opensuse.org%2Fimages%2F4%2F44%2FButton-filled-colour.png';
private const ICON = 'test.png';
private const TOTP_FULL_CUSTOM_URI = 'otpauth://totp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&period='.self::PERIOD_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
private const HOTP_FULL_CUSTOM_URI = 'otpauth://hotp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&counter='.self::COUNTER_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
private const TOTP_SHORT_URI = 'otpauth://totp/'.self::ACCOUNT.'?secret='.self::SECRET;
private const HOTP_SHORT_URI = 'otpauth://hotp/'.self::ACCOUNT.'?secret='.self::SECRET;
private const TOTP_URI_WITH_UNREACHABLE_IMAGE = 'otpauth://totp/service:account?secret=A4GRFHVVRBGY7UIW&image=https%3A%2F%2Fen.opensuse.org%2Fimage.png';
private const INVALID_OTPAUTH_URI = 'otpauth://Xotp/'.self::ACCOUNT.'?secret='.self::SECRET;
private const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP = [
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'icon' => self::ICON,
'otp_type' => 'totp',
'secret' => self::SECRET,
'digits' => self::DIGITS_CUSTOM,
'algorithm' => self::ALGORITHM_CUSTOM,
'period' => self::PERIOD_CUSTOM,
'counter' => null,
];
private const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP = [
'account' => self::ACCOUNT,
'otp_type' => 'totp',
'secret' => self::SECRET,
];
private const ARRAY_OF_PARAMETERS_FOR_UNSUPPORTED_OTP_TYPE = [
'account' => self::ACCOUNT,
'otp_type' => 'Xotp',
'secret' => self::SECRET,
];
private const ARRAY_OF_INVALID_PARAMETERS_FOR_TOTP = [
'account' => self::ACCOUNT,
'otp_type' => 'totp',
'secret' => 0,
];
private const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP = [
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'icon' => self::ICON,
'otp_type' => 'hotp',
'secret' => self::SECRET,
'digits' => self::DIGITS_CUSTOM,
'algorithm' => self::ALGORITHM_CUSTOM,
'period' => null,
'counter' => self::COUNTER_CUSTOM,
];
private const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP = [
'account' => self::ACCOUNT,
'otp_type' => 'hotp',
'secret' => self::SECRET,
];
private const GOOGLE_AUTH_MIGRATION_URI = 'otpauth-migration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY2UgASgBMAIKLAoKBw0SnrWITY/RFhILYWNjb3VudF9iaXMaC3NlcnZpY2VfYmlzIAEoATACEAEYASAA';
private const GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA = 'otpauth-migration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY';
/**
* @test
@ -111,29 +47,29 @@ public function setUp() : void
$this->twofaccountService = $this->app->make('App\Services\TwoFAccountService');
$this->customTotpTwofaccount = new TwoFAccount;
$this->customTotpTwofaccount->legacy_uri = self::TOTP_FULL_CUSTOM_URI;
$this->customTotpTwofaccount->service = self::SERVICE;
$this->customTotpTwofaccount->account = self::ACCOUNT;
$this->customTotpTwofaccount->icon = self::ICON;
$this->customTotpTwofaccount->legacy_uri = OtpTestData::TOTP_FULL_CUSTOM_URI;
$this->customTotpTwofaccount->service = OtpTestData::SERVICE;
$this->customTotpTwofaccount->account = OtpTestData::ACCOUNT;
$this->customTotpTwofaccount->icon = OtpTestData::ICON;
$this->customTotpTwofaccount->otp_type = 'totp';
$this->customTotpTwofaccount->secret = self::SECRET;
$this->customTotpTwofaccount->digits = self::DIGITS_CUSTOM;
$this->customTotpTwofaccount->algorithm = self::ALGORITHM_CUSTOM;
$this->customTotpTwofaccount->period = self::PERIOD_CUSTOM;
$this->customTotpTwofaccount->secret = OtpTestData::SECRET;
$this->customTotpTwofaccount->digits = OtpTestData::DIGITS_CUSTOM;
$this->customTotpTwofaccount->algorithm = OtpTestData::ALGORITHM_CUSTOM;
$this->customTotpTwofaccount->period = OtpTestData::PERIOD_CUSTOM;
$this->customTotpTwofaccount->counter = null;
$this->customTotpTwofaccount->save();
$this->customHotpTwofaccount = new TwoFAccount;
$this->customHotpTwofaccount->legacy_uri = self::HOTP_FULL_CUSTOM_URI;
$this->customHotpTwofaccount->service = self::SERVICE;
$this->customHotpTwofaccount->account = self::ACCOUNT;
$this->customHotpTwofaccount->icon = self::ICON;
$this->customHotpTwofaccount->legacy_uri = OtpTestData::HOTP_FULL_CUSTOM_URI;
$this->customHotpTwofaccount->service = OtpTestData::SERVICE;
$this->customHotpTwofaccount->account = OtpTestData::ACCOUNT;
$this->customHotpTwofaccount->icon = OtpTestData::ICON;
$this->customHotpTwofaccount->otp_type = 'hotp';
$this->customHotpTwofaccount->secret = self::SECRET;
$this->customHotpTwofaccount->digits = self::DIGITS_CUSTOM;
$this->customHotpTwofaccount->algorithm = self::ALGORITHM_CUSTOM;
$this->customHotpTwofaccount->secret = OtpTestData::SECRET;
$this->customHotpTwofaccount->digits = OtpTestData::DIGITS_CUSTOM;
$this->customHotpTwofaccount->algorithm = OtpTestData::ALGORITHM_CUSTOM;
$this->customHotpTwofaccount->period = null;
$this->customHotpTwofaccount->counter = self::COUNTER_CUSTOM;
$this->customHotpTwofaccount->counter = OtpTestData::COUNTER_CUSTOM;
$this->customHotpTwofaccount->save();
@ -143,510 +79,6 @@ public function setUp() : void
}
/**
* @test
*/
public function test_create_custom_totp_from_uri_returns_correct_value()
{
$twofaccount = $this->twofaccountService->createFromUri(self::TOTP_FULL_CUSTOM_URI);
$this->assertEquals('totp', $twofaccount->otp_type);
$this->assertEquals(self::TOTP_FULL_CUSTOM_URI, $twofaccount->legacy_uri);
$this->assertEquals(self::SERVICE, $twofaccount->service);
$this->assertEquals(self::ACCOUNT, $twofaccount->account);
$this->assertEquals(self::SECRET, $twofaccount->secret);
$this->assertEquals(self::DIGITS_CUSTOM, $twofaccount->digits);
$this->assertEquals(self::PERIOD_CUSTOM, $twofaccount->period);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(self::ALGORITHM_CUSTOM, $twofaccount->algorithm);
$this->assertStringEndsWith('.png',$twofaccount->icon);
}
/**
* @test
*/
public function test_create_basic_totp_from_uri_returns_default_value()
{
$twofaccount = $this->twofaccountService->createFromUri(self::TOTP_SHORT_URI);
$this->assertEquals('totp', $twofaccount->otp_type);
$this->assertEquals(self::TOTP_SHORT_URI, $twofaccount->legacy_uri);
$this->assertEquals(self::ACCOUNT, $twofaccount->account);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(self::SECRET, $twofaccount->secret);
$this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(self::PERIOD_DEFAULT, $twofaccount->period);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_create_custom_hotp_from_uri_returns_correct_value()
{
$twofaccount = $this->twofaccountService->createFromUri(self::HOTP_FULL_CUSTOM_URI);
$this->assertEquals('hotp', $twofaccount->otp_type);
$this->assertEquals(self::HOTP_FULL_CUSTOM_URI, $twofaccount->legacy_uri);
$this->assertEquals(self::SERVICE, $twofaccount->service);
$this->assertEquals(self::ACCOUNT, $twofaccount->account);
$this->assertEquals(self::SECRET, $twofaccount->secret);
$this->assertEquals(self::DIGITS_CUSTOM, $twofaccount->digits);
$this->assertEquals(null, $twofaccount->period);
$this->assertEquals(self::COUNTER_CUSTOM, $twofaccount->counter);
$this->assertEquals(self::ALGORITHM_CUSTOM, $twofaccount->algorithm);
$this->assertStringEndsWith('.png',$twofaccount->icon);
}
/**
* @test
*/
public function test_create_basic_hotp_from_uri_returns_default_value()
{
$twofaccount = $this->twofaccountService->createFromUri(self::HOTP_SHORT_URI);
$this->assertEquals('hotp', $twofaccount->otp_type);
$this->assertEquals(self::HOTP_SHORT_URI, $twofaccount->legacy_uri);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(self::ACCOUNT, $twofaccount->account);
$this->assertEquals(self::SECRET, $twofaccount->secret);
$this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(null, $twofaccount->period);
$this->assertEquals(self::COUNTER_DEFAULT, $twofaccount->counter);
$this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_create_from_uri_persists_to_db()
{
$twofaccount = $this->twofaccountService->createFromUri(self::TOTP_SHORT_URI);
$this->assertDatabaseHas('twofaccounts', [
'otp_type' => 'totp',
'legacy_uri' => self::TOTP_SHORT_URI,
'service' => null,
'account' => self::ACCOUNT,
'secret' => self::SECRET,
'digits' => self::DIGITS_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'counter' => null,
'algorithm' => self::ALGORITHM_DEFAULT,
'icon' => null,
]);
}
/**
* @test
*/
public function test_create_from_uri_does_not_persist_to_db()
{
$twofaccount = $this->twofaccountService->createFromUri(self::TOTP_SHORT_URI, false);
$this->assertDatabaseMissing('twofaccounts', [
'otp_type' => 'totp',
'legacy_uri' => self::TOTP_SHORT_URI,
'service' => null,
'account' => self::ACCOUNT,
'secret' => self::SECRET,
'digits' => self::DIGITS_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'counter' => null,
'algorithm' => self::ALGORITHM_DEFAULT,
'icon' => null,
]);
}
/**
* @test
*/
public function test_create_from_invalid_uri_returns_ValidationException()
{
$this->expectException(\Illuminate\Validation\ValidationException::class);
$twofaccount = $this->twofaccountService->createFromUri(self::INVALID_OTPAUTH_URI);
}
/**
* @test
*/
public function test_create_from_uri_without_label_returns_ValidationException()
{
$this->expectException(\Illuminate\Validation\ValidationException::class);
$twofaccount = $this->twofaccountService->createFromUri('otpauth://totp/?secret='.self::SECRET);
}
/**
* @test
*/
public function test_create_custom_totp_from_parameters_returns_correct_value()
{
$twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP);
$this->assertEquals('totp', $twofaccount->otp_type);
$this->assertEquals(self::SERVICE, $twofaccount->service);
$this->assertEquals(self::ACCOUNT, $twofaccount->account);
$this->assertEquals(self::SECRET, $twofaccount->secret);
$this->assertEquals(self::DIGITS_CUSTOM, $twofaccount->digits);
$this->assertEquals(self::PERIOD_CUSTOM, $twofaccount->period);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(self::ALGORITHM_CUSTOM, $twofaccount->algorithm);
$this->assertStringEndsWith('.png',$twofaccount->icon);
}
/**
* @test
*/
public function test_create_basic_totp_from_parameters_returns_correct_value()
{
$twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
$this->assertEquals('totp', $twofaccount->otp_type);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(self::ACCOUNT, $twofaccount->account);
$this->assertEquals(self::SECRET, $twofaccount->secret);
$this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(self::PERIOD_DEFAULT, $twofaccount->period);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_create_custom_hotp_from_parameters_returns_correct_value()
{
$twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP);
$this->assertEquals('hotp', $twofaccount->otp_type);
$this->assertEquals(self::SERVICE, $twofaccount->service);
$this->assertEquals(self::ACCOUNT, $twofaccount->account);
$this->assertEquals(self::SECRET, $twofaccount->secret);
$this->assertEquals(self::DIGITS_CUSTOM, $twofaccount->digits);
$this->assertEquals(null, $twofaccount->period);
$this->assertEquals(self::COUNTER_CUSTOM, $twofaccount->counter);
$this->assertEquals(self::ALGORITHM_CUSTOM, $twofaccount->algorithm);
$this->assertStringEndsWith('.png',$twofaccount->icon);
}
/**
* @test
*/
public function test_create_basic_hotp_from_parameters_returns_correct_value()
{
$twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP);
$this->assertEquals('hotp', $twofaccount->otp_type);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(self::ACCOUNT, $twofaccount->account);
$this->assertEquals(self::SECRET, $twofaccount->secret);
$this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(null, $twofaccount->period);
$this->assertEquals(self::COUNTER_DEFAULT, $twofaccount->counter);
$this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_create_from_parameters_persists_to_db()
{
$twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
$this->assertDatabaseHas('twofaccounts', [
'otp_type' => 'totp',
'legacy_uri' => self::TOTP_SHORT_URI,
'service' => null,
'account' => self::ACCOUNT,
'secret' => self::SECRET,
'digits' => self::DIGITS_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'counter' => null,
'algorithm' => self::ALGORITHM_DEFAULT,
'icon' => null,
]);
}
/**
* @test
*/
public function test_create_from_parameters_does_not_persist_to_db()
{
$twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP, false);
$this->assertDatabaseMissing('twofaccounts', [
'otp_type' => 'totp',
'legacy_uri' => self::TOTP_SHORT_URI,
'service' => null,
'account' => self::ACCOUNT,
'secret' => self::SECRET,
'digits' => self::DIGITS_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'counter' => null,
'algorithm' => self::ALGORITHM_DEFAULT,
'icon' => null,
]);
}
/**
* @test
*/
public function test_create_from_unsupported_parameters_returns_ValidationException()
{
$this->expectException(\Illuminate\Validation\ValidationException::class);
$twofaccount = $this->twofaccountService->createFromParameters(self::ARRAY_OF_PARAMETERS_FOR_UNSUPPORTED_OTP_TYPE);
}
/**
* @test
*/
public function test_create_from_invalid_parameters_type_returns_InvalidOtpParameterException()
{
$this->expectException(\App\Exceptions\InvalidOtpParameterException::class);
$twofaccount = $this->twofaccountService->createFromParameters([
'account' => self::ACCOUNT,
'otp_type' => 'totp',
'digits' => 'notsupported',
]);
}
/**
* @test
*/
public function test_create_from_invalid_parameters_returns_InvalidOtpParameterException()
{
$this->expectException(\App\Exceptions\InvalidOtpParameterException::class);
$twofaccount = $this->twofaccountService->createFromParameters([
'account' => self::ACCOUNT,
'otp_type' => 'totp',
'algorithm' => 'notsupported',
]);
}
/**
* @test
*/
public function test_update_totp_returns_updated_model()
{
$twofaccount = $this->twofaccountService->update($this->customTotpTwofaccount, self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
$this->assertEquals('totp', $twofaccount->otp_type);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(self::ACCOUNT, $twofaccount->account);
$this->assertEquals(self::SECRET, $twofaccount->secret);
$this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(self::PERIOD_DEFAULT, $twofaccount->period);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_update_hotp_returns_updated_model()
{
$twofaccount = $this->twofaccountService->update($this->customTotpTwofaccount, self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP);
$this->assertEquals('hotp', $twofaccount->otp_type);
$this->assertEquals(null, $twofaccount->service);
$this->assertEquals(self::ACCOUNT, $twofaccount->account);
$this->assertEquals(self::SECRET, $twofaccount->secret);
$this->assertEquals(self::DIGITS_DEFAULT, $twofaccount->digits);
$this->assertEquals(null, $twofaccount->period);
$this->assertEquals(self::COUNTER_DEFAULT, $twofaccount->counter);
$this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccount->algorithm);
$this->assertEquals(null, $twofaccount->counter);
$this->assertEquals(null, $twofaccount->icon);
}
/**
* @test
*/
public function test_update_totp_persists_updated_model()
{
$twofaccount = $this->twofaccountService->update($this->customTotpTwofaccount, self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
$this->assertDatabaseHas('twofaccounts', [
'otp_type' => 'totp',
'service' => null,
'account' => self::ACCOUNT,
'secret' => self::SECRET,
'digits' => self::DIGITS_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'counter' => null,
'algorithm' => self::ALGORITHM_DEFAULT,
'icon' => null,
]);
}
/**
* @test
*/
public function test_getOTP_for_totp_returns_the_same_password()
{
$otp_from_model = $this->twofaccountService->getOTP($this->customTotpTwofaccount);
$otp_from_id = $this->twofaccountService->getOTP($this->customTotpTwofaccount->id);
$otp_from_uri = $this->twofaccountService->getOTP(self::TOTP_FULL_CUSTOM_URI);
// Those assertions may fail if the 3 previous assignments are not done at the same exact timestamp
$this->assertEquals($otp_from_model, $otp_from_id);
$this->assertEquals($otp_from_model, $otp_from_uri);
}
/**
* @test
*/
public function test_getOTP_for_hotp_returns_the_same_password()
{
$otp_from_model = $this->twofaccountService->getOTP($this->customHotpTwofaccount);
$otp_from_id = $this->twofaccountService->getOTP($this->customHotpTwofaccount->id);
$otp_from_uri = $this->twofaccountService->getOTP(self::HOTP_FULL_CUSTOM_URI);
// Those assertions may fail if the 3 previous assignments are not done at the same exact timestamp
$this->assertEquals($otp_from_model, $otp_from_id);
$this->assertEquals($otp_from_model, $otp_from_uri);
}
/**
* @test
*/
public function test_getOTP_for_totp_with_invalid_secret_returns_InvalidSecretException()
{
$this->expectException(\App\Exceptions\InvalidSecretException::class);
$otp_from_uri = $this->twofaccountService->getOTP('otpauth://totp/'.self::ACCOUNT.'?secret=0');
}
/**
* @test
*/
public function test_getOTP_for_totp_with_undecipherable_secret_returns_UndecipherableException()
{
$this->expectException(\App\Exceptions\UndecipherableException::class);
$otp_from_uri = $this->twofaccountService->getOTP([
'account' => self::ACCOUNT,
'otp_type' => 'totp',
'secret' => __('errors.indecipherable'),
]);
}
/**
* @test
*/
public function test_getURI_for_custom_totp_model_returns_uri()
{
$uri = $this->twofaccountService->getURI($this->customTotpTwofaccount);
$this->assertStringContainsString('otpauth://totp/', $uri);
$this->assertStringContainsString(self::SERVICE, $uri);
$this->assertStringContainsString(self::ACCOUNT, $uri);
$this->assertStringContainsString('secret='.self::SECRET, $uri);
$this->assertStringContainsString('digits='.self::DIGITS_CUSTOM, $uri);
$this->assertStringContainsString('period='.self::PERIOD_CUSTOM, $uri);
$this->assertStringContainsString('algorithm='.self::ALGORITHM_CUSTOM, $uri);
}
/**
* @test
*/
public function test_getURI_for_custom_totp_model_id_returns_uri()
{
$uri = $this->twofaccountService->getURI($this->customTotpTwofaccount->id);
$this->assertStringContainsString('otpauth://totp/', $uri);
$this->assertStringContainsString(self::SERVICE, $uri);
$this->assertStringContainsString(self::ACCOUNT, $uri);
$this->assertStringContainsString('secret='.self::SECRET, $uri);
$this->assertStringContainsString('digits='.self::DIGITS_CUSTOM, $uri);
$this->assertStringContainsString('period='.self::PERIOD_CUSTOM, $uri);
$this->assertStringContainsString('algorithm='.self::ALGORITHM_CUSTOM, $uri);
}
/**
* @test
*/
public function test_getURI_for_custom_hotp_model_returns_uri()
{
$uri = $this->twofaccountService->getURI($this->customHotpTwofaccount);
$this->assertStringContainsString('otpauth://hotp/', $uri);
$this->assertStringContainsString(self::SERVICE, $uri);
$this->assertStringContainsString(self::ACCOUNT, $uri);
$this->assertStringContainsString('secret='.self::SECRET, $uri);
$this->assertStringContainsString('digits='.self::DIGITS_CUSTOM, $uri);
$this->assertStringContainsString('counter='.self::COUNTER_CUSTOM, $uri);
$this->assertStringContainsString('algorithm='.self::ALGORITHM_CUSTOM, $uri);
}
/**
* @test
*/
public function test_getURI_for_custom_hotp_model_id_returns_uri()
{
$uri = $this->twofaccountService->getURI($this->customHotpTwofaccount->id);
$this->assertStringContainsString('otpauth://hotp/', $uri);
$this->assertStringContainsString(self::SERVICE, $uri);
$this->assertStringContainsString(self::ACCOUNT, $uri);
$this->assertStringContainsString('secret='.self::SECRET, $uri);
$this->assertStringContainsString('digits='.self::DIGITS_CUSTOM, $uri);
$this->assertStringContainsString('counter='.self::COUNTER_CUSTOM, $uri);
$this->assertStringContainsString('algorithm='.self::ALGORITHM_CUSTOM, $uri);
}
/**
* @test
*/
public function test_getURI_for_totp_dto_returns_uri()
{
$dto = new \App\Services\Dto\TwoFAccountDto;
$dto->otp_type = 'totp';
$dto->account = self::ACCOUNT;
$dto->secret = self::SECRET;
$uri = $this->twofaccountService->getURI($dto);
$this->assertStringContainsString('otpauth://totp/', $uri);
$this->assertStringContainsString(self::ACCOUNT, $uri);
$this->assertStringContainsString('secret='.self::SECRET, $uri);
}
/**
* @test
*/
@ -767,27 +199,27 @@ public function test_delete_single_id()
*/
public function test_convert_migration_from_gauth_returns_correct_accounts()
{
$twofaccounts = $this->twofaccountService->convertMigrationFromGA(self::GOOGLE_AUTH_MIGRATION_URI);
$twofaccounts = $this->twofaccountService->convertMigrationFromGA(OtpTestData::GOOGLE_AUTH_MIGRATION_URI);
$this->assertCount(2, $twofaccounts);
$this->assertEquals('totp', $twofaccounts->first()->otp_type);
$this->assertEquals(self::SERVICE, $twofaccounts->first()->service);
$this->assertEquals(self::ACCOUNT, $twofaccounts->first()->account);
$this->assertEquals(self::SECRET, $twofaccounts->first()->secret);
$this->assertEquals(self::DIGITS_DEFAULT, $twofaccounts->first()->digits);
$this->assertEquals(self::PERIOD_DEFAULT, $twofaccounts->first()->period);
$this->assertEquals(OtpTestData::SERVICE, $twofaccounts->first()->service);
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccounts->first()->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccounts->first()->secret);
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccounts->first()->digits);
$this->assertEquals(OtpTestData::PERIOD_DEFAULT, $twofaccounts->first()->period);
$this->assertEquals(null, $twofaccounts->first()->counter);
$this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccounts->first()->algorithm);
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccounts->first()->algorithm);
$this->assertEquals('totp', $twofaccounts->last()->otp_type);
$this->assertEquals(self::SERVICE.'_bis', $twofaccounts->last()->service);
$this->assertEquals(self::ACCOUNT.'_bis', $twofaccounts->last()->account);
$this->assertEquals(self::SECRET, $twofaccounts->last()->secret);
$this->assertEquals(self::DIGITS_DEFAULT, $twofaccounts->last()->digits);
$this->assertEquals(self::PERIOD_DEFAULT, $twofaccounts->last()->period);
$this->assertEquals(OtpTestData::SERVICE.'_bis', $twofaccounts->last()->service);
$this->assertEquals(OtpTestData::ACCOUNT.'_bis', $twofaccounts->last()->account);
$this->assertEquals(OtpTestData::SECRET, $twofaccounts->last()->secret);
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccounts->last()->digits);
$this->assertEquals(OtpTestData::PERIOD_DEFAULT, $twofaccounts->last()->period);
$this->assertEquals(null, $twofaccounts->last()->counter);
$this->assertEquals(self::ALGORITHM_DEFAULT, $twofaccounts->last()->algorithm);
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccounts->last()->algorithm);
}
@ -797,22 +229,25 @@ public function test_convert_migration_from_gauth_returns_correct_accounts()
public function test_convert_migration_from_gauth_returns_flagged_duplicates()
{
$parameters = [
'service' => self::SERVICE,
'account' => self::ACCOUNT,
'icon' => self::ICON,
'service' => OtpTestData::SERVICE,
'account' => OtpTestData::ACCOUNT,
'icon' => OtpTestData::ICON,
'otp_type' => 'totp',
'secret' => self::SECRET,
'digits' => self::DIGITS_DEFAULT,
'algorithm' => self::ALGORITHM_DEFAULT,
'period' => self::PERIOD_DEFAULT,
'secret' => OtpTestData::SECRET,
'digits' => OtpTestData::DIGITS_DEFAULT,
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
'period' => OtpTestData::PERIOD_DEFAULT,
];
$twofaccount = $this->twofaccountService->createFromParameters($parameters);
$twofaccount = new TwoFAccount;
$twofaccount->fillWithOtpParameters($parameters)->save();
$parameters['service'] = self::SERVICE.'_bis';
$parameters['account'] = self::ACCOUNT.'_bis';
$twofaccount = $this->twofaccountService->createFromParameters($parameters);
$parameters['service'] = OtpTestData::SERVICE.'_bis';
$parameters['account'] = OtpTestData::ACCOUNT.'_bis';
$twofaccounts = $this->twofaccountService->convertMigrationFromGA(self::GOOGLE_AUTH_MIGRATION_URI);
$twofaccount = new TwoFAccount;
$twofaccount->fillWithOtpParameters($parameters)->save();
$twofaccounts = $this->twofaccountService->convertMigrationFromGA(OtpTestData::GOOGLE_AUTH_MIGRATION_URI);
$this->assertEquals(-1, $twofaccounts->first()->id);
$this->assertEquals(-1, $twofaccounts->last()->id);
@ -825,7 +260,7 @@ public function test_convert_migration_from_gauth_returns_flagged_duplicates()
public function test_convert_invalid_migration_from_gauth_returns_InvalidGoogleAuthMigration_excpetion()
{
$this->expectException(\App\Exceptions\InvalidGoogleAuthMigration::class);
$twofaccounts = $this->twofaccountService->convertMigrationFromGA(self::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA);
$twofaccounts = $this->twofaccountService->convertMigrationFromGA(OtpTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA);
}
}

View File

@ -61,6 +61,12 @@ public function provideExceptionsforBadRequest() : array
[
'\App\Exceptions\InvalidGoogleAuthMigration'
],
[
'\App\Exceptions\UndecipherableException'
],
[
'\App\Exceptions\UnsupportedOtpTypeException'
],
];
}
@ -103,4 +109,30 @@ public function provideExceptionsforNotFound() : array
],
];
}
/**
* @test
*/
public function test_authenticationException_returns_proxyAuthRequired_json_response_with_proxy_guard()
{
$request = $this->createMock(Request::class);
$instance = new Handler($this->createMock(Container::class));
$class = new \ReflectionClass(Handler::class);
$method = $class->getMethod('render');
$method->setAccessible(true);
$mockException = $this->createMock(\Illuminate\Auth\AuthenticationException::class);
$mockException->method("guards")->willReturn(['reverse-proxy-guard']);
$response = $method->invokeArgs($instance, [$request, $mockException]);
$this->assertInstanceOf(JsonResponse::class, $response);
$response = \Illuminate\Testing\TestResponse::fromBaseResponse($response);
$response->assertStatus(407)
->assertJsonStructure([
'message'
]);
}
}

View File

@ -22,7 +22,17 @@ public function test_model_configuration()
{
$this->runConfigurationAssertions(
new TwoFAccount(),
[],
[
'service',
'account',
'otp_type',
'digits',
'secret',
'algorithm',
'counter',
'period',
'icon'
],
[],
['*'],
[],