mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-01-22 22:30:05 +01:00
Refactoring - Move OTPHP logic to TwoFAccount model
This commit is contained in:
parent
1d99c27675
commit
720eb16750
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
@ -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([
|
||||
|
@ -5,7 +5,7 @@
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class UndecipherableException.
|
||||
* Class InvalidGoogleAuthMigration.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
|
14
app/Exceptions/UnsupportedOtpTypeException.php
Normal file
14
app/Exceptions/UnsupportedOtpTypeException.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class NotImplementedException.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class UnsupportedOtpTypeException extends Exception
|
||||
{
|
||||
}
|
9
app/Models/Dto/HotpDto.php
Normal file
9
app/Models/Dto/HotpDto.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Dto;
|
||||
|
||||
class HotpDto extends OtpDto
|
||||
{
|
||||
/* @var integer */
|
||||
public int $counter;
|
||||
}
|
12
app/Models/Dto/OtpDto.php
Normal file
12
app/Models/Dto/OtpDto.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Dto;
|
||||
|
||||
class OtpDto
|
||||
{
|
||||
/* @var integer */
|
||||
public string $password;
|
||||
|
||||
/* @var integer */
|
||||
public string $otp_type;
|
||||
}
|
12
app/Models/Dto/TotpDto.php
Normal file
12
app/Models/Dto/TotpDto.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Dto;
|
||||
|
||||
class TotpDto extends OtpDto
|
||||
{
|
||||
/* @var integer */
|
||||
public int $generated_at;
|
||||
|
||||
/* @var integer */
|
||||
public int $period;
|
||||
}
|
@ -3,27 +3,77 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Exception;
|
||||
use App\Models\Dto\TotpDto;
|
||||
use App\Models\Dto\HotpDto;
|
||||
use App\Events\TwoFAccountDeleted;
|
||||
use App\Exceptions\InvalidSecretException;
|
||||
use App\Exceptions\InvalidOtpParameterException;
|
||||
use App\Exceptions\UnsupportedOtpTypeException;
|
||||
use App\Exceptions\UndecipherableException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Facades\App\Services\SettingService;
|
||||
use Spatie\EloquentSortable\Sortable;
|
||||
use Spatie\EloquentSortable\SortableTrait;
|
||||
use OTPHP\TOTP;
|
||||
use OTPHP\HOTP;
|
||||
use OTPHP\Factory;
|
||||
use SteamTotp\SteamTotp;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use ParagonIE\ConstantTime\Base32;
|
||||
|
||||
class TwoFAccount extends Model implements Sortable
|
||||
{
|
||||
|
||||
use SortableTrait, HasFactory;
|
||||
|
||||
const TOTP = 'totp';
|
||||
const HOTP = 'hotp';
|
||||
const STEAM_TOTP = 'steamtotp';
|
||||
|
||||
const SHA1 = 'sha1';
|
||||
const MD5 = 'md5';
|
||||
const SHA256 = 'sha256';
|
||||
const SHA512 = 'sha512';
|
||||
|
||||
const DEFAULT_PERIOD = 30;
|
||||
const DEFAULT_COUNTER = 0;
|
||||
const DEFAULT_DIGITS = 6;
|
||||
const DEFAULT_ALGORITHM = self::SHA1;
|
||||
|
||||
private const IMAGELINK_STORAGE_PATH = 'imagesLink/';
|
||||
private const ICON_STORAGE_PATH = 'public/icons/';
|
||||
|
||||
|
||||
/**
|
||||
* List of OTP types supported by 2FAuth
|
||||
*/
|
||||
private array $generatorClassMap = [
|
||||
'OTPHP\TOTP' => self::TOTP,
|
||||
'OTPHP\HOTP' => self::HOTP,
|
||||
];
|
||||
|
||||
/**
|
||||
* model's array form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [];
|
||||
protected $fillable = [
|
||||
// 'service',
|
||||
// 'account',
|
||||
// 'otp_type',
|
||||
// 'digits',
|
||||
// 'secret',
|
||||
// 'algorithm',
|
||||
// 'counter',
|
||||
// 'period',
|
||||
// 'icon'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
@ -40,6 +90,17 @@ class TwoFAccount extends Model implements Sortable
|
||||
* @var array
|
||||
*/
|
||||
public $appends = [];
|
||||
|
||||
|
||||
/**
|
||||
* The model's default values for attributes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attributes = [
|
||||
'digits' => 6,
|
||||
'algorithm' => self::SHA1,
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
@ -77,11 +138,35 @@ protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::saving(function ($twofaccount) {
|
||||
if (!$twofaccount->legacy_uri) $twofaccount->legacy_uri = $twofaccount->getURI();
|
||||
if ($twofaccount->otp_type == TwoFAccount::TOTP && !$twofaccount->period) $twofaccount->period = TwoFAccount::DEFAULT_PERIOD;
|
||||
if ($twofaccount->otp_type == TwoFAccount::HOTP && !$twofaccount->counter) $twofaccount->counter = TwoFAccount::DEFAULT_COUNTER;
|
||||
});
|
||||
|
||||
// static::deleted(function ($model) {
|
||||
// Log::info(sprintf('TwoFAccount #%d deleted', $model->id));
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the model with an array of attributes.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\MassAssignmentException
|
||||
*/
|
||||
// public function fill(array $attributes)
|
||||
// {
|
||||
// parent::fill($attributes);
|
||||
|
||||
// if ($this->otp_type == self::TOTP && !$this->period) $this->period = self::DEFAULT_PERIOD;
|
||||
// if ($this->otp_type == self::HOTP && !$this->counter) $this->counter = self::DEFAULT_COUNTER;
|
||||
|
||||
// return $this;
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* Settings for @spatie/eloquent-sortable package
|
||||
@ -94,6 +179,15 @@ protected static function boot()
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* The OTP generator.
|
||||
* Instanciated as null to keep the model light
|
||||
*
|
||||
* @var
|
||||
*/
|
||||
protected $generator = null;
|
||||
|
||||
|
||||
/**
|
||||
* Get legacy_uri attribute
|
||||
*
|
||||
@ -166,6 +260,309 @@ public function setSecretAttribute($value)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set digits attribute
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function setDigitsAttribute($value)
|
||||
{
|
||||
$this->attributes['digits'] = !$value ? 6 : $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set algorithm attribute
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function setAlgorithmAttribute($value)
|
||||
{
|
||||
$this->attributes['algorithm'] = !$value ? self::SHA1 : $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set period attribute
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function setPeriodAttribute($value)
|
||||
{
|
||||
$this->attributes['period'] = !$value && $this->otp_type === self::TOTP ? self::DEFAULT_PERIOD : $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set counter attribute
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function setCounterAttribute($value)
|
||||
{
|
||||
$this->attributes['counter'] = is_null($value) && $this->otp_type === self::HOTP ? self::DEFAULT_COUNTER : $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a One-Time Password with its parameters
|
||||
*
|
||||
* @throws InvalidSecretException The secret is not a valid base32 encoded string
|
||||
* @throws UndecipherableException The secret cannot be deciphered
|
||||
*/
|
||||
public function getOTP() : TotpDto|HotpDto
|
||||
{
|
||||
Log::info(sprintf('OTP requested for TwoFAccount #%s', $this->id));
|
||||
|
||||
// Early exit if the model has an undecipherable secret
|
||||
if (strtolower($this->secret) === __('errors.indecipherable')) {
|
||||
Log::error('Secret cannot be deciphered, OTP generation aborted');
|
||||
|
||||
throw new UndecipherableException();
|
||||
}
|
||||
|
||||
$this->initGenerator();
|
||||
|
||||
try {
|
||||
if ( $this->otp_type === self::TOTP || $this->otp_type === self::STEAM_TOTP ) {
|
||||
|
||||
$OtpDto = new TotpDto();
|
||||
$OtpDto->otp_type = $this->otp_type;
|
||||
$OtpDto->generated_at = time();
|
||||
$OtpDto->password = $this->otp_type === self::TOTP
|
||||
? $this->generator->at($OtpDto->generated_at)
|
||||
: SteamTotp::getAuthCode(base64_encode(Base32::decodeUpper($this->secret)));
|
||||
$OtpDto->period = $this->period;
|
||||
}
|
||||
else if ( $this->otp_type === self::HOTP ) {
|
||||
|
||||
$OtpDto = new HotpDto();
|
||||
$OtpDto->otp_type = $this->otp_type;
|
||||
$counter = $this->generator->getCounter();
|
||||
$OtpDto->password = $this->generator->at($counter);
|
||||
$OtpDto->counter = $this->counter = $counter + 1;
|
||||
|
||||
}
|
||||
else throw new UnsupportedOtpTypeException();
|
||||
|
||||
Log::info(sprintf('New OTP generated for TwoFAccount #%s', $this->id));
|
||||
|
||||
return $OtpDto;
|
||||
|
||||
}
|
||||
catch (\Exception|\Throwable $ex) {
|
||||
Log::error('An error occured, OTP generation aborted');
|
||||
// Currently a secret issue is the only possible exception thrown by OTPHP for this stack
|
||||
// so it is Ok to send the corresponding 2FAuth exception.
|
||||
// If the generator package change it could be necessary to throw a more generic exception.
|
||||
throw new InvalidSecretException($ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fill the model using an array of OTP parameters.
|
||||
* Missing parameters will be set with default values
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function fillWithOtpParameters(array $parameters, bool $isSteamTotp = false)
|
||||
{
|
||||
$this->otp_type = Arr::get($parameters, 'otp_type');
|
||||
$this->account = Arr::get($parameters, 'account');
|
||||
$this->service = Arr::get($parameters, 'service');
|
||||
$this->icon = Arr::get($parameters, 'icon');
|
||||
$this->secret = Arr::get($parameters, 'secret');
|
||||
$this->algorithm = Arr::get($parameters, 'algorithm', self::SHA1);
|
||||
$this->digits = Arr::get($parameters, 'digits', self::DEFAULT_DIGITS);
|
||||
$this->period = Arr::get($parameters, 'period', $this->otp_type == self::TOTP ? self::DEFAULT_PERIOD : null);
|
||||
$this->counter = Arr::get($parameters, 'counter', $this->otp_type == self::HOTP ? self::DEFAULT_COUNTER : null);
|
||||
|
||||
$this->initGenerator();
|
||||
|
||||
if ($isSteamTotp) {
|
||||
$this->enforceAsSteam();
|
||||
}
|
||||
|
||||
Log::info(sprintf('TwoFAccount filled with OTP parameters'));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fill the model by parsing an otpauth URI
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function fillWithURI(string $uri, bool $isSteamTotp = false)
|
||||
{
|
||||
// First we instanciate the OTP generator
|
||||
try {
|
||||
$this->generator = Factory::loadFromProvisioningUri($uri);
|
||||
}
|
||||
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
|
||||
throw ValidationException::withMessages([
|
||||
'uri' => __('validation.custom.uri.regex', ['attribute' => 'uri'])
|
||||
]);
|
||||
}
|
||||
|
||||
// As loadFromProvisioningUri() accept URI without label (nor account nor service) we check
|
||||
// that the account is set
|
||||
if ( ! $this->generator->getLabel() ) {
|
||||
Log::error('URI passed to fillWithURI() must contain a label');
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'label' => __('validation.custom.label.required')
|
||||
]);
|
||||
}
|
||||
|
||||
$this->otp_type = $this->getGeneratorOtpType();
|
||||
$this->account = $this->generator->getLabel();
|
||||
$this->secret = $this->generator->getSecret();
|
||||
$this->service = $this->generator->getIssuer();
|
||||
$this->algorithm = $this->generator->getDigest();
|
||||
$this->digits = $this->generator->getDigits();
|
||||
$this->period = $this->generator->hasParameter('period') ? $this->generator->getParameter('period') : null;
|
||||
$this->counter = $this->generator->hasParameter('counter') ? $this->generator->getParameter('counter') : null;
|
||||
$this->legacy_uri = $uri;
|
||||
|
||||
if ($isSteamTotp) {
|
||||
$this->enforceAsSteam();
|
||||
}
|
||||
|
||||
if ( $this->generator->hasParameter('image') ) {
|
||||
$this->icon = $this->storeTokenImageAsIcon();
|
||||
}
|
||||
|
||||
Log::info(sprintf('TwoFAccount filled with an URI'));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets model attributes to STEAM values
|
||||
*/
|
||||
private function enforceAsSteam()
|
||||
{
|
||||
$this->otp_type = self::STEAM_TOTP;
|
||||
$this->digits = 5;
|
||||
$this->algorithm = self::SHA1;
|
||||
$this->period = 30;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the OTP type of the instanciated OTP generator
|
||||
*/
|
||||
private function getGeneratorOtpType()
|
||||
{
|
||||
return Arr::get($this->generatorClassMap, $this->generator::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an otpauth URI built with model attribute values
|
||||
*/
|
||||
public function getURI() : string
|
||||
{
|
||||
$this->initGenerator();
|
||||
|
||||
return $this->generator->getProvisioningUri();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Instanciates the OTP generator with model attribute values
|
||||
*/
|
||||
private function initGenerator()
|
||||
{
|
||||
try {
|
||||
switch ($this->otp_type) {
|
||||
case self::TOTP:
|
||||
$this->generator = TOTP::create(
|
||||
$this->secret,
|
||||
$this->period ?: self::DEFAULT_PERIOD,
|
||||
$this->algorithm ?: self::DEFAULT_ALGORITHM,
|
||||
$this->digits ?: self::DEFAULT_DIGITS
|
||||
);
|
||||
break;
|
||||
|
||||
case self::STEAM_TOTP:
|
||||
$this->generator = TOTP::create($this->secret, 30, self::SHA1, 5);
|
||||
break;
|
||||
|
||||
case self::HOTP:
|
||||
$this->generator = HOTP::create(
|
||||
$this->secret,
|
||||
$this->counter ?: self::DEFAULT_COUNTER,
|
||||
$this->algorithm ?: self::DEFAULT_ALGORITHM,
|
||||
$this->digits ?: self::DEFAULT_DIGITS
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOtpTypeException();
|
||||
break;
|
||||
}
|
||||
|
||||
if ($this->service) $this->generator->setIssuer($this->service);
|
||||
if ($this->account) $this->generator->setLabel($this->account);
|
||||
}
|
||||
catch (UnsupportedOtpTypeException $exception) {
|
||||
Log::error(sprintf('%s is not an OTP type supported by the current generator', $this->otp_type));
|
||||
throw $exception;
|
||||
}
|
||||
catch (\Exception|\Throwable $exception) {
|
||||
throw new InvalidOtpParameterException($exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the image resource pointed by the generator image parameter and store it as an icon
|
||||
*
|
||||
* @return string|null The filename of the stored icon or null if the operation fails
|
||||
*/
|
||||
private function storeTokenImageAsIcon()
|
||||
{
|
||||
try {
|
||||
$remoteImageURL = $this->generator->getParameter('image');
|
||||
$path_parts = pathinfo($remoteImageURL);
|
||||
$newFilename = Str::random(40) . '.' . $path_parts['extension'];
|
||||
$imageFile = self::IMAGELINK_STORAGE_PATH . $newFilename;
|
||||
$iconFile = self::ICON_STORAGE_PATH . $newFilename;
|
||||
|
||||
Storage::disk('local')->put($imageFile, file_get_contents($remoteImageURL));
|
||||
|
||||
if ( in_array(Storage::mimeType($imageFile), ['image/png', 'image/jpeg', 'image/webp', 'image/bmp'])
|
||||
&& getimagesize(storage_path() . '/app/' . $imageFile) )
|
||||
{
|
||||
// Should be a valid image
|
||||
Storage::move($imageFile, $iconFile);
|
||||
|
||||
Log::info(sprintf('Icon file %s stored', $newFilename));
|
||||
}
|
||||
else {
|
||||
// @codeCoverageIgnoreStart
|
||||
Storage::delete($imageFile);
|
||||
throw new \Exception;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return $newFilename;
|
||||
}
|
||||
// @codeCoverageIgnoreStart
|
||||
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
|
||||
return null;
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an acceptable value
|
||||
*/
|
||||
|
@ -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() {}
|
||||
|
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Dto;
|
||||
|
||||
class OtpDto
|
||||
{
|
||||
/* @var integer */
|
||||
public string $password;
|
||||
|
||||
/* @var integer */
|
||||
public string $otp_type;
|
||||
|
||||
/* @var integer */
|
||||
public ?int $generated_at;
|
||||
|
||||
/* @var integer */
|
||||
public ?int $period;
|
||||
|
||||
/* @var integer */
|
||||
public ?int $counter;
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Dto;
|
||||
|
||||
class TwoFAccountDto
|
||||
{
|
||||
/* @var string */
|
||||
public string $otp_type;
|
||||
|
||||
/* @var string */
|
||||
public string $account = '';
|
||||
|
||||
/* @var string */
|
||||
public ?string $service = null;
|
||||
|
||||
/* @var string */
|
||||
public ?string $icon = null;
|
||||
|
||||
/* @var string */
|
||||
public ?string $secret = null;
|
||||
|
||||
/* @var string */
|
||||
public ?string $algorithm = 'sha1';
|
||||
|
||||
/* @var integer */
|
||||
public ?int $digits = 6;
|
||||
|
||||
/* @var integer */
|
||||
public ?int $period = 30;
|
||||
|
||||
/* @var integer */
|
||||
public ?int $counter = 0;
|
||||
}
|
@ -3,22 +3,11 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\TwoFAccount;
|
||||
use App\Exceptions\InvalidSecretException;
|
||||
use App\Exceptions\InvalidOtpParameterException;
|
||||
use App\Exceptions\UndecipherableException;
|
||||
use App\Exceptions\InvalidGoogleAuthMigration;
|
||||
use App\Services\Dto\OtpDto;
|
||||
use App\Services\Dto\TwoFAccountDto;
|
||||
use Exception;
|
||||
use OTPHP\TOTP;
|
||||
use OTPHP\HOTP;
|
||||
use OTPHP\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use ParagonIE\ConstantTime\Base32;
|
||||
use App\Protobuf\GAuthValueMapping;
|
||||
use App\Protobuf\GoogleAuth\Payload;
|
||||
@ -28,172 +17,6 @@
|
||||
|
||||
class TwoFAccountService
|
||||
{
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private $token;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private array $supportedOtpTypes = [
|
||||
"OTPHP\TOTP" => "totp",
|
||||
"OTPHP\HOTP" => "hotp"
|
||||
];
|
||||
|
||||
private const IMAGELINK_STORAGE_PATH = 'imagesLink/';
|
||||
private const ICON_STORAGE_PATH = 'public/icons/';
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
//$this->token = $otpType === TOTP::create($secret) : HOTP::create($secret);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates an account using an otpauth URI
|
||||
*
|
||||
* @param string $uri
|
||||
* @param bool $saveToDB Whether or not the created account should be saved to DB
|
||||
*
|
||||
* @return \App\Models\TwoFAccount The created account
|
||||
*/
|
||||
public function createFromUri(string $uri, bool $saveToDB = true ) : TwoFAccount
|
||||
{
|
||||
// Instanciate the token
|
||||
$this->initTokenWith($uri);
|
||||
|
||||
// Create the account
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->legacy_uri = $uri;
|
||||
$this->fillWithToken($twofaccount);
|
||||
|
||||
if ( $saveToDB ) {
|
||||
$twofaccount->save();
|
||||
|
||||
Log::info(sprintf('TwoFAccount #%d created (from URI)', $twofaccount->id));
|
||||
}
|
||||
|
||||
return $twofaccount;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates an account using a list of parameters
|
||||
*
|
||||
* @param array $data
|
||||
* @param bool $saveToDB Whether or not the created account should be saved to DB
|
||||
*
|
||||
* @return \App\Models\TwoFAccount The created account
|
||||
*/
|
||||
public function createFromParameters(array $data, bool $saveToDB = true) : TwoFAccount
|
||||
{
|
||||
// Instanciate the token
|
||||
$this->initTokenWith($data);
|
||||
|
||||
// Create and fill the account
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->legacy_uri = $this->token->getProvisioningUri();
|
||||
$twofaccount->icon = Arr::get($data, 'icon', null);
|
||||
$this->fillWithToken($twofaccount);
|
||||
|
||||
if ( $saveToDB ) {
|
||||
$twofaccount->save();
|
||||
|
||||
Log::info(sprintf('TwoFAccount #%d created (from parameters)', $twofaccount->id));
|
||||
}
|
||||
|
||||
return $twofaccount;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates an account using a list of parameters
|
||||
*
|
||||
* @param \App\Models\TwoFAccount $twofaccount The account
|
||||
* @param array $data The parameters
|
||||
*
|
||||
* @return \App\Models\TwoFAccount The updated account
|
||||
*/
|
||||
public function update(TwoFAccount $twofaccount, array $data) : TwoFAccount
|
||||
{
|
||||
// Instanciate the token
|
||||
$this->initTokenWith($data);
|
||||
|
||||
$this->fillWithToken($twofaccount);
|
||||
$twofaccount->icon = Arr::get($data, 'icon', null);
|
||||
$twofaccount->save();
|
||||
|
||||
Log::info(sprintf('TwoFAccount #%d updated', $twofaccount->id));
|
||||
|
||||
return $twofaccount;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a One-Time Password (with its parameters) for the specified account
|
||||
*
|
||||
* @param \App\Models\TwoFAccount|TwoFAccountDto|int|string $data Data defining an account
|
||||
*
|
||||
* @return OtpDto an OTP DTO
|
||||
*
|
||||
* @throws InvalidSecretException The secret is not a valid base32 encoded string
|
||||
* @throws UndecipherableException The secret cannot be deciphered
|
||||
*/
|
||||
public function getOTP($data) : OtpDto
|
||||
{
|
||||
$this->initTokenWith($data);
|
||||
$OtpDto = new OtpDto();
|
||||
|
||||
// Early exit if the model returned an undecipherable secret
|
||||
if (strtolower($this->token->getSecret()) === __('errors.indecipherable')) {
|
||||
Log::error('Secret cannot be deciphered, OTP generation aborted');
|
||||
|
||||
throw new UndecipherableException();
|
||||
}
|
||||
|
||||
try {
|
||||
if ( $this->tokenOtpType() === 'totp' ) {
|
||||
|
||||
$OtpDto->generated_at = time();
|
||||
$OtpDto->otp_type = 'totp';
|
||||
$OtpDto->password = $this->token->at($OtpDto->generated_at);
|
||||
$OtpDto->period = $this->token->getParameter('period');
|
||||
}
|
||||
else if ( $this->tokenOtpType() === 'hotp' ) {
|
||||
|
||||
$counter = $this->token->getCounter();
|
||||
$OtpDto->otp_type = 'hotp';
|
||||
$OtpDto->password = $this->token->at($counter);
|
||||
$OtpDto->counter = $counter + 1;
|
||||
}
|
||||
}
|
||||
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
|
||||
// Currently a secret issue is the only possible exception thrown by OTPHP for this stack
|
||||
// so it is Ok to send the corresponding 2FAuth exception.
|
||||
// If the token package change it could be necessary to throw a more generic exception.
|
||||
throw new InvalidSecretException($ex->getMessage());
|
||||
}
|
||||
|
||||
Log::info(sprintf('New %s generated', $OtpDto->otp_type));
|
||||
|
||||
return $OtpDto;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a generated otpauth URI for the specified account
|
||||
*
|
||||
* @param \App\Models\TwoFAccount|TwoFAccountDto|int $data Data defining an account
|
||||
*/
|
||||
public function getURI($data) : string
|
||||
{
|
||||
$this->initTokenWith($data);
|
||||
|
||||
return $this->token->getProvisioningUri();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Withdraw one or more twofaccounts from their group
|
||||
@ -268,10 +91,11 @@ public function convertMigrationFromGA($migrationUri) : Collection
|
||||
$parameters['secret'] = Base32::encodeUpper($otp_parameters->getSecret());
|
||||
$parameters['algorithm'] = GAuthValueMapping::ALGORITHM[Algorithm::name($otp_parameters->getAlgorithm())];
|
||||
$parameters['digits'] = GAuthValueMapping::DIGIT_COUNT[DigitCount::name($otp_parameters->getDigits())];
|
||||
$parameters['counter'] = $otp_parameters->getCounter();
|
||||
// $parameters['period'] = $otp_parameters->getPeriod();
|
||||
$parameters['counter'] = $parameters['otp_type'] === TwoFAccount::HOTP ? $otp_parameters->getCounter() : null;
|
||||
$parameters['period'] = $parameters['otp_type'] === TwoFAccount::TOTP ? $otp_parameters->getPeriod() : null;
|
||||
|
||||
$twofaccounts[$key] = $this->createFromParameters($parameters, false);
|
||||
$twofaccounts[$key] = new TwoFAccount;
|
||||
$twofaccounts[$key]->fillWithOtpParameters($parameters);
|
||||
}
|
||||
catch (Exception $exception) {
|
||||
|
||||
@ -281,7 +105,7 @@ public function convertMigrationFromGA($migrationUri) : Collection
|
||||
// The token failed to generate a valid account so we create a fake account to be returned.
|
||||
$fakeAccount = new TwoFAccount();
|
||||
$fakeAccount->id = -2;
|
||||
$fakeAccount->otp_type = 'totp';
|
||||
$fakeAccount->otp_type = $fakeAccount::TOTP;
|
||||
// Only basic fields are filled to limit the risk of another exception.
|
||||
$fakeAccount->account = $otp_parameters->getName();
|
||||
$fakeAccount->service = $otp_parameters->getIssuer();
|
||||
@ -297,11 +121,6 @@ public function convertMigrationFromGA($migrationUri) : Collection
|
||||
}
|
||||
|
||||
|
||||
// ########################################################################################################################
|
||||
// ########################################################################################################################
|
||||
// ########################################################################################################################
|
||||
// ########################################################################################################################
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@ -318,231 +137,6 @@ private function commaSeparatedToArray($ids)
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inits the Token
|
||||
*/
|
||||
private function initTokenWith($data) : void
|
||||
{
|
||||
// init with a TwoFAccount instance
|
||||
if ( is_object($data) && get_class($data) === 'App\Models\TwoFAccount' ) {
|
||||
$this->initTokenWithTwoFAccount($data);
|
||||
}
|
||||
// init with a TwoFAccountDto instance
|
||||
else if ( is_object($data) && get_class($data) === 'App\Services\Dto\TwoFAccountDto' ) {
|
||||
$this->initTokenWithParameters($data);
|
||||
}
|
||||
// init with an account ID
|
||||
else if ( is_integer($data) ) {
|
||||
// we should have an ID
|
||||
$twofaccount = TwoFAccount::findOrFail($data);
|
||||
$this->initTokenWithTwoFAccount($twofaccount);
|
||||
}
|
||||
// init with an array of property
|
||||
else if( is_array($data) ) {
|
||||
$dto = $this->mapArrayToDto($data);
|
||||
$this->initTokenWithParameters($dto);
|
||||
}
|
||||
// or with a string that should be an otpauth URI
|
||||
else {
|
||||
$this->initTokenWithUri($data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps array items to a TwoFAccountDto instance
|
||||
*
|
||||
* @param array $array The array to map
|
||||
*
|
||||
* @returns TwoFAccountDto
|
||||
*/
|
||||
private function mapArrayToDto($array) : TwoFAccountDto
|
||||
{
|
||||
$dto = new TwoFAccountDto();
|
||||
|
||||
try {
|
||||
foreach ($array as $key => $value) {
|
||||
$dto->$key = ! Arr::has($array, $key) ?: $value;
|
||||
}
|
||||
}
|
||||
catch (\TypeError $ex) {
|
||||
throw new InvalidOtpParameterException($ex->getMessage());
|
||||
}
|
||||
|
||||
return $dto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Instanciates the token with a TwoFAccount
|
||||
*
|
||||
* @param \App\Models\TwoFAccount $twofaccount
|
||||
*
|
||||
* @param bool $usingUri Whether or not the token should be fed with the account uri
|
||||
*/
|
||||
private function initTokenWithTwoFAccount(TwoFAccount $twofaccount) : void
|
||||
{
|
||||
$dto = new TwoFAccountDto();
|
||||
|
||||
$dto->otp_type = $twofaccount->otp_type;
|
||||
$dto->account = $twofaccount->account;
|
||||
$dto->service = $twofaccount->service;
|
||||
$dto->icon = $twofaccount->icon;
|
||||
$dto->secret = $twofaccount->secret;
|
||||
$dto->algorithm = $twofaccount->algorithm;
|
||||
$dto->digits = $twofaccount->digits;
|
||||
|
||||
if ( $twofaccount->period ) $dto->period = $twofaccount->period;
|
||||
if ( $twofaccount->counter ) $dto->counter = $twofaccount->counter;
|
||||
|
||||
$this->initTokenWithParameters($dto);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Instanciates the token object by parsing an otpauth URI
|
||||
*
|
||||
* @throws ValidationException The URI is not a valid otpauth URI
|
||||
*/
|
||||
private function initTokenWithUri(string $uri) : void
|
||||
{
|
||||
try {
|
||||
$this->token = Factory::loadFromProvisioningUri($uri);
|
||||
}
|
||||
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
|
||||
throw ValidationException::withMessages([
|
||||
'uri' => __('validation.custom.uri.regex', ['attribute' => 'uri'])
|
||||
]);
|
||||
}
|
||||
|
||||
// As loadFromProvisioningUri() accept URI without label (nor account nor service) we check
|
||||
// that the account is set
|
||||
if ( ! $this->token->getLabel() ) {
|
||||
Log::error('URI passed to initTokenWithUri() must contain a label');
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'label' => __('validation.custom.label.required')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Instanciates the token object by passing a list of parameters
|
||||
*
|
||||
* @throws ValidationException otp type not supported
|
||||
* @throws InvalidOtpParameterException invalid otp parameters
|
||||
*/
|
||||
private function initTokenWithParameters(TwoFAccountDto $dto) : void
|
||||
{
|
||||
// Check OTP type again to ensure the upcoming OTPHP instanciation
|
||||
if ( ! in_array($dto->otp_type, $this->supportedOtpTypes, true) ) {
|
||||
Log::error(sprintf('%s is not an OTP type supported by the current token', $dto->otp_type));
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'otp_type' => __('validation.custom.otp_type.in', ['attribute' => 'otp type'])
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
if ( $dto->otp_type === 'totp' ) {
|
||||
$this->token = TOTP::create(
|
||||
$dto->secret
|
||||
);
|
||||
|
||||
if ($dto->period) $this->token->setParameter('period', $dto->period);
|
||||
}
|
||||
else if ( $dto->otp_type === 'hotp' ) {
|
||||
$this->token = HOTP::create(
|
||||
$dto->secret
|
||||
);
|
||||
|
||||
if ($dto->counter) $this->token->setParameter('counter', $dto->counter);
|
||||
}
|
||||
|
||||
if ($dto->algorithm) $this->token->setParameter('algorithm', $dto->algorithm);
|
||||
if ($dto->digits) $this->token->setParameter('digits', $dto->digits);
|
||||
// if ($dto->epoch) $this->token->setParameter('epoch', $dto->epoch);
|
||||
if ($dto->service) $this->token->setIssuer($dto->service);
|
||||
if ($dto->account) $this->token->setLabel($dto->account);
|
||||
}
|
||||
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
|
||||
throw new InvalidOtpParameterException($ex->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fills a TwoFAccount with token's parameters
|
||||
*/
|
||||
private function fillWithToken(TwoFAccount &$twofaccount) : void
|
||||
{
|
||||
$twofaccount->otp_type = $this->tokenOtpType();
|
||||
$twofaccount->account = $this->token->getLabel();
|
||||
$twofaccount->secret = $this->token->getSecret();
|
||||
$twofaccount->service = $this->token->getIssuer();
|
||||
$twofaccount->algorithm = $this->token->getDigest();
|
||||
$twofaccount->digits = $this->token->getDigits();
|
||||
$twofaccount->period = $this->token->hasParameter('period') ? $this->token->getParameter('period') : null;
|
||||
$twofaccount->counter = $this->token->hasParameter('counter') ? $this->token->getParameter('counter') : null;
|
||||
|
||||
if ( $this->token->hasParameter('image') ) {
|
||||
$twofaccount->icon = $this->storeTokenImageAsIcon();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the otp_type that matchs the token instance class
|
||||
*/
|
||||
private function tokenOtpType() : string
|
||||
{
|
||||
return $this->supportedOtpTypes[get_class($this->token)];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the image resource pointed by the token image parameter and store it as an icon
|
||||
*
|
||||
* @return string|null The filename of the stored icon or null if the operation fails
|
||||
*/
|
||||
private function storeTokenImageAsIcon()
|
||||
{
|
||||
try {
|
||||
$remoteImageURL = $this->token->getParameter('image');
|
||||
$path_parts = pathinfo($remoteImageURL);
|
||||
$newFilename = Str::random(40) . '.' . $path_parts['extension'];
|
||||
$imageFile = self::IMAGELINK_STORAGE_PATH . $newFilename;
|
||||
$iconFile = self::ICON_STORAGE_PATH . $newFilename;
|
||||
|
||||
Storage::disk('local')->put($imageFile, file_get_contents($remoteImageURL));
|
||||
|
||||
if ( in_array(Storage::mimeType($imageFile), ['image/png', 'image/jpeg', 'image/webp', 'image/bmp'])
|
||||
&& getimagesize(storage_path() . '/app/' . $imageFile) )
|
||||
{
|
||||
// Should be a valid image
|
||||
Storage::move($imageFile, $iconFile);
|
||||
|
||||
Log::info(sprintf('Icon file %s stored', $newFilename));
|
||||
}
|
||||
else {
|
||||
// @codeCoverageIgnoreStart
|
||||
Storage::delete($imageFile);
|
||||
throw new \Exception;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return $newFilename;
|
||||
}
|
||||
// @codeCoverageIgnoreStart
|
||||
catch (\Assert\AssertionFailedException|\Assert\InvalidArgumentException|\Exception|\Throwable $ex) {
|
||||
return null;
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the given collection with items marked as Duplicates (using id=-1) if a similar record exists in database
|
||||
|
@ -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",
|
||||
|
@ -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',
|
||||
];
|
@ -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',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
|
73
tests/Classes/OtpTestData.php
Normal file
73
tests/Classes/OtpTestData.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Classes;
|
||||
|
||||
class OtpTestData
|
||||
{
|
||||
const ACCOUNT = 'account';
|
||||
const SERVICE = 'service';
|
||||
const SECRET = 'A4GRFHVVRBGY7UIW';
|
||||
const ALGORITHM_DEFAULT = 'sha1';
|
||||
const ALGORITHM_CUSTOM = 'sha256';
|
||||
const DIGITS_DEFAULT = 6;
|
||||
const DIGITS_CUSTOM = 7;
|
||||
const PERIOD_DEFAULT = 30;
|
||||
const PERIOD_CUSTOM = 40;
|
||||
const COUNTER_DEFAULT = 0;
|
||||
const COUNTER_CUSTOM = 5;
|
||||
const IMAGE = 'https%3A%2F%2Fen.opensuse.org%2Fimages%2F4%2F44%2FButton-filled-colour.png';
|
||||
const ICON = 'test.png';
|
||||
const TOTP_FULL_CUSTOM_URI = 'otpauth://totp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&period='.self::PERIOD_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
|
||||
const HOTP_FULL_CUSTOM_URI = 'otpauth://hotp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&counter='.self::COUNTER_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
|
||||
const TOTP_SHORT_URI = 'otpauth://totp/'.self::ACCOUNT.'?secret='.self::SECRET;
|
||||
const HOTP_SHORT_URI = 'otpauth://hotp/'.self::ACCOUNT.'?secret='.self::SECRET;
|
||||
const TOTP_URI_WITH_UNREACHABLE_IMAGE = 'otpauth://totp/service:account?secret=A4GRFHVVRBGY7UIW&image=https%3A%2F%2Fen.opensuse.org%2Fimage.png';
|
||||
const INVALID_OTPAUTH_URI = 'otpauth://Xotp/'.self::ACCOUNT.'?secret='.self::SECRET;
|
||||
|
||||
const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP = [
|
||||
'service' => self::SERVICE,
|
||||
'account' => self::ACCOUNT,
|
||||
'icon' => self::ICON,
|
||||
'otp_type' => 'totp',
|
||||
'secret' => self::SECRET,
|
||||
'digits' => self::DIGITS_CUSTOM,
|
||||
'algorithm' => self::ALGORITHM_CUSTOM,
|
||||
'period' => self::PERIOD_CUSTOM,
|
||||
'counter' => null,
|
||||
];
|
||||
const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP = [
|
||||
'account' => self::ACCOUNT,
|
||||
'otp_type' => 'totp',
|
||||
'secret' => self::SECRET,
|
||||
];
|
||||
const ARRAY_OF_PARAMETERS_FOR_UNSUPPORTED_OTP_TYPE = [
|
||||
'account' => self::ACCOUNT,
|
||||
'otp_type' => 'Xotp',
|
||||
'secret' => self::SECRET,
|
||||
];
|
||||
const ARRAY_OF_INVALID_PARAMETERS_FOR_TOTP = [
|
||||
'account' => self::ACCOUNT,
|
||||
'otp_type' => 'totp',
|
||||
'secret' => 0,
|
||||
];
|
||||
const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP = [
|
||||
'service' => self::SERVICE,
|
||||
'account' => self::ACCOUNT,
|
||||
'icon' => self::ICON,
|
||||
'otp_type' => 'hotp',
|
||||
'secret' => self::SECRET,
|
||||
'digits' => self::DIGITS_CUSTOM,
|
||||
'algorithm' => self::ALGORITHM_CUSTOM,
|
||||
'period' => null,
|
||||
'counter' => self::COUNTER_CUSTOM,
|
||||
];
|
||||
const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP = [
|
||||
'account' => self::ACCOUNT,
|
||||
'otp_type' => 'hotp',
|
||||
'secret' => self::SECRET,
|
||||
];
|
||||
|
||||
const GOOGLE_AUTH_MIGRATION_URI = 'otpauth-migration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY2UgASgBMAIKLAoKBw0SnrWITY/RFhILYWNjb3VudF9iaXMaC3NlcnZpY2VfYmlzIAEoATACEAEYASAA';
|
||||
const INVALID_GOOGLE_AUTH_MIGRATION_URI = 'otpauthmigration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY2UgASgBMAIKLAoKBw0SnrWITY/RFhILYWNjb3VudF9iaXMaC3NlcnZpY2VfYmlzIAEoATACEAEYASAA';
|
||||
const GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA = 'otpauth-migration://offline?data=CiQKCgcNEp61iE2P0RYSB2FjY291bnQaB3NlcnZpY';
|
||||
}
|
510
tests/Feature/Models/TwoFAccountModelTest.php
Normal file
510
tests/Feature/Models/TwoFAccountModelTest.php
Normal file
@ -0,0 +1,510 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Models;
|
||||
|
||||
use App\Models\TwoFAccount;
|
||||
use Tests\FeatureTestCase;
|
||||
use Tests\Classes\OtpTestData;
|
||||
|
||||
/**
|
||||
* @covers \App\Models\TwoFAccount
|
||||
*/
|
||||
class TwoFAccountModelTest extends FeatureTestCase
|
||||
{
|
||||
/**
|
||||
* App\Models\TwoFAccount $customTotpTwofaccount
|
||||
*/
|
||||
protected $customTotpTwofaccount;
|
||||
|
||||
|
||||
/**
|
||||
* App\Models\TwoFAccount $customTotpTwofaccount
|
||||
*/
|
||||
protected $customHotpTwofaccount;
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function setUp() : void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// $this->twofaccountService = $this->app->make('App\Services\TwoFAccountService');
|
||||
|
||||
$this->customTotpTwofaccount = new TwoFAccount;
|
||||
$this->customTotpTwofaccount->legacy_uri = OtpTestData::TOTP_FULL_CUSTOM_URI;
|
||||
$this->customTotpTwofaccount->service = OtpTestData::SERVICE;
|
||||
$this->customTotpTwofaccount->account = OtpTestData::ACCOUNT;
|
||||
$this->customTotpTwofaccount->icon = OtpTestData::ICON;
|
||||
$this->customTotpTwofaccount->otp_type = 'totp';
|
||||
$this->customTotpTwofaccount->secret = OtpTestData::SECRET;
|
||||
$this->customTotpTwofaccount->digits = OtpTestData::DIGITS_CUSTOM;
|
||||
$this->customTotpTwofaccount->algorithm = OtpTestData::ALGORITHM_CUSTOM;
|
||||
$this->customTotpTwofaccount->period = OtpTestData::PERIOD_CUSTOM;
|
||||
$this->customTotpTwofaccount->counter = null;
|
||||
$this->customTotpTwofaccount->save();
|
||||
|
||||
$this->customHotpTwofaccount = new TwoFAccount;
|
||||
$this->customHotpTwofaccount->legacy_uri = OtpTestData::HOTP_FULL_CUSTOM_URI;
|
||||
$this->customHotpTwofaccount->service = OtpTestData::SERVICE;
|
||||
$this->customHotpTwofaccount->account = OtpTestData::ACCOUNT;
|
||||
$this->customHotpTwofaccount->icon = OtpTestData::ICON;
|
||||
$this->customHotpTwofaccount->otp_type = 'hotp';
|
||||
$this->customHotpTwofaccount->secret = OtpTestData::SECRET;
|
||||
$this->customHotpTwofaccount->digits = OtpTestData::DIGITS_CUSTOM;
|
||||
$this->customHotpTwofaccount->algorithm = OtpTestData::ALGORITHM_CUSTOM;
|
||||
$this->customHotpTwofaccount->period = null;
|
||||
$this->customHotpTwofaccount->counter = OtpTestData::COUNTER_CUSTOM;
|
||||
$this->customHotpTwofaccount->save();
|
||||
|
||||
|
||||
// $this->group = new Group;
|
||||
// $this->group->name = 'MyGroup';
|
||||
// $this->group->save();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_fill_with_custom_totp_uri_returns_correct_value()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithURI(OtpTestData::TOTP_FULL_CUSTOM_URI);
|
||||
|
||||
$this->assertEquals('totp', $twofaccount->otp_type);
|
||||
$this->assertEquals(OtpTestData::TOTP_FULL_CUSTOM_URI, $twofaccount->legacy_uri);
|
||||
$this->assertEquals(OtpTestData::SERVICE, $twofaccount->service);
|
||||
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
|
||||
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
|
||||
$this->assertEquals(OtpTestData::DIGITS_CUSTOM, $twofaccount->digits);
|
||||
$this->assertEquals(OtpTestData::PERIOD_CUSTOM, $twofaccount->period);
|
||||
$this->assertEquals(null, $twofaccount->counter);
|
||||
$this->assertEquals(OtpTestData::ALGORITHM_CUSTOM, $twofaccount->algorithm);
|
||||
$this->assertStringEndsWith('.png',$twofaccount->icon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_fill_with_basic_totp_uri_returns_default_value()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithURI(OtpTestData::TOTP_SHORT_URI);
|
||||
|
||||
$this->assertEquals('totp', $twofaccount->otp_type);
|
||||
$this->assertEquals(OtpTestData::TOTP_SHORT_URI, $twofaccount->legacy_uri);
|
||||
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
|
||||
$this->assertEquals(null, $twofaccount->service);
|
||||
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
|
||||
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
|
||||
$this->assertEquals(OtpTestData::PERIOD_DEFAULT, $twofaccount->period);
|
||||
$this->assertEquals(null, $twofaccount->counter);
|
||||
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
|
||||
$this->assertEquals(null, $twofaccount->icon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_fill_with_custom_hotp_uri_returns_correct_value()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithURI(OtpTestData::HOTP_FULL_CUSTOM_URI);
|
||||
|
||||
$this->assertEquals('hotp', $twofaccount->otp_type);
|
||||
$this->assertEquals(OtpTestData::HOTP_FULL_CUSTOM_URI, $twofaccount->legacy_uri);
|
||||
$this->assertEquals(OtpTestData::SERVICE, $twofaccount->service);
|
||||
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
|
||||
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
|
||||
$this->assertEquals(OtpTestData::DIGITS_CUSTOM, $twofaccount->digits);
|
||||
$this->assertEquals(null, $twofaccount->period);
|
||||
$this->assertEquals(OtpTestData::COUNTER_CUSTOM, $twofaccount->counter);
|
||||
$this->assertEquals(OtpTestData::ALGORITHM_CUSTOM, $twofaccount->algorithm);
|
||||
$this->assertStringEndsWith('.png',$twofaccount->icon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_fill_with_basic_hotp_uri_returns_default_value()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithURI(OtpTestData::HOTP_SHORT_URI);
|
||||
|
||||
$this->assertEquals('hotp', $twofaccount->otp_type);
|
||||
$this->assertEquals(OtpTestData::HOTP_SHORT_URI, $twofaccount->legacy_uri);
|
||||
$this->assertEquals(null, $twofaccount->service);
|
||||
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
|
||||
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
|
||||
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
|
||||
$this->assertEquals(null, $twofaccount->period);
|
||||
$this->assertEquals(OtpTestData::COUNTER_DEFAULT, $twofaccount->counter);
|
||||
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
|
||||
$this->assertEquals(null, $twofaccount->icon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_filled_with_uri_persists_correct_values_to_db()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithURI(OtpTestData::TOTP_SHORT_URI);
|
||||
$twofaccount->save();
|
||||
|
||||
$this->assertDatabaseHas('twofaccounts', [
|
||||
'otp_type' => 'totp',
|
||||
'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
|
||||
'service' => null,
|
||||
'account' => OtpTestData::ACCOUNT,
|
||||
'secret' => OtpTestData::SECRET,
|
||||
'digits' => OtpTestData::DIGITS_DEFAULT,
|
||||
'period' => OtpTestData::PERIOD_DEFAULT,
|
||||
'counter' => null,
|
||||
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
|
||||
'icon' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_fill_with_invalid_uri_returns_ValidationException()
|
||||
{
|
||||
$this->expectException(\Illuminate\Validation\ValidationException::class);
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithURI(OtpTestData::INVALID_OTPAUTH_URI);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_fill_with_uri_without_label_returns_ValidationException()
|
||||
{
|
||||
$this->expectException(\Illuminate\Validation\ValidationException::class);
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithURI('otpauth://totp/?secret='.OtpTestData::SECRET);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_create_custom_totp_from_parameters_returns_correct_value()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP);
|
||||
|
||||
$this->assertEquals('totp', $twofaccount->otp_type);
|
||||
$this->assertEquals(OtpTestData::SERVICE, $twofaccount->service);
|
||||
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
|
||||
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
|
||||
$this->assertEquals(OtpTestData::DIGITS_CUSTOM, $twofaccount->digits);
|
||||
$this->assertEquals(OtpTestData::PERIOD_CUSTOM, $twofaccount->period);
|
||||
$this->assertEquals(null, $twofaccount->counter);
|
||||
$this->assertEquals(OtpTestData::ALGORITHM_CUSTOM, $twofaccount->algorithm);
|
||||
$this->assertStringEndsWith('.png',$twofaccount->icon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_create_basic_totp_from_parameters_returns_correct_value()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
|
||||
|
||||
$this->assertEquals('totp', $twofaccount->otp_type);
|
||||
$this->assertEquals(null, $twofaccount->service);
|
||||
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
|
||||
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
|
||||
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
|
||||
$this->assertEquals(OtpTestData::PERIOD_DEFAULT, $twofaccount->period);
|
||||
$this->assertEquals(null, $twofaccount->counter);
|
||||
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
|
||||
$this->assertEquals(null, $twofaccount->icon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_create_custom_hotp_from_parameters_returns_correct_value()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP);
|
||||
|
||||
$this->assertEquals('hotp', $twofaccount->otp_type);
|
||||
$this->assertEquals(OtpTestData::SERVICE, $twofaccount->service);
|
||||
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
|
||||
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
|
||||
$this->assertEquals(OtpTestData::DIGITS_CUSTOM, $twofaccount->digits);
|
||||
$this->assertEquals(null, $twofaccount->period);
|
||||
$this->assertEquals(OtpTestData::COUNTER_CUSTOM, $twofaccount->counter);
|
||||
$this->assertEquals(OtpTestData::ALGORITHM_CUSTOM, $twofaccount->algorithm);
|
||||
$this->assertStringEndsWith('.png',$twofaccount->icon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_create_basic_hotp_from_parameters_returns_correct_value()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP);
|
||||
|
||||
$this->assertEquals('hotp', $twofaccount->otp_type);
|
||||
$this->assertEquals(null, $twofaccount->service);
|
||||
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
|
||||
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
|
||||
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
|
||||
$this->assertEquals(null, $twofaccount->period);
|
||||
$this->assertEquals(OtpTestData::COUNTER_DEFAULT, $twofaccount->counter);
|
||||
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
|
||||
$this->assertEquals(null, $twofaccount->icon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_create_from_parameters_persists_correct_values_to_db()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
|
||||
$twofaccount->save();
|
||||
|
||||
$this->assertDatabaseHas('twofaccounts', [
|
||||
'otp_type' => 'totp',
|
||||
'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
|
||||
'service' => null,
|
||||
'account' => OtpTestData::ACCOUNT,
|
||||
'secret' => OtpTestData::SECRET,
|
||||
'digits' => OtpTestData::DIGITS_DEFAULT,
|
||||
'period' => OtpTestData::PERIOD_DEFAULT,
|
||||
'counter' => null,
|
||||
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
|
||||
'icon' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_create_from_unsupported_parameters_returns_unsupportedOtpTypeException()
|
||||
{
|
||||
$this->expectException(\App\Exceptions\UnsupportedOtpTypeException::class);
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_PARAMETERS_FOR_UNSUPPORTED_OTP_TYPE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_create_from_invalid_parameters_type_returns_InvalidOtpParameterException()
|
||||
{
|
||||
$this->expectException(\App\Exceptions\InvalidOtpParameterException::class);
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithOtpParameters([
|
||||
'account' => OtpTestData::ACCOUNT,
|
||||
'otp_type' => 'totp',
|
||||
'digits' => 'notsupported',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_create_from_invalid_parameters_returns_InvalidOtpParameterException()
|
||||
{
|
||||
$this->expectException(\App\Exceptions\InvalidOtpParameterException::class);
|
||||
$twofaccount = new TwoFAccount;
|
||||
$twofaccount->fillWithOtpParameters([
|
||||
'account' => OtpTestData::ACCOUNT,
|
||||
'otp_type' => 'totp',
|
||||
'algorithm' => 'notsupported',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_update_totp_returns_updated_model()
|
||||
{
|
||||
$twofaccount = $this->customTotpTwofaccount;
|
||||
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
|
||||
|
||||
$this->assertEquals('totp', $twofaccount->otp_type);
|
||||
$this->assertEquals(null, $twofaccount->service);
|
||||
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
|
||||
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
|
||||
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
|
||||
$this->assertEquals(OtpTestData::PERIOD_DEFAULT, $twofaccount->period);
|
||||
$this->assertEquals(null, $twofaccount->counter);
|
||||
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
|
||||
$this->assertEquals(null, $twofaccount->counter);
|
||||
$this->assertEquals(null, $twofaccount->icon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_update_hotp_returns_updated_model()
|
||||
{
|
||||
$twofaccount = $this->customTotpTwofaccount;
|
||||
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP);
|
||||
|
||||
$this->assertEquals('hotp', $twofaccount->otp_type);
|
||||
$this->assertEquals(null, $twofaccount->service);
|
||||
$this->assertEquals(OtpTestData::ACCOUNT, $twofaccount->account);
|
||||
$this->assertEquals(OtpTestData::SECRET, $twofaccount->secret);
|
||||
$this->assertEquals(OtpTestData::DIGITS_DEFAULT, $twofaccount->digits);
|
||||
$this->assertEquals(null, $twofaccount->period);
|
||||
$this->assertEquals(OtpTestData::COUNTER_DEFAULT, $twofaccount->counter);
|
||||
$this->assertEquals(OtpTestData::ALGORITHM_DEFAULT, $twofaccount->algorithm);
|
||||
$this->assertEquals(null, $twofaccount->counter);
|
||||
$this->assertEquals(null, $twofaccount->icon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_update_totp_persists_updated_model()
|
||||
{
|
||||
$twofaccount = $this->customTotpTwofaccount;
|
||||
$twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP);
|
||||
$twofaccount->save();
|
||||
|
||||
$this->assertDatabaseHas('twofaccounts', [
|
||||
'otp_type' => 'totp',
|
||||
'service' => null,
|
||||
'account' => OtpTestData::ACCOUNT,
|
||||
'secret' => OtpTestData::SECRET,
|
||||
'digits' => OtpTestData::DIGITS_DEFAULT,
|
||||
'period' => OtpTestData::PERIOD_DEFAULT,
|
||||
'counter' => null,
|
||||
'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
|
||||
'icon' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_getOTP_for_totp_returns_the_same_password()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
|
||||
$otp_from_model = $this->customTotpTwofaccount->getOTP();
|
||||
$otp_from_uri = $twofaccount->fillWithURI(OtpTestData::TOTP_FULL_CUSTOM_URI)->getOTP();
|
||||
|
||||
if ($otp_from_model->generated_at === $otp_from_uri->generated_at) {
|
||||
$this->assertEquals($otp_from_model, $otp_from_uri);
|
||||
}
|
||||
|
||||
$otp_from_model = $this->customTotpTwofaccount->getOTP();
|
||||
$otp_from_parameters = $twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)->getOTP();
|
||||
|
||||
if ($otp_from_model->generated_at === $otp_from_parameters->generated_at) {
|
||||
$this->assertEquals($otp_from_model, $otp_from_parameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_getOTP_for_hotp_returns_the_same_password()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
|
||||
$otp_from_model = $this->customHotpTwofaccount->getOTP();
|
||||
$otp_from_uri = $twofaccount->fillWithURI(OtpTestData::HOTP_FULL_CUSTOM_URI)->getOTP();
|
||||
|
||||
$this->assertEquals($otp_from_model, $otp_from_uri);
|
||||
|
||||
$otp_from_parameters = $twofaccount->fillWithOtpParameters(OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)->getOTP();
|
||||
|
||||
$this->assertEquals($otp_from_model, $otp_from_parameters);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_getOTP_for_totp_with_invalid_secret_returns_InvalidSecretException()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
|
||||
$this->expectException(\App\Exceptions\InvalidSecretException::class);
|
||||
$otp_from_uri = $twofaccount->fillWithURI('otpauth://totp/'.OtpTestData::ACCOUNT.'?secret=0')->getOTP();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_getOTP_for_totp_with_undecipherable_secret_returns_UndecipherableException()
|
||||
{
|
||||
$twofaccount = new TwoFAccount;
|
||||
|
||||
$this->expectException(\App\Exceptions\UndecipherableException::class);
|
||||
$otp_from_uri = $twofaccount->fillWithOtpParameters([
|
||||
'account' => OtpTestData::ACCOUNT,
|
||||
'otp_type' => 'totp',
|
||||
'secret' => __('errors.indecipherable'),
|
||||
])->getOTP();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_getURI_for_custom_totp_model_returns_uri()
|
||||
{
|
||||
$uri = $this->customTotpTwofaccount->getURI();
|
||||
|
||||
$this->assertStringContainsString('otpauth://totp/', $uri);
|
||||
$this->assertStringContainsString(OtpTestData::SERVICE, $uri);
|
||||
$this->assertStringContainsString(OtpTestData::ACCOUNT, $uri);
|
||||
$this->assertStringContainsString('secret='.OtpTestData::SECRET, $uri);
|
||||
$this->assertStringContainsString('digits='.OtpTestData::DIGITS_CUSTOM, $uri);
|
||||
$this->assertStringContainsString('period='.OtpTestData::PERIOD_CUSTOM, $uri);
|
||||
$this->assertStringContainsString('algorithm='.OtpTestData::ALGORITHM_CUSTOM, $uri);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_getURI_for_custom_hotp_model_returns_uri()
|
||||
{
|
||||
$uri = $this->customHotpTwofaccount->getURI();
|
||||
|
||||
$this->assertStringContainsString('otpauth://hotp/', $uri);
|
||||
$this->assertStringContainsString(OtpTestData::SERVICE, $uri);
|
||||
$this->assertStringContainsString(OtpTestData::ACCOUNT, $uri);
|
||||
$this->assertStringContainsString('secret='.OtpTestData::SECRET, $uri);
|
||||
$this->assertStringContainsString('digits='.OtpTestData::DIGITS_CUSTOM, $uri);
|
||||
$this->assertStringContainsString('counter='.OtpTestData::COUNTER_CUSTOM, $uri);
|
||||
$this->assertStringContainsString('algorithm='.OtpTestData::ALGORITHM_CUSTOM, $uri);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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'
|
||||
]);
|
||||
}
|
||||
}
|
@ -22,7 +22,17 @@ public function test_model_configuration()
|
||||
{
|
||||
$this->runConfigurationAssertions(
|
||||
new TwoFAccount(),
|
||||
[],
|
||||
[
|
||||
'service',
|
||||
'account',
|
||||
'otp_type',
|
||||
'digits',
|
||||
'secret',
|
||||
'algorithm',
|
||||
'counter',
|
||||
'period',
|
||||
'icon'
|
||||
],
|
||||
[],
|
||||
['*'],
|
||||
[],
|
||||
|
Loading…
Reference in New Issue
Block a user