Explorar el Código

Added backup code for 2FA

Will Browning hace 5 años
padre
commit
7b48390431

+ 53 - 0
app/Http/Controllers/BackupCodeController.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Hash;
+use PragmaRX\Google2FALaravel\Support\Authenticator;
+
+class BackupCodeController extends Controller
+{
+    public function __construct()
+    {
+        $this->middleware('throttle:3,1')->only('login');
+    }
+
+    public function index(Request $request)
+    {
+        $authenticator = app(Authenticator::class)->boot($request);
+
+        if ($authenticator->isAuthenticated() || ! $request->user()->two_factor_enabled) {
+            return redirect('/');
+        }
+
+        return view('auth.backup_code');
+    }
+
+    public function login(Request $request)
+    {
+        $this->validate($request, [
+            'backup_code' => 'required',
+        ]);
+
+        if (! Hash::check($request->backup_code, user()->two_factor_backup_code)) {
+            return back()->withErrors([
+                'backup_code' => __('The backup code was invalid.')
+            ]);
+        }
+
+        $twoFactor = app('pragmarx.google2fa');
+
+        user()->update([
+            'two_factor_enabled' => false,
+            'two_factor_secret' => $twoFactor->generateSecretKey(),
+            'two_factor_backup_code' => null
+        ]);
+
+        if ($request->session()->has('intended_path')) {
+            return redirect($request->session()->pull('intended_path'));
+        }
+
+        return redirect()->intended($request->redirectPath);
+    }
+}

+ 1 - 1
app/Http/Controllers/PasswordController.php

@@ -10,7 +10,7 @@ class PasswordController extends Controller
     public function update(UpdatePasswordRequest $request)
     {
         if (!Hash::check($request->current, user()->password)) {
-            return back()->withErrors(['current' => 'Current password incorrect']);
+            return redirect(url()->previous().'#update-password')->withErrors(['current' => 'Current password incorrect']);
         }
 
         user()->password = Hash::make($request->password);

+ 11 - 4
app/Http/Controllers/TwoFactorAuthController.php

@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
 use App\Http\Requests\EnableTwoFactorAuthRequest;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Str;
 use PragmaRX\Google2FALaravel\Support\Authenticator;
 
 class TwoFactorAuthController extends Controller
@@ -21,14 +22,17 @@ class TwoFactorAuthController extends Controller
     public function store(EnableTwoFactorAuthRequest $request)
     {
         if (!$this->twoFactor->verifyKey(user()->two_factor_secret, $request->two_factor_token)) {
-            return back()->withErrors(['two_factor_token' => 'The token you entered was incorrect']);
+            return redirect(url()->previous().'#two-factor')->withErrors(['two_factor_token' => 'The token you entered was incorrect']);
         }
 
-        user()->update(['two_factor_enabled' => true]);
+        user()->update([
+            'two_factor_enabled' => true,
+            'two_factor_backup_code' => bcrypt($code = Str::random(40))
+        ]);
 
         $this->authenticator->login();
 
-        return back()->with(['status' => '2FA Enabled Successfully']);
+        return back()->with(['backupCode' => $code]);
     }
 
     public function update()
@@ -48,7 +52,10 @@ class TwoFactorAuthController extends Controller
             return back()->withErrors(['current_password_2fa' => 'Current password incorrect']);
         }
 
-        user()->update(['two_factor_enabled' => false]);
+        user()->update([
+            'two_factor_enabled' => false,
+            'two_factor_secret' => $this->twoFactor->generateSecretKey()
+        ]);
 
         $this->authenticator->logout();
 

+ 5 - 3
app/User.php

@@ -29,7 +29,8 @@ class User extends Authenticatable implements MustVerifyEmail
         'default_recipient_id',
         'password',
         'two_factor_enabled',
