Skip to content
This repository has been archived by the owner on Feb 17, 2022. It is now read-only.

Commit

Permalink
Merge pull request #5 from DarkGhostHunter/master
Browse files Browse the repository at this point in the history
Now it works. Entirely.
  • Loading branch information
DarkGhostHunter committed Feb 11, 2020
2 parents c01df6d + f83c39d commit 97d868e
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 231 deletions.
18 changes: 9 additions & 9 deletions .github/workflows/php.yml
Expand Up @@ -15,12 +15,9 @@ jobs:
uses: shivammathur/setup-php@v1
with:
php-version: '7.4'
extension-csv: mbstring, intl
extensions: mbstring, intl
coverage: xdebug

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest

- name: Get Composer Cache Directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
Expand All @@ -29,16 +26,19 @@ jobs:
uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest

- name: Run test suite
run: composer run-script test

- name: Upload coverage results to Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_SERVICE_NAME: github
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_SERVICE_NAME: github
run: |
composer require cedx/coveralls
vendor/bin/coveralls build/logs/clover.xml
composer require cedx/coveralls
vendor/bin/coveralls build/logs/clover.xml
34 changes: 25 additions & 9 deletions README.md
Expand Up @@ -11,6 +11,11 @@ Two Factor Authentication via TOTP for all your Users out-of-the-box.

This package _silently_ enables authentication using 6 digits codes, without Internet or external providers.

## Requirements

