Does Laravel Auth::check() always connect to db to check user? - laravel

I'm using Auth::check() to check user's login status.
Does Auth::check() connect to database to perform every login check?

Auth::check() verifies that the current session has an authenticated user, either already verified or from the session (which gonna use DB first time) or null.
Illuminate\Auth\GuardHelpers.php
**
* Determine if the current user is authenticated.
*
* #return bool
*/
public function check()
{
return ! is_null($this->user());
}
Example # Illuminate\Auth\RequestGuard.php
/**
* Get the currently authenticated user.
*
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
// If we've already retrieved the user for the current request we can just
// return it back immediately. We do not want to fetch the user data on
// every call to this method because that would be tremendously slow.
if (! is_null($this->user)) {
return $this->user;
}
return $this->user = call_user_func(
$this->callback, $this->request, $this->getProvider()
);
}

Instead of the RequestGuard, the default guard is the SessionGuard. And yes, the first time you call Auth::check(), it will perform one database lookup to check for the currently logged in User.
After that, every consecutive call in the same request will not perform another database lookup.

The check() method do this:
/**
* Determine if the current user is authenticated.
*
* #return bool
*/
public function check()
{
return ! is_null($this->user());
}
Now, the interesting part is what the user() method does. You can see it in detail and well explained in the source code:
public function user()
{
if ($this->loggedOut) {
return;
}
// If we've already retrieved the user for the current request we can just
// return it back immediately. We do not want to fetch the user data on
// every call to this method because that would be tremendously slow.
if (! is_null($this->user)) {
return $this->user;
}
$id = $this->session->get($this->getName());
// First we will try to load the user using the identifier in the session if
// one exists. Otherwise we will check for a "remember me" cookie in this
// request, and if one exists, attempt to retrieve the user using that.
if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
$this->fireAuthenticatedEvent($this->user);
}
// If the user is null, but we decrypt a "recaller" cookie we can attempt to
// pull the user data on that cookie which serves as a remember cookie on
// the application. Once we have a user we can return it to the caller.
if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
$this->user = $this->userFromRecaller($recaller);
if ($this->user) {
$this->updateSession($this->user->getAuthIdentifier());
$this->fireLoginEvent($this->user, true);
}
}
return $this->user;
}

Related

Where is remember_me cookie checked in Laravel?

When I login using the remember_me feature in Laravel 5.8, a remember_me token is stored in the users table and in the cookie. When a user opens the page with expired session, then at some place in Laravel the cookie value must be compared to the remember_token and re-login the user. But where exactly does this take place?
I checked Illuminate\Auth\SessionGuard but could not find that anywhere. I need to catch this event, because I need to update the session on my Login-server using the refresh token.
Its in Illuminate\Auth\SessionGuard in the user() method.
There is this code section:
// If the user is null, but we decrypt a "recaller" cookie we can attempt to
// pull the user data on that cookie which serves as a remember cookie on
// the application. Once we have a user we can return it to the caller.
$recaller = $this->recaller();
if (is_null($this->user) && ! is_null($recaller)) {
$this->user = $this->userFromRecaller($recaller);
if ($this->user) {
$this->updateSession($this->user->getAuthIdentifier());
$this->fireLoginEvent($this->user, true);
}
}
The variable $recaller holds the cookie value.
The method userFromRecaller will try to retrieve user by Token:
protected function userFromRecaller($recaller)
{
if (! $recaller->valid() || $this->recallAttempted) {
return;
}
// If the user is null, but we decrypt a "recaller" cookie we can attempt to
// pull the user data on that cookie which serves as a remember cookie on
// the application. Once we have a user we can return it to the caller.
$this->recallAttempted = true;
$this->viaRemember = ! is_null($user = $this->provider->retrieveByToken(
$recaller->id(), $recaller->token()
));
return $user;
}
From here you have to check which provider driver you have specifified in config/auth.php (eloquent or database), depending on that you can continue the path on the method retrieveByToken on EloquentUserProvider or DatabaseUserProvider.
For example, the EloquentUserProvider will compare the token value with the remembertoken of the model like this:
public function retrieveByToken($identifier, $token)
{
$model = $this->createModel();
$model = $model->where($model->getAuthIdentifierName(), $identifier)->first();
if (! $model) {
return null;
}
$rememberToken = $model->getRememberToken();
return $rememberToken && hash_equals($rememberToken, $token) ? $model : null;
}
The method getRememberToken() is definied on the Users model via the Authenticatable trait.

