├── .gitignore ├── src ├── Facades │ └── Authy.php ├── Contracts │ └── Auth │ │ └── TwoFactor │ │ ├── SMSToken.php │ │ ├── PhoneToken.php │ │ ├── Authenticatable.php │ │ └── Provider.php ├── AuthyFacadeAccessor.php ├── Providers │ └── AuthyServiceProvider.php ├── Auth │ └── TwoFactor │ │ └── Authenticatable.php └── Services │ └── Authy.php ├── config └── config.php ├── composer.json ├── migrations └── migration.php ├── views └── form.blade.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /bootstrap/compiled.php 2 | .env.*.php 3 | .env.php 4 | .env 5 | /vendor 6 | composer.lock 7 | /.idea 8 | -------------------------------------------------------------------------------- /src/Facades/Authy.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | return [ 10 | 'mode' => env('AUTHY_MODE', 'live'), // Can be either 'live' or 'sandbox'. If empty or invalid 'live' will be used 11 | 'sandbox' => [ 12 | 'key' => env('AUTHY_TEST_KEY', ''), 13 | ], 14 | 'live' => [ 15 | 'key' => env('AUTHY_LIVE_KEY', ''), 16 | ], 17 | 'sms' => env('AUTHY_SEND_SMS', false), 18 | ]; 19 | -------------------------------------------------------------------------------- /src/Contracts/Auth/TwoFactor/SMSToken.php: -------------------------------------------------------------------------------- 1 | string('phone_country_code')->nullable(); 18 | $table->string('phone_number')->nullable(); 19 | $table->text('two_factor_options')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table('users', function (Blueprint $table) { 31 | $table->dropColumn([ 32 | 'phone_country_code', 33 | 'phone_number', 34 | 'two_factor_options', 35 | ]); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AuthyFacadeAccessor.php: -------------------------------------------------------------------------------- 1 | 3 | 4 | @endsection 5 | 6 |
7 |
8 | {{csrf_field()}} 9 |

Enable Two-Factor Authentication

10 |
11 |
Country:
12 |
13 | 14 |
15 |
16 |
17 |
Cellphone:
18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 | 29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 | @section('scripts') 41 | 42 | @endsection 43 | -------------------------------------------------------------------------------- /src/Providers/AuthyServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 27 | __DIR__.'/../../config/config.php' => config_path('authy.php'), 28 | ]); 29 | 30 | if (!class_exists('UpdateUsersTable')) { 31 | $this->publishes([ 32 | __DIR__.'/../../migrations/migration.php' => database_path('/migrations/'. 33 | str_replace(':', '', str_replace('-', '_', Carbon::now()->format('Y-m-d_H:i:s'))).'_update_users_table.php'), 34 | ]); 35 | } 36 | 37 | // Load Authy View Files 38 | $this->loadViewsFrom(__DIR__.'/../../views', 'authy'); 39 | $this->publishes([ 40 | __DIR__.'/../../views' => base_path('resources/views/vendor/authy'), 41 | ]); 42 | } 43 | 44 | /** 45 | * Register the service provider. 46 | * 47 | * @return void 48 | */ 49 | public function register() 50 | { 51 | $this->registerAuthy(); 52 | 53 | $this->mergeConfig(); 54 | } 55 | 56 | /** 57 | * Register the Authy class with application. 58 | * 59 | * @return void 60 | */ 61 | private function registerAuthy() 62 | { 63 | $this->app->singleton('authy', function () { 64 | return new Authy(); 65 | }); 66 | } 67 | 68 | /** 69 | * Merges user's and paypal's config files. 70 | * 71 | * @return void 72 | */ 73 | private function mergeConfig() 74 | { 75 | $this->mergeConfigFrom( 76 | __DIR__.'/../../config/config.php', 77 | 'authy' 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Auth/TwoFactor/Authenticatable.php: -------------------------------------------------------------------------------- 1 | email; 15 | } 16 | 17 | /** 18 | * Get the country code used for two-factor authentication. 19 | * 20 | * @return string 21 | */ 22 | public function getAuthCountryCode() 23 | { 24 | return $this->phone_country_code; 25 | } 26 | 27 | /** 28 | * Get the phone number used for two-factor authentication. 29 | * 30 | * @return string 31 | */ 32 | public function getAuthPhoneNumber() 33 | { 34 | return $this->phone_number; 35 | } 36 | 37 | /** 38 | * Set the country code and phone number used for two-factor authentication. 39 | * 40 | * @param string $countryCode 41 | * @param string $phoneNumber 42 | * 43 | * @return void 44 | */ 45 | public function setAuthPhoneInformation($countryCode, $phoneNumber) 46 | { 47 | $this->phone_country_code = $countryCode; 48 | 49 | $this->phone_number = $phoneNumber; 50 | } 51 | 52 | /** 53 | * Get the two-factor provider options in array format. 54 | * 55 | * @return array 56 | */ 57 | public function getTwoFactorAuthProviderOptions() 58 | { 59 | return json_decode($this->two_factor_options, true) ?: []; 60 | } 61 | 62 | /** 63 | * Set the two-factor provider options in array format. 64 | * 65 | * @param array $options 66 | * 67 | * @return void 68 | */ 69 | public function setTwoFactorAuthProviderOptions(array $options) 70 | { 71 | $this->two_factor_options = json_encode($options); 72 | } 73 | 74 | /** 75 | * Determine if the user is using two-factor authentication. 76 | * 77 | * @return bool 78 | */ 79 | public function getUsingTwoFactorAuthAttribute() 80 | { 81 | $options = $this->getTwoFactorAuthProviderOptions(); 82 | 83 | return isset($options['id']); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Services/Authy.php: -------------------------------------------------------------------------------- 1 | config['api_key'] = config('authy.sandbox.key'); 29 | $this->config['api_url'] = 'http://sandbox-api.authy.com'; 30 | } else { 31 | $this->config['api_key'] = config('authy.live.key'); 32 | $this->config['api_url'] = 'https://api.authy.com'; 33 | } 34 | } 35 | 36 | /** 37 | * Determine if the given user has two-factor authentication enabled. 38 | * 39 | * @param \Srmklive\Authy\Contracts\Auth\TwoFactor\Authenticatable $user 40 | * 41 | * @return bool 42 | */ 43 | public function isEnabled(TwoFactorAuthenticatable $user) 44 | { 45 | return isset($user->getTwoFactorAuthProviderOptions()['id']); 46 | } 47 | 48 | /** 49 | * Register the given user with the provider. 50 | * 51 | * @param \Srmklive\Authy\Contracts\Auth\TwoFactor\Authenticatable $user 52 | * @param bool $sms 53 | * 54 | * @throws \Exception 55 | * 56 | * @return void 57 | */ 58 | public function register(TwoFactorAuthenticatable $user, $sms = false) 59 | { 60 | try { 61 | $request = (new HttpClient())->post($this->config['api_url'].'/protected/json/users/new?api_key='.$this->config['api_key'], [ 62 | 'form_params' => [ 63 | 'user' => [ 64 | 'email' => $user->getEmailForTwoFactorAuth(), 65 | 'cellphone' => preg_replace('/[^0-9]/', '', $user->getAuthPhoneNumber()), 66 | 'country_code' => $user->getAuthCountryCode(), 67 | ], 68 | ], 69 | ]); 70 | 71 | $response = $request->getBody()->getContents(); 72 | $response = \GuzzleHttp\json_decode($response, true); 73 | 74 | $user->setTwoFactorAuthProviderOptions([ 75 | 'id' => $response['user']['id'], 76 | 'sms' => $sms, 77 | ]); 78 | } catch (ClientException $e) { 79 | $errors = $e->getResponse()->getBody()->getContents(); 80 | $errors = \GuzzleHttp\json_decode($errors, true); 81 | 82 | throw new \Exception($errors['message']); 83 | } 84 | } 85 | 86 | /** 87 | * Send the user two-factor authentication token via SMS. 88 | * 89 | * @param \Srmklive\Authy\Contracts\Auth\TwoFactor\Authenticatable $user 90 | * 91 | * @return void 92 | */ 93 | public function sendSmsToken(TwoFactorAuthenticatable $user) 94 | { 95 | try { 96 | $options = $user->getTwoFactorAuthProviderOptions(); 97 | 98 | $response = json_decode((new HttpClient())->get( 99 | $this->config['api_url'].'/protected/json/sms/'.$options['id']. 100 | '?force=true&api_key='.$this->config['api_key'] 101 | )->getBody(), true); 102 | 103 | return $response['success'] === true; 104 | } catch (Exception $e) { 105 | return false; 106 | } 107 | } 108 | 109 | /** 110 | * Start the user two-factor authentication via phone call. 111 | * 112 | * @param \Srmklive\Authy\Contracts\Auth\TwoFactor\Authenticatable $user 113 | * 114 | * @return void 115 | */ 116 | public function sendPhoneCallToken(TwoFactorAuthenticatable $user) 117 | { 118 | try { 119 | $options = $user->getTwoFactorAuthProviderOptions(); 120 | 121 | $response = json_decode((new HttpClient())->get( 122 | $this->config['api_url'].'/protected/json/call/'.$options['id']. 123 | '?force=true&api_key='.$this->config['api_key'] 124 | )->getBody(), true); 125 | 126 | return $response['success'] === true; 127 | } catch (Exception $e) { 128 | return false; 129 | } 130 | } 131 | 132 | /** 133 | * Determine if the given token is valid for the given user. 134 | * 135 | * @param \Srmklive\Authy\Contracts\Auth\TwoFactor\Authenticatable $user 136 | * @param string $token 137 | * 138 | * @return bool 139 | */ 140 | public function tokenIsValid(TwoFactorAuthenticatable $user, $token) 141 | { 142 | try { 143 | $options = $user->getTwoFactorAuthProviderOptions(); 144 | 145 | $response = json_decode((new HttpClient())->get( 146 | $this->config['api_url'].'/protected/json/verify/'. 147 | $token.'/'.$options['id'].'?force=true&api_key='. 148 | $this->config['api_key'] 149 | )->getBody(), true); 150 | 151 | return $response['token'] === 'is valid'; 152 | } catch (Exception $e) { 153 | return false; 154 | } 155 | } 156 | 157 | /** 158 | * Delete the given user from the provider. 159 | * 160 | * @param \Srmklive\Authy\Contracts\Auth\TwoFactor\Authenticatable $user 161 | * 162 | * @return bool 163 | */ 164 | public function delete(TwoFactorAuthenticatable $user) 165 | { 166 | $options = $user->getTwoFactorAuthProviderOptions(); 167 | 168 | (new HttpClient())->post( 169 | $this->config['api_url'].'/protected/json/users/delete/'. 170 | $options['id'].'?api_key='.$this->config['api_key'] 171 | ); 172 | 173 | $user->setTwoFactorAuthProviderOptions([]); 174 | } 175 | 176 | /** 177 | * Determine if the given user should be sent two-factor authentication token via SMS/phone call. 178 | * 179 | * @param \Srmklive\Authy\Contracts\Auth\TwoFactor\Authenticatable $user 180 | * 181 | * @return bool 182 | */ 183 | public function canSendToken(TwoFactorAuthenticatable $user) 184 | { 185 | $sendToken = collect( 186 | $user->getTwoFactorAuthProviderOptions() 187 | )->pluck(['sms', 'phone', 'email'])->filter(function ($value) { 188 | return !empty($value) ? $value : null; 189 | })->isEmpty(); 190 | 191 | return ($this->isEnabled($user) && !$sendToken) ? true : false; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Two-Factor Authentication 2 | 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/srmklive/authy.svg?style=flat-square)](https://packagist.org/packages/srmklive/authy) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/srmklive/authy.svg?style=flat-square)](https://packagist.org/packages/srmklive/authy) 6 | [![StyleCI](https://styleci.io/repos/473authy()98032/shield?style=flat)](https://styleci.io/repos/47398032) 7 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/srmklive/laravel-twofactor-authentication/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/srmklive/laravel-twofactor-authentication/?branch=master) 8 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/1f1e2abe-aefe-4490-a011-0ec8fac6860f/small.png)](https://insight.sensiolabs.com/projects/1f1e2abe-aefe-4490-a011-0ec8fac6860f) 9 | 10 | - [Introduction](#introduction) 11 | - [Installation](#installation) 12 | - [Modify Login Workflow](#modify-login-workflow) 13 | - [Usage](#usage) 14 | - [Add a new TwoFactor Authentication Provider](#implement-new-provider) 15 | - [Demo Application](#demo-application) 16 | 17 | 18 | ## Introduction 19 | 20 | This plugins allows you to enable two-factor authentication in your Laravel applications. 21 | 22 | **Only Laravel 5.1 or greater supported** 23 | 24 | 25 | 26 | ## Installation 27 | 28 | * Use following command to install: 29 | 30 | ```bash 31 | composer require srmklive/authy 32 | ``` 33 | 34 | * Add the service provider to your $providers array in config/app.php file like: 35 | 36 | ```php 37 | Srmklive\Authy\Providers\AuthyServiceProvider::class 38 | ``` 39 | 40 | * Add the alias to your $aliases array in config/app.php file like: 41 | 42 | ```php 43 | 'Authy' => Srmklive\Authy\Facades\Authy::class 44 | ``` 45 | 46 | * Run the following command to publish configuration: 47 | 48 | ```bash 49 | php artisan vendor:publish --provider "Srmklive\Authy\Providers\AuthyServiceProvider" 50 | ``` 51 | 52 | * Run the following command to migrate user table changes to database: 53 | 54 | ```bash 55 | php artisan migrate 56 | ``` 57 | 58 | * Add the following lines in your User model (e.g App\User.php) 59 | 60 | * Before the class declaration, add these lines: 61 | 62 | ```php 63 | use Srmklive\Authy\Auth\TwoFactor\Authenticatable as TwoFactorAuthenticatable; 64 | use Srmklive\Authy\Contracts\Auth\TwoFactor\Authenticatable as TwoFactorAuthenticatableContract; 65 | ``` 66 | 67 | * Now the change the class declaration. For example, if your class declaration is 68 | 69 | ```php 70 | class User extends Model implements AuthenticatableContract, 71 | AuthorizableContract, 72 | CanResetPasswordContract 73 | ``` 74 | 75 | then change it to this: 76 | 77 | ```php 78 | class User extends Model implements AuthenticatableContract, 79 | AuthorizableContract, 80 | CanResetPasswordContract, 81 | TwoFactorAuthenticatableContract 82 | ``` 83 | 84 | * Now change the import traits line accordingly in user model file. For example if the line is: 85 | 86 | ```php 87 | use Authenticatable, Authorizable, CanResetPassword; 88 | ``` 89 | 90 | to 91 | 92 | ```php 93 | use Authenticatable, Authorizable, CanResetPassword, TwoFactorAuthenticatable; 94 | ``` 95 | 96 | * Lastly, add/update $hidden variable to hide 'two_factor_options' field from any DB call for user detail: 97 | 98 | ```php 99 | protected $hidden = [ 100 | 'two_factor_options' 101 | ]; 102 | ``` 103 | 104 | 105 | 106 | ## Modifying Login Workflow 107 | 108 | * You need to add the following code to your `app\Http\Controllers\Auth\AuthController.php`. 109 | 110 | ```php 111 | 112 | /** 113 | * Send the post-authentication response. 114 | * 115 | * @param \Illuminate\Http\Request $request 116 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 117 | * @return \Illuminate\Http\Response 118 | */ 119 | protected function authenticated(Request $request, Authenticatable $user) 120 | { 121 | if (Authy::getProvider()->isEnabled($user)) { 122 | return $this->logoutAndRedirectToTokenScreen($request, $user); 123 | } 124 | 125 | return redirect()->intended($this->redirectPath()); 126 | } 127 | 128 | /** 129 | * Generate a redirect response to the two-factor token screen. 130 | * 131 | * @param \Illuminate\Http\Request $request 132 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 133 | * @return \Illuminate\Http\Response 134 | */ 135 | protected function logoutAndRedirectToTokenScreen(Request $request, Authenticatable $user) 136 | { 137 | // Uncomment this line for Laravel 5.2+ 138 | //auth($this->getGuard())->logout(); 139 | 140 | // Uncomment this line for Laravel 5.1 141 | // auth()->logout(); 142 | 143 | $request->session()->put('authy:auth:id', $user->id); 144 | 145 | return redirect(url('auth/token')); 146 | } 147 | 148 | /** 149 | * Show two-factor authentication page 150 | * 151 | * @return \Illuminate\Http\Response|\Illuminate\View\View 152 | */ 153 | public function getToken() 154 | { 155 | return session('authy:auth:id') ? view('auth.token') : redirect(url('login')); 156 | } 157 | 158 | /** 159 | * Verify the two-factor authentication token. 160 | * 161 | * @param \Illuminate\Http\Request $request 162 | * @return \Illuminate\Http\Response 163 | */ 164 | public function postToken(Request $request) 165 | { 166 | $this->validate($request, ['token' => 'required']); 167 | if (! session('authy:auth:id')) { 168 | return redirect(url('login')); 169 | } 170 | 171 | // Uncomment these lines for use in Laravel 5.2+ 172 | //$guard = config('auth.defaults.guard'); 173 | //$provider = config('auth.guards.' . $guard . '.provider'); 174 | //$model = config('auth.providers.' . $provider . '.model'); 175 | 176 | // Uncomment the line below for use in Laravel 5.1 177 | // $model = config('auth.model'); 178 | 179 | $user = (new $model)->findOrFail( 180 | $request->session()->pull('authy:auth:id') 181 | ); 182 | 183 | if (Authy::getProvider()->tokenIsValid($user, $request->token)) { 184 | // Uncomment this line for Laravel 5.2+ 185 | //auth($this->getGuard())->login($user); 186 | 187 | // Uncomment this line for Laravel 5.1 188 | //auth()->login($user); 189 | 190 | return redirect()->intended($this->redirectPath()); 191 | } else { 192 | return redirect(url('login'))->withErrors('Invalid two-factor authentication token provided!'); 193 | } 194 | } 195 | ``` 196 | 197 | * Add route to verify two-factor authentication token 198 | 199 | ```php 200 | Route::get('auth/token','Auth\AuthController@getToken'); 201 | Route::post('auth/token','Auth\AuthController@postToken'); 202 | ``` 203 | 204 | * Create view file in `resources/views/auth/token.blade.php`. Change this accordingly for your application. I have used code from [AdminLTE](https://github.com/almasaeed2010/AdminLTE) theme here. 205 | 206 | ```blade 207 | @extends('layouts.app') 208 | 209 | @section('content') 210 | 213 | 214 |
215 |