* Laravel [6.15](https://blog.laravel.com/laravel-v6-15-0-released) or later.
* PHP 7.2+

## Table of Contents

* [Installation](#installation)
Expand All @@ -29,7 +34,7 @@ This package _silently_ enables authentication using 6 digits codes, without Int
+ [Cache Store](#cache-store)
+ [Recovery](#recovery)
+ [Safe devices](#safe-devices)
+ [Secret length](#secret-bytes)
+ [Secret length](#secret-length)
+ [TOTP configuration](#totp-configuration)
+ [Custom view](#custom-view)
* [Security](#security)
Expand Down Expand Up @@ -146,7 +151,7 @@ public function showRecoveryCodes(Request $request)
### Logging in

This package hooks into the `Validated` event (or `Attempting` if it doesn't exists) to check the User's Two Factor Authentication configuration preemptively.
This package hooks into the `Attempting` and `Validated` events to check the User's Two Factor Authentication configuration preemptively.

1. If the User has set up Two Factor Authentication, it will be prompted for a 2FA Code, otherwise authentication will proceed as normal.
2. If the Login attempt contains a `2fa_code` with the 2FA Code inside the Request, it will be used to check if its valid and proceed as normal.
Expand Down Expand Up @@ -191,11 +196,18 @@ Route::get('system/settings')
->middleware('2fa');
```

This middleware works much like the `verified` middleware: if the User has not enabled Two Factor Authentication, it will be redirected to a route name containing the warning, which is `2fa.notice` by default.
This middleware works much like the `verified` middleware: if the User has not enabled Two Factor Authentication, it will be redirected to a route name containing the warning, which is `2fa.notice` by default.

You can implement this easily using this package:

```php
Route::view('2fa-required', 'laraguard::notice')
->name('2fa.notice');
```

## Protecting the Login

Two Factor Authentication can be victim of brute-force attacks. The attacker will need between 16.000~34.000 requests each second to get the correct codes.
Two Factor Authentication can be victim of brute-force attacks. The attacker will need between 16.000~34.000 requests each second to get the correct code, or less depending on the lifetime of the code.

Since the listener throws a response before the default Login throttler increments its failed tries, its recommended to use a try-catch in the `attemptLogin()` method to keep the throttler working.

Expand Down Expand Up @@ -233,6 +245,10 @@ You will receive the authentication view in `resources/views/vendor/laraguard/au
return [
'listener' => true,
'input' => '2fa_code',
'cache' => [
'store' => null,
'prefix' => '2fa.code'
],
'recovery' => [
'enabled' => true,
'codes' => 10,
Expand Down Expand Up @@ -261,7 +277,7 @@ return [
];
```

This package works by hooking up the `ForcesTwoFactorAuth` listener to the `Validated`, or `Attempting` event as a fallback.
This package works by hooking up the `ForcesTwoFactorAuth` listener to the `Attempting` and `Validated` events as a fallback.

This may work wonders out-of-the-box, but if you want tighter control on how and when prompt for Two Factor Authentication, you can disable it. For example, to create your own 2FA Guard or greatly modify the Login Controller.

Expand All @@ -280,15 +296,15 @@ This allows to seamlessly intercept the log in attempt and proceed with Two Fact
### Cache Store

```php
return [
return [
'cache' => [
'store' => null,
'prefix' => '2fa.code'
],
];
```

[RFC 6238](https://tools.ietf.org/html/rfc6238#section-5) states that one-time passwords shouldn't be able to be usable again, even if inside the time window. For this, we need to use the Cache to save code for a given period of time.
[RFC 6238](https://tools.ietf.org/html/rfc6238#section-5) states that one-time passwords shouldn't be able to be usable again, even if inside the time window. For this, we need to use the Cache to save the code for a given period of time.

You can change the store to use, which it's the default used by your application, and the prefix to use as cache keys, in case of collisions.

Expand Down Expand Up @@ -362,7 +378,7 @@ This controls TOTP code generation and verification mechanisms:

This configuration values are always passed down to the authentication app as URI parameters:

otpauth://totp/Laravel:taylor@laravel.com?secret=THISISMYSECRETPLEASEDONOTSHAREIT&issuer=Laravel&algorithm=SHA1&digits=6&period=30
otpauth://totp/Laravel:taylor@laravel.com?secret=THISISMYSECRETPLEASEDONOTSHAREIT&issuer=Laravel&label=taylor%40laravel.com&algorithm=SHA1&digits=6&period=30

These values are printed to each 2FA data inside the application. Changes will only take effect for new activations.

Expand All @@ -382,7 +398,7 @@ You can override the view, which handles the Two Factor Code verification for th

The way it works is very simple: it will hold the User credentials in a hidden input while it asks for the Two Factor Code. The User will send everything again along with the Code, the application will ensure its correct, and complete the log in.

This view and its controller is bypassed if the User doesn't uses Two Factor Authentication, making the log in transparent and non-invasive.
This view and its controller is bypassed if the User doesn't uses Two Factor Authentication, making the log in transparent and non-invasive.

## Security

Expand Down
4 changes: 2 additions & 2 deletions composer.json
Expand Up @@ -23,8 +23,8 @@
"ext-json": "*",
"bacon/bacon-qr-code": "2.*",
"paragonie/constant_time_encoding": "2.*",
"illuminate/support": "^6.13",
"illuminate/auth": "^6.13"
"illuminate/support": "^6.15",
"illuminate/auth": "^6.15"
},
"require-dev": {
"orchestra/testbench": "^4.0",
Expand Down
@@ -1,8 +1,8 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTwoFactorAuthenticationsTable extends Migration
{
Expand All @@ -15,7 +15,7 @@ public function up()
{
Schema::create('two_factor_authentications', function (Blueprint $table) {
$table->bigIncrements('id');
$table->morphs('authenticatable');
$table->morphs('authenticatable', '2fa_auth_type_auth_id_index');
$table->binary('shared_secret');
$table->timestampTz('enabled_at')->nullable();
$table->string('label');
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Expand Up @@ -22,7 +22,7 @@
<logging>
<log type="tap" target="build/report.tap"/>
<log type="junit" target="build/report.junit.xml"/>
<log type="coverage-html" target="build/coverage" charset="UTF-8" yui="true" highlight="true"/>
<log type="coverage-html" target="build/coverage"/>
<log type="coverage-text" target="build/coverage.txt"/>
<log type="coverage-clover" target="build/logs/clover.xml"/>
</logging>
Expand Down
31 changes: 13 additions & 18 deletions src/LaraguardServiceProvider.php
Expand Up @@ -3,7 +3,10 @@
namespace DarkGhostHunter\Laraguard;

use Illuminate\Routing\Router;
use Illuminate\Auth\Events\Validated;
use Illuminate\Auth\Events\Attempting;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Config\Repository;

class LaraguardServiceProvider extends ServiceProvider
Expand All @@ -25,9 +28,9 @@ public function register()
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function boot(Repository $config, Router $router)
public function boot(Repository $config, Router $router, Dispatcher $dispatcher)
{
$this->registerListener($config);
$this->registerListener($config, $dispatcher);
$this->registerMiddleware($router);

$this->loadViewsFrom(__DIR__ . '/../resources/views', 'laraguard');
Expand Down Expand Up @@ -59,28 +62,20 @@ protected function registerMiddleware(Router $router)
* Register a listeners to tackle authentication.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
*/
protected function registerListener(Repository $config)
protected function registerListener(Repository $config, Dispatcher $dispatcher)
{
if (! $config['laraguard.listener']) {
return;
}

// We will check if the "Validated" auth event exists. If it is, this will allow our
// listener to not retrieve the user beforehand, since it'll be already retrieved.
// If not, we listen to the "Attempting" to retrieve and validate it ourselves.
$this->app['events']->listen(
$this->getEventName(), Listeners\ForcesTwoFactorAuth::class
$this->app->singleton(Listeners\EnforceTwoFactorAuth::class);
$dispatcher->listen(Attempting::class,
'DarkGhostHunter\Laraguard\Listeners\EnforceTwoFactorAuth@saveCredentials'
);
$dispatcher->listen(Validated::class,
'DarkGhostHunter\Laraguard\Listeners\EnforceTwoFactorAuth@checkTwoFactor'
);
}

/**
* Checks if the "Validated" event exists, otherwise fallback to "Attempting".
*
* @return string
*/
protected function getEventName()
{
return class_exists('Illuminate\Auth\Events\Validated') ? 'Illuminate\Auth\Events\Validated' : 'Illuminate\Auth\Events\Attempting';
}
}
75 changes: 75 additions & 0 deletions src/Listeners/ChecksTwoFactorCode.php
@@ -0,0 +1,75 @@
<?php

namespace DarkGhostHunter\Laraguard\Listeners;

use DarkGhostHunter\Laraguard\Contracts\TwoFactorAuthenticatable;

trait ChecksTwoFactorCode
{
/**
* Returns if the login attempt should enforce Two Factor Authentication.
*
* @param null|\DarkGhostHunter\Laraguard\Contracts\TwoFactorAuthenticatable|\Illuminate\Contracts\Auth\Authenticatable $user
* @return bool
*/
protected function shouldUseTwoFactorAuth($user = null)
{
if (! $user instanceof TwoFactorAuthenticatable) {
return false;
}

$shouldUse = $user->hasTwoFactorEnabled();

if ($this->config['laraguard.safe_devices.enabled']) {
return $shouldUse && ! $user->isSafeDevice($this->request);
}

return $shouldUse;
}

/**
* Returns if the Request is from a Safe Device.
*
* @param \DarkGhostHunter\Laraguard\Contracts\TwoFactorAuthenticatable $user
* @return bool
*/
protected function isSafeDevice(TwoFactorAuthenticatable $user)
{
return $this->config['laraguard.safe_devices.enabled'] && $user->isSafeDevice($this->request);
}

/**
* Returns if the Request has the Two Factor Code.
*
* @return bool
*/
protected function hasCode()
{
return $this->request->has($this->input);
}

/**
* Checks if the Request has a Two Factor Code and is correct (even if is invalid).
*
* @param \DarkGhostHunter\Laraguard\Contracts\TwoFactorAuthenticatable $user
* @return bool
*/
protected function hasValidCode(TwoFactorAuthenticatable $user)
{
return ! validator($this->request->only($this->input), [$this->input => 'alphanum'])->fails()
&& $user->validateTwoFactorCode($this->request->input($this->input));
}

/**
* Adds a safe device to Two Factor Authentication data.
*
* @param \DarkGhostHunter\Laraguard\Contracts\TwoFactorAuthenticatable $user
* @return void
*/
protected function addSafeDevice(TwoFactorAuthenticatable $user)
{
if ($this->config['laraguard.safe_devices.enabled']) {
$user->addSafeDevice($this->request);
}
}
}

0 comments on commit 97d868e

Please sign in to comment.