Redirecting a particular user to a specific view in laravel

I have been thinking so so hard on how to go about it. I have a single registration form for all users. However, I want to redirect a particular user to a specific route but other users will be redirected to another page upon registration.
How do I tell laravel’s request method that when this particular user registers, then redirect to this specific page. But others, send them somewhere else. I have nothing in the form to indicate the user, I would like to use request to detect that user. Please someone come to my aid. Thanks.
You can override the registered method on the RegisterController to handle the response after the user is registered:
use Illuminate\Http\Request;
...
/**
* The user has been registered.
*
* #param \Illuminate\Http\Request $request
* #param mixed $user
* #return mixed
*/
protected function registered(Request $request, $user)
{
// $user is the currently registered user
if ($user->nationality == ...) {
return redirect()->route(..., [...]);
}
...
// if that information isn't being stored on the User itself
// you can still pull it from the Request
// $request->input('nationality');
}
An idea to get you started. Just make sure to return some type of Response from this method otherwise it will end up defaulting to its normal redirect location after registration.
You could potentially have a list of Locations to locales(assuming) that could be generated from a static array or even a database query.
$nationalities = [
'Spain' => 'es',
'England' => 'en',
...
];
return redirect()->route(
'some-where',
['locale' => $nationalities[$nationality] ?? 'en']
);
I am not sure what URLs you are trying to generate; that is just an example above.
not sure if this helps - but in my app i redirect user based on roles using the following method:
public function handle($request, Closure $next)
{
$user_role = $request->user()->role();
switch ($user_role) {
case 'admin':
return redirect('admin/home');
break;
case 'regular':
return redirect('account/home');
break;
case 'manager':
return redirect('manager/home');
break;
case 'supervisor':
return redirect('supervisor/home');
break;
default:
return redirect('account/home');
break;
}
return $next($request);
}

Laravel HTTP Redirect in a Controller API Call

