Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
iamgergo committed May 24, 2024
1 parent b6514d7 commit 3a3c84a
Show file tree
Hide file tree
Showing 14 changed files with 363 additions and 84 deletions.
30 changes: 30 additions & 0 deletions database/migrations/2024_05_14_091321_create_auth_codes_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

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

return new class() extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('root_auth_codes', static function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->integer('code')->unsigned();
$table->timestamp('expires_at');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('root_auth_codes');
}
};
42 changes: 37 additions & 5 deletions resources/views/auth/two-factor.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,41 @@

{{-- Content --}}
@section('content')
<p>{{ __('To finish the two factor authentication, please use the link we sent, or request a new one!') }}</p>
<form method="POST" action="{{ URL::route('root.auth.two-factor.resend') }}">
<p>{{ __('To finish the two factor authentication, please add the verification code, or request a new one!') }}</p>
<form method="POST" action="{{ URL::route('root.auth.two-factor.verify') }}">
@csrf
<div class="form-group-stack">
<div class="form-group">
<button type="submit" class="btn btn--primary btn--lg btn--block btn--primary-shadow">
{{ __('Resend Two Factor Authentication Link') }}
<label class="form-label" for="email">{{ __('Code') }}</label>
<input
@class(['form-control', 'form-control--lg', 'form-control--invalid' => $errors->has('code')])
id="code"
type="number"
name="code"
required
value="{{ Request::old('code', $code) }}"
>
@error('code')
<span class="field-feedback field-feedback--invalid">{{ $message }}</span>
@enderror
</div>
<div class="form-group">
<label class="form-check form-check--lg" for="trust">
<input class="form-check__control" id="trust" type="checkbox" name="trust" value="1">
<span class="form-label form-check__label">{{ __('Trust in this browser') }}</span>
</label>
</div>
<div class="form-group">
<button class="btn btn--primary btn--lg btn--block btn--primary-shadow">
{{ __('Verify') }}
</button>
</div>
</div>
</form>
<span class="or-separator" aria-hidden="true">{{ __('or') }}</span>
@endsection

{{-- Footer --}}
@section('footer')
<form method="POST" action="{{ URL::route('root.auth.logout') }}">
@csrf
<div class="form-group">
Expand All @@ -25,4 +48,13 @@
</button>
</div>
</form>

<form method="POST" action="{{ URL::route('root.auth.two-factor.resend') }}">
@csrf
<div class="form-group">
<button type="submit" class="btn btn--light btn--sm">
{{ __('Resend Verification Code') }}
</button>
</div>
</form>
@endsection
6 changes: 3 additions & 3 deletions routes/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
Route::get('/password/reset/{token}/{email}', [ResetPasswordController::class, 'show'])->name('password.reset');
Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update');

// Verify
Route::get('/two-factor', [TwoFactorController::class, 'verify'])->name('two-factor.verify');
Route::get('/two-factor/resend', [TwoFactorController::class, 'show'])->name('two-factor.show');
// Two Factor Verification
Route::get('/two-factor', [TwoFactorController::class, 'show'])->name('two-factor.show');
Route::post('/two-factor', [TwoFactorController::class, 'verify'])->name('two-factor.verify');
Route::post('/two-factor/resend', [TwoFactorController::class, 'resend'])->name('two-factor.resend');
12 changes: 5 additions & 7 deletions src/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
namespace Cone\Root\Http\Controllers\Auth;

use Cone\Root\Http\Controllers\Controller;
use Cone\Root\Interfaces\TwoFactorAuthenticatable;
use Cone\Root\Notifications\TwoFactorLink;
use Cone\Root\Notifications\AuthCodeNotification;
use Illuminate\Auth\Events\Login;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
Expand Down Expand Up @@ -62,11 +61,10 @@ public function login(Request $request): RedirectResponse
new Login(Auth::getDefaultDriver(), $request->user(), $request->filled('remember'))
);

