2020-06-27 11:06:35 -07:00
< ? php
2024-03-12 22:39:16 -04:00
namespace App\Tests\Integration\Api\Client ;
2020-06-27 11:06:35 -07:00
use Carbon\Carbon ;
2024-03-12 22:39:16 -04:00
use App\Models\User ;
2020-06-27 11:06:35 -07:00
use Illuminate\Http\Response ;
use PragmaRX\Google2FA\Google2FA ;
2024-03-12 22:39:16 -04:00
use App\Models\RecoveryToken ;
2020-07-09 21:32:31 -07:00
use PHPUnit\Framework\ExpectationFailedException ;
2020-06-27 11:06:35 -07:00
2020-06-27 12:04:41 -07:00
class TwoFactorControllerTest extends ClientApiIntegrationTestCase
2020-06-27 11:06:35 -07:00
{
/**
* Test that image data for enabling 2 FA is returned by the endpoint and that the user
* record in the database is updated as expected .
*/
2025-02-25 14:22:07 +01:00
public function test_two_factor_image_data_is_returned () : void
2020-06-27 11:06:35 -07:00
{
2024-03-12 22:39:16 -04:00
/** @var \App\Models\User $user */
2021-01-23 12:09:16 -08:00
$user = User :: factory () -> create ([ 'use_totp' => false ]);
2020-06-27 11:06:35 -07:00
$this -> assertFalse ( $user -> use_totp );
$this -> assertEmpty ( $user -> totp_secret );
$this -> assertEmpty ( $user -> totp_authenticated_at );
$response = $this -> actingAs ( $user ) -> getJson ( '/api/client/account/two-factor' );
$response -> assertOk ();
$response -> assertJsonStructure ([ 'data' => [ 'image_url_data' ]]);
$user = $user -> refresh ();
$this -> assertFalse ( $user -> use_totp );
$this -> assertNotEmpty ( $user -> totp_secret );
$this -> assertEmpty ( $user -> totp_authenticated_at );
}
/**
* Test that an error is returned if the user ' s account already has 2 FA enabled on it .
*/
2025-02-25 14:22:07 +01:00
public function test_error_is_returned_when_two_factor_is_already_enabled () : void
2020-06-27 11:06:35 -07:00
{
2024-03-12 22:39:16 -04:00
/** @var \App\Models\User $user */
2021-01-23 12:09:16 -08:00
$user = User :: factory () -> create ([ 'use_totp' => true ]);
2020-06-27 11:06:35 -07:00
$response = $this -> actingAs ( $user ) -> getJson ( '/api/client/account/two-factor' );
$response -> assertStatus ( Response :: HTTP_BAD_REQUEST );
$response -> assertJsonPath ( 'errors.0.code' , 'BadRequestHttpException' );
$response -> assertJsonPath ( 'errors.0.detail' , 'Two-factor authentication is already enabled on this account.' );
}
/**
* Test that a validation error is thrown if invalid data is passed to the 2 FA endpoint .
*/
2025-02-25 14:22:07 +01:00
public function test_validation_error_is_returned_if_invalid_data_is_passed_to_enabled2_fa () : void
2020-06-27 11:06:35 -07:00
{
2024-03-12 22:39:16 -04:00
/** @var \App\Models\User $user */
2021-01-23 12:09:16 -08:00
$user = User :: factory () -> create ([ 'use_totp' => false ]);
2020-06-27 11:06:35 -07:00
2022-07-03 14:27:37 -04:00
$this -> actingAs ( $user )
-> postJson ( '/api/client/account/two-factor' , [ 'code' => '' ])
-> assertUnprocessable ()
-> assertJsonPath ( 'errors.0.meta.rule' , 'required' )
-> assertJsonPath ( 'errors.0.meta.source_field' , 'code' )
-> assertJsonPath ( 'errors.1.meta.rule' , 'required' )
-> assertJsonPath ( 'errors.1.meta.source_field' , 'password' );
2020-06-27 11:06:35 -07:00
}
/**
* Tests that 2 FA can be enabled on an account for the user .
*/
2025-02-25 14:22:07 +01:00
public function test_two_factor_can_be_enabled_on_account () : void
2020-06-27 11:06:35 -07:00
{
2024-03-12 22:39:16 -04:00
/** @var \App\Models\User $user */
2021-01-23 12:09:16 -08:00
$user = User :: factory () -> create ([ 'use_totp' => false ]);
2020-06-27 11:06:35 -07:00
// Make the initial call to get the account setup for 2FA.
$this -> actingAs ( $user ) -> getJson ( '/api/client/account/two-factor' ) -> assertOk ();
$user = $user -> refresh ();
$this -> assertNotNull ( $user -> totp_secret );
/** @var \PragmaRX\Google2FA\Google2FA $service */
$service = $this -> app -> make ( Google2FA :: class );
2024-05-28 15:24:20 +02:00
$token = $service -> getCurrentOtp ( $user -> totp_secret );
2020-06-27 11:06:35 -07:00
$response = $this -> actingAs ( $user ) -> postJson ( '/api/client/account/two-factor' , [
'code' => $token ,
2022-07-03 14:27:37 -04:00
'password' => 'password' ,
2020-06-27 11:06:35 -07:00
]);
2020-07-09 21:32:31 -07:00
$response -> assertOk ();
$response -> assertJsonPath ( 'object' , 'recovery_tokens' );
2020-06-27 11:06:35 -07:00
$user = $user -> refresh ();
$this -> assertTrue ( $user -> use_totp );
2020-07-09 21:32:31 -07:00
$tokens = RecoveryToken :: query () -> where ( 'user_id' , $user -> id ) -> get ();
$this -> assertCount ( 10 , $tokens );
2025-01-30 16:39:00 -05:00
$this -> assertStringStartsWith ( '$2y$' , $tokens [ 0 ] -> token );
2024-03-23 11:20:15 -04:00
// Ensure the recovery tokens that were created include a "created_at" timestamp value on them.
2021-03-21 10:43:01 -07:00
$this -> assertNotNull ( $tokens [ 0 ] -> created_at );
2020-07-09 21:32:31 -07:00
$tokens = $tokens -> pluck ( 'token' ) -> toArray ();
2024-03-23 11:20:15 -04:00
$rawTokens = $response -> json ( 'attributes.tokens' );
$rawToken = reset ( $rawTokens );
2024-03-23 11:27:26 -04:00
$hashed = reset ( $tokens );
2020-07-09 21:32:31 -07:00
2024-03-23 11:27:26 -04:00
throw_unless ( password_verify ( $rawToken , $hashed ), new ExpectationFailedException ( sprintf ( 'Failed asserting that token [%s] exists as a hashed value in recovery_tokens table.' , $rawToken )));
2020-06-27 11:06:35 -07:00
}
/**
2022-10-14 10:59:20 -06:00
* Test that two - factor authentication can be disabled on an account as long as the password
2020-06-27 11:06:35 -07:00
* provided is valid for the account .
*/
2025-02-25 14:22:07 +01:00
public function test_two_factor_can_be_disabled_on_account () : void
2020-06-27 11:06:35 -07:00
{
Carbon :: setTestNow ( Carbon :: now ());
2024-03-12 22:39:16 -04:00
/** @var \App\Models\User $user */
2021-01-23 12:09:16 -08:00
$user = User :: factory () -> create ([ 'use_totp' => true ]);
2020-06-27 11:06:35 -07:00
$response = $this -> actingAs ( $user ) -> deleteJson ( '/api/client/account/two-factor' , [
'password' => 'invalid' ,
]);
$response -> assertStatus ( Response :: HTTP_BAD_REQUEST );
$response -> assertJsonPath ( 'errors.0.code' , 'BadRequestHttpException' );
$response -> assertJsonPath ( 'errors.0.detail' , 'The password provided was not valid.' );
$response = $this -> actingAs ( $user ) -> deleteJson ( '/api/client/account/two-factor' , [
'password' => 'password' ,
]);
$response -> assertStatus ( Response :: HTTP_NO_CONTENT );
$user = $user -> refresh ();
$this -> assertFalse ( $user -> use_totp );
$this -> assertNotNull ( $user -> totp_authenticated_at );
2022-10-14 10:59:20 -06:00
$this -> assertSame ( Carbon :: now () -> toAtomString (), $user -> totp_authenticated_at -> toAtomString ());
2020-06-27 11:06:35 -07:00
}
/**
* Test that no error is returned when trying to disabled two factor on an account where it
* was not enabled in the first place .
*/
2025-02-25 14:22:07 +01:00
public function test_no_error_is_returned_if_two_factor_is_not_enabled () : void
2020-06-27 11:06:35 -07:00
{
Carbon :: setTestNow ( Carbon :: now ());
2024-03-12 22:39:16 -04:00
/** @var \App\Models\User $user */
2021-01-23 12:09:16 -08:00
$user = User :: factory () -> create ([ 'use_totp' => false ]);
2020-06-27 11:06:35 -07:00
$response = $this -> actingAs ( $user ) -> deleteJson ( '/api/client/account/two-factor' , [
'password' => 'password' ,
]);
$response -> assertStatus ( Response :: HTTP_NO_CONTENT );
}
2022-07-03 14:27:37 -04:00
/**
* Test that a valid account password is required when enabling two - factor .
*/
2025-02-25 14:22:07 +01:00
public function test_enabling_two_factor_requires_valid_password () : void
2022-07-03 14:27:37 -04:00
{
$user = User :: factory () -> create ([ 'use_totp' => false ]);
$this -> actingAs ( $user )
-> postJson ( '/api/client/account/two-factor' , [
'code' => '123456' ,
'password' => 'foo' ,
])
-> assertStatus ( Response :: HTTP_BAD_REQUEST )
-> assertJsonPath ( 'errors.0.detail' , 'The password provided was not valid.' );
$this -> assertFalse ( $user -> refresh () -> use_totp );
}
/**
* Test that a valid account password is required when disabling two - factor .
*/
2025-02-25 14:22:07 +01:00
public function test_disabling_two_factor_requires_valid_password () : void
2022-07-03 14:27:37 -04:00
{
$user = User :: factory () -> create ([ 'use_totp' => true ]);
$this -> actingAs ( $user )
-> deleteJson ( '/api/client/account/two-factor' , [
'password' => 'foo' ,
])
-> assertStatus ( Response :: HTTP_BAD_REQUEST )
-> assertJsonPath ( 'errors.0.detail' , 'The password provided was not valid.' );
$this -> assertTrue ( $user -> refresh () -> use_totp );
}
2020-06-27 11:06:35 -07:00
}