-
-
Notifications
You must be signed in to change notification settings - Fork 152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 2FA Progress Sync #920
Closed
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?php | ||
|
||
namespace App\Console\Commands\Overrides; | ||
|
||
use Illuminate\Foundation\Console\KeyGenerateCommand as BaseKeyGenerateCommand; | ||
|
||
class KeyGenerateCommand extends BaseKeyGenerateCommand | ||
{ | ||
/** | ||
* Override the default Laravel key generation command to throw a warning to the user | ||
* if it appears that they have already generated an application encryption key. | ||
* Credits: Pterodactyl Panel | ||
*/ | ||
public function handle() | ||
{ | ||
if (!empty(config('app.key')) && $this->input->isInteractive()) { | ||
$this->output->warning('It appears you have already configured an application encryption key. Continuing with this process with overwrite that key and cause data corruption for any existing encrypted data. DO NOT CONTINUE UNLESS YOU KNOW WHAT YOU ARE DOING.'); | ||
if (!$this->confirm('I understand the consequences of performing this command and accept all responsibility for the loss of encrypted data.')) { | ||
return; | ||
} | ||
|
||
if (!$this->confirm('Are you sure you wish to continue? Changing the application encryption key WILL CAUSE DATA LOSS. WE CANNOT HELP YOU RECOVER YOUR DATA IF YOU PROCEED.')) { | ||
return; | ||
} | ||
} | ||
|
||
parent::handle(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<?php | ||
|
||
namespace App\Http\Controllers\Auth; | ||
|
||
use App\Http\Controllers\Controller; | ||
use App\Models\User; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Validation\ValidationException; | ||
use PragmaRX\Google2FA\Google2FA; | ||
use BaconQrCode\Renderer\ImageRenderer; | ||
use BaconQrCode\Renderer\Image\ImagickImageBackEnd; | ||
use BaconQrCode\Renderer\RendererStyle\RendererStyle; | ||
use BaconQrCode\Writer; | ||
|
||
class Login2FAController extends Controller | ||
{ | ||
private $secret; | ||
|
||
|
||
/** | ||
* Create a new controller instance. | ||
* | ||
* @return void | ||
*/ | ||
public function __construct() | ||
{ | ||
$this->middleware('guest'); | ||
} | ||
|
||
/** | ||
* @throws ValidationException | ||
*/ | ||
public function authenticate(Request $request, User $user) | ||
{ | ||
$google2fa = app(Google2FA::class); | ||
|
||
$valid = $google2fa->verifyKey('6TZBNEJT5I4VKTHN', $request->input('one_time_password')); | ||
logger()->info('2FA Valid: ' . $valid); | ||
if ($valid) { | ||
// Authentication passed... | ||
logger()->info('2FA Passed: ' . $request->input('one_time_password')); | ||
return redirect()->route('home'); | ||
} | ||
else { | ||
//throw error | ||
logger()->info('2FA Failed: ' . $request->input('one_time_password')); | ||
return redirect()->back()->withErrors([ | ||
'one_time_password' => 'The one time password is invalid.' | ||
]); | ||
} | ||
} | ||
|
||
// Create a new secret and display the QR code | ||
public function Attempt2FA() | ||
{ | ||
$google2fa = app(Google2FA::class); | ||
$this->secret = $google2fa->generateSecretKey(); | ||
|
||
logger()->info('2FA Secret: ' . $this->secret); | ||
$g2faUrl = $google2fa->getQRCodeUrl( | ||
'pragmarx', | ||
'[email protected]', | ||
'6TZBNEJT5I4VKTHN' | ||
); | ||
|
||
$writer = new Writer( | ||
new ImageRenderer( | ||
new RendererStyle(400), | ||
new ImagickImageBackEnd() | ||
) | ||
); | ||
|
||
$qrcode_image = base64_encode($writer->writeString($g2faUrl)); | ||
|
||
return view('auth.2fa-secret')->with([ | ||
'qrcode_image' => $qrcode_image, | ||
'secret' => '6TZBNEJT5I4VKTHN' | ||
]); | ||
} | ||
} |
130 changes: 130 additions & 0 deletions
130
app/Http/Controllers/Auth/LoginCheckpointController.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
<?php | ||
|
||
namespace Pterodactyl\Http\Controllers\Auth; | ||
|
||
use App\Models\User; | ||
use Carbon\CarbonImmutable; | ||
use Carbon\CarbonInterface; | ||
use Illuminate\Foundation\Auth\AuthenticatesUsers; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Http\JsonResponse; | ||
use PragmaRX\Google2FA\Google2FA; | ||
use Illuminate\Contracts\Encryption\Encrypter; | ||
use Illuminate\Database\Eloquent\ModelNotFoundException; | ||
use Illuminate\Contracts\Validation\Factory as ValidationFactory; | ||
|
||
class LoginCheckpointController extends LoginController | ||
{ | ||
use AuthenticatesUsers; | ||
|
||
private const TOKEN_EXPIRED_MESSAGE = 'The authentication token provided has expired, please refresh the page and try again.'; | ||
|
||
/** | ||
* LoginCheckpointController constructor. | ||
*/ | ||
public function __construct( | ||
private Encrypter $encrypter, | ||
private Google2FA $google2FA, | ||
private ValidationFactory $validation | ||
) { | ||
parent::__construct(); | ||
} | ||
|
||
/** | ||
* Handle a login where the user is required to provide a TOTP authentication | ||
* token. Once a user has reached this stage it is assumed that they have already | ||
* provided a valid username and password. | ||
* | ||
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException | ||
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException | ||
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException | ||
* @throws \Exception | ||
* @throws \Illuminate\Validation\ValidationException | ||
*/ | ||
public function __invoke(Request $request): JsonResponse | ||
{ | ||
if ($this->hasTooManyLoginAttempts($request)) { | ||
$this->sendLockoutResponse($request); | ||
} | ||
|
||
$details = $request->session()->get('auth_confirmation_token'); | ||
if (!$this->hasValidSessionData($details)) { | ||
$this->sendFailedLoginResponse($request); // token expired | ||
} | ||
|
||
if (!hash_equals($request->input('confirmation_token') ?? '', $details['token_value'])) { | ||
$this->sendFailedLoginResponse($request); // token invalid | ||
} | ||
|
||
try { | ||
/** @var User $user */ | ||
$user = User::query()->findOrFail($details['user_id']); | ||
} catch (ModelNotFoundException) { | ||
$this->sendFailedLoginResponse($request); // user not found | ||
} | ||
|
||
// Recovery tokens go through a slightly different pathway for usage. | ||
if (!is_null($recoveryToken = $request->input('recovery_token'))) { | ||
if ($this->isValidRecoveryToken($user, $recoveryToken)) { | ||
// If the recovery token is valid, send the login response | ||
return $this->sendLoginResponse($request); | ||
} | ||
} else { | ||
$decrypted = $this->encrypter->decrypt($user->totp_secret); | ||
|
||
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) { | ||
|
||
return $this->sendLoginResponse($request); | ||
} | ||
} | ||
|
||
$this->sendFailedLoginResponse($request); // recovery token invalid | ||
} | ||
|
||
/** | ||
* Determines if a given recovery token is valid for the user account. If we find a matching token | ||
* it will be deleted from the database. | ||
* | ||
* @throws \Exception | ||
*/ | ||
protected function isValidRecoveryToken(User $user, string $value): bool | ||
{ | ||
foreach ($user->recoveryTokens as $token) { | ||
if (password_verify($value, $token->token)) { | ||
$token->delete(); | ||
|
||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* Determines if the data provided from the session is valid or not. This | ||
* will return false if the data is invalid, or if more time has passed than | ||
* was configured when the session was written. | ||
*/ | ||
protected function hasValidSessionData(array $data): bool | ||
{ | ||
$validator = $this->validation->make($data, [ | ||
'user_id' => 'required|integer|min:1', | ||
'token_value' => 'required|string', | ||
'expires_at' => 'required', | ||
]); | ||
|
||
if ($validator->fails()) { | ||
return false; | ||
} | ||
|
||
if (!$data['expires_at'] instanceof CarbonInterface) { | ||
return false; | ||
} | ||
|
||
if ($data['expires_at']->isBefore(CarbonImmutable::now())) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
namespace App\Models; | ||
|
||
use Carbon\CarbonImmutable; | ||
use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||
|
||
/** | ||
* @property int $id | ||
* @property int $user_id | ||
* @property string $token | ||
* @property CarbonImmutable $created_at | ||
* @property User $user | ||
*/ | ||
class RecoveryToken extends Model | ||
{ | ||
/** | ||
* There are no updates to this model, only inserts and deletes. | ||
*/ | ||
public const UPDATED_AT = null; | ||
|
||
public $timestamps = true; | ||
|
||
protected bool $immutableDates = true; | ||
|
||
public static array $validationRules = [ | ||
'token' => 'required|string', | ||
]; | ||
|
||
public function user(): BelongsTo | ||
{ | ||
return $this->belongsTo(User::class); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
use App\Classes\PterodactylClient; | ||
use App\Settings\PterodactylSettings; | ||
use Illuminate\Contracts\Auth\MustVerifyEmail; | ||
use Illuminate\Database\Eloquent\Casts\Attribute; | ||
use Illuminate\Database\Eloquent\Factories\HasFactory; | ||
use Illuminate\Database\Eloquent\Relations\BelongsToMany; | ||
use Illuminate\Database\Eloquent\Relations\HasMany; | ||
|
@@ -67,6 +68,9 @@ class User extends Authenticatable implements MustVerifyEmail | |
'suspended', | ||
'referral_code', | ||
'email_verified_reward', | ||
'use_totp', | ||
'totp_secret', | ||
'totp_authenticated_at', | ||
]; | ||
|
||
/** | ||
|
@@ -77,6 +81,8 @@ class User extends Authenticatable implements MustVerifyEmail | |
protected $hidden = [ | ||
'password', | ||
'remember_token', | ||
'totp_secret', | ||
'totp_authenticated_at' | ||
]; | ||
|
||
/** | ||
|
@@ -89,7 +95,9 @@ class User extends Authenticatable implements MustVerifyEmail | |
'last_seen' => 'datetime', | ||
'credits' => 'float', | ||
'server_limit' => 'float', | ||
'email_verified_reward' => 'boolean' | ||
'email_verified_reward' => 'boolean', | ||
'use_totp' => 'boolean', | ||
'totp_secret' => 'nullable|string' | ||
]; | ||
|
||
public function __construct() | ||
|
@@ -312,4 +320,24 @@ public function getActivitylogOptions(): LogOptions | |
->logOnlyDirty() | ||
->dontSubmitEmptyLogs(); | ||
} | ||
|
||
public function recoveryTokens(): HasMany | ||
{ | ||
return $this->hasMany(RecoveryToken::class); | ||
} | ||
|
||
/** | ||
* Interact with the 2fa secret attribute. | ||
* | ||
* @param string $value | ||
* @return \Illuminate\Database\Eloquent\Casts\Attribute | ||
*/ | ||
protected function google2faSecret(): Attribute | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. U can use |
||
{ | ||
return new Attribute( | ||
get: fn ($value) => decrypt($value), | ||
set: fn ($value) => encrypt($value), | ||
); | ||
} | ||
|
||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
attempt2Fa()
Use one function naming style bcs lower is for example
isValidRecoveryKey