diff --git a/app/Containers/AppSection/Authentication/Actions/ApiLoginProxyForWebClientAction.php b/app/Containers/AppSection/Authentication/Actions/ApiLoginProxyForWebClientAction.php index e9a18ebae..67030d3d3 100644 --- a/app/Containers/AppSection/Authentication/Actions/ApiLoginProxyForWebClientAction.php +++ b/app/Containers/AppSection/Authentication/Actions/ApiLoginProxyForWebClientAction.php @@ -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 @@ -38,7 +32,6 @@ 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 [ @@ -46,24 +39,4 @@ public function run(LoginProxyPasswordGrantRequest $request): array '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); - } } diff --git a/app/Containers/AppSection/Authentication/Actions/WebLoginAction.php b/app/Containers/AppSection/Authentication/Actions/WebLoginAction.php index f2c147820..41af66031 100644 --- a/app/Containers/AppSection/Authentication/Actions/WebLoginAction.php +++ b/app/Containers/AppSection/Authentication/Actions/WebLoginAction.php @@ -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; @@ -17,7 +15,6 @@ class WebLoginAction extends Action { /** - * @throws UserNotConfirmedException * @throws LoginFailedException * @throws NotFoundException */ @@ -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(); } } diff --git a/app/Containers/AppSection/Authentication/Configs/appSection-authentication.php b/app/Containers/AppSection/Authentication/Configs/appSection-authentication.php index 53f5179a2..f12f36879 100644 --- a/app/Containers/AppSection/Authentication/Configs/appSection-authentication.php +++ b/app/Containers/AppSection/Authentication/Configs/appSection-authentication.php @@ -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, /* |-------------------------------------------------------------------------- diff --git a/app/Containers/AppSection/Authentication/Exceptions/EmailNotVerifiedException.php b/app/Containers/AppSection/Authentication/Exceptions/EmailNotVerifiedException.php new file mode 100644 index 000000000..6039ae2e5 --- /dev/null +++ b/app/Containers/AppSection/Authentication/Exceptions/EmailNotVerifiedException.php @@ -0,0 +1,12 @@ +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); + } +} diff --git a/app/Containers/AppSection/Authentication/Providers/MiddlewareServiceProvider.php b/app/Containers/AppSection/Authentication/Providers/MiddlewareServiceProvider.php index a5a61dc14..d8ee0c415 100644 --- a/app/Containers/AppSection/Authentication/Providers/MiddlewareServiceProvider.php +++ b/app/Containers/AppSection/Authentication/Providers/MiddlewareServiceProvider.php @@ -2,6 +2,7 @@ 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; @@ -9,7 +10,11 @@ class MiddlewareServiceProvider extends ParentMiddlewareServiceProvider { protected array $middlewares = []; - protected array $middlewareGroups = []; + protected array $middlewareGroups = [ + 'api' => [ + EnsureEmailIsVerified::class, + ], + ]; protected array $middlewarePriority = []; diff --git a/app/Containers/AppSection/Authentication/Tasks/CheckIfUserEmailIsConfirmedTask.php b/app/Containers/AppSection/Authentication/Tasks/CheckIfUserEmailIsConfirmedTask.php deleted file mode 100644 index c071c2efe..000000000 --- a/app/Containers/AppSection/Authentication/Tasks/CheckIfUserEmailIsConfirmedTask.php +++ /dev/null @@ -1,28 +0,0 @@ -emailConfirmationRequired()) { - return $this->isEmailConfirmed($user); - } - - return true; - } - - private function emailConfirmationRequired() - { - return config('appSection-authentication.require_email_confirmation'); - } - - private function isEmailConfirmed(User $user): bool - { - return !is_null($user->email_verified_at); - } -} diff --git a/app/Containers/AppSection/Authentication/Tests/Unit/EnsureEmailIsVerifiedMiddlewareTest.php b/app/Containers/AppSection/Authentication/Tests/Unit/EnsureEmailIsVerifiedMiddlewareTest.php new file mode 100644 index 000000000..c9368dd58 --- /dev/null +++ b/app/Containers/AppSection/Authentication/Tests/Unit/EnsureEmailIsVerifiedMiddlewareTest.php @@ -0,0 +1,58 @@ + 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 () { + }); + } +} diff --git a/app/Containers/AppSection/Authentication/Tests/Unit/RedirectIfAuthenticatedMiddlewareTest.php b/app/Containers/AppSection/Authentication/Tests/Unit/RedirectIfAuthenticatedMiddlewareTest.php index 0f6dc0cb8..09fb42536 100644 --- a/app/Containers/AppSection/Authentication/Tests/Unit/RedirectIfAuthenticatedMiddlewareTest.php +++ b/app/Containers/AppSection/Authentication/Tests/Unit/RedirectIfAuthenticatedMiddlewareTest.php @@ -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. @@ -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); + }); } } diff --git a/app/Containers/AppSection/Authentication/Tests/Unit/WebLoginActionTest.php b/app/Containers/AppSection/Authentication/Tests/Unit/WebLoginActionTest.php index 5b4d76c7f..d80ea9467 100644 --- a/app/Containers/AppSection/Authentication/Tests/Unit/WebLoginActionTest.php +++ b/app/Containers/AppSection/Authentication/Tests/Unit/WebLoginActionTest.php @@ -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; @@ -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(); diff --git a/app/Containers/AppSection/Authentication/UI/API/Controllers/LoginProxyForWebClientController.php b/app/Containers/AppSection/Authentication/UI/API/Controllers/LoginProxyForWebClientController.php index 46c0b8e5e..49fd47da6 100644 --- a/app/Containers/AppSection/Authentication/UI/API/Controllers/LoginProxyForWebClientController.php +++ b/app/Containers/AppSection/Authentication/UI/API/Controllers/LoginProxyForWebClientController.php @@ -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; @@ -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 { diff --git a/app/Containers/AppSection/Authentication/UI/API/Tests/Functional/ApiLoginProxyForWebClientTest.php b/app/Containers/AppSection/Authentication/UI/API/Tests/Functional/ApiLoginProxyForWebClientTest.php index 3f388963e..235cb3b52 100644 --- a/app/Containers/AppSection/Authentication/UI/API/Tests/Functional/ApiLoginProxyForWebClientTest.php +++ b/app/Containers/AppSection/Authentication/UI/API/Tests/Functional/ApiLoginProxyForWebClientTest.php @@ -32,24 +32,6 @@ public function testClientWebAdminProxyLogin(): void $this->assertResponseContainKeys(['expires_in', 'access_token']); } - public function testClientWebAdminProxyUnconfirmedLogin(): void - { - $data = [ - 'email' => 'testing2@mail.com', - 'password' => 'testingpass', - 'email_verified_at' => null, - ]; - $this->getTestingUser($data); - - $response = $this->makeCall($data); - - if (config('appSection-authentication.require_email_confirmation')) { - $response->assertStatus(409); - } else { - $response->assertStatus(200); - } - } - public function testLoginWithNameAttribute(): void { $data = [ diff --git a/app/Ship/Kernels/HttpKernel.php b/app/Ship/Kernels/HttpKernel.php index d7befc052..3674a80fc 100644 --- a/app/Ship/Kernels/HttpKernel.php +++ b/app/Ship/Kernels/HttpKernel.php @@ -85,12 +85,14 @@ class HttpKernel extends LaravelHttpKernel 'cache.headers' => SetCacheHeaders::class, // Note: This middleware is disabled because Authorization functionality is provided by the Authorization container. // 'can' => \Illuminate\Auth\Middleware\Authorize::class, - // Note: The "guest" Middleware is registered by MiddlewareServiceProvider in Authentication Container + // Note: The "guest" middleware is registered by MiddlewareServiceProvider in Authentication Container // 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => RequirePassword::class, 'signed' => ValidateSignature::class, 'throttle' => ThrottleRequests::class, - 'verified' => EnsureEmailIsVerified::class, + // Note: The "verified" middleware is custom implemented in Authentication Container (Middlewares/EnsureEmailIsVerified) + // and registered by its MiddlewareServiceProvider + // 'verified' => EnsureEmailIsVerified::class, ]; /**