From c7c2c1a45eea92274cfaf4f4a441e2255ba030d1 Mon Sep 17 00:00:00 2001
From: Dane Everitt <dane@daneeveritt.com>
Date: Sat, 18 Nov 2017 13:35:33 -0500
Subject: [PATCH] Implement changes to 2FA system (#761)

---
 CHANGELOG.md                                  |   1 +
 app/Http/Controllers/Auth/LoginController.php |   2 +-
 .../Controllers/Base/SecurityController.php   |  13 +-
 app/Models/User.php                           |   6 +
 app/Services/Users/ToggleTwoFactorService.php |  56 ++++--
 app/Services/Users/TwoFactorSetupService.php  |  45 +++--
 composer.json                                 |   4 +-
 composer.lock                                 | 189 ++++++++++--------
 config/app.php                                |   2 -
 config/pterodactyl.php                        |   5 +
 ...1922_Add2FaLastAuthorizationTimeColumn.php |  60 ++++++
 .../pterodactyl/js/frontend/2fa-modal.js      |   1 -
 .../pterodactyl/base/security.blade.php       |   4 +-
 .../Base/SecurityControllerTest.php           | 107 ++++------
 tests/Unit/Jobs/Schedule/RunTaskJobTest.php   |   2 +-
 .../DaemonKeyProviderServiceTest.php          |   2 +-
 .../Users/ToggleTwoFactorServiceTest.php      |  97 +++++----
 .../Users/TwoFactorSetupServiceTest.php       |  62 +++---
 18 files changed, 360 insertions(+), 298 deletions(-)
 create mode 100644 database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 804d01dd..6b450865 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
 
 ### Changed
 * Moved Docker image setting to be on the startup management page for a server rather than the details page. This value changes based on the Nest and Egg that are selected.
+* Two-Factor authentication tokens are now 32 bytes in length, and are stored encrypted at rest in the database.
 
 ## v0.7.0-beta.1 (Derelict Dermodactylus)
 ### Added
diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php
index 12f3df53..9fab7b53 100644
--- a/app/Http/Controllers/Auth/LoginController.php
+++ b/app/Http/Controllers/Auth/LoginController.php
@@ -202,7 +202,7 @@ class LoginController extends Controller
             return $this->sendFailedLoginResponse($request);
         }
 
