Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

CyberList's avatar

Test Laravel Auth with 2FA OTP

Good morning! After several months of using Laravel, I finally got around to writing my tests.

My login function uses the package https://github.com/ChristianRiesen/otp/ to have a unique code in addition to the password:

UserController.php

if (Auth::attemptWhen([
			'email' => $credentials['email'],
			'password' => $credentials['password']
		], function (User $user) {
			$otp = new Otp();
			return $otp->checkTotp(Encoding::base32DecodeUpper($user->totp_key), request('code'));
		}, $credentials['remember'])) {
			request()->session()->regenerate();
			return redirect()->route('admin')->with('status', 'Login OK.');
		}

		return back()->withErrors([
			'email' => 'Error',
		])->onlyInput('email');

Test.php

public function test_login()
	{
		Mail::fake();

		/* ???
		$this->instance(
			Otp::class,
			Mockery::mock(Otp::class, function (MockInterface $mock) {
				$mock->shouldReceive('checkTotp')->once()->andReturn(true);
			})
		);
		*/

		/* ???
		$mock = Mockery::mock(Otp::class);
		$mock->shouldReceive('checkTotp')
			->once()
			->andReturn(true);
		*/

		$user = User::first();

		$formData = [
			'name' => $user->name,
			'email' => $user->email,
			'password' => $user->password,
			'code' => "????"
		];

		$this->post((route('user.login')), $formData);
		$this->assertAuthenticated();

		/*->assertStatus(302)
			->assertSessionHas('status');
		*/

	}

I don't understand how to test this feature because of double authentication? I tried different methods with mock but obviously I don't understand something!

Thanks for your help!

0 likes
2 replies
LaryAI's avatar
Level 58

To test your Laravel authentication with 2FA OTP, you can indeed use mocking to simulate the behavior of the Otp class. You were on the right track with your attempts to mock the Otp class. Here's how you can complete your test:

public function test_login()
{
    Mail::fake();

    // Create a mock of the Otp class
    $otpMock = Mockery::mock(Otp::class);
    $otpMock->shouldReceive('checkTotp')
        ->once()
        ->withArgs(function ($key, $code) {
            // You can add additional checks here if necessary
            return true;
        })
        ->andReturn(true);

    // Bind the mock to the service container
    $this->app->instance(Otp::class, $otpMock);

    // Create a user or use an existing one
    $user = User::factory()->create([
        'password' => bcrypt($password = 'i-love-laravel'), // Set a known password
        'totp_key' => 'your-encoded-totp-key', // Set a known TOTP key
    ]);

    // Prepare the form data
    $formData = [
        'email' => $user->email,
        'password' => $password, // Use the known password
        'code' => 'valid-otp-code', // Use a valid OTP code
    ];

    // Make a POST request to the login route
    $response = $this->post(route('user.login'), $formData);

    // Assert the user was authenticated
    $this->assertAuthenticatedAs($user);

    // Additional assertions
    $response->assertStatus(302);
    $response->assertSessionHas('status', 'Login OK.');
}

In this test, we're using Mockery to create a mock of the Otp class. We then specify that the checkTotp method should be called once and return true. We bind this mock to the Laravel service container, so when the Otp class is resolved during the test, it will use our mock instead of the actual implementation.

We also create a user with a known password and TOTP key, which we can use to send the form data to the login route. After making the POST request, we assert that the user is authenticated and check the response status and session data.

Make sure to include the necessary use statements at the top of your test file:

use App\Models\User;
use Illuminate\Support\Facades\Mail;
use Mockery;
use Otp\Otp;

Also, ensure that you have a user factory set up for creating users in your tests. If you don't have one, you can create it using the make:factory Artisan command.

Remember to run your tests with a testing environment configured to avoid affecting your actual database. You can use an in-memory SQLite database or a separate testing database for this purpose.

1 like
CyberList's avatar
CyberList
OP
Best Answer
Level 8

I found a solution !

UserController.php

if (Auth::attemptWhen([
			'email' => $credentials['email'],
			'password' => $credentials['password']
		], function (User $user) use ($otp) {
			return $otp->checkTotp(Encoding::base32DecodeUpper($user->totp_key), request('code'));
		}, $credentials['remember'])) {
			Mail::to(config('mail.from.reply'))->send(new UserLoged($credentials));
			request()->session()->regenerate();
			return redirect()->route('admin')->with('status', 'Login ok .');
		}

Test.php

public function test_login()
	{
		$this->mock(Otp::class, function (MockInterface $mock) {
			$mock
				->shouldReceive('checkTotp')
				->once()
				->andReturn(true);
		});

		User::factory()->create([
			'email' => 'EMAIL',
			'password' => 'PASWORD', 
			'totp_key' => 'TOTP', 
		]);
		$user = User::where('email', 'EMAIL')->first();

		$formData = [
			'email' => $user->email,
			'password' => "PASSWORD",
			'code' => "000000",
			'remember' => 1,
			'g-recaptcha-response' => 'PASSED',
		];

		$response = $this->post(route('user.dologin'), $formData);
		$this->assertAuthenticatedAs($user);
	}

Please or to participate in this conversation.