Skip to content

Commit

Permalink
WIP - move email verification functionality to middleware - only impl…
Browse files Browse the repository at this point in the history
…emented for API
  • Loading branch information
Mohammad-Alavi committed Oct 10, 2021
1 parent 3ab026a commit 24ccf5b
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 130 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,15 @@
namespace App\Containers\AppSection\Authentication\Actions;

use App\Containers\AppSection\Authentication\Exceptions\LoginFailedException;
use App\Containers\AppSection\Authentication\Exceptions\UserNotConfirmedException;
use App\Containers\AppSection\Authentication\Tasks\CallOAuthServerTask;
use App\Containers\AppSection\Authentication\Tasks\CheckIfUserEmailIsConfirmedTask;
use App\Containers\AppSection\Authentication\Tasks\ExtractLoginCustomAttributeTask;
use App\Containers\AppSection\Authentication\Tasks\MakeRefreshCookieTask;
use App\Containers\AppSection\Authentication\UI\API\Requests\LoginProxyPasswordGrantRequest;
use App\Containers\AppSection\User\Models\User;
use App\Ship\Parents\Actions\Action;
use Illuminate\Support\Facades\DB;
use Lcobucci\JWT\Parser;

class ApiLoginProxyForWebClientAction extends Action
{
/**
* @throws UserNotConfirmedException
* @throws LoginFailedException
*/
public function run(LoginProxyPasswordGrantRequest $request): array
Expand All @@ -38,32 +32,11 @@ public function run(LoginProxyPasswordGrantRequest $request): array
$sanitizedData['scope'] = '';

$responseContent = app(CallOAuthServerTask::class)->run($sanitizedData, $request->headers->get('accept-language'));
$this->processEmailConfirmation($responseContent);
$refreshCookie = app(MakeRefreshCookieTask::class)->run($responseContent['refresh_token']);

return [
'response_content' => $responseContent,
'refresh_cookie' => $refreshCookie,
];
}

/**
* @throws UserNotConfirmedException
*/
private function processEmailConfirmation($response): void
{
$user = $this->extractUserFromAuthServerResponse($response);
$isUserConfirmed = app(CheckIfUserEmailIsConfirmedTask::class)->run($user);

if (!$isUserConfirmed) {
throw new UserNotConfirmedException();
}
}