I am updating a Laravel 5.2 project to restrict access based on an array of allowed user ids that I am extracting from an SSO token. The setup of my application is that there is a custom middleware class created to handle SSO stuff. I want users whose ids are in the restricted array to not be able to access certain pages. It works half-way: users whose ids are considered restricted can allow access an array of pages. However, those pages use API calls, and while the users can access those pages, when they view them the pages are broken because the API calls are being restricted.
Here's the relevent piece of code in my middleware:
//Check to see if user is limited access user
$user_id = $request->server('ssotokenid');
if(in_array($user_id, $this->restrictedUsers))
{
//If restricted user, ensure that requested page is within allowable access array
$uri = $request->path();
//If allowed page
if(in_array($uri, $this->allowedPagesArray))
return $next($request);
else
return redirect('/restricted-landing-page');
}
//Continue on to oher stuff here...this works fine
Here is an example of a page in my routes.php; note that I am using the controller method instead of get:
Route::controller('/subdashboard/subpage', 'Dashboard\PageController');
Here is an example of some methods within this controller:
class PageController extends DataController
{
protected $dashboard = 'my-dashboard';
protected $page = 'my-page';
private $table = 'my-db-table';
/**
* Render page
*
* #param \Illuminate\Http\Request $request
*
* #return \Illuminate\View\View
*/
public function getIndex()
{
return view("$this->dashboard/$this->page", [
'appUrl' => $this->rootPath,
]);
}
/**
* An example method and first API call that runs when the page loads
* #param none
* #return array
*/
public function getData()
{
$data= $this->db->table($this->table)
->lists('myfield');
return response()->json($data);
}
My view file for the page pulls in a js file. The first thing that loads is an API call to the getData method in the controller. Example:
function init()
{
//This should call the getData method in the above controller
//This fails here
$.getJSON(apiURL + '/data/')
.done( function(returnData) {
//do stuff with json response
})
}
The above API call fails, because the HTTP request is resolving to the allowedpage/data URL. The user is able to go to allowedpage, but anything after that fails because instead of fetching data from allowedpage/data, the request is redirected to 'restricted-landing-page'.
I have tried to use strpos to determine if URL contains allowedpage, but that is not working.
//If allowed page
if(in_array($uri, $this->allowedPagesArray))
return $next($request);
else
{
foreach($this->allowedPagesArrayas $page)
{
if(strpos($page, $uri) !== false)
return $next($request);
}
return redirect('/restricted-landing-page');
}
Does anyone have any ideas on how to resolve this?
I'm an idiot. I was using the parameters in strpos() in the wrong order. When I corrected this, it works fine. Also, it occurred to me that I could get rid of the in_array comparison, and just use strpos.
So instead of this...
//If allowed page
if(in_array($uri, $this->allowedPagesArray))
return $next($request);
else
{
foreach($this->allowedPagesArrayas $page)
{
if(strpos($page, $uri) !== false)
return $next($request);
}
return redirect('/restricted-landing-page');
}
I updated it to this:
foreach($this->allowedPagesArrayas $page)
{
if(strpos($uri, $page) !== false)
return $next($request);
}
return redirect('/restricted-landing-page');

How to Verify Email Without Asking the User to Login to Laravel

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());
}

Laravel API-Auth-Guard is not case-sensitive?

i'm prototyping an API with Laravel and noticed that the API-Token is not case-sensitive when using the standard Auth-Guard for API. So api_tokens like 'CVC' and 'cvc' are treated the same.
Is that an expected behaviour? Is that ideal in regard of security? Dont think so, even with a 60-byte-string, or what do you think? And is there a way to change that?
Thanks for your thoughts!
Carsten
This shouldn't be the case. Laravel attempts to resolve the token in several ways first
* Get the token for the current request.
*
* #return string
*/
public function getTokenForRequest()
{
$token = $this->request->query($this->inputKey);
if (empty($token)) {
$token = $this->request->input($this->inputKey);
}
if (empty($token)) {
$token = $this->request->bearerToken();
}
if (empty($token)) {
$token = $this->request->getPassword();
}
return $token;
}
Where that method is invoked when attempting to resolve an instance of the user:
/**
* Get the currently authenticated user.
*
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
// If we've already retrieved the user for the current request we can just
// return it back immediately. We do not want to fetch the user data on
// every call to this method because that would be tremendously slow.
if (! is_null($this->user)) {
return $this->user;
}
$user = null;
$token = $this->getTokenForRequest();
if (! empty($token)) {
$user = $this->provider->retrieveByCredentials(
[$this->storageKey => $token]
);
}
return $this->user = $user;
}
And the provider in this case is the DatabaseUserProvider, which the method retrieveByCredentials performs a strict case-sensitive check using the Database Factories ->where() method, no like is used, you can see that here:
public function retrieveByCredentials(array $credentials)
{
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// generic "user" object that will be utilized by the Guard instances.
$query = $this->conn->table($this->table);
foreach ($credentials as $key => $value) {
if (! Str::contains($key, 'password')) {
$query->where($key, $value);
}
}
// Now we are ready to execute the query to see if we have an user matching
// the given credentials. If not, we will just return nulls and indicate
// that there are no matching users for these given credential arrays.
$user = $query->first();
return $this->getGenericUser($user);
}
So no, your case is not typical, and likely there are other components in play here that we're not privy to.

Resources