You can achieve this by issuing a extra cookie (lets say browser_cookie) to remember the already authenticated browser.
Implementation:
Create a following table (browser_management) :
token (pk)| user_id (fk) | series_identifier
where:
token : hashed form of token issued to the user (using bcrypt or similar algorithm ) (token issued to user, itself, is unguessable randomly generated key from a suitably large space)
series_identifier : unguessable randomly generated key from a suitably large space
Whenever, user logs in check for the browser_cookie
.
Case 1: User is logging for the first time.
Considering user is logging in for first time browser_cookie
won't be present. So, you would send an email with the authentication code.
Once authenticated, generate two random numbers each for token
and series_identifier
. Store the hashed token
and series_identifier
in the browser_management
table, for the user identified by user_id
.
Also, issue the browser_cookie
to the user with token
and series_identifier
.
Case 2: User is re-logging next time.
Now, when same user logs in next time,take the token
and find the entry in the browser_management
table with the hashed token
.
If found, check if the user_id
and series_identifier
matches.
Case 2.1: Entries matched:
Allow the user to enter the system without the need to re-authenticate the email code.
Generate another token and replace the token
in the cookie
as well as table
with the new one. (This will lower the risk of session hijacking).
Case 2.2: Entries does not match:
Follow the steps for email authentication and notify the user regarding the possible theft.(Like gmail notifying new browser logins).
References:
Update:
Sample code:
Migration:
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class browser_management extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('browser_management', function (Blueprint $table) {
$table->string('token');
$table->string('user_id');
$table->string('series_identifier');
$table->timestamps();
$table->primary('token');
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('users');
}
}
Middleware: Create a new middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Cookies;
class Email_verification
{
public function handle($request, Closure $next, $guard = null)
{
//retrieve $token from the user's cookie
$token = $request->cookie('browser_cookie');
//check id token is present
if($token == null){
//if token is not present allow the request to the email_verification
return $next($request);
}
else{
//Retrieve the series_identifier issued to the user
$series_identifier = Auth::user()
->series_identifier(Hash::make($token))
->first()
->series_identifier;
//Check if series_identifier matches
if($series_identifier != $request->cookie('series_identifier')){
//if series_identifier does not match allow the request to the email_verification
return $next($request);
}
}
return redirect('/dashboard'); //replace this with your route for home page
}
}
Make the middleware's entry in the kernel.php
protected $routeMiddleware = [
'email_verification' => \App\Http\Middleware\Email_verification::class,
//your middlewares
];
User Model: Add the following method's to your user model
// method to retrieve series_identifier related to token
public function series_identifier($token){
return $this->hasMany(Browser_management::class)->where('token',$token);
}
//method to retriev the tokens related to user
public function tokens (){
return $this->hasMany(Browser_management::class);
}
Browser_management Model: Create a model to represent browser_managements table
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Browser_management extends Model
{
protected $primaryKey = 'token';
protected $fillable = array('token','series_identifier');
public function User(){
return $this->hasOne('App\Models\User');
}
}
Email Verification Methods: Add the following methods to your AuthController for handling the email verification
public function getVerification(Request $request){
//Create a random string to represent the token to be sent to user via email.
//You can use any string as we are going to hash it in our DB
$token = str_random(16);
//Generate random string to represent series_identifier
$series_identifier = str_random(64);
//Issue cookie to user with the generated series_identifier
Cookie::queue('series_identifier', $series_identifier,43200,null,null,true,true);
//Store the hashed token and series_identifier ini DB
Auth::user()->tokens()->create(['token'=>Hash::make($token)]);
//Your code to send an email for authentication
//return the view with form asking for token
return view('auth.email_verification');
}
public function postVerification(Request $request){
//Retrieve the series_identifier issued to the user in above method
$series_identifier = $request->cookie('series_identifier');
//Retrieve the token associated with the series_identifier
$token = Auth::user()
->tokens()
->where('series_identifier',$series_identifier)
->first()
->value('token');
//Check if the user's token's hash matches our token entry
if(Hash::check($request->token,$token)){
// If token matched, issue the cookie with token id in it. Which we can use in future to authenticate the user
Cookie::queue('token', $token,43200,null,null,true,true);
return redirect('dashboard');
}
//If token did not match, redirect user bak to the form with error
return redirect()->back()
->with('msg','Tokens did not match');
}
Routes: Add these routes for handling email verification requests. We will also add the email_verification middleware to it.
Route::get('/auth/email_verification',`AuthController@getVerification')->middleware('email_verification');
Route::post('/auth/email_verification',`AuthController@postVerification')->middleware('email_verification');<br/>
Update 2:
Regarding the flow of gmail..
I followed following steps:
1)Log into gmail followed by 2-step verification.
2)Log out
3)Clear cache link
4)Log in again
When I loged in again, after clearing the cache, it did not ask me for 2-step verification.
Though, If you clear the cookies, it will ask for 2-step verification.
Reason:
All the user data which identifies the user (here token
) is stored in the cookies. And if you clear the cookies, server will have no mechanism to identify the user.
Update 3:
Gmail asking for 2-step verification:
First of all Gmail or any other website is not notified about the clearing of cache
As given here:
The cache is nothing more than a place on your hard disk where the
browser keeps things that it downloaded once in case they’re needed
again.
Now, cookies are the small text files issued by server to store user related information. As given here
The main purpose of a cookie is to identify users and possibly prepare
customized Web pages or to save site login information for you.
So, basically when you clear the cookies in your browser, webserver will not get any user data. So, the user will be considered as guest and will be treated accordingly.