if ($request->user()->can('viewRoot')
&& $request->user() instanceof TwoFactorAuthenticatable
&& $request->user()->requiresTwoFactorAuthentication()
) {
$request->user()->notify(new TwoFactorLink());
if ($request->user()->can('viewRoot') && $request->user()->shouldTwoFactorAuthenticate($request)) {
$request->user()->notify(
new AuthCodeNotification($request->user()->generateAuthCode())
);

$request->session()->flash('status', __('The two factor authentication link has been sent!'));
}
Expand Down
38 changes: 27 additions & 11 deletions src/Http/Controllers/Auth/TwoFactorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
use Closure;
use Cone\Root\Http\Controllers\Controller;
use Cone\Root\Http\Middleware\Authenticate;
use Cone\Root\Interfaces\TwoFactorAuthenticatable;
use Cone\Root\Notifications\TwoFactorLink;
use Cone\Root\Notifications\AuthCodeNotification;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Response as ResponseFactory;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpFoundation\Response as BaseResponse;
Expand All @@ -24,10 +25,7 @@ public function __construct()
$this->middleware(Authenticate::class);
$this->middleware('throttle:6,1')->only(['resend']);
$this->middleware(static function (Request $request, Closure $next): BaseResponse {
if (! $request->user() instanceof TwoFactorAuthenticatable
|| ! $request->user()->requiresTwoFactorAuthentication()
|| $request->session()->has('root.auth.two-factor')
) {
if (! $request->user()->shouldTwoFactorAuthenticate($request)) {
return ResponseFactory::redirectToIntended(URL::route('root.dashboard'));
}

Expand All @@ -40,21 +38,37 @@ public function __construct()
*/
public function show(Request $request): Response|RedirectResponse
{
return ResponseFactory::view('root::auth.two-factor');
return ResponseFactory::view('root::auth.two-factor', [
'code' => $request->input('code'),
]);
}

/**
* Verify the link.
*/
public function verify(Request $request): RedirectResponse
{
if (! $request->hasValidSignature() || ! hash_equals($request->input('hash'), sha1($request->user()->email))) {
$data = $request->validate([
'code' => ['required', 'numeric'],
]);

if ($request->user()->authCode?->code !== (int) $data['code']) {
return ResponseFactory::redirectToRoute('root.auth.two-factor.show')
->with('status', __('The authentication link is not valid! Please request a new link!'));
->withErrors(['code' => __('The authentication code is not valid!')]);
}

$request->session()->put('root.auth.two-factor', true);

$request->user()->authCodes()->delete();

if ($request->boolean('trust')) {
Cookie::queue(
'device_token',
sha1(sprintf('%s:%s', $request->user()->getKey(), $request->user()->email)),
Date::now()->addYear()->diffInMinutes(absolute: true),
);
}

return ResponseFactory::redirectToIntended(URL::route('root.dashboard'));
}

Expand All @@ -63,9 +77,11 @@ public function verify(Request $request): RedirectResponse
*/
public function resend(Request $request): RedirectResponse
{
$request->user()->notify(new TwoFactorLink());
$code = $request->user()->generateAuthCode();

$request->user()->notify(new AuthCodeNotification($code));

return ResponseFactory::redirectToRoute('root.auth.two-factor.show')
->with('status', __('The two factor authentication link has been sent!'));
->with('status', __('The authentication code has been sent!'));
}
}
6 changes: 1 addition & 5 deletions src/Http/Middleware/TwoFactorAuthenticate.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Cone\Root\Http\Middleware;

use Closure;
use Cone\Root\Interfaces\TwoFactorAuthenticatable;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -17,10 +16,7 @@ class TwoFactorAuthenticate
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->user() instanceof TwoFactorAuthenticatable
&& $request->user()->requiresTwoFactorAuthentication()
&& ! $request->session()->has('root.auth.two-factor')
) {
if ($request->user()->shouldTwoFactorAuthenticate($request)) {
return Redirect::route('root.auth.two-factor.show');
}

Expand Down
23 changes: 23 additions & 0 deletions src/Interfaces/Models/AuthCode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Cone\Root\Interfaces\Models;

use Illuminate\Database\Eloquent\Relations\BelongsTo;

interface AuthCode
{
/**
* Get the user for the model.
*/
public function user(): BelongsTo;

/**
* Determine whether the code is active.
*/
public function active(): bool;

/**
* Determine whether the code is expired.
*/
public function expired(): bool;
}
28 changes: 28 additions & 0 deletions src/Interfaces/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,40 @@

namespace Cone\Root\Interfaces\Models;

use Cone\Root\Models\AuthCode;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;

interface User
{
/**
* Get the uploads for the user.
*/
public function uploads(): HasMany;

/**
* Get the Root notifications for the user.
*/
public function rootNotifications(): MorphMany;

/**
* Get the current auth code for the user.
*/
public function authCode(): HasOne;

/**
* Get the auth codes for the user.
*/
public function authCodes(): HasMany;

/**
* Determine whether the object requires two factor authentitaction.
*/
public function requiresTwoFactorAuthentication(): bool;

/**
* Generate a new auth code for the user.
*/
public function generateAuthCode(): AuthCode;
}
11 changes: 0 additions & 11 deletions src/Interfaces/TwoFactorAuthenticatable.php

This file was deleted.

89 changes: 89 additions & 0 deletions src/Models/AuthCode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace Cone\Root\Models;

use Cone\Root\Interfaces\Models\AuthCode as Contract;
use Cone\Root\Traits\InteractsWithProxy;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Date;

class AuthCode extends Model implements Contract
{
use HasFactory;
use InteractsWithProxy;

/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'code' => 'int',
'expires_at' => 'datetime',
];

/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [];

/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'root_auth_codes';

/**
* Get the proxied interface.
*/
public static function getProxiedInterface(): string
{
return Contract::class;
}

/**
* Get the user for the model.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

/**
* Determine whether the code is active.
*/
public function active(): bool
{
return $this->expires_at->gt(Date::now());
}

/**
* Determine whether the code is expired.
*/
public function expired(): bool
{
return ! $this->active();
}

/**
* Scope the query only to include the active codes.
*/
public function scopeActive(Builder $query): Builder
{
return $query->where($query->qualifyColumn('expires_at'), '>', Date::now());
}

/**
* Scope the query only to include the expired codes.
*/
public function scopeExpired(Builder $query): Builder
{
return $query->where($query->qualifyColumn('expires_at'), '<=', Date::now());
}
}
Loading

0 comments on commit 3a3c84a

Please sign in to comment.