I have a login route that makes sure that a user is verified before logging in like so.
Route::post('/login', [AuthController::class, 'login'])->middleware('userIsVerified');
However in the case of the user entering a wrong password, it passes through the middleware first and then returns that the user is not verified in case of the user is not verified.
How to tweak this middleware to make it run after the request is processed by the controller?
Here is the middleware code:
class UserIsVerified
{
public function handle(Request $request, Closure $next)
{
if (auth()->user()) {
$userIsVerified = auth()->user()->verified;
} else {
$user = Client::where('phone', $request->phone)->first();
if (!$user)
return response(['message' => 'User doesnt exist.'], Response::HTTP_NOT_FOUND);
$userIsVerified = $user->verified;
}
if (!! $userIsVerified)
return $next($request);
else
return response(['message' => 'User is not verified.'], Response::HTTP_UNAUTHORIZED);
}
}
Laravel middleware has one more method callled terminate(), this method apply after the response. Its called terminable middleware.
Read More About terminate
I have a project that it has two sections "admin" and "student".
I want to make a middleware that if a user who authenticated if his role was 1 automatically redirect it to admin panel and if his role was 2 automatically redirect it to student panel.
i made this middleware but when im test it i have "Too Many Rediredcts" error :
My Middleware Code :
if (auth()->check() && auth()->user()->role === 1){
return $next($request);
}else{
return redirect()->route('front.index');
}
My Routes :
How can i solve this?
I was facing the same issue and resolve it by using redirection with Url-like redirect('/admin/login'). Check and try the below code in your Laravel app.
if (auth()->check() && auth()->user()->role === 1){
return $next($request);
}else{
return redirect('/admin/login')->withErrors(['Access Denied']);
}
OR
if (auth()->check() && auth()->user()->role === 1){
return $next($request);
}else{
return redirect()->to('/admin/login')->withErrors(['Access Denied']);
}
Without seeing your middleware, it's hard to identify the problem, but the rest of your code looks fine. Try changing your your middleware to something like this:
public function handle(Request $request, Closure $next, string $roleName)
{
if (Auth::user()->isRole($roleName)) {
return $next($request);
}
abort(403);
}
We're passing a role into the middleware from the route, in your case you might pass admin. So any of your admin routes will now look like this:
Route::middleware('checkRole:admin')->prefix('admin')->group(function () {
Route::get('/', [BackController::class 'index'])->name('admin.index')
});
You are now passing a string variable to your middleware $roleName
Of course you'll also need to implement the isRole() method on your User.php Model. This will be something like:
/**
* #return HasOne
*/
public function role(): HasOne
{
return $this->hasOne(Role::class, 'id', 'role_id');
}
/**
* #return bool
*/
public function isRole($roleName): bool
{
$role = $this->role()->first();
if(!is_null($role) && $role->name == $roleName) {
return true;
}
return false;
}
I've made an assumption that you have a role_id column on your User.php Model, that you have a Role.php Model with name column which will match admin etc and you also have $routeMiddleware in Kernel.php something similar to 'checkRole' => checkRole::class,
Tested and working in Laravel 6/Laravel 8
If you wanted to test auth, I would update your route middleware to take an array, perhaps similar to:
['auth', 'checkRole:admin']
Where I have abort(403) you'll need a map of routes to roles, example: if you are an admin return redirect()->route('admin.index')
I am trying to protect a route using two middle-wares so that both expert and user can access the same route but as soon a user tries to access the route he is logged out.
I had created two middle-wares for expert and user and protect the route using these middle-wares.
Web.php
Route::group(['middleware' => ['expert','user']], function () {
Route::post('/showForm','UserController#showFormFilled');
});
User Middle ware
public function handle($request, Closure $next)
{
//////////////////// check if user is logged in ///////////////////
if(Auth::check())
{
////////////////// check user role id //////////////////////////
if(auth()->user()->role_id == 3)
{
return $next($request);
}
else if (auth()->user()->role_id==2)
{
return redirect('/expert');
}
}
else
{
return redirect('/login');
}
}
Expert Middle ware
public function handle($request, Closure $next)
{
if(Auth::check()){
if(auth()->user()->role_id == 2)
return $next($request);
else if (auth()->user()->role_id==3)
return redirect('/dashboard');
}
else {
return redirect('/login');
}
}
Both the users should be able to access the same route.
#hamzahummam - there is no way to achieve what you are looking for using the above separate-middlware-for-each-type method. Each middleware prematurely redirects [either to /dashboard or to /expert etc] the request without allowing it to passthrough other middleware. Best would be to use a third-party package that provides a more comprehensive and fine-grained access control [example: https://github.com/Zizaco/entrust]
If that's not an option, the best case would be to implement a single middleware and pass the role as parameter. See: Laravel Middleware Parameters
A minimal example would look like:
public function handle($request, Closure $next, $role)
{
// Assuming Auth::check() passes
$roleId = auth()->user()->role_id;
if ($roleId == 2 && strpos($role, 'expert') !== false) {
// Logged in user is `expert` and route allows `expert` access
return $next($request);
} else if ($roleId == 3 && strpos($role, 'user') !== false) {
// Logged in user is `user` and route allows `user` access
return $next($request);
} // and so on...
// Handle failures here
if ($roleId == 2 && strpos($role, 'expert') === false) {
// an `expert` is trying to access route that can't be accessed
return redirect('/expert-dashboard');
} // and so on...
}
You'd define routes as:
Route::group(['middleware' => ['new_middleware:expert,user' ]], function () {
Route::post('/showForm','UserController#showFormFilled');
});
Hope this helps.
I am developing a Laravel application. My application is using Laravel built-in auth feature. In the Laravel auth when a user registers, a verification email is sent. When a user verifies the email click on the link inside the email, the user has to login again to confirm the email if the user is not already logged in.
VerificationController
class VerificationController extends Controller
{
use VerifiesEmails, RedirectsUsersBasedOnRoles;
/**
* Create a new controller instance.
* #return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
public function redirectPath()
{
return $this->getRedirectTo(Auth::guard()->user());
}
}
I tried commenting on this line.
$this->middleware('auth');
But it's s not working and instead, throwing an error. How can I enable Laravel to be able to verify email even if the user is not logged in?
First, remove the line $this->middleware('auth');, like you did.
Next, copy the verify method from the VerifiesEmails trait to your VerificationController and change it up a bit. The method should look like this:
public function verify(Request $request)
{
$user = User::find($request->route('id'));
if (!hash_equals((string) $request->route('hash'), sha1($user->getEmailForVerification()))) {
throw new AuthorizationException;
}
if ($user->markEmailAsVerified())
event(new Verified($user));
return redirect($this->redirectPath())->with('verified', true);
}
This overrides the method in the VerifiesUsers trait and removes the authorization check.
Security (correct me if I'm wrong!)
It's still secure, as the request is signed and verified. Someone could verify another user's email address if they somehow gain access to the verification email, but in 99% of cases this is hardly a risk at all.
Here's a more future proof solution to the problem:
class VerificationController extends Controller
{
// …
use VerifiesEmails {
verify as originalVerify;
}
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth'); // DON'T REMOVE THIS
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
/**
* Mark the authenticated user's email address as verified.
*
* #param Request $request
* #return Response
*
* #throws AuthorizationException
*/
public function verify(Request $request)
{
$request->setUserResolver(function () use ($request) {
return User::findOrFail($request->route('id'));
});
return $this->originalVerify($request);
}
}
So when an email confirmation link is clicked by an unauthenticated user the following will happen:
User will be redirected to the login view 1
User enters credentials; logs in successfully 2
User will be redirect back to the email confirmation URL
Email will be marked as confirmed
1 The email will not be marked as confirmed at this point.
2 The user may enter bad credentials multiple times. As soon as he enters the correct credentials he will be redirected to the intended email confirmation URL.
// For Laravel 6 and Above
use Illuminate\Auth\Events\Verified;
use Illuminate\Http\Request;
use App\User;
// comment auth middleware
//$this->middleware('auth');
public function verify(Request $request)
{
$user = User::find($request->route('id'));
if (!hash_equals((string) $request->route('hash'), sha1($user->getEmailForVerification()))) {
throw new AuthorizationException;
}
if ($user->markEmailAsVerified())
event(new Verified($user));
return redirect($this->redirectPath())->with('verified', true);
}
Solution to allow email verification for users who are not logged in (i.e. without auth):
Changes to: app/Http/Controllers/Auth/VerificationController.php:
$this->middleware('auth'); to $this->middleware('auth')->except('verify');
Copy verify() method from the VerifiesEmails trait.
Edit verify method to work without expected $request->user() data.
My verify() method in the VerificationController looks like this:
public function verify(\Illuminate\Http\Request $request)
{
$user = User::find($request->route('id'));
if ($request->route('id') != $user->getKey()) {
throw new AuthorizationException;
}
if ($user->markEmailAsVerified())
event(new Verified($user));
return redirect()->route('login')->with('verified', true);
}
Signed middleware
Laravel uses a middleware named signed to check the integrity of URLs that were generated by the application. Signed checks whether the URL has been changed since it was created. Try changing the id, expiry time or the signature in the url and it will lead to an error - very effective and useful middleware to protect the verify() method
For more information: https://laravel.com/docs/8.x/urls#signed-urls
(Optional)
I redirected my users to the login route, rather than the intended route for two reasons. 1) After login, it would try to redirect the user to the email verification link, leading to an error; 2) I wanted to use the verified true flash data that was attached to the redirect, to show an alert on the login page, if the user had successfully verified their email address.
Example of my login page alert:
#if(session()->has('verified'))
<div class="alert alert-success">Your email address has been successfully verified.</div>
#endif
Suggestions
If you have any suggestions on how I could improve this code, please let me know. I'd be happy to edit this answer.
You should not remove $this->middleware('auth') altogether as that will effect the redirects. If you remove it, the unauthenticated users will be redirected to "/email/verify" instead of "/login"
so $this->middleware('auth'); will be changed to $this->middleware('auth')->except('verify'); in "VerificationController"
Also copy the "verify" function from "VerifiesEmails" into "VerificationController"
add these two lines of code at the top of the function
$user = User::find($request->route('id'));
auth()->login($user);
so you are logging in the user programmatically and then performing further actions
Here's my take on the situation. Verification requires user to login before it can complete the verification, so we can override the verify function and login user using ID we received in the link. It is safe cause verify function is not called if Laravel can't verify the signature from URL so even if someone temper the URL they won't be able to bypass it.
Go to your VerificationController and add the following function at the end of the file.
public function verify(Request $request)
{
if (!auth()->check()) {
auth()->loginUsingId($request->route('id'));
}
if ($request->route('id') != $request->user()->getKey()) {
throw new AuthorizationException;
}
if ($request->user()->hasVerifiedEmail()) {
return redirect($this->redirectPath());
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect($this->redirectPath())->with('verified', true);
}
Note
Make sure you have same_site value in 'config/session.php' set to 'lax'. If it is set to 'strict' then it won't persist session if you were redirected from another site. For example, if you click a verification link from Gmail then your session cookie won't persist, so it won't redirect you to dashboard, but it sets 'email_verified_at' field in the database marking the verification successful. The user won't get any idea what was happened because it will redirect the user to the login page. When you have set it to 'strict', it will work if you copy the verification link directly in the browser address bar but not if the user clicks the link from the Gmail web client because it uses redirect to track the link.
if you want to active user account without login you can do that in 2 steps
1- Remove or comment Auth middleware in VerificationController
Example below:
public function __construct()
{
//$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
2- since verify route passing the {id} you can just edit verify function to find the user by the route id request like code below :
file path : *:\yourproject\vendor\laravel\framework\src\Illuminate\Foundation\Auth\VerifiesEmails.php
$user = User::findOrfail($request->route('id'));
Complete example
public function verify(Request $request)
{
$user = User::findOrfail($request->route('id'));
if (! hash_equals((string) $request->route('id'), (string) $user->getKey())) {
throw new AuthorizationException;
}
if (! hash_equals((string) $request->route('hash'), sha1($user->getEmailForVerification()))) {
throw new AuthorizationException;
}
if ($user->hasVerifiedEmail()) {
return redirect($this->redirectPath())->with('verified', true);
}
if ($user->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect($this->redirectPath())->with('registered', true);
}
I change EmailVerificationRequest but i now this is wrong, any way it's work.
Warning
This change on the vendor
protected $user;
public function authorize()
{
$this->user = \App\Models\User::find($this->route('id'));
if ($this->user != null){
if (! hash_equals((string) $this->route('id'),
(string) $this->user->getKey())) {
return false;
}
if (! hash_equals((string) $this->route('hash'),
sha1($this->user->getEmailForVerification()))) {
return false;
}
return true;
}
return false;
}
To use inner laravel logic (without overriding the logic), we simply create $request->user() and call trait's verify method. And manually sign in the user when the verification is successful.
use VerifiesEmails {
verify as parentVerify;
}
public function verify(Request $request)
{
$user = User::find($request->route('id'));
if (!$user) return abort(404);
$request->setUserResolver(function () use($user) {
return $user;
});
return $this->parentVerify($request);
}
public function verified(Request $request)
{
Auth::login($request->user());
}
Route::group(['middleware' => ['role:0','auth']],function(){
Route::prefix('s')->group(function () {
Route::name('s.')->group(function ()
{Route::get('order/po/download/{item}', function($item = ''){
return response()->download(storage_path('app/public'.$item));
});
The routing URL will be like:
s/order/po/download/$item
The issue is everytime I access this url it will bounce to login page for no reason.
Can any help with this? It works in my local vagrant. However, when I push the project onto a live server, it always redirect to login even I have already logged in.
If I put it outside which is accessible for everyone before the Middleware Route (not the best solution but it works):
Route::get('order/po/download/{item}', function($item = ''){
return response()->download(storage_path('app/public'.$item));
});
Added Role middleware :-
public function handle($request, Closure $next,$role)
{
$user = Auth::user();
// Check if user has the role This check will depend on how your roles are set up
if($user->role_id == $role)
{
return $next($request);
}
else
{
return redirect()->back();
}
}
}