Validate your two-factor authentication token

216 |
217 | {!! csrf_field() !!} 218 | 219 | @if (count($errors) > 0) 220 |
221 |
    222 | @foreach ($errors->all() as $error) 223 |
  • {{ $error }}
  • 224 | @endforeach 225 |
226 |
227 | @endif 228 | 229 |
230 | 231 | 232 |
233 |
234 |
235 |
236 | 237 |
238 |
239 |
240 |
241 | @endsection 242 | 243 | ``` 244 | 245 | 246 | 247 | ## Usage 248 | 249 | * Registering User 250 | 251 | ```php 252 | $phone = '405-342-5699'; 253 | $code = 1; 254 | 255 | $user = User::find(1); 256 | 257 | $user->setAuthPhoneInformation( 258 | $code, $phone 259 | ); 260 | 261 | try { 262 | Authy::getProvider()->register($user); 263 | 264 | $user->save(); 265 | } catch (Exception $e) { 266 | app(ExceptionHandler::class)->report($e); 267 | 268 | return response()->json(['error' => ['Unable To Register User']], 422); 269 | } 270 | ``` 271 | 272 | * Send token via SMS 273 | 274 | ```php 275 | $user = User::find(1); 276 | 277 | try { 278 | Authy::getProvider()->sendSmsToken($user); 279 | } catch (Exception $e) { 280 | app(ExceptionHandler::class)->report($e); 281 | 282 | return response()->json(['error' => ['Unable To Send 2FA Login Token']], 422); 283 | } 284 | ``` 285 | 286 | * Send token via phone call 287 | 288 | ```php 289 | $user = User::find(1); 290 | 291 | try { 292 | Authy::getProvider()->sendPhoneCallToken($user); 293 | } catch (Exception $e) { 294 | app(ExceptionHandler::class)->report($e); 295 | 296 | return response()->json(['error' => ['Unable To Send 2FA Login Token']], 422); 297 | } 298 | ``` 299 | 300 | * Validating two-factor token 301 | 302 | ```php 303 | $user = User::find(1); 304 | 305 | try { 306 | Authy::getProvider()->tokenIsValid($user, $token); 307 | } catch (Exception $e) { 308 | app(ExceptionHandler::class)->report($e); 309 | 310 | return response()->json(['error' => ['Invalid 2FA Login Token Provided']], 422); 311 | } 312 | ``` 313 | 314 | * Deleting User 315 | 316 | ```php 317 | $user = User::find(1); 318 | 319 | try { 320 | Authy::getProvider()->delete($user); 321 | 322 | $user->save(); 323 | } catch (Exception $e) { 324 | app(ExceptionHandler::class)->report($e); 325 | 326 | return response()->json(['error' => ['Unable to Delete User']], 422); 327 | } 328 | ``` 329 | 330 | 331 | ## Add a new TwoFactor Authentication Provider 332 | 333 | Currently this package uses two-factor authentication services from [**Authy**](https://www.authy.com). You can also implement another two-factor authentication provider by doing the following: 334 | 335 | ```php 336 | 411 | ## Demo Application 412 | 413 | I have also implemented this package in a simple laravel application. You can view installation instructions [here](https://github.com/srmklive/laravel-2fa-demo). Through this application, you can do: 414 | 415 | * User login & registration. 416 | * Enable/Disable two-factor authentication for a user. 417 | --------------------------------------------------------------------------------