LoginCheckpointController.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. <?php
  2. namespace Pterodactyl\Http\Controllers\Auth;
  3. use App\Models\User;
  4. use Carbon\CarbonImmutable;
  5. use Carbon\CarbonInterface;
  6. use Illuminate\Foundation\Auth\AuthenticatesUsers;
  7. use Illuminate\Http\Request;
  8. use Illuminate\Http\JsonResponse;
  9. use PragmaRX\Google2FA\Google2FA;
  10. use Illuminate\Contracts\Encryption\Encrypter;
  11. use Illuminate\Database\Eloquent\ModelNotFoundException;
  12. use Illuminate\Contracts\Validation\Factory as ValidationFactory;
  13. class LoginCheckpointController extends LoginController
  14. {
  15. use AuthenticatesUsers;
  16. private const TOKEN_EXPIRED_MESSAGE = 'The authentication token provided has expired, please refresh the page and try again.';
  17. /**
  18. * LoginCheckpointController constructor.
  19. */
  20. public function __construct(
  21. private Encrypter $encrypter,
  22. private Google2FA $google2FA,
  23. private ValidationFactory $validation
  24. ) {
  25. parent::__construct();
  26. }
  27. /**
  28. * Handle a login where the user is required to provide a TOTP authentication
  29. * token. Once a user has reached this stage it is assumed that they have already
  30. * provided a valid username and password.
  31. *
  32. * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
  33. * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
  34. * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
  35. * @throws \Exception
  36. * @throws \Illuminate\Validation\ValidationException
  37. */
  38. public function __invoke(Request $request): JsonResponse
  39. {
  40. if ($this->hasTooManyLoginAttempts($request)) {
  41. $this->sendLockoutResponse($request);
  42. }
  43. $details = $request->session()->get('auth_confirmation_token');
  44. if (!$this->hasValidSessionData($details)) {
  45. $this->sendFailedLoginResponse($request); // token expired
  46. }
  47. if (!hash_equals($request->input('confirmation_token') ?? '', $details['token_value'])) {
  48. $this->sendFailedLoginResponse($request); // token invalid
  49. }
  50. try {
  51. /** @var User $user */
  52. $user = User::query()->findOrFail($details['user_id']);
  53. } catch (ModelNotFoundException) {
  54. $this->sendFailedLoginResponse($request); // user not found
  55. }
  56. // Recovery tokens go through a slightly different pathway for usage.
  57. if (!is_null($recoveryToken = $request->input('recovery_token'))) {
  58. if ($this->isValidRecoveryToken($user, $recoveryToken)) {
  59. // If the recovery token is valid, send the login response
  60. return $this->sendLoginResponse($request);
  61. }
  62. } else {
  63. $decrypted = $this->encrypter->decrypt($user->totp_secret);
  64. if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) {
  65. return $this->sendLoginResponse($request);
  66. }
  67. }
  68. $this->sendFailedLoginResponse($request); // recovery token invalid
  69. }
  70. /**
  71. * Determines if a given recovery token is valid for the user account. If we find a matching token
  72. * it will be deleted from the database.
  73. *
  74. * @throws \Exception
  75. */
  76. protected function isValidRecoveryToken(User $user, string $value): bool
  77. {
  78. foreach ($user->recoveryTokens as $token) {
  79. if (password_verify($value, $token->token)) {
  80. $token->delete();
  81. return true;
  82. }
  83. }
  84. return false;
  85. }
  86. /**
  87. * Determines if the data provided from the session is valid or not. This
  88. * will return false if the data is invalid, or if more time has passed than
  89. * was configured when the session was written.
  90. */
  91. protected function hasValidSessionData(array $data): bool
  92. {
  93. $validator = $this->validation->make($data, [
  94. 'user_id' => 'required|integer|min:1',
  95. 'token_value' => 'required|string',
  96. 'expires_at' => 'required',
  97. ]);
  98. if ($validator->fails()) {
  99. return false;
  100. }
  101. if (!$data['expires_at'] instanceof CarbonInterface) {
  102. return false;
  103. }
  104. if ($data['expires_at']->isBefore(CarbonImmutable::now())) {
  105. return false;
  106. }
  107. return true;
  108. }
  109. }