-        if (! $G2FA->verifyKey($user->totp_secret, $request->input('2fa_token'), 2)) {
+        if (! $G2FA->verifyKey(Crypt::decrypt($user->totp_secret), $request->input('2fa_token'), 2)) {
             event(new \Illuminate\Auth\Events\Failed($user, $credentials));
 
             return $this->sendFailedLoginResponse($request);
diff --git a/app/Http/Controllers/Base/SecurityController.php b/app/Http/Controllers/Base/SecurityController.php
index d22c0ddb..62f07738 100644
--- a/app/Http/Controllers/Base/SecurityController.php
+++ b/app/Http/Controllers/Base/SecurityController.php
@@ -27,7 +27,6 @@ namespace Pterodactyl\Http\Controllers\Base;
 
 use Illuminate\Http\Request;
 use Prologue\Alerts\AlertsMessageBag;
-use Illuminate\Contracts\Session\Session;
 use Pterodactyl\Http\Controllers\Controller;
 use Pterodactyl\Services\Users\TwoFactorSetupService;
 use Pterodactyl\Services\Users\ToggleTwoFactorService;
@@ -52,11 +51,6 @@ class SecurityController extends Controller
      */
     protected $repository;
 
-    /**
-     * @var \Illuminate\Contracts\Session\Session
-     */
-    protected $session;
-
     /**
      * @var \Pterodactyl\Services\Users\ToggleTwoFactorService
      */
@@ -72,7 +66,6 @@ class SecurityController extends Controller
      *
      * @param \Prologue\Alerts\AlertsMessageBag                            $alert
      * @param \Illuminate\Contracts\Config\Repository                      $config
-     * @param \Illuminate\Contracts\Session\Session                        $session
      * @param \Pterodactyl\Contracts\Repository\SessionRepositoryInterface $repository
      * @param \Pterodactyl\Services\Users\ToggleTwoFactorService           $toggleTwoFactorService
      * @param \Pterodactyl\Services\Users\TwoFactorSetupService            $twoFactorSetupService
@@ -80,7 +73,6 @@ class SecurityController extends Controller
     public function __construct(
         AlertsMessageBag $alert,
         ConfigRepository $config,
-        Session $session,
         SessionRepositoryInterface $repository,
         ToggleTwoFactorService $toggleTwoFactorService,
         TwoFactorSetupService $twoFactorSetupService
@@ -88,7 +80,6 @@ class SecurityController extends Controller
         $this->alert = $alert;
         $this->config = $config;
         $this->repository = $repository;
-        $this->session = $session;
         $this->toggleTwoFactorService = $toggleTwoFactorService;
         $this->twoFactorSetupService = $twoFactorSetupService;
     }
@@ -122,7 +113,9 @@ class SecurityController extends Controller
      */
     public function generateTotp(Request $request)
     {
-        return response()->json($this->twoFactorSetupService->handle($request->user()));
+        return response()->json([
+            'qrImage' => $this->twoFactorSetupService->handle($request->user()),
+        ]);
     }
 
     /**
diff --git a/app/Models/User.php b/app/Models/User.php
index 7b09165a..39e4a0a0 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -63,6 +63,7 @@ class User extends Model implements
         'language',
         'use_totp',
         'totp_secret',
+        'totp_authenticated_at',
         'gravatar',
         'root_admin',
     ];
@@ -78,6 +79,11 @@ class User extends Model implements
         'gravatar' => 'boolean',
     ];
 
+    /**
+     * @var array
+     */
+    protected $dates = [self::CREATED_AT, self::UPDATED_AT, 'totp_authenticated_at'];
+
     /**
      * The attributes excluded from the model's JSON form.
      *
diff --git a/app/Services/Users/ToggleTwoFactorService.php b/app/Services/Users/ToggleTwoFactorService.php
index 56ec6953..e03a7638 100644
--- a/app/Services/Users/ToggleTwoFactorService.php
+++ b/app/Services/Users/ToggleTwoFactorService.php
@@ -1,66 +1,82 @@
 <?php
-/**
- * Pterodactyl - Panel
- * Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
- *
- * This software is licensed under the terms of the MIT license.
- * https://opensource.org/licenses/MIT
- */
 
 namespace Pterodactyl\Services\Users;
 
+use Carbon\Carbon;
 use Pterodactyl\Models\User;
-use PragmaRX\Google2FA\Contracts\Google2FA;
+use PragmaRX\Google2FA\Google2FA;
+use Illuminate\Contracts\Config\Repository;
+use Illuminate\Contracts\Encryption\Encrypter;
 use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
 use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
 
 class ToggleTwoFactorService
 {
     /**
-     * @var \PragmaRX\Google2FA\Contracts\Google2FA
+     * @var \Illuminate\Contracts\Config\Repository
      */
-    protected $google2FA;
+    private $config;
+
+    /**
+     * @var \Illuminate\Contracts\Encryption\Encrypter
+     */
+    private $encrypter;
+
+    /**
+     * @var \PragmaRX\Google2FA\Google2FA
+     */
+    private $google2FA;
 
     /**
      * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
      */
-    protected $repository;
+    private $repository;
 
     /**
      * ToggleTwoFactorService constructor.
      *
-     * @param \PragmaRX\Google2FA\Contracts\Google2FA                   $google2FA
+     * @param \Illuminate\Contracts\Encryption\Encrypter                $encrypter
+     * @param \PragmaRX\Google2FA\Google2FA                             $google2FA
+     * @param \Illuminate\Contracts\Config\Repository                   $config
      * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
      */
     public function __construct(
+        Encrypter $encrypter,
         Google2FA $google2FA,
+        Repository $config,
         UserRepositoryInterface $repository
     ) {
+        $this->config = $config;
+        $this->encrypter = $encrypter;
         $this->google2FA = $google2FA;
         $this->repository = $repository;
     }
 
     /**
-     * @param int|\Pterodactyl\Models\User $user
-     * @param string                       $token
-     * @param null|bool                    $toggleState
+     * Toggle 2FA on an account only if the token provided is valid.
+     *
+     * @param \Pterodactyl\Models\User $user
+     * @param string                   $token
+     * @param bool|null                $toggleState
      * @return bool
      *
      * @throws \Pterodactyl\Exceptions\Model\DataValidationException
      * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
      * @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
      */
-    public function handle($user, $token, $toggleState = null)
+    public function handle(User $user, string $token, bool $toggleState = null): bool
     {
-        if (! $user instanceof User) {
-            $user = $this->repository->find($user);
-        }
+        $window = $this->config->get('pterodactyl.auth.2fa.window');
+        $secret = $this->encrypter->decrypt($user->totp_secret);
 
-        if (! $this->google2FA->verifyKey($user->totp_secret, $token, 2)) {
+        $isValidToken = $this->google2FA->verifyKey($secret, $token, $window);
+
+        if (! $isValidToken) {
             throw new TwoFactorAuthenticationTokenInvalid;
         }
 
         $this->repository->withoutFresh()->update($user->id, [
+            'totp_authenticated_at' => Carbon::now(),
             'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState),
         ]);
 
diff --git a/app/Services/Users/TwoFactorSetupService.php b/app/Services/Users/TwoFactorSetupService.php
index 608a3643..a8554ccf 100644
--- a/app/Services/Users/TwoFactorSetupService.php
+++ b/app/Services/Users/TwoFactorSetupService.php
@@ -10,7 +10,8 @@
 namespace Pterodactyl\Services\Users;
 
 use Pterodactyl\Models\User;
-use PragmaRX\Google2FA\Contracts\Google2FA;
+use PragmaRX\Google2FA\Google2FA;
+use Illuminate\Contracts\Encryption\Encrypter;
 use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
 use Illuminate\Contracts\Config\Repository as ConfigRepository;
 
@@ -19,58 +20,62 @@ class TwoFactorSetupService
     /**
      * @var \Illuminate\Contracts\Config\Repository
      */
-    protected $config;
+    private $config;
 
     /**
-     * @var \PragmaRX\Google2FA\Contracts\Google2FA
+     * @var \Illuminate\Contracts\Encryption\Encrypter
      */
-    protected $google2FA;
+    private $encrypter;
+
+    /**
+     * @var \PragmaRX\Google2FA\Google2FA
+     */
+    private $google2FA;
 
     /**
      * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
      */
-    protected $repository;
+    private $repository;
 
     /**
      * TwoFactorSetupService constructor.
      *
      * @param \Illuminate\Contracts\Config\Repository                   $config
-     * @param \PragmaRX\Google2FA\Contracts\Google2FA                   $google2FA
+     * @param \Illuminate\Contracts\Encryption\Encrypter                $encrypter
+     * @param \PragmaRX\Google2FA\Google2FA                             $google2FA
      * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
      */
     public function __construct(
         ConfigRepository $config,
+        Encrypter $encrypter,
         Google2FA $google2FA,
         UserRepositoryInterface $repository
     ) {
         $this->config = $config;
+        $this->encrypter = $encrypter;
         $this->google2FA = $google2FA;
         $this->repository = $repository;
     }
 
     /**
-     * Generate a 2FA token and store it in the database.
+     * Generate a 2FA token and store it in the database before returning the
+     * QR code image.
      *
-     * @param int|\Pterodactyl\Models\User $user
-     * @return array
+     * @param \Pterodactyl\Models\User $user
+     * @return string
      *
      * @throws \Pterodactyl\Exceptions\Model\DataValidationException
      * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
      */
-    public function handle($user)
+    public function handle(User $user): string
     {
-        if (! $user instanceof User) {
-            $user = $this->repository->find($user);
-        }
-
-        $secret = $this->google2FA->generateSecretKey();
+        $secret = $this->google2FA->generateSecretKey($this->config->get('pterodactyl.auth.2fa.bytes'));
         $image = $this->google2FA->getQRCodeGoogleUrl($this->config->get('app.name'), $user->email, $secret);
 
-        $this->repository->withoutFresh()->update($user->id, ['totp_secret' => $secret]);
+        $this->repository->withoutFresh()->update($user->id, [
+            'totp_secret' => $this->encrypter->encrypt($secret),
+        ]);
 
-        return [
-            'qrImage' => $image,
-            'secret' => $secret,
-        ];
+        return $image;
     }
 }
diff --git a/composer.json b/composer.json
index 58903551..fabe01f0 100644
--- a/composer.json
+++ b/composer.json
@@ -31,7 +31,7 @@
         "mtdowling/cron-expression": "^1.2",
         "nesbot/carbon": "^1.22",
         "nicolaslopezj/searchable": "^1.9",
-        "pragmarx/google2fa": "^1.0",
+        "pragmarx/google2fa": "^2.0",
         "predis/predis": "^1.1",
         "prologue/alerts": "^0.4",
         "ramsey/uuid": "^3.7",
@@ -46,7 +46,7 @@
     "require-dev": {
         "barryvdh/laravel-debugbar": "^2.4",
         "barryvdh/laravel-ide-helper": "^2.4",
-        "friendsofphp/php-cs-fixer": "^2.4",
+        "friendsofphp/php-cs-fixer": "^2.8.0",
         "fzaninotto/faker": "^1.6",
         "mockery/mockery": "^0.9",
         "php-mock/php-mock-phpunit": "^1.1",
diff --git a/composer.lock b/composer.lock
index 2979fad3..9895b833 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "3758867d4fb2d20e4b4e45b7c410f79b",
+    "content-hash": "a393763d136e25a93fd5b636229496cf",
     "packages": [
         {
             "name": "appstract/laravel-blade-directives",
@@ -61,16 +61,16 @@
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.36.37",
+            "version": "3.38.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "a6d7fd9f32c63d018a6603a36174b4cb971fccd9"
+                "reference": "9f704274f4748d2039a16d45b3388ed8dde74e89"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a6d7fd9f32c63d018a6603a36174b4cb971fccd9",
-                "reference": "a6d7fd9f32c63d018a6603a36174b4cb971fccd9",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9f704274f4748d2039a16d45b3388ed8dde74e89",
+                "reference": "9f704274f4748d2039a16d45b3388ed8dde74e89",
                 "shasum": ""
             },
             "require": {
@@ -137,61 +137,7 @@
                 "s3",
                 "sdk"
             ],
-            "time": "2017-11-03T16:39:35+00:00"
-        },
-        {
-            "name": "christian-riesen/base32",
-            "version": "1.3.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/ChristianRiesen/base32.git",
-                "reference": "0a31e50c0fa9b1692d077c86ac188eecdcbaf7fa"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/0a31e50c0fa9b1692d077c86ac188eecdcbaf7fa",
-                "reference": "0a31e50c0fa9b1692d077c86ac188eecdcbaf7fa",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "4.*",
-                "satooshi/php-coveralls": "0.*"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Base32\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Christian Riesen",
-                    "email": "chris.riesen@gmail.com",
-                    "homepage": "http://christianriesen.com",
-                    "role": "Developer"
-                }
-            ],
-            "description": "Base32 encoder/decoder according to RFC 4648",
-            "homepage": "https://github.com/ChristianRiesen/base32",
-            "keywords": [
-                "base32",
-                "decode",
-                "encode",
-                "rfc4648"
-            ],
-            "time": "2016-05-05T11:49:03+00:00"
+            "time": "2017-11-09T19:15:59+00:00"
         },
         {
             "name": "daneeveritt/login-notifications",
@@ -2055,6 +2001,68 @@
             ],
             "time": "2017-11-04T11:48:34+00:00"
         },
+        {
+            "name": "paragonie/constant_time_encoding",
+            "version": "v2.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paragonie/constant_time_encoding.git",
+                "reference": "9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4",
+                "reference": "9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6",
+                "vimeo/psalm": "^0.3|^1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "ParagonIE\\ConstantTime\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Paragon Initiative Enterprises",
+                    "email": "security@paragonie.com",
+                    "homepage": "https://paragonie.com",
+                    "role": "Maintainer"
+                },
+                {
+                    "name": "Steve 'Sc00bz' Thomas",
+                    "email": "steve@tobtu.com",
+                    "homepage": "https://www.tobtu.com",
+                    "role": "Original Developer"
+                }
+            ],
+            "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+            "keywords": [
+                "base16",
+                "base32",
+                "base32_decode",
+                "base32_encode",
+                "base64",
+                "base64_decode",
+                "base64_encode",
+                "bin2hex",
+                "encoding",
+                "hex",
+                "hex2bin",
+                "rfc4648"
+            ],
+            "time": "2017-09-22T14:55:37+00:00"
+        },
         {
             "name": "paragonie/random_compat",
             "version": "v2.0.11",
@@ -2105,26 +2113,28 @@
         },
         {
             "name": "pragmarx/google2fa",
-            "version": "v1.0.1",
+            "version": "v2.0.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/antonioribeiro/google2fa.git",
-                "reference": "b346dc138339b745c5831405d00cff7c1351aa0d"
+                "reference": "bc2d654305e4d09254125f8cd390a7fbc4742d46"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/b346dc138339b745c5831405d00cff7c1351aa0d",
-                "reference": "b346dc138339b745c5831405d00cff7c1351aa0d",
+                "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/bc2d654305e4d09254125f8cd390a7fbc4742d46",
+                "reference": "bc2d654305e4d09254125f8cd390a7fbc4742d46",
                 "shasum": ""
             },
             "require": {
-                "christian-riesen/base32": "~1.3",
+                "paragonie/constant_time_encoding": "~1.0|~2.0",
                 "paragonie/random_compat": "~1.4|~2.0",
                 "php": ">=5.4",
                 "symfony/polyfill-php56": "~1.2"
             },
             "require-dev": {
-                "phpspec/phpspec": "~2.1"
+                "bacon/bacon-qr-code": "~1.0",
+                "phpspec/phpspec": "~2.1",
+                "phpunit/phpunit": "~4"
             },
             "suggest": {
                 "bacon/bacon-qr-code": "Required to generate inline QR Codes."
@@ -2132,11 +2142,8 @@
             "type": "library",
             "extra": {
                 "component": "package",
-                "frameworks": [
-                    "Laravel"
-                ],
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "2.0-dev"
                 }
             },
             "autoload": {
@@ -2157,12 +2164,13 @@
             ],
             "description": "A One Time Password Authentication package, compatible with Google Authenticator.",
             "keywords": [
+                "2fa",
                 "Authentication",
                 "Two Factor Authentication",
                 "google2fa",
                 "laravel"
             ],
