Related
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());
}
I have two different tables for users credentials: "users" and "contacts". In contacts I have stored users emails, phone and other contacts informations. I want to use the emails in contacts table for the login and authentication methods in Laravel but I don't know how to setup the User and Contacts models or something else that I need. Any idea how can I use contacts and users tables for the login in the application? I'm new in Laravel, any help is appreciated, thank you!
Below a basic tables structure in mysql db:
users: id | username | password
contacts: email | phone | other | user_id
In your LoginController, you can overwrite the attemptLogin method as follow:
public function attemptLogin(Request $request) {
$contact = Contact::where('email', $email)->first();
if (Auth::attempt(['id' => $contact->user_id, 'password' => $password])) {
// Authentication passed...
}
}
OR
public function attemptLogin(Request $request) {
$user = User::whereHas('contacts', function($query){
$query->where('email', $email);
});
if (Auth::login($user)) {
// Authentication passed...
}
}
Instead of overwriting attemptLogin(), which somehow does not work out for me. I prefer overwriting credentials(), which ensures that all auth which is "correctly" implemented will use the "correct" values.
How this could look like:
/**
* Get authorization credentials from request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function credentials(Request $request)
{
// username is set in LoginController to email
$credentials = $request->only($this->username(), 'password');
$contact = Contact::where($this->username(), $credentials[$this->username()])->first();
// if we've not found a contact, we fallback to default
if (! $contact) {
return $credentials;
}
// else we set user_id and password as credential params
return array_merge($request->only('password'), [
'user_id' => $contact->user_id,
]);
}
I'm currently using 2 projects. 1 front end (with laravel backend to communicate with API) and another laravel project (the API).
Now I use Laravel Passport to authenticate users and to make sure every API call is an authorized call.
Now when I want to log out my user, I send a post request to my API (with Bearer token) and try to log him out of the API (and clear session, cookies,...)
Then on the client I also refresh my session so the token is no longer known. Now when I go back to the login page, it automatically logs in my user. (Or my user is just still logged in).
Can someone explain me how to properly log out a user with Laravel passport?
Make sure that in User model, you have this imported
use Laravel\Passport\HasApiTokens;
and you're using the trait HasApiTokens in the User model class using
use HasApiTokens
inside the user class.
Now you create the log out route and in the controller,
do this
$user = Auth::user()->token();
$user->revoke();
return 'logged out'; // modify as per your need
This will log the user out from the current device where he requested to log out. If you want to log out from all the devices where he's logged in. Then do this instead
$tokens = $user->tokens->pluck('id');
Token::whereIn('id', $tokens)
->update(['revoked'=> true]);
RefreshToken::whereIn('access_token_id', $tokens)->update(['revoked' => true]);
Make sure to import these two at the top
use Laravel\Passport\RefreshToken;
use Laravel\Passport\Token;
This will revoke all the access and refresh tokens issued to that user. This will log the user out from everywhere. This really comes into help when the user changes his password using reset password or forget password option and you have to log the user out from everywhere.
You need to delete the token from the database table oauth_access_tokens
you can do that by creating a new model like OauthAccessToken
Run the command php artisan make:model OauthAccessToken to create the model.
Then create a relation between the User model and the new created OauthAccessToken Model , in User.php add :
public function AauthAcessToken(){
return $this->hasMany('\App\OauthAccessToken');
}
in UserController.php , create a new function for logout:
public function logoutApi()
{
if (Auth::check()) {
Auth::user()->AauthAcessToken()->delete();
}
}
In api.php router , create new route :
Route::post('logout','UserController#logoutApi');
Now you can logout by calling posting to URL /api/logout
This is sample code i'm used for log out
public function logout(Request $request)
{
$request->user()->token()->revoke();
return response()->json([
'message' => 'Successfully logged out'
]);
}
Create a route for logout:
$router->group(['middleware' => 'auth:api'], function () use ($router) {
Route::get('me/logout', 'UserController#logout');
});
Create a logout function in userController ( or as mentioned in your route)
public function logout() {
$accessToken = Auth::user()->token();
DB::table('oauth_refresh_tokens')
->where('access_token_id', $accessToken->id)
->update([
'revoked' => true
]);
$accessToken->revoke();
return response()->json(null, 204);
}
I am using Laravel 6.12.0, below function is working for me.
public function logout(Request $request){
$accessToken = Auth::user()->token();
$token= $request->user()->tokens->find($accessToken);
$token->revoke();
$response=array();
$response['status']=1;
$response['statuscode']=200;
$response['msg']="Successfully logout";
return response()->json($response)->header('Content-Type', 'application/json');
}
This is my first post.. and i find a clean solution (Laravel last Version)
/**
* Logout api
*
* #return \Illuminate\Http\Response
*/
public function logout(Request $request)
{
if (Auth::check()) {
$token = Auth::user()->token();
$token->revoke();
return $this->sendResponse(null, 'User is logout');
}
else{
return $this->sendError('Unauthorised.', ['error'=>'Unauthorised'] , Response::HTTP_UNAUTHORIZED);
}
}
Below is the simplest way I found to do it.
1. USE database SESSION INSTEAD OF file SESSION
Official documention
php artisan session:table
php artisan migrate
Replace SESSION_DRIVER=file by SESSION_DRIVER=database in your .env file.
2. DELETE USER SESSION RIGHT AFTER LOGIN
After a user is redirected to your frontend and logs in to finally get a token, you probably call a route in api/routes.php to get the user information, that's where I'm closing the user backend session before sending back user information to the frontend:
Route::middleware('auth:api')->get('/user', function (Request $request) {
// Close user session here
Illuminate\Support\Facades\DB::table('sessions')
->whereUserId($request->user()->id)
->delete();
return $request->user();
});
3. REVOKE TOKENS AT LOGOUT
Then, to "log out" (actually, revoke tokens) the user from the frontend, you just need to call another route to revoke the token and refresh_token:
Route::middleware('auth:api')->post('/logout', function (Request $request) {
// Revoke access token
// => Set oauth_access_tokens.revoked to TRUE (t)
$request->user()->token()->revoke();
// Revoke all of the token's refresh tokens
// => Set oauth_refresh_tokens.revoked to TRUE (t)
$refreshTokenRepository = app('Laravel\Passport\RefreshTokenRepository');
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($request->user()->token()->id);
return;
});
You may prefer to put these two closures in the UserController.
Hope help someone:
if (Auth::check()) {
$request->user()->tokens->each(function ($token, $key) {
$token->delete();
});
}
Good Luck.
I use this in my project to logout from multiple device.
public function logout(Request $request, $devices = FALSE)
{
$this->logoutMultiple(\Auth::user(), $devices);
return response()->json([], 204);
}
private function logoutMultiple(\App\Models\User $user, $devices = FALSE)
{
$accessTokens = $user->tokens();
if ($devices == 'all') {
} else if ($devices == 'other') {
$accessTokens->where('id', '!=', $user->token()->id);
} else {
$accessTokens->where('id', '=', $user->token()->id);
}
$accessTokens = $accessTokens->get();
foreach ($accessTokens as $accessToken) {
$refreshToken = \DB::table('oauth_refresh_tokens')
->where('access_token_id', $accessToken->id)
->update(['revoked' => TRUE]);
$accessToken->revoke();
}
}
Try this code to help you to logout from passport authentication.
Route::post('/logout', function(){
if (Auth::check()) {
Auth::user()->AauthAcessToken()->delete();
}
return response()->json([
'status' => 1,
'message' => 'User Logout',
], 200);
});
check whether your model contains OauthAccessToken which needs to connect with the database oauth_access_tokens. The access token is stored in the database table oauth_access_tokens. and makes a relation from users to oauth_access_tokens.
public function AauthAcessToken(){
return $this->hasMany(OauthAccessToken::class);
}
You can use following code to remove to token for logged in user.
$request->user()->token()->revoke();
If you want to learn about this in-depth then watch this tutorial:
https://www.youtube.com/watch?v=UKSQdg1uPbQ
public function logout(Request $request)
{
$request->user()->token()->revoke();
if ($request->everywhere) {
foreach ($request->user()->tokens()->whereRevoked(0)->get() as $token) {
$token->revoke();
}
}
return response()->json(['message' => 'success']);
}
i was using laravel bcrypt authentication in a back end application but client asked plain password authentication so that he can see the password of each user as administrator. My whole app logic is on laravel inbuilt authentication method an bcrypt hashing. how can i replace it to authenticate with plain password mach stored in database instead of storing hash ?
class AuthController extends Controller
{
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
public function __construct()
{
$this->middleware('guest', ['except' => ['getLogout', 'getLogin']]);
}
public function postLogin()
{
$data = \Request::all();
$rules = [
'email' => 'required|email|max:255|exists:users',
'password' => 'required|exists:users'
];
$validator = \Validator::make($data, $rules);
if ($validator->fails()) {
//login data not exist in db
return redirect('/login')->withErrors($validator)->withInput();
} else {
$email = Request::input('email');
$pass = Request::input('password');
//in my table users, status must be 1 to login into app
$matchWhere = ['login' => $email, 'password' => $pass, 'status' => 1];
$count = \App\User::where($matchWhere)->count();
if ($count == 1) {
$user = \App\User::where($matchWhere)->first();
Auth::loginUsingId($user->id);
return redirect()->intended('/');
} else {
//not status active or password or email is wrong
$validator->errors()->add('Unauthorized', 'Not accepted in community yet');
return redirect('/login')->withErrors($validator)->withInput();
}
}
}
public function getLogin()
{
if (Auth::check()) {
return redirect()->intended('/');
} else {
return view('auth.login');
}
}
public function getLogout()
{
Auth::logout();
return redirect()->intended('/login');
}
}
If you are now using Laravel 5^, you can do that by searching for the class Illuminate/Auth/EloquentUserProvider and do some minor tweaks in there.
For e.g. find the public function retrieveByCredentials() and validateCredentials(). In the second function, you can see that the laravel is checking the hashed passwords to be fed into Auth::attempt() method. Just change it to plain checking and you are done.
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials)) {
return;
}
// 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
// Eloquent User "model" that will be utilized by the Guard instances.
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value) {
if (! Str::contains($key, 'password')) {
$query->where($key, $value);
}
}
return $query->first();
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['password'];
return $this->hasher->check($plain, $user->getAuthPassword());
}
Change $this->hasher->check to normal check and you will be done. :)
In laravel 4 you could have rewritten the HASH module . This stackoverflow thread explains how to use SHA1 instead of bycrypt [ check the accepted answer and comments ] .
You can make use of the method explained here and save your password without hashing .
Well, that really compromises your client's website security.
Storing plain passwords in the DB is not recommended at all. If someone gained access to the database his/her site will be really vulnerable, anyone with a copy of the database would have easy access to all kind of accounts. I insist you should create a reset/change password functionality instead of storing plain passwords in the DB.
Anyway, you could just get the plain password with
$password = Input::get('password');
And I guess you could authenticate users with
if (Auth::attempt(array('password' => $password)))
{
return Redirect::route('home');
}
Wow, these are all so complicated, it's as simple as.
if ($user = User::where('email', request()->email)->where('password', request()->password)->first()) {
Auth::login($user);
return redirect()->to('/');
}
Though I do agree that in a production environment you should not do this. But I can see for some applications if the users are aware the passwords are stored in plain text it may be ok.
I am trying to incorporate Socialite into a site and have come across a problem with what should be a rather trivial thing to do - change where the user is redirected after authenticated by a social provider.
I have three actions that I will be using Socialite for:
Register - Authenticate with the provider and then redirect to the Registration Form. Their name and email address are filled in for them, more fields are required.
Login - Typical login scenario, the provider information is checked against the Users table and they are logged in if there is a match
Link - User already has an active account but would like to link an external (Faceebook or Google) account. This authenticates the user and adds the proper social media ID to their row in the Users table.
The problem is that you specify the authentication redirect in the services.php file, I cannot figure out how to change where the user is redirected after successfully authenticating with the third party provider.
Here is what I have so far -
Route: This seems to work up to when the social provider redirects, because driver=facebook&action=login (for example) is missing. Ideally, I'd like to specify where the user is redirected when the initial auth request is made - this was just my first attempt at figuring this out.
get('social-auth', function(AuthenticateUser $authenticateUser, Request $request) {
$driver = $request->input('driver');
$action = $request->input('action');
return $authenticateUser->execute($request->has('code'), $driver, $action);
});
AuthenticateUser class - Hopefully the comments here are sufficient:
<?php namespace App;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Laravel\Socialite\Contracts\Factory as Socialite;
use Auth;
use Flash;
class AuthenticateUser
{
/**
* #var Socialite
*/
private $socialite;
/**
* #var Auth
*/
private $auth;
public function __construct(Socialite $socialite, Auth $auth)
{
$this->socialite = $socialite;
$this->auth = $auth;
}
/**
* #param boolean $hasCode Whether or not we have been authenticated already
* #param string $driver Driver to use, Facebook or Google
* #param string $type Type of call - Login, Register, or Link
* #return array|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function execute($hasCode, $driver, $type)
{
if (!$hasCode) {
return $this->getAuthorizationFirst($driver);
}
// Get the User details from the Social Provider
$socialUser = $this->socialite->driver($driver)->user();
$socialUserArray = $socialUser->user;
if ($driver == 'facebook') {
// Get Facebook specific fields
$first_name = $socialUserArray['first_name'];
$last_name = $socialUserArray['last_name'];
} else if ($driver == 'google') {
// Get Google specific fields
$first_name = $socialUserArray['name']['givenName'];
$last_name = $socialUserArray['name']['familyName'];
}
$email = $socialUser->email;
$id = $socialUser->id;
// Perform an action - login, register, or link
switch ($type) {
case 'login':
// Log the user in with the Facebook ID
try {
if ($driver == 'facebook') {
$user = User::whereFacebookAuth($id)->firstOrFail();
} else {
$user = User::whereGoogleAuth($id);
}
} catch (ModelNotFoundException $e) {
flash::error('Could not find a user associated with this ' . ucfirst($driver) . ' account.');
return redirect('auth/login');
}
Auth::login($user);
return redirect('members');
break;
case 'register':
// Register using social media account
return redirect('register')
->with('social_type', $driver)
->with('social_id', $id)
->with('email', $email)
->with('first_name', $first_name)
->with('last_name', $last_name);
break;
case 'link':
// Associate this Social Media account with the current User
$driver == 'facebook' ? Auth::user()->facebook_auth = $id : Auth::user()->google_auth = $id;
Auth::user()->save();
return ['status' => 'success'];
break;
}
}
/**
* Authorize the user before proceeding
* #param $driver
* #return mixed
*/
private function getAuthorizationFirst($driver)
{
return $this->socialite->with($driver)->redirect('hopefully-somewhere-else');
// Anything inside redirect doesn't seem to do anything
}
}
I tend to include the provider in my URLs. For example:
Route::get('auth/{provider}', 'AuthController#redirectToProvider');
Route::get('auth/{provider}/callback', 'AuthController#handleProviderCallback');
That way I can access the provider name from the request in my controller actions:
public function callback($provider)
{
$user = $this->socialite->driver($provider)->getUser();
try {
// Try and find user by their social profile UID
$appUser = SocialAccount::whereUid($user->getId())
->whereProvider($provider)
->firstOrFail();
Auth::loginUsingId($appUser->user_id);
} catch (ModelNotFoundException $e) {
if (Auth::user()) {
// Attach social profile to logged in user
} else {
// User is not logged in, and account does not exist
// Prompt to register
}
}
}
Late to join party but may help out others. If you want to handle multiple redirection like login, register, link social account to user profile, that can be done by overriding service config from your controller side.
config(['services.google.redirect' => <Your New URL here> ]);
Socialite::driver('google')->redirect();
Note :- There are some social platforms that strictly requires to mention redirects URL into their developer console. So you need to mentioned each redirect URL there.