Skip to content

Commit

Permalink
Merge branch 'main' into feature/localize-timestamp-in-notification
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoraddatz authored Sep 17, 2024
2 parents acbe7c6 + d0bb5c7 commit 9b1f73b
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 15 deletions.
42 changes: 29 additions & 13 deletions config/totp-login.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
'notification' => \Empuxa\TotpLogin\Notifications\LoginCode::class,

'columns' => [
'columns' => [
/**
* The main identifier of the user model.
* We will use this column to authenticate the user and to send the PIN to.
Expand All @@ -34,7 +34,7 @@
'code_valid_until' => 'login_totp_code_valid_until',
],

'route' => [
'route' => [
/**
* The middleware to use for the route.
* Default: ['web', 'guest']
Expand All @@ -48,7 +48,7 @@
'prefix' => 'login',
],

'identifier' => [
'identifier' => [
/**
* The maximum number of attempts to get the user per minute.
* Afterward, the user gets blocked for 60 seconds.
Expand All @@ -71,7 +71,7 @@
'enable_throttling' => true,
],

'code' => [
'code' => [
/**
* The length of the PIN.
* Keep in mind that longer PINs might break the layout.
Expand Down Expand Up @@ -108,21 +108,37 @@
'enable_throttling' => true,
],

/**
* Enable the "superpin" feature.
* When enabled, any user can also sign in with the PIN of your choice on non-production environments.
* Set the environment variable `TOTP_LOGIN_SUPERPIN` to the PIN you want to use.
* Default: env('TOTP_LOGIN_SUPERPIN', false)
*/
'superpin' => env('TOTP_LOGIN_SUPERPIN', false),
'superpin' => [
/**
* Enable the "superpin" feature.
* When enabled, any user can also sign in with the PIN of your choice.
* Set the environment variable `TOTP_LOGIN_SUPERPIN` to the PIN you want to use.
* Default: env('TOTP_LOGIN_SUPERPIN', false)
*/
'pin' => env('TOTP_LOGIN_SUPERPIN', false),

/**
* The environments where the superpin is allowed.
* This is an extra security layer to prevent the superpin from being used in production.
* Default: ['local', 'testing']
*/
'environments' => ['local', 'testing'],

/**
* The identifiers that can bypass the environment check.
* This is useful for testing the superpin in production or providing test accounts to vendors.
* Default: []
*/
'bypassing_identifiers' => [],
],

/**
* The redirect path after a successful login.
* Default: '/'
*/
'redirect' => '/',
'redirect' => '/',

'events' => [
'events' => [
/**
* This event is fired when a user submits a TOTP.
* Default: \Empuxa\TotpLogin\Events\PinRequested::class
Expand Down
21 changes: 20 additions & 1 deletion src/Requests/CodeRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,33 @@ public function ensureCodeIsNotExpired(): void
]);
}

public static function runsOnAllowedEnvironment(?string $environment = null): bool
{
return filled($environment)
&& $environment !== 'production'
&& in_array($environment, config('totp-login.superpin.environments', ['local', 'testing']), true);
}

public static function bypassesEnvironment(?string $identifier = null): bool
{
return filled($identifier)
&& in_array($identifier, config('totp-login.superpin.bypassing_identifiers', []), true);
}

/**
* @throws \Illuminate\Validation\ValidationException
*/
public function validateCode(): void
{
$this->formatCode();

if ($this->formattedCode === (string) config('totp-login.superpin') && ! app()->isProduction()) {
$codeMatchesSuperPin = $this->formattedCode === (string) config('totp-login.superpin.pin', false);

if ($codeMatchesSuperPin && self::runsOnAllowedEnvironment(app()->environment())) {
return;
}

if ($codeMatchesSuperPin && self::bypassesEnvironment($this->user->{config('totp-login.columns.identifier')})) {
return;
}

Expand Down
62 changes: 61 additions & 1 deletion tests/Feature/Controllers/HandleCodeRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public function test_can_login_with_superpin(): void
{
Notification::fake();

Config::set('totp-login.superpin', 333333);
Config::set('totp-login.superpin.pin', 333333);

$user = $this->createUser([
config('totp-login.columns.code_valid_until') => now()->addMinutes(10),
Expand All @@ -220,4 +220,64 @@ public function test_can_login_with_superpin(): void

Notification::assertNothingSent();
}

public function test_cannot_login_with_superpin_on_wrong_environment(): void
{
Notification::fake();

Config::set('totp-login.superpin.pin', 333333);
Config::set('totp-login.superpin.environments', ['production']);

$user = $this->createUser([
config('totp-login.columns.code_valid_until') => now()->addMinutes(10),
]);

$response = $this
->withSession([
config('totp-login.columns.identifier') => $user->{config('totp-login.columns.identifier')},
])
->post(route('totp-login.code.handle'), [
'code' => [3, 3, 3, 3, 3, 3],
]);

$response->assertRedirect();

$response->assertSessionHasErrors('code', __('controllers/session.store.error.totp_wrong', [
'attempts_left' => config('totp-login.code.max_attempts') - 1,
]));

$this->assertGuest();

Notification::assertNothingSent();
}

public function test_can_login_with_superpin_on_wrong_environment_with_bypassing_identifier(): void
{
Notification::fake();

Config::set('totp-login.superpin.pin', 333333);
Config::set('totp-login.superpin.environments', ['production']);
Config::set('totp-login.superpin.bypassing_identifiers', ['[email protected]']);

$user = $this->createUser([
config('totp-login.columns.identifier') => '[email protected]',
config('totp-login.columns.code_valid_until') => now()->addMinutes(10),
]);

$response = $this
->withSession([
config('totp-login.columns.identifier') => $user->{config('totp-login.columns.identifier')},
])
->post(route('totp-login.code.handle'), [
'code' => [3, 3, 3, 3, 3, 3],
]);

$response->assertSessionHasNoErrors();

$response->assertRedirect(config('totp-login.redirect'));

$this->assertAuthenticatedAs($user);

Notification::assertNothingSent();
}
}
55 changes: 55 additions & 0 deletions tests/Unit/HandleCodeRequestTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Empuxa\TotpLogin\Tests\Unit;

use Empuxa\TotpLogin\Requests\CodeRequest;
use Illuminate\Support\Facades\Config;
use Orchestra\Testbench\TestCase;

class HandleCodeRequestTest extends TestCase
{
public function test_runs_on_allowed_environment(): void
{
$this->assertFalse(CodeRequest::runsOnAllowedEnvironment(''));
$this->assertFalse(CodeRequest::runsOnAllowedEnvironment());

Config::set('totp-login.superpin.pin', 333333);
Config::set('totp-login.superpin.environments', ['production']);

$data = [
'production' => false,
'prod*' => false,
'staging' => false,
'testing' => false,
'local' => false,
];

foreach ($data as $environment => $expected) {
$this->assertEquals($expected, CodeRequest::runsOnAllowedEnvironment($environment), $environment);
}

Config::set('totp-login.superpin.environments', ['staging']);

$this->assertTrue(CodeRequest::runsOnAllowedEnvironment('staging'));
}

public function test_bypasses_environment(): void
{
$this->assertFalse(CodeRequest::bypassesEnvironment(''));
$this->assertFalse(CodeRequest::bypassesEnvironment());

Config::set('totp-login.superpin.pin', 333333);
Config::set('totp-login.superpin.environments', ['non-existing']);
Config::set('totp-login.superpin.bypassing_identifiers', ['[email protected]']);

$data = [
'[email protected]' => true,
'test@*' => false,
'[email protected]' => false,
];

foreach ($data as $email => $expected) {
$this->assertEquals($expected, CodeRequest::bypassesEnvironment($email), $email);
}
}
}

0 comments on commit 9b1f73b

Please sign in to comment.