-            "time": "2016-07-18T20:25:04+00:00"
+            "time": "2017-09-12T06:55:05+00:00"
         },
         {
             "name": "predis/predis",
@@ -3796,16 +3804,16 @@
         },
         {
             "name": "watson/validating",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/dwightwatson/validating.git",
-                "reference": "ade13078bf2e820e244603446114a28eda51b08c"
+                "reference": "22edd06d45893f5d4f79c9e901bd7fbce174a79f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/dwightwatson/validating/zipball/ade13078bf2e820e244603446114a28eda51b08c",
-                "reference": "ade13078bf2e820e244603446114a28eda51b08c",
+                "url": "https://api.github.com/repos/dwightwatson/validating/zipball/22edd06d45893f5d4f79c9e901bd7fbce174a79f",
+                "reference": "22edd06d45893f5d4f79c9e901bd7fbce174a79f",
                 "shasum": ""
             },
             "require": {
@@ -3842,7 +3850,7 @@
                 "laravel",
                 "validation"
             ],
-            "time": "2017-10-08T22:42:01+00:00"
+            "time": "2017-11-06T21:35:49+00:00"
         },
         {
             "name": "webmozart/assert",
@@ -4291,16 +4299,16 @@
         },
         {
             "name": "friendsofphp/php-cs-fixer",
-            "version": "v2.8.0",
+            "version": "v2.8.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
-                "reference": "89e7b083f27241e03dd776cb8d6781c77e341db6"
+                "reference": "04f71e56e03ba2627e345e8c949c80dcef0e683e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/89e7b083f27241e03dd776cb8d6781c77e341db6",
-                "reference": "89e7b083f27241e03dd776cb8d6781c77e341db6",
+                "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/04f71e56e03ba2627e345e8c949c80dcef0e683e",
+                "reference": "04f71e56e03ba2627e345e8c949c80dcef0e683e",
                 "shasum": ""
             },
             "require": {
@@ -4367,7 +4375,7 @@
                 }
             ],
             "description": "A tool to automatically fix PHP code style",
