diff --git a/app/Api/v1/Controllers/QrCodeController.php b/app/Api/v1/Controllers/QrCodeController.php index b4ec92b3..bce35a59 100644 --- a/app/Api/v1/Controllers/QrCodeController.php +++ b/app/Api/v1/Controllers/QrCodeController.php @@ -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); } diff --git a/app/Api/v1/Controllers/TwoFAccountController.php b/app/Api/v1/Controllers/TwoFAccountController.php index b46b3fb8..d34d075b 100644 --- a/app/Api/v1/Controllers/TwoFAccountController.php +++ b/app/Api/v1/Controllers/TwoFAccountController.php @@ -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); } diff --git a/app/Api/v1/Requests/TwoFAccountUriRequest.php b/app/Api/v1/Requests/TwoFAccountUriRequest.php index 886fbce6..f1cbe321 100644 --- a/app/Api/v1/Requests/TwoFAccountUriRequest.php +++ b/app/Api/v1/Requests/TwoFAccountUriRequest.php @@ -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', ]; } } \ No newline at end of file diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index de36efeb..56485c7c 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -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([ diff --git a/app/Exceptions/InvalidGoogleAuthMigration.php b/app/Exceptions/InvalidGoogleAuthMigration.php index b0aa217f..cdb3474a 100644 --- a/app/Exceptions/InvalidGoogleAuthMigration.php +++ b/app/Exceptions/InvalidGoogleAuthMigration.php @@ -5,7 +5,7 @@ use Exception; /** - * Class UndecipherableException. + * Class InvalidGoogleAuthMigration. * * @codeCoverageIgnore */ diff --git a/app/Exceptions/UnsupportedOtpTypeException.php b/app/Exceptions/UnsupportedOtpTypeException.php new file mode 100644 index 00000000..16316574 --- /dev/null +++ b/app/Exceptions/UnsupportedOtpTypeException.php @@ -0,0 +1,14 @@ + 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 */ diff --git a/app/Protobuf/GAuthValueMapping.php b/app/Protobuf/GAuthValueMapping.php index 9a6bffe9..45f13c81 100644 --- a/app/Protobuf/GAuthValueMapping.php +++ b/app/Protobuf/GAuthValueMapping.php @@ -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() {} diff --git a/app/Services/Dto/OtpDto.php b/app/Services/Dto/OtpDto.php deleted file mode 100644 index cb3c16bd..00000000 --- a/app/Services/Dto/OtpDto.php +++ /dev/null @@ -1,21 +0,0 @@ - "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 diff --git a/composer.json b/composer.json index c4715c82..fe4b0618 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index 787b7479..c3303fca 100644 --- a/resources/lang/en/errors.php +++ b/resources/lang/en/errors.php @@ -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', ]; \ No newline at end of file diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index a8205245..2f964a64 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -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', /* |-------------------------------------------------------------------------- diff --git a/tests/Api/v1/Controllers/TwoFAccountControllerTest.php b/tests/Api/v1/Controllers/TwoFAccountControllerTest.php index f49addf1..0ec60c5b 100644 --- a/tests/Api/v1/Controllers/TwoFAccountControllerTest.php +++ b/tests/Api/v1/Controllers/TwoFAccountControllerTest.php @@ -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); } diff --git a/tests/Api/v1/Requests/TwoFAccountUriRequestTest.php b/tests/Api/v1/Requests/TwoFAccountUriRequestTest.php index fff07e7c..dcc582d5 100644 --- a/tests/Api/v1/Requests/TwoFAccountUriRequestTest.php +++ b/tests/Api/v1/Requests/TwoFAccountUriRequestTest.php @@ -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 + ]], ]; } diff --git a/tests/Classes/OtpTestData.php b/tests/Classes/OtpTestData.php new file mode 100644 index 00000000..5aab0bc8 --- /dev/null +++ b/tests/Classes/OtpTestData.php @@ -0,0 +1,73 @@ + 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'; +} diff --git a/tests/Feature/Models/TwoFAccountModelTest.php b/tests/Feature/Models/TwoFAccountModelTest.php new file mode 100644 index 00000000..a23f843d --- /dev/null +++ b/tests/Feature/Models/TwoFAccountModelTest.php @@ -0,0 +1,510 @@ +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); + } + +} \ No newline at end of file diff --git a/tests/Feature/Services/TwoFAccountServiceTest.php b/tests/Feature/Services/TwoFAccountServiceTest.php index 4e494fec..217047aa 100644 --- a/tests/Feature/Services/TwoFAccountServiceTest.php +++ b/tests/Feature/Services/TwoFAccountServiceTest.php @@ -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); } } \ No newline at end of file diff --git a/tests/Unit/Exceptions/HandlerTest.php b/tests/Unit/Exceptions/HandlerTest.php index 7b6fde66..af7ed6a6 100644 --- a/tests/Unit/Exceptions/HandlerTest.php +++ b/tests/Unit/Exceptions/HandlerTest.php @@ -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' + ]); + } } \ No newline at end of file diff --git a/tests/Unit/TwoFAccountModelTest.php b/tests/Unit/TwoFAccountModelTest.php index 873c05c9..4c0c4b16 100644 --- a/tests/Unit/TwoFAccountModelTest.php +++ b/tests/Unit/TwoFAccountModelTest.php @@ -22,7 +22,17 @@ public function test_model_configuration() { $this->runConfigurationAssertions( new TwoFAccount(), - [], + [ + 'service', + 'account', + 'otp_type', + 'digits', + 'secret', + 'algorithm', + 'counter', + 'period', + 'icon' + ], [], ['*'], [],