Skip to content

Commit

Permalink
Add an Account Security Indicator (#11)
Browse files Browse the repository at this point in the history
* Add Account Security Indicator
  • Loading branch information
claudiodekker authored Dec 22, 2022
1 parent 317e905 commit 86af2d5
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- PHP 8.2 Support ([#6](https://github.com/claudiodekker/laravel-auth/pull/6))
- The Passkey-based registration flow can now be cancelled, directly releasing the claimed user ([#7](https://github.com/claudiodekker/laravel-auth/pull/7))
- `exec` generator method, providing an easy way to run cli commands ([#8](https://github.com/claudiodekker/laravel-auth/pull/8))
- New Account Security Strength Indicator ([#11](https://github.com/claudiodekker/laravel-auth/pull/11))

## Fixed

Expand Down
9 changes: 7 additions & 2 deletions lang/en/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
'totp' => 'The provided one-time-password code is incorrect.',
],
'settings' => [
'credential-deleted' => 'The two-factor credential has been deleted.',
'credential-deleted' => 'The multi-factor credential has been deleted.',
'password-changed' => 'Your password has been changed successfully.',
'public-key-registered' => 'Public key credential successfully registered.',
'recovery-configured' => 'Account recovery codes successfully configured.',
Expand All @@ -40,5 +40,10 @@
'sent' => 'A verification link has been sent to the email address you provided during registration.',
'verified' => 'Your email address has been verified.',
],

'security-indicator' => [
'no-mfa-no-recovery-codes' => 'Your account is vulnerable. Please enable multi-factor authentication and set up account recovery codes.',
'no-mfa-has-recovery-codes' => 'Your account is vulnerable without multi-factor authentication. Please enable it to secure your account.',
'has-mfa-no-recovery-codes' => 'Your account could be compromised if someone gains access to your email account. Protect yourself by setting up account recovery codes.',
'has-mfa-has-recovery-codes' => 'Your account is well-protected.',
],
];
41 changes: 21 additions & 20 deletions src/Console/GenerateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ abstract class GenerateCommand extends Command
*/
abstract protected function determinePackagePath(): string;

/**
* Installs the extending package's authentication routes.
*
* @return void
*/
abstract protected function installRoutes(): void;

/**
* Installs the extending package's authentication views.
*
* @return void
*/
abstract protected function installViews(): void;

/**
* Create a new console command instance.
*
Expand Down Expand Up @@ -90,30 +104,13 @@ protected function install(): void
{
$this->installRoutes();
$this->installControllers();
$this->installTests();

if (! $this->determinedOptions['withoutViews']) {
$this->installViews();
}

$this->installCoreOverrides();
}

/**
* Installs the extending package's authentication routes.
*
* @return void
*/
abstract protected function installRoutes(): void;

/**
* Installs the extending package's authentication tests.
*
* @return void
*/
protected function installTests(): void
{
$this->rawGenerate('Tests.PruneUnclaimedUsersTest', base_path('tests/Unit/PruneUnclaimedUsersTest.php'));
$this->installTests();
}

/**
Expand All @@ -138,11 +135,15 @@ protected function installControllers(): void
}

/**
* Installs the extending package's authentication views.
* Installs the extending package's authentication tests.
*
* @return void
*/
abstract protected function installViews(): void;
protected function installTests(): void
{
$this->rawGenerate('Tests.PruneUnclaimedUsersTest', base_path('tests/Unit/PruneUnclaimedUsersTest.php'));
$this->rawGenerate('Tests.UserTest', base_path('tests/Unit/UserTest.php'));
}

/**
* Overrides some of the files in Laravel's application scaffolding with the core package's own versions.
Expand Down
16 changes: 0 additions & 16 deletions src/HasMultiFactorCredentials.php

This file was deleted.

66 changes: 66 additions & 0 deletions src/Support/AccountSecurityIndicator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace ClaudioDekker\LaravelAuth\Support;

use Illuminate\Contracts\Support\Arrayable;

enum AccountSecurityIndicator implements Arrayable
{
case NO_MFA_NO_RECOVERY_CODES;
case NO_MFA_HAS_RECOVERY_CODES;
case HAS_MFA_NO_RECOVERY_CODES;
case HAS_MFA_HAS_RECOVERY_CODES;

/**
* Determine the color of the indicator.
*
* @return string
*/
public function color(): string
{
return match ($this) {
self::NO_MFA_NO_RECOVERY_CODES, self::NO_MFA_HAS_RECOVERY_CODES => 'RED',
self::HAS_MFA_NO_RECOVERY_CODES => 'ORANGE',
self::HAS_MFA_HAS_RECOVERY_CODES => 'GREEN',
};
}

/**
* Determine whether the account security indicator has any issues to indicate.
*
* @return bool
*/
public function hasIssues(): bool
{
return $this->color() !== 'GREEN';
}

/**
* Determine the message that should be displayed for the indicator.
*
* @return string
*/
public function message(): string
{
return match ($this) {
self::NO_MFA_NO_RECOVERY_CODES => __('laravel-auth::auth.security-indicator.no-mfa-no-recovery-codes'),
self::NO_MFA_HAS_RECOVERY_CODES => __('laravel-auth::auth.security-indicator.no-mfa-has-recovery-codes'),
self::HAS_MFA_NO_RECOVERY_CODES => __('laravel-auth::auth.security-indicator.has-mfa-no-recovery-codes'),
self::HAS_MFA_HAS_RECOVERY_CODES => __('laravel-auth::auth.security-indicator.has-mfa-has-recovery-codes'),
};
}

/**
* Get the instance as an array.
*
* @return array<TKey, TValue>
*/
public function toArray()
{
return [
'color' => $this->color(),
'has_issues' => $this->hasIssues(),
'message' => $this->message(),
];
}
}
39 changes: 36 additions & 3 deletions templates/Database/User.bladetmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace App\Models;

use ClaudioDekker\LaravelAuth\HasMultiFactorCredentials;
use ClaudioDekker\LaravelAuth\LaravelAuth;
use ClaudioDekker\LaravelAuth\Support\AccountSecurityIndicator;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Prunable;
Expand All @@ -10,7 +11,7 @@ use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
use HasApiTokens, HasFactory, HasMultiFactorCredentials, Notifiable, Prunable;
use HasApiTokens, HasFactory, Notifiable, Prunable;

/**
* The attributes that are mass assignable.
Expand Down Expand Up @@ -50,10 +51,42 @@ class User extends Authenticatable
'recovery_codes' => 'array',
];

/**
* Get all of the multi factor credentials for the user.
*
* {!! '@' !!}return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function multiFactorCredentials()
{
return $this->hasMany(LaravelAuth::multiFactorCredentialModel(), 'user_id')->orderBy('created_at', 'desc');
}

/**
* Determine the current account safety level.
*
* {!! '@' !!}return \ClaudioDekker\LaravelAuth\Support\AccountSecurityIndicator
*/
public function accountSecurityIndicator(): AccountSecurityIndicator
{
if (! $this->recovery_codes && $this->multiFactorCredentials->isEmpty()) {
return AccountSecurityIndicator::NO_MFA_NO_RECOVERY_CODES;
}

if ($this->multiFactorCredentials->isEmpty()) {
return AccountSecurityIndicator::NO_MFA_HAS_RECOVERY_CODES;
}

if (! $this->recovery_codes) {
return AccountSecurityIndicator::HAS_MFA_NO_RECOVERY_CODES;
}

return AccountSecurityIndicator::HAS_MFA_HAS_RECOVERY_CODES;
}

/**
* Get the prunable model query.
*
* {!! ! '@' !!}return \Illuminate\Database\Eloquent\Builder
* {!! '@' !!}return \Illuminate\Database\Eloquent\Builder
*/
public function prunable()
{
Expand Down
62 changes: 62 additions & 0 deletions templates/Tests/UserTest.bladetmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Tests\Unit;

use App\Models\User;
use ClaudioDekker\LaravelAuth\MultiFactorCredential;
use ClaudioDekker\LaravelAuth\Support\AccountSecurityIndicator;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\TestCase;

class UserTest extends TestCase
{
use DatabaseMigrations;

/** {!! '@' !!}test */
public function the_account_security_indicator_indicates_the_user_has_both_mfa_and_recovery_codes(): void
{
$user = User::factory()->create(['recovery_codes' => ['foo', 'bar']]);
MultiFactorCredential::factory()->forUser($user)->publicKey()->create();

tap($user->accountSecurityIndicator(), function (AccountSecurityIndicator $indicator) {
$this->assertFalse($indicator->hasIssues());
$this->assertSame("GREEN", $indicator->color());
$this->assertSame(__('laravel-auth::auth.security-indicator.has-mfa-has-recovery-codes'), $indicator->message());
});
}

/** {!! '@' !!}test */
public function the_account_security_indicator_indicates_the_user_has_no_mfa_and_no_recovery_codes(): void
{
$user = User::factory()->create();

tap($user->accountSecurityIndicator(), function (AccountSecurityIndicator $indicator) {
$this->assertTrue($indicator->hasIssues());
$this->assertSame("RED", $indicator->color());
$this->assertSame(__('laravel-auth::auth.security-indicator.no-mfa-no-recovery-codes'), $indicator->message());
});
}

/** {!! '@' !!}test */
public function the_account_security_indicator_indicates_the_user_has_no_mfa(): void
{
$user = User::factory()->create(['recovery_codes' => ['foo', 'bar']]);

tap($user->accountSecurityIndicator(), function (AccountSecurityIndicator $indicator) {
$this->assertTrue($indicator->hasIssues());
$this->assertSame("RED", $indicator->color());
$this->assertSame(__('laravel-auth::auth.security-indicator.no-mfa-has-recovery-codes'), $indicator->message());
});
}

/** {!! '@' !!}test */
public function the_account_security_indicator_indicates_the_user_has_mfa_but_no_recovery_codes(): void
{
$user = User::factory()->create();
MultiFactorCredential::factory()->forUser($user)->publicKey()->create();

tap($user->accountSecurityIndicator(), function (AccountSecurityIndicator $indicator) {
$this->assertTrue($indicator->hasIssues());
$this->assertSame("ORANGE", $indicator->color());
$this->assertSame(__('laravel-auth::auth.security-indicator.has-mfa-no-recovery-codes'), $indicator->message());
});
}
}

0 comments on commit 86af2d5

Please sign in to comment.