-            "time": "2017-11-03T02:21:46+00:00"
+            "time": "2017-11-09T13:31:39+00:00"
         },
         {
             "name": "fzaninotto/faker",
@@ -4421,23 +4429,23 @@
         },
         {
             "name": "gecko-packages/gecko-php-unit",
-            "version": "v2.2",
+            "version": "v3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/GeckoPackages/GeckoPHPUnit.git",
-                "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1"
+                "reference": "6a866551dffc2154c1b091bae3a7877d39c25ca3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/ab525fac9a9ffea219687f261b02008b18ebf2d1",
-                "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1",
+                "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/6a866551dffc2154c1b091bae3a7877d39c25ca3",
+                "reference": "6a866551dffc2154c1b091bae3a7877d39c25ca3",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.6 || ^7.0"
+                "php": "^7.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.4.3"
+                "phpunit/phpunit": "^6.0"
             },
             "suggest": {
                 "ext-dom": "When testing with xml.",
@@ -4445,6 +4453,11 @@
                 "phpunit/phpunit": "This is an extension for it so make sure you have it some way."
             },
             "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
             "autoload": {
                 "psr-4": {
                     "GeckoPackages\\PHPUnit\\": "src/PHPUnit"
@@ -4461,7 +4474,7 @@
                 "filesystem",
                 "phpunit"
             ],
-            "time": "2017-08-23T07:39:54+00:00"
+            "time": "2017-08-23T07:46:41+00:00"
         },
         {
             "name": "hamcrest/hamcrest-php",
diff --git a/config/app.php b/config/app.php
index c193e42e..2f9da670 100644
--- a/config/app.php
+++ b/config/app.php
@@ -171,7 +171,6 @@ return [
         /*
          * Additional Dependencies
          */
-        PragmaRX\Google2FA\Vendor\Laravel\ServiceProvider::class,
         igaster\laravelTheme\themeServiceProvider::class,
         Prologue\Alerts\AlertsServiceProvider::class,
         Krucas\Settings\Providers\SettingsServiceProvider::class,
@@ -213,7 +212,6 @@ return [
         'File' => Illuminate\Support\Facades\File::class,
         'Fractal' => Spatie\Fractal\FractalFacade::class,
         'Gate' => Illuminate\Support\Facades\Gate::class,
-        'Google2FA' => PragmaRX\Google2FA\Vendor\Laravel\Facade::class,
         'Hash' => Illuminate\Support\Facades\Hash::class,
         'Input' => Illuminate\Support\Facades\Input::class,
         'Inspiring' => Illuminate\Foundation\Inspiring::class,
diff --git a/config/pterodactyl.php b/config/pterodactyl.php
index bd157df2..ad371bce 100644
--- a/config/pterodactyl.php
+++ b/config/pterodactyl.php
@@ -23,6 +23,11 @@ return [
     */
     'auth' => [
         'notifications' => env('LOGIN_NOTIFICATIONS', false),
+        '2fa' => [
+            'bytes' => 32,
+            'window' => env('APP_2FA_WINDOW', 4),
+            'verify_newer' => true,
+        ],
     ],
 
     /*
diff --git a/database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php b/database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php
new file mode 100644
index 00000000..53cb6526
--- /dev/null
+++ b/database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php
@@ -0,0 +1,60 @@
+<?php
+
+use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Crypt;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class Add2FaLastAuthorizationTimeColumn extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->text('totp_secret')->nullable()->change();
+            $table->timestampTz('totp_authenticated_at')->after('totp_secret')->nullable();
+        });
+
+        DB::transaction(function () {
+            DB::table('users')->get()->each(function ($user) {
+                if (is_null($user->totp_secret)) {
+                    return;
+                }
+
+                DB::table('users')->where('id', $user->id)->update([
+                    'totp_secret' => Crypt::encrypt($user->totp_secret),
+                    'updated_at' => Carbon::now()->toIso8601String(),
+                ]);
+            });
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down()
+    {
+        DB::transaction(function () {
+            DB::table('users')->get()->each(function ($user) {
+                if (is_null($user->totp_secret)) {
+                    return;
+                }
+
+                DB::table('users')->where('id', $user->id)->update([
+                    'totp_secret' => Crypt::decrypt($user->totp_secret),
+                    'updated_at' => Carbon::now()->toIso8601String(),
+                ]);
+            });
+        });
+
+        DB::statement('ALTER TABLE users MODIFY totp_secret CHAR(16) DEFAULT NULL');
+
+        Schema::table('users', function (Blueprint $table) {
+            $table->dropColumn('totp_authenticated_at');
+        });
+    }
+}
diff --git a/public/themes/pterodactyl/js/frontend/2fa-modal.js b/public/themes/pterodactyl/js/frontend/2fa-modal.js
index 022ece2f..d542b377 100644
--- a/public/themes/pterodactyl/js/frontend/2fa-modal.js
+++ b/public/themes/pterodactyl/js/frontend/2fa-modal.js
@@ -42,7 +42,6 @@ var TwoFactorModal = (function () {
                             $('#qr_image_insert').attr('src', image.src).slideDown();
                         });
                     });
-                    $('#2fa_secret_insert').html(data.secret);
                     $('#open2fa').modal('show');
                 }).fail(function (jqXHR) {
                     alert('An error occured while attempting to load the 2FA setup modal. Please try again.');
diff --git a/resources/themes/pterodactyl/base/security.blade.php b/resources/themes/pterodactyl/base/security.blade.php
index a3a6cc51..7c4693dd 100644
--- a/resources/themes/pterodactyl/base/security.blade.php
+++ b/resources/themes/pterodactyl/base/security.blade.php
@@ -106,8 +106,8 @@
                         <div class="col-md-12" id="notice_box_2fa" style="display:none;"></div>
                     </div>
                     <div class="row">
-                        <div class="col-md-6">
-                            <center><span id="hide_img_load"><i class="fa fa-spinner fa-spin"></i> Loading QR Code...</span><img src="" id="qr_image_insert" style="display:none;"/><br /><code id="2fa_secret_insert"></code></center>
+                        <div class="col-md-6 text-center">
+                            <span id="hide_img_load"><i class="fa fa-spinner fa-spin"></i> Loading QR Code...</span><img src="" id="qr_image_insert" style="display:none;"/>
                         </div>
                         <div class="col-md-6">
                             <div class="alert alert-info">@lang('base.security.2fa_checkpoint_help')</div>
diff --git a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php
index 727f2ab5..3c821729 100644
--- a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php
+++ b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php
@@ -1,69 +1,41 @@
 <?php
-/**
- * Pterodactyl - Panel
- * Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
- *
- * This software is licensed under the terms of the MIT license.
- * https://opensource.org/licenses/MIT
- */
 
 namespace Tests\Unit\Http\Controllers\Base;
 
 use Mockery as m;
-use Tests\TestCase;
-use Illuminate\Http\Request;
-use Pterodactyl\Models\User;
 use Prologue\Alerts\AlertsMessageBag;
-use Illuminate\Contracts\Session\Session;
 use Illuminate\Contracts\Config\Repository;
-use Tests\Assertions\ControllerAssertionsTrait;
+use Tests\Unit\Http\Controllers\ControllerTestCase;
 use Pterodactyl\Services\Users\TwoFactorSetupService;
 use Pterodactyl\Services\Users\ToggleTwoFactorService;
 use Pterodactyl\Http\Controllers\Base\SecurityController;
 use Pterodactyl\Contracts\Repository\SessionRepositoryInterface;
 use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
 
-class SecurityControllerTest extends TestCase
+class SecurityControllerTest extends ControllerTestCase
 {
-    use ControllerAssertionsTrait;
-
     /**
-     * @var \Prologue\Alerts\AlertsMessageBag
+     * @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock
      */
     protected $alert;
 
     /**
-     * @var \Illuminate\Contracts\Config\Repository
+     * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
      */
     protected $config;
 
     /**
-     * @var \Pterodactyl\Http\Controllers\Base\SecurityController
-     */
-    protected $controller;
-
-    /**
-     * @var \Pterodactyl\Contracts\Repository\SessionRepositoryInterface
+     * @var \Pterodactyl\Contracts\Repository\SessionRepositoryInterface|\Mockery\Mock
      */
     protected $repository;
 
     /**
-     * @var \Illuminate\Http\Request
-     */
-    protected $request;
-
-    /**
-     * @var \Illuminate\Contracts\Session\Session
-     */
-    protected $session;
-
-    /**
-     * @var \Pterodactyl\Services\Users\ToggleTwoFactorService
+     * @var \Pterodactyl\Services\Users\ToggleTwoFactorService|\Mockery\Mock
      */
     protected $toggleTwoFactorService;
 
     /**
-     * @var \Pterodactyl\Services\Users\TwoFactorSetupService
+     * @var \Pterodactyl\Services\Users\TwoFactorSetupService|\Mockery\Mock
      */
     protected $twoFactorSetupService;
 
@@ -77,19 +49,8 @@ class SecurityControllerTest extends TestCase
         $this->alert = m::mock(AlertsMessageBag::class);
         $this->config = m::mock(Repository::class);
         $this->repository = m::mock(SessionRepositoryInterface::class);
-        $this->request = m::mock(Request::class);
-        $this->session = m::mock(Session::class);
         $this->toggleTwoFactorService = m::mock(ToggleTwoFactorService::class);
         $this->twoFactorSetupService = m::mock(TwoFactorSetupService::class);
-
-        $this->controller = new SecurityController(
-            $this->alert,
-            $this->config,
-            $this->session,
-            $this->repository,
-            $this->toggleTwoFactorService,
-            $this->twoFactorSetupService
-        );
     }
 
     /**
@@ -97,13 +58,12 @@ class SecurityControllerTest extends TestCase
      */
     public function testIndexControllerWithDatabaseDriver()
     {
-        $model = factory(User::class)->make();
+        $model = $this->setRequestUser();
 
         $this->config->shouldReceive('get')->with('session.driver')->once()->andReturn('database');
-        $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model);
         $this->repository->shouldReceive('getUserSessions')->with($model->id)->once()->andReturn(['sessions']);
 
-        $response = $this->controller->index($this->request);
+        $response = $this->getController()->index($this->request);
         $this->assertIsViewResponse($response);
         $this->assertViewNameEquals('base.security', $response);
         $this->assertViewHasKey('sessions', $response);
@@ -117,7 +77,7 @@ class SecurityControllerTest extends TestCase
     {
         $this->config->shouldReceive('get')->with('session.driver')->once()->andReturn('redis');
 
-        $response = $this->controller->index($this->request);
+        $response = $this->getController()->index($this->request);
         $this->assertIsViewResponse($response);
         $this->assertViewNameEquals('base.security', $response);
         $this->assertViewHasKey('sessions', $response);
@@ -129,14 +89,13 @@ class SecurityControllerTest extends TestCase
      */
     public function testGenerateTotpController()
     {
-        $model = factory(User::class)->make();
+        $model = $this->setRequestUser();
 
-        $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model);
-        $this->twoFactorSetupService->shouldReceive('handle')->with($model)->once()->andReturn(['string']);
+        $this->twoFactorSetupService->shouldReceive('handle')->with($model)->once()->andReturn('qrCodeImage');
 
-        $response = $this->controller->generateTotp($this->request);
+        $response = $this->getController()->generateTotp($this->request);
         $this->assertIsJsonResponse($response);
-        $this->assertResponseJsonEquals(['string'], $response);
+        $this->assertResponseJsonEquals(['qrImage' => 'qrCodeImage'], $response);
     }
 
     /**
@@ -144,13 +103,12 @@ class SecurityControllerTest extends TestCase
      */
     public function testDisableTotpControllerSuccess()
     {
-        $model = factory(User::class)->make();
+        $model = $this->setRequestUser();
 
-        $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model);
         $this->request->shouldReceive('input')->with('token')->once()->andReturn('testToken');
         $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once()->andReturnNull();
 
-        $response = $this->controller->disableTotp($this->request);
+        $response = $this->getController()->disableTotp($this->request);
         $this->assertIsRedirectResponse($response);
         $this->assertRedirectRouteEquals('account.security', $response);
     }
@@ -160,16 +118,14 @@ class SecurityControllerTest extends TestCase
      */
     public function testDisableTotpControllerWhenExceptionIsThrown()
     {
-        $model = factory(User::class)->make();
+        $model = $this->setRequestUser();
 
-        $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model);
         $this->request->shouldReceive('input')->with('token')->once()->andReturn('testToken');
-        $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once()
-            ->andThrow(new TwoFactorAuthenticationTokenInvalid);
-        $this->alert->shouldReceive('danger')->with(trans('base.security.2fa_disable_error'))->once()->andReturnSelf()
-            ->shouldReceive('flash')->withNoArgs()->once()->andReturnNull();
+        $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once()->andThrow(new TwoFactorAuthenticationTokenInvalid);
+        $this->alert->shouldReceive('danger')->with(trans('base.security.2fa_disable_error'))->once()->andReturnSelf();
+        $this->alert->shouldReceive('flash')->withNoArgs()->once()->andReturnNull();
 
-        $response = $this->controller->disableTotp($this->request);
+        $response = $this->getController()->disableTotp($this->request);
         $this->assertIsRedirectResponse($response);
         $this->assertRedirectRouteEquals('account.security', $response);
     }
@@ -179,13 +135,28 @@ class SecurityControllerTest extends TestCase
      */
     public function testRevokeController()
     {
-        $model = factory(User::class)->make();
+        $model = $this->setRequestUser();
 
-        $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model);
         $this->repository->shouldReceive('deleteUserSession')->with($model->id, 123)->once()->andReturnNull();
 
-        $response = $this->controller->revoke($this->request, 123);
+        $response = $this->getController()->revoke($this->request, 123);
         $this->assertIsRedirectResponse($response);
         $this->assertRedirectRouteEquals('account.security', $response);
     }
+
+    /**
+     * Return an instance of the controller for testing with mocked dependencies.
+     *
+     * @return \Pterodactyl\Http\Controllers\Base\SecurityController
+     */
+    private function getController(): SecurityController
+    {
+        return new SecurityController(
+            $this->alert,
+            $this->config,
+            $this->repository,
+            $this->toggleTwoFactorService,
+            $this->twoFactorSetupService
+        );
+    }
 }