private function extractUserFromAuthServerResponse($response)
{
$tokenId = app(Parser::class)->parse($response['access_token'])->claims()->get('jti');
$userAccessRecord = DB::table('oauth_access_tokens')->find($tokenId);
return User::find($userAccessRecord->user_id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
namespace App\Containers\AppSection\Authentication\Actions;

use App\Containers\AppSection\Authentication\Exceptions\LoginFailedException;
use App\Containers\AppSection\Authentication\Exceptions\UserNotConfirmedException;
use App\Containers\AppSection\Authentication\Tasks\CheckIfUserEmailIsConfirmedTask;
use App\Containers\AppSection\Authentication\Tasks\ExtractLoginCustomAttributeTask;
use App\Containers\AppSection\Authentication\Tasks\GetAuthenticatedUserTask;
use App\Containers\AppSection\Authentication\Tasks\LoginTask;
Expand All @@ -17,7 +15,6 @@
class WebLoginAction extends Action
{
/**
* @throws UserNotConfirmedException
* @throws LoginFailedException
* @throws NotFoundException
*/
Expand All @@ -42,21 +39,6 @@ public function run(LoginRequest $request): User|Authenticatable|null
throw new LoginFailedException('Invalid Login Credentials.');
}

$user = app(GetAuthenticatedUserTask::class)->run();
$this->processEmailConfirmation($user);

return $user;
}

/**
* @throws UserNotConfirmedException
*/
private function processEmailConfirmation(User $user): void
{
$userConfirmed = app(CheckIfUserEmailIsConfirmedTask::class)->run($user);

if (!$userConfirmed) {
throw new UserNotConfirmedException();
}
return app(GetAuthenticatedUserTask::class)->run();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
| Email Confirmation
|--------------------------------------------------------------------------
|
| When set to true, the user must confirm his email before being able to
| When set to true, the user must verify his email before being able to
| Login, after his registration.
|
*/

'require_email_confirmation' => false,
'require_email_verification' => false,

/*
|--------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace App\Containers\AppSection\Authentication\Exceptions;

use App\Ship\Parents\Exceptions\Exception;
use Symfony\Component\HttpFoundation\Response;

class EmailNotVerifiedException extends Exception
{
protected $code = Response::HTTP_FORBIDDEN;
protected $message = 'Your email address is not verified.';
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace App\Containers\AppSection\Authentication\Middlewares;

use App\Containers\AppSection\Authentication\Exceptions\EmailNotVerifiedException;
use App\Ship\Parents\Middlewares\Middleware as ParentMiddleware;
use Closure;
use Illuminate\Http\Request;

class EnsureEmailIsVerified extends ParentMiddleware
{
/**
* Exclude these routes from authentication check.
*
* Note: `$request->is('api/fragment*')` https://laravel.com/docs/7.x/requests
*
* @var array
*/
protected array $except = [
'v1/oauth/token',
'v1/clients/web/login',
];

/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @param null $redirectToRoute
* @return mixed
* @throws EmailNotVerifiedException
*/
public function handle(Request $request, Closure $next, $redirectToRoute = null): mixed
{
if (!$this->emailVerificationRequired() || !$request->user()) {
return $next($request);
}

foreach ($this->except as $excludedRoute) {
if ($request->path() === $excludedRoute) {
return $next($request);
}
}

if (!$this->isEmailVerified($request->user())) {
throw new EmailNotVerifiedException();
}

return $next($request);
}

private function emailVerificationRequired()
{
return config('appSection-authentication.require_email_verification');
}

private function isEmailVerified($user): bool
{
return !is_null($user->email_verified_at);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@

namespace App\Containers\AppSection\Authentication\Providers;

use App\Containers\AppSection\Authentication\Middlewares\EnsureEmailIsVerified;
use App\Containers\AppSection\Authentication\Middlewares\RedirectIfAuthenticated;
use App\Ship\Parents\Providers\MiddlewareServiceProvider as ParentMiddlewareServiceProvider;

class MiddlewareServiceProvider extends ParentMiddlewareServiceProvider
{
protected array $middlewares = [];

protected array $middlewareGroups = [];
protected array $middlewareGroups = [
'api' => [
EnsureEmailIsVerified::class,
],
];

protected array $middlewarePriority = [];

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace App\Containers\AppSection\Authentication\Tests\Unit;

use App\Containers\AppSection\Authentication\Exceptions\EmailNotVerifiedException;
use App\Containers\AppSection\Authentication\Middlewares\EnsureEmailIsVerified;
use App\Containers\AppSection\Authentication\Tests\TestCase;
use Illuminate\Http\Request;

/**
* Class EnsureEmailIsVerifiedMiddlewareTest.
*
* @group authentication
* @group unit
*/
class EnsureEmailIsVerifiedMiddlewareTest extends TestCase
{
private Request $request;
private EnsureEmailIsVerified $middleware;

protected function setUp(): void
{
parent::setUp();

config(['appSection-authentication.require_email_verification' => true]);
$this->request = Request::create('/user/profile');
$this->middleware = new EnsureEmailIsVerified();
}

public function testIfEmailVerificationIsDisabled_ShouldSkipProcessing(): void
{
config(['appSection-authentication.require_email_verification' => false]);

$this->middleware->handle($this->request, function ($req) {
$this->assertInstanceOf(Request::class, $req);
});
}

public function testIfUserNotAuthenticated_ShouldSkipProcessing(): void
{
$this->middleware->handle($this->request, function ($req) {
$this->assertInstanceOf(Request::class, $req);
});
}

public function testAPI_IfEmailVerificationIsRequired_GivenEmailNotVerified_ShouldThrowException(): void
{
$this->expectException(EmailNotVerifiedException::class);

$user = $this->getTestingUser(['email_verified_at' => null]);
$this->request->merge(['user' => $user]);
$this->request->headers->set('Accept', 'application/json');
$this->request->setUserResolver(fn () => $user);

$this->middleware->handle($this->request, static function () {
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use App\Containers\AppSection\Authentication\Tests\TestCase;
use App\Containers\AppSection\User\Models\User;
use App\Ship\Providers\RouteServiceProvider;
use Illuminate\Support\Facades\Request;
use Illuminate\Http\Request;

/**
* Class RedirectIfAuthenticatedMiddlewareTest.
Expand All @@ -25,10 +25,20 @@ public function testRedirectIfAuthenticated(): void

$middleware = new RedirectIfAuthenticated();

$response = $middleware->handle($request, function ($req) {
$this->assertInstanceOf(Request::class, $req);
$response = $middleware->handle($request, static function () {
});

$this->assertEquals($response->getStatusCode(), 302);
$this->assertEquals(302, $response->getStatusCode());
}

public function testSkipIfUnAuthenticated(): void
{
$request = Request::create(RouteServiceProvider::LOGIN);

$middleware = new RedirectIfAuthenticated();

$middleware->handle($request, function ($req) {
$this->assertInstanceOf(Request::class, $req);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use App\Containers\AppSection\Authentication\Actions\WebLoginAction;
use App\Containers\AppSection\Authentication\Exceptions\LoginFailedException;
use App\Containers\AppSection\Authentication\Exceptions\UserNotConfirmedException;
use App\Containers\AppSection\Authentication\Exceptions\EmailNotVerifiedException;
use App\Containers\AppSection\Authentication\Tests\TestCase;
use App\Containers\AppSection\Authentication\UI\WEB\Requests\LoginRequest;
use App\Containers\AppSection\User\Models\User;
Expand Down Expand Up @@ -40,20 +40,6 @@ public function testLoginWithInvalidCredentialsThrowsAnException(): void
$this->action->run($this->request);
}

public function testGivenEmailConfirmationIsRequiredAndUserIsNotConfirmedThrowsAnException(): void
{
$this->expectException(UserNotConfirmedException::class);

$configInitialValue = config('appSection-authentication.require_email_confirmation');
Config::set('appSection-authentication.require_email_confirmation', true);
$this->testingUser->email_verified_at = null;
$this->testingUser->save();

$this->action->run($this->request);

Config::set('appSection-authentication.require_email_confirmation', $configInitialValue);
}

protected function setUp(): void
{
parent::setUp();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use App\Containers\AppSection\Authentication\Actions\ApiLoginProxyForWebClientAction;
use App\Containers\AppSection\Authentication\Exceptions\LoginFailedException;
use App\Containers\AppSection\Authentication\Exceptions\UserNotConfirmedException;
use App\Containers\AppSection\Authentication\Exceptions\EmailNotVerifiedException;
use App\Containers\AppSection\Authentication\UI\API\Requests\LoginProxyPasswordGrantRequest;
use App\Ship\Parents\Controllers\ApiController;
use Illuminate\Http\JsonResponse;
Expand All @@ -19,8 +19,9 @@ class LoginProxyForWebClientController extends ApiController
* This is only to help the Web Apps (JavaScript clients) hide
* their ID's and Secrets when contacting the OAuth server and obtain Tokens.
*
* @param LoginProxyPasswordGrantRequest $request
* @return JsonResponse
* @throws LoginFailedException
* @throws UserNotConfirmedException
*/
public function loginProxyForWebClient(LoginProxyPasswordGrantRequest $request): JsonResponse
{
Expand Down
Loading

0 comments on commit 24ccf5b

Please sign in to comment.