-        'two_factor_secret'
+        'two_factor_secret',
+        'two_factor_backup_code'
     ];
 
     protected $encrypted = [
@@ -46,7 +47,8 @@ class User extends Authenticatable implements MustVerifyEmail
     protected $hidden = [
         'password',
         'remember_token',
-        'two_factor_secret'
+        'two_factor_secret',
+        'two_factor_backup_code'
     ];
 
     /**
@@ -57,7 +59,7 @@ class User extends Authenticatable implements MustVerifyEmail
     protected $casts = [
         'id' => 'string',
         'default_recipient_id' => 'string',
-        'two_factor_enabled' => 'boolean',
+        'two_factor_enabled' => 'boolean'
     ];
 
     protected $dates = [

+ 32 - 0
database/migrations/2019_09_12_141803_add_two_factor_backup_code_column_to_users_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddTwoFactorBackupCodeColumnToUsersTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->string('two_factor_backup_code', 100)->after('two_factor_secret')->nullable();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->dropColumn('two_factor_backup_code');
+        });
+    }
+}

+ 0 - 1
resources/js/pages/Domains.vue

@@ -249,7 +249,6 @@
                 :disabled="recheckRecordsLoading"
               >
                 Recheck domain
-                <loader v-if="recheckRecordsLoading" />
               </button>
             </div>
           </td>

+ 54 - 0
resources/views/auth/backup_code.blade.php

@@ -0,0 +1,54 @@
+@extends('layouts.app')
+
+@section('content')
+    <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">
+        <div class="w-full max-w-md">
+            <div class="flex justify-center text-white mb-6 text-5xl font-bold">
+                <img class="w-48" alt="AnonAddy Logo" src="/svg/logo.svg">
+            </div>
+            <div class="flex flex-col break-words bg-white border border-2 rounded-lg shadow-lg overflow-hidden">
+                <form method="POST" action="{{ route('login.backup_code.login') }}">
+                    @csrf
+
+                    <div class="px-6 py-8 md:p-10">
+
+                        <h1 class="text-center font-bold text-3xl">
+                            Login Using 2FA Backup Code
+                        </h1>
+
+                        <div class="mx-auto mt-6 w-24 border-b-2 border-grey-200"></div>
+
+                        <div class="text-sm border-t-8 rounded text-yellow-800 border-yellow-600 bg-yellow-100 px-3 py-4 mt-4" role="alert">
+                            After logging in using your backup code, two factor authentication will be disabled on your account. If you would like to use 2FA, you should re-enable it after logging in.
+                        </div>
+
+                        <div class="mt-8 flex flex-wrap">
+                            <label for="backup_code" class="block text-grey-700 text-sm mb-2">
+                                Backup Code:
+                            </label>
+
+                            <input id="backup_code" type="text" class="appearance-none bg-grey-100 rounded w-full p-3 text-grey-700 focus:shadow-outline{{ $errors->has('backup_code') ? ' border border-red-500' : '' }}" name="backup_code" required autofocus>
+
+                            @if ($errors->has('backup_code'))
+                                <p class="text-red-500 text-xs italic mt-4">
+                                    {{ $errors->first('backup_code') }}
+                                </p>
+                            @endif
+                        </div>
+
+                    </div>
+
+                    <div class="px-6 md:px-10 py-4 bg-grey-50 border-t border-grey-100 flex flex-wrap items-center justify-center">
+                        <button type="submit" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none">
+                            {{ __('Authenticate') }}
+                        </button>
+                    </div>
+                </form>
+            </div>
+                <form action="{{ route('logout') }}" method="POST" class="w-full text-xs text-center mt-6">
+                    {{ csrf_field() }}
+                    <input type="submit" class="bg-transparent cursor-pointer text-white hover:text-indigo-50 no-underline" value="{{ __('Logout') }}">
+                </form>
+        </div>
+    </div>
+@endsection

+ 5 - 2
resources/views/auth/two_factor.blade.php

@@ -29,7 +29,7 @@
                                 {{ __('One Time Token') }}:
                             </label>
 
-                            <input id="one_time_password" type="text" class="appearance-none bg-grey-100 rounded w-full p-3 text-grey-700 focus:shadow-outline{{ $errors->has('one_time_password') ? ' border-red-500' : '' }}" name="one_time_password" placeholder="123456" required autofocus>
+                            <input id="one_time_password" type="text" class="appearance-none bg-grey-100 rounded w-full p-3 text-grey-700 focus:shadow-outline{{ $errors->has('message') ? ' border border-red-500' : '' }}" name="one_time_password" placeholder="123456" required autofocus>
 
                             @if ($errors->has('message'))
                                 <p class="text-red-500 text-xs italic mt-4">
@@ -47,10 +47,13 @@
                     </div>
                 </form>
             </div>
-                <form action="{{ route('logout') }}" method="POST" class="w-full text-xs text-center mt-6">
+            <div class="flex justify-between mt-6">
+                <form action="{{ route('logout') }}" method="POST" class="text-xs">
                     {{ csrf_field() }}
                     <input type="submit" class="bg-transparent cursor-pointer text-white hover:text-indigo-50 no-underline" value="{{ __('Logout') }}">
                 </form>
+                <a class="text-xs text-white hover:text-indigo-50" href="{{ route('login.backup_code.index') }}">Use backup code</a>
+            </div>
         </div>
     </div>
 @endsection

+ 83 - 68
resources/views/settings/show.blade.php

@@ -4,6 +4,20 @@
     <div class="container py-8">
         @include('shared.status')
 
+        @if(session('backupCode'))
+            <div class="text-sm border-t-8 rounded text-yellow-800 border-yellow-600 bg-yellow-100 px-3 py-4 mb-4" role="alert">
+                <div class="flex items-center mb-2">
+                    <span class="rounded-full bg-yellow-400 uppercase px-2 py-1 text-xs font-bold mr-2">Important</span>
+                    <div>
+                        2FA enabled successfully, please <b>make a copy of your backup code below</b>. If you lose your 2FA device you can use this backup code to disable 2FA on your account. <b>This is the only time this code will be displayed, so be sure not to lose it!</b>
+                    </div>
+                </div>
+                <pre class="flex p-3 text-grey-900 bg-white border rounded">
+                    <code class="break-all whitespace-normal">{{ session('backupCode') }}</code>
+                </pre>
+            </div>
+        @endif
+
         <div class="mb-4">
             <h2 class="text-3xl font-bold">
                 Usage
@@ -37,7 +51,7 @@
 
             @if($user->hasVerifiedDefaultRecipient())
 
-                <form class="mb-16" method="POST" action="{{ route('settings.default_recipient') }}">
+                <form method="POST" action="{{ route('settings.default_recipient') }}">
                     @csrf
 
                     <div class="mb-6">
@@ -83,7 +97,7 @@
 
             @else
 
-                <form class="mb-16" method="POST" action="{{ route('settings.edit_default_recipient') }}">
+                <form method="POST" action="{{ route('settings.edit_default_recipient') }}">
                     @csrf
 
                     <div class="mb-6">
@@ -132,7 +146,7 @@
 
             @endif
 
-            <form class="mb-16" method="POST" action="{{ route('settings.password') }}">
+            <form id="update-password" method="POST" action="{{ route('settings.password') }}" class="pt-16">
                 @csrf
 
                 <div class="mb-6">
@@ -188,97 +202,98 @@
             </form>
 
 
+            <div id="two-factor" class="pt-16">
+                @if($user->two_factor_enabled)
 
-            @if($user->two_factor_enabled)
+                    <form method="POST" action="{{ route('settings.2fa_disable') }}">
+                        @csrf
 
-                <form method="POST" action="{{ route('settings.2fa_disable') }}">
-                    @csrf
+                        <div class="mb-6">
 
-                    <div class="mb-6">
+                            <h3 class="font-bold text-xl">
+                                Disable 2 Factor Authentication
+                            </h3>
 
-                        <h3 class="font-bold text-xl">
-                            Disable 2 Factor Authentication
-                        </h3>
+                            <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
 
-                        <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
+                            <p class="mt-6">To disable 2 factor authentication enter your password below. You can always enable it again later if you wish.</p>
 
-                        <p class="mt-6">To disable 2 factor authentication enter your password below. You can always enable it again later if you wish.</p>
+                            <div class="mt-6 flex flex-wrap">
+                                <label for="current_password_2fa" class="block text-grey-700 text-sm mb-2">
+                                    {{ __('Current Password') }}:
+                                </label>
 
-                        <div class="mt-6 flex flex-wrap">
-                            <label for="current_password_2fa" class="block text-grey-700 text-sm mb-2">
-                                {{ __('Current Password') }}:
-                            </label>
+                                <input id="current_password_2fa" type="password" class="appearance-none bg-grey-100 rounded w-full p-3 text-grey-700 focus:shadow-outline{{ $errors->has('current_password_2fa') ? ' border-red-500' : '' }}" name="current_password_2fa" placeholder="********" required>
 
-                            <input id="current_password_2fa" type="password" class="appearance-none bg-grey-100 rounded w-full p-3 text-grey-700 focus:shadow-outline{{ $errors->has('current_password_2fa') ? ' border-red-500' : '' }}" name="current_password_2fa" placeholder="********" required>
+                                @if ($errors->has('current_password_2fa'))
+                                    <p class="text-red-500 text-xs italic mt-4">
+                                        {{ $errors->first('current_password_2fa') }}
+                                    </p>
+                                @endif
+                            </div>
 
-                            @if ($errors->has('current_password_2fa'))
-                                <p class="text-red-500 text-xs italic mt-4">
-                                    {{ $errors->first('current_password_2fa') }}
-                                </p>
-                            @endif
                         </div>
 
-                    </div>
+                        <button type="submit" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none">
+                            {{ __('Disable 2FA') }}
+                        </button>
 
-                    <button type="submit" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none">
-                        {{ __('Disable 2FA') }}
-                    </button>
+                    </form>
 
-                </form>
+                @else
 
-            @else
 
+                    <div class="mb-6">
 
-                <div class="mb-6">
+                        <h3 class="font-bold text-xl">
+                            Enable 2 Factor Authentication
+                        </h3>
 
-                    <h3 class="font-bold text-xl">
-                        Enable 2 Factor Authentication
-                    </h3>
+                        <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
 
-                    <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
+                        <p class="mt-6">2 factor authentication requires the use of Google Authenticator or another compatible app such as Aegis or andOTP (both on F-droid) for Android. Alternatively, you can use the code below. Make sure that you write down your secret code in a safe place.</p>
+
+                        <div>
+                            <img src="{{ $qrCode }}">
+                            <p class="mb-2">Secret: {{ $authSecret }}</p>
+                            <form method="POST" action="{{ route('settings.2fa_regenerate') }}">
+                                @csrf
+                                <input type="submit" class="text-indigo-900 bg-transparent cursor-pointer" value="Click here to regenerate your secret key">
+
+                                @if ($errors->has('regenerate_2fa'))
+                                    <p class="text-red-500 text-xs italic mt-4">
+                                        {{ $errors->first('regenerate_2fa') }}
+                                    </p>
+                                @endif
+                            </form>
+                        </div>
 
-                    <p class="mt-6">2 factor authentication requires the use of Google Authenticator or another compatible app such as Aegis or andOTP (both on F-droid) for Android. Alternatively, you can use the code below. Make sure that you write down your secret code in a safe place.</p>
+                    </div>
 
-                    <div>
-                        <img src="{{ $qrCode }}">
-                        <p class="mb-2">Secret: {{ $authSecret }}</p>
-                        <form method="POST" action="{{ route('settings.2fa_regenerate') }}">
-                            @csrf
-                            <input type="submit" class="text-indigo-900 bg-transparent cursor-pointer" value="Click here to regenerate your secret key">
+                    <form method="POST" action="{{ route('settings.2fa_enable') }}">
+                        @csrf
+                        <div class="my-6 flex flex-wrap">
+                            <label for="two_factor_token" class="block text-grey-700 text-sm mb-2">
+                                {{ __('Verify and Enable') }}:
+                            </label>
 
-                            @if ($errors->has('regenerate_2fa'))
+                            <div class="block relative w-full">
+                                <input id="two_factor_token" type="text" class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:shadow-outline" name="two_factor_token" placeholder="123456" />
+                            </div>
+
+                            @if ($errors->has('two_factor_token'))
                                 <p class="text-red-500 text-xs italic mt-4">
-                                    {{ $errors->first('regenerate_2fa') }}
+                                    {{ $errors->first('two_factor_token') }}
                                 </p>
                             @endif
-                        </form>
-                    </div>
-
-                </div>
-
-                <form method="POST" action="{{ route('settings.2fa_enable') }}">
-                    @csrf
-                    <div class="my-6 flex flex-wrap">
-                        <label for="two_factor_token" class="block text-grey-700 text-sm mb-2">
-                            {{ __('Verify and Enable') }}:
-                        </label>
-
-                        <div class="block relative w-full">
-                            <input id="two_factor_token" type="text" class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:shadow-outline" name="two_factor_token" placeholder="123456" />
                         </div>
+                        <button type="submit" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none">
+                            {{ __('Verify and Enable') }}
+                        </button>
+                    </form>
 
-                        @if ($errors->has('two_factor_token'))
-                            <p class="text-red-500 text-xs italic mt-4">
-                                {{ $errors->first('two_factor_token') }}
-                            </p>
-                        @endif
-                    </div>
-                    <button type="submit" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none">
-                        {{ __('Verify and Enable') }}
-                    </button>
-                </form>
-
-            @endif
+                @endif
+            </div>
 
         </div>
 

+ 4 - 0
routes/web.php

@@ -15,6 +15,10 @@ Auth::routes(['verify' => true, 'register' => config('anonaddy.enable_registrati
 
 Route::post('/login/2fa', 'TwoFactorAuthController@authenticateTwoFactor')->name('login.2fa')->middleware(['2fa', 'throttle', 'auth']);
 
+Route::get('/login/backup-code', 'BackupCodeController@index')->name('login.backup_code.index')->middleware('auth');
+Route::post('/login/backup-code', 'BackupCodeController@login')->name('login.backup_code.login')->middleware('auth');
+
+
 Route::middleware(['auth', 'verified', '2fa'])->group(function () {
     Route::get('/', 'AliasController@index')->name('aliases.index');
     Route::post('/aliases', 'AliasController@store')->name('aliases.store');

+ 36 - 0
tests/Feature/LoginTest.php

@@ -5,6 +5,7 @@ namespace Tests\Feature;
 use App\User;
 use Illuminate\Foundation\Testing\RefreshDatabase;
 use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Str;
 use Tests\TestCase;
 
 class LoginTest extends TestCase
@@ -36,4 +37,39 @@ class LoginTest extends TestCase
             ->assertRedirect('/')
             ->assertSessionHasNoErrors();
     }
+
+    /** @test */
+    public function user_can_login_successfully_using_backup_code()
+    {
+        $this->user->update([
+            'two_factor_enabled' => true,
+            'two_factor_secret' => 'secret',
+            'two_factor_backup_code' => bcrypt($code = Str::random(40))
+        ]);
+
+        $response = $this->post('/login', [
+            'username' => 'johndoe',
+            'password' => 'mypassword'
+        ]);
+
+        $response
+            ->assertRedirect('/')
+            ->assertSessionHasNoErrors();
+
+        $secondFactor = $this->get('/recipients');
+
+        $secondFactor->assertSee('2nd Factor Authentication');
+
+        $backupCodeView = $this->get('/login/backup-code');
+
+        $backupCodeView->assertSee('Login Using 2FA Backup Code');
+
+        $backupCodeLogin = $this->post('/login/backup-code', [
+            'backup_code' => $code
+        ]);
+
+        $backupCodeLogin
+            ->assertRedirect('/recipients')
+            ->assertSessionHasNoErrors();
+    }
 }