diff --git a/tests/Unit/Jobs/Schedule/RunTaskJobTest.php b/tests/Unit/Jobs/Schedule/RunTaskJobTest.php
index 176eb4d8..c72ab33b 100644
--- a/tests/Unit/Jobs/Schedule/RunTaskJobTest.php
+++ b/tests/Unit/Jobs/Schedule/RunTaskJobTest.php
@@ -64,7 +64,7 @@ class RunTaskJobTest extends TestCase
     {
         parent::setUp();
         Bus::fake();
-        Carbon::setTestNow();
+        Carbon::setTestNow(Carbon::now());
 
         $this->commandRepository = m::mock(CommandRepositoryInterface::class);
         $this->config = m::mock(Repository::class);
diff --git a/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php b/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php
index 7c240b08..87d5f506 100644
--- a/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php
+++ b/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php
@@ -44,7 +44,7 @@ class DaemonKeyProviderServiceTest extends TestCase
     public function setUp()
     {
         parent::setUp();
-        Carbon::setTestNow();
+        Carbon::setTestNow(Carbon::now());
 
         $this->keyCreationService = m::mock(DaemonKeyCreationService::class);
         $this->keyUpdateService = m::mock(DaemonKeyUpdateService::class);
diff --git a/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php b/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php
index ae45ec8f..c8d1cc85 100644
--- a/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php
+++ b/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php
@@ -1,37 +1,42 @@
 <?php
-/**
- * Pterodactyl - Panel
- * Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
- *
- * This software is licensed under the terms of the MIT license.
- * https://opensource.org/licenses/MIT
- */
 
 namespace Tests\Unit\Services\Users;
 
 use Mockery as m;
+use Carbon\Carbon;
 use Tests\TestCase;
 use Pterodactyl\Models\User;
-use PragmaRX\Google2FA\Contracts\Google2FA;
+use PragmaRX\Google2FA\Google2FA;
+use Illuminate\Contracts\Config\Repository;
+use Illuminate\Contracts\Encryption\Encrypter;
 use Pterodactyl\Services\Users\ToggleTwoFactorService;
 use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
 
 class ToggleTwoFactorServiceTest extends TestCase
 {
-    /**
-     * @var \PragmaRX\Google2FA\Contracts\Google2FA
-     */
-    protected $google2FA;
+    const TEST_WINDOW_INT = 4;
+    const USER_TOTP_SECRET = 'encryptedValue';
+    const DECRYPTED_USER_SECRET = 'decryptedValue';
 
     /**
-     * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
+     * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
      */
-    protected $repository;
+    private $config;
 
     /**
-     * @var \Pterodactyl\Services\Users\ToggleTwoFactorService
+     * @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
      */
-    protected $service;
+    private $encrypter;
+
+    /**
+     * @var \PragmaRX\Google2FA\Google2FA|\Mockery\Mock
+     */
+    private $google2FA;
+
+    /**
+     * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
+     */
+    private $repository;
 
     /**
      * Setup tests.
@@ -39,11 +44,15 @@ class ToggleTwoFactorServiceTest extends TestCase
     public function setUp()
     {
         parent::setUp();
+        Carbon::setTestNow(Carbon::now());
 
+        $this->config = m::mock(Repository::class);
+        $this->encrypter = m::mock(Encrypter::class);
         $this->google2FA = m::mock(Google2FA::class);
         $this->repository = m::mock(UserRepositoryInterface::class);
 
-        $this->service = new ToggleTwoFactorService($this->google2FA, $this->repository);
+        $this->config->shouldReceive('get')->with('pterodactyl.auth.2fa.window')->once()->andReturn(self::TEST_WINDOW_INT);
+        $this->encrypter->shouldReceive('decrypt')->with(self::USER_TOTP_SECRET)->once()->andReturn(self::DECRYPTED_USER_SECRET);
     }
 
     /**
@@ -51,13 +60,15 @@ class ToggleTwoFactorServiceTest extends TestCase
      */
     public function testTwoFactorIsEnabledForUser()
     {
-        $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]);
+        $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => false]);
 
