Skip to content

Commit

Permalink
bypass superpin environment
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoraddatz committed Sep 17, 2024
1 parent c6cef9c commit cf45fd6
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 36 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
4 changes: 1 addition & 3 deletions src/Events/LoggedInViaTotp.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,5 @@ class LoggedInViaTotp
use InteractsWithSockets;
use SerializesModels;

public function __construct(public $user, public $request)
{
}
public function __construct(public $user, public $request) {}
}
4 changes: 1 addition & 3 deletions src/Events/LoginRequestViaTotp.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,5 @@ class LoginRequestViaTotp
use InteractsWithSockets;
use SerializesModels;

public function __construct(public $user, public $request)
{
}
public function __construct(public $user, public $request) {}
}
4 changes: 1 addition & 3 deletions src/Exceptions/MissingCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,4 @@

use Exception;

class MissingCode extends Exception
{
}
class MissingCode extends Exception {}
4 changes: 1 addition & 3 deletions src/Exceptions/MissingSessionInformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,4 @@

use Exception;

class MissingSessionInformation extends Exception
{
}
class MissingSessionInformation extends Exception {}
4 changes: 1 addition & 3 deletions src/Jobs/CreateAndSendLoginCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ class CreateAndSendLoginCode
use Dispatchable;
use SerializesModels;

public function __construct(public $user, public readonly string $ip = '')
{
}
public function __construct(public $user, public readonly string $ip = '') {}

/**
* @throws \Exception
Expand Down
4 changes: 1 addition & 3 deletions src/Jobs/ResetLoginCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ class ResetLoginCode
use Dispatchable;
use SerializesModels;

public function __construct(public $user)
{
}
public function __construct(public $user) {}

/**
* @throws \Exception
Expand Down
4 changes: 1 addition & 3 deletions src/Notifications/LoginCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
*/
class LoginCode extends Notification
{
public function __construct(protected readonly string $code, protected readonly string $ip)
{
}
public function __construct(protected readonly string $code, protected readonly string $ip) {}

/**
* @param array<string> $notifiable
Expand Down
18 changes: 17 additions & 1 deletion src/Requests/CodeRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,30 @@ public function ensureCodeIsNotExpired(): void
]);
}

public static function runsOnAllowedEnvironment(string $environment): bool
{
return in_array($environment, config('totp-login.superpin.environments'), true);
}

public static function bypassesEnvironment(string $identifier): bool
{
return 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');

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', ['test@example.com']);

$user = $this->createUser([
config('totp-login.columns.identifier') => 'test@example.com',
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();
}
}
45 changes: 45 additions & 0 deletions tests/Unit/HandleCodeRequestTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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
{
Config::set('totp-login.superpin.pin', 333333);
Config::set('totp-login.superpin.environments', ['production']);

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

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

public function test_bypasses_environment(): void
{
Config::set('totp-login.superpin.pin', 333333);
Config::set('totp-login.superpin.environments', ['non-existing']);
Config::set('totp-login.superpin.bypassing_identifiers', ['test@example.com']);

$data = [
'test@example.com' => true,
'test@*' => false,
'test2@example.com' => false,
];

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

0 comments on commit cf45fd6

Please sign in to comment.