-        $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true);
-        $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf()
-            ->shouldReceive('update')->with($model->id, ['use_totp' => true])->once()->andReturnNull();
+        $this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true);
+        $this->repository->shouldReceive('withoutFresh->update')->with($model->id, [
+            'totp_authenticated_at' => Carbon::now(),
+            'use_totp' => true,
+        ])->once()->andReturnNull();
 
-        $this->assertTrue($this->service->handle($model, 'test-token'));
+        $this->assertTrue($this->getService()->handle($model, 'test-token'));
     }
 
     /**
@@ -65,13 +76,15 @@ class ToggleTwoFactorServiceTest extends TestCase
      */
     public function testTwoFactorIsDisabled()
     {
-        $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => true]);
+        $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => true]);
 
-        $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true);
-        $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf()
-            ->shouldReceive('update')->with($model->id, ['use_totp' => false])->once()->andReturnNull();
+        $this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true);
+        $this->repository->shouldReceive('withoutFresh->update')->with($model->id, [
+            'totp_authenticated_at' => Carbon::now(),
+            'use_totp' => false,
+        ])->once()->andReturnNull();
 
-        $this->assertTrue($this->service->handle($model, 'test-token'));
+        $this->assertTrue($this->getService()->handle($model, 'test-token'));
     }
 
     /**
@@ -79,13 +92,15 @@ class ToggleTwoFactorServiceTest extends TestCase
      */
     public function testTwoFactorRemainsDisabledForUser()
     {
-        $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]);
+        $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => false]);
 
-        $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true);
-        $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf()
-            ->shouldReceive('update')->with($model->id, ['use_totp' => false])->once()->andReturnNull();
+        $this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true);
+        $this->repository->shouldReceive('withoutFresh->update')->with($model->id, [
+            'totp_authenticated_at' => Carbon::now(),
+            'use_totp' => false,
+        ])->once()->andReturnNull();
 
-        $this->assertTrue($this->service->handle($model, 'test-token', false));
+        $this->assertTrue($this->getService()->handle($model, 'test-token', false));
     }
 
     /**
@@ -95,23 +110,19 @@ class ToggleTwoFactorServiceTest extends TestCase
      */
     public function testExceptionIsThrownIfTokenIsInvalid()
     {
-        $model = factory(User::class)->make();
+        $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET]);
         $this->google2FA->shouldReceive('verifyKey')->once()->andReturn(false);
 
-        $this->service->handle($model, 'test-token');
+        $this->getService()->handle($model, 'test-token');
     }
 
     /**
-     * Test that an integer can be passed in place of a user model.
+     * Return an instance of the service with mocked dependencies.
+     *
+     * @return \Pterodactyl\Services\Users\ToggleTwoFactorService
      */
-    public function testIntegerCanBePassedInPlaceOfUserModel()
+    private function getService(): ToggleTwoFactorService
     {
-        $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]);
-
-        $this->repository->shouldReceive('find')->with($model->id)->once()->andReturn($model);
-        $this->google2FA->shouldReceive('verifyKey')->once()->andReturn(true);
-        $this->repository->shouldReceive('withoutFresh->update')->once()->andReturnNull();
-
-        $this->assertTrue($this->service->handle($model->id, 'test-token'));
+        return new ToggleTwoFactorService($this->encrypter, $this->google2FA, $this->config, $this->repository);
     }
 }
diff --git a/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php b/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php
index e58d99f2..d6f5f8b9 100644
--- a/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php
+++ b/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php
@@ -1,43 +1,37 @@
 <?php
-/**
- * Pterodactyl - Panel
- * Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
- *
- * This software is licensed under the terms of the MIT license.
- * https://opensource.org/licenses/MIT
- */
 
 namespace Tests\Unit\Services\Users;
 
 use Mockery as m;
 use Tests\TestCase;
 use Pterodactyl\Models\User;
+use PragmaRX\Google2FA\Google2FA;
 use Illuminate\Contracts\Config\Repository;
-use PragmaRX\Google2FA\Contracts\Google2FA;
+use Illuminate\Contracts\Encryption\Encrypter;
 use Pterodactyl\Services\Users\TwoFactorSetupService;
 use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
 
 class TwoFactorSetupServiceTest extends TestCase
 {
     /**
-     * @var \Illuminate\Contracts\Config\Repository
+     * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
      */
-    protected $config;
+    private $config;
 
     /**
-     * @var \PragmaRX\Google2FA\Contracts\Google2FA
+     * @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
      */
-    protected $google2FA;
+    private $encrypter;
 
     /**
-     * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
+     * @var \PragmaRX\Google2FA\Google2FA|\Mockery\Mock
      */
-    protected $repository;
+    private $google2FA;
 
     /**
-     * @var \Pterodactyl\Services\Users\TwoFactorSetupService
+     * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
      */
-    protected $service;
+    private $repository;
 
     /**
      * Setup tests.
@@ -47,10 +41,9 @@ class TwoFactorSetupServiceTest extends TestCase
         parent::setUp();
 
         $this->config = m::mock(Repository::class);
+        $this->encrypter = m::mock(Encrypter::class);
         $this->google2FA = m::mock(Google2FA::class);
         $this->repository = m::mock(UserRepositoryInterface::class);
-
-        $this->service = new TwoFactorSetupService($this->config, $this->google2FA, $this->repository);
     }
 
     /**
@@ -60,34 +53,25 @@ class TwoFactorSetupServiceTest extends TestCase
     {
         $model = factory(User::class)->make();
 
-        $this->google2FA->shouldReceive('generateSecretKey')->withNoArgs()->once()->andReturn('secretKey');
+        $this->config->shouldReceive('get')->with('pterodactyl.auth.2fa.bytes')->once()->andReturn(32);
+        $this->google2FA->shouldReceive('generateSecretKey')->with(32)->once()->andReturn('secretKey');
         $this->config->shouldReceive('get')->with('app.name')->once()->andReturn('CompanyName');
-        $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->with('CompanyName', $model->email, 'secretKey')
-            ->once()->andReturn('http://url.com');
-        $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf()
-            ->shouldReceive('update')->with($model->id, ['totp_secret' => 'secretKey'])->once()->andReturnNull();
+        $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->with('CompanyName', $model->email, 'secretKey')->once()->andReturn('http://url.com');
+        $this->encrypter->shouldReceive('encrypt')->with('secretKey')->once()->andReturn('encryptedSecret');
+        $this->repository->shouldReceive('withoutFresh->update')->with($model->id, ['totp_secret' => 'encryptedSecret'])->once()->andReturnNull();
 
-        $response = $this->service->handle($model);
+        $response = $this->getService()->handle($model);
         $this->assertNotEmpty($response);
-        $this->assertArrayHasKey('qrImage', $response);
-        $this->assertArrayHasKey('secret', $response);
-        $this->assertEquals('http://url.com', $response['qrImage']);
-        $this->assertEquals('secretKey', $response['secret']);
+        $this->assertSame('http://url.com', $response);
     }
 
     /**
-     * Test that an integer can be passed in place of the user model.
+     * Return an instance of the service to test with mocked dependencies.
+     *
+     * @return \Pterodactyl\Services\Users\TwoFactorSetupService
      */
-    public function testIntegerCanBePassedInPlaceOfUserModel()
+    private function getService(): TwoFactorSetupService
     {
-        $model = factory(User::class)->make();
-
-        $this->repository->shouldReceive('find')->with($model->id)->once()->andReturn($model);
-        $this->google2FA->shouldReceive('generateSecretKey')->withNoArgs()->once()->andReturnNull();
-        $this->config->shouldReceive('get')->with('app.name')->once()->andReturnNull();
-        $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->once()->andReturnNull();
-        $this->repository->shouldReceive('withoutFresh->update')->once()->andReturnNull();
-
-        $this->assertTrue(is_array($this->service->handle($model->id)));
+        return new TwoFactorSetupService($this->config, $this->encrypter, $this->google2FA, $this->repository);
     }
 }