Adrián Daraš
My GitHub profile: https://github.com/atgg
This article was originally published on Medium.
It does not explain the idea of the password-less system, Justin Balthrop has explained it in Passwords are Obsolete.
Of course, you can save time creating an authentication system by using make:auth
, but that creates routes and views for general email–password authentication.
In this article, I'll show you how to create the password-less authentication system.
First of all, we need to make changes to our generated migrations.
If you are trying to follow this article on an existing application, please do not change anything in your existing migrations. Instead, create a new migration where you delete tables and fields from the database.
First of all, delete the create_password_resets_table
migration. It will be not needed as we will create our own password-less authentication system.
Now, make the following change to the create_users_table
migration:
Schema::create("users", function(Blueprint $table) {
$table->increments("id");
$table->string("name");
$table->string("email")->unique();
// $table->string("password"); // delete this line as we won't use passwords
$table->rememberToken();
$table->timestamps();
});
After changing the boilerplate code, we need to store tokens somewhere. Let's make a new migration by running php artisan make:migration --create=tokens create_tokens
and add the following code to the migration:
Schema::create("tokens", function(Blueprint $table) {
$table->increments("id");
$table->string("email")->index();
$table->string("token")->unique()->index();
// we need only expires_at
$table->timestamp("expires_at");
});
Notice that I added the email
column as a string instead of referring to the user in a relationship. That's because we will be creating user after a user clicks on a valid link.
Run php artisan migrate
to run all migrations.
After running all migrations, it's time to create a model for our tokens. Run php artisan make:model Token
and edit the Token
class. The model should use the Notifiable
trait:
use Illuminate\Notifications\Notifiable;
class Token extends Model {
use Notifiable;
}
Now, we can take advantage of the Notifications feature in Laravel to send emails with a link to authenticate users. Later in this article, we'll be creating a notification service.
We must tell the Token class that the model doesn't use timestamps and it's a good idea to tell the model to only accept keys that are specified in an array.
For this we'll be using $timestamps
and $fillable
properties:
protected $fillable = ["email"];
public $timestamps = false;
Of course, we also need to store a token string along with the email to a database.
We could set a token string in our controller, but I think it's better when the model handles the generation. Also, we need to set the expiration time for the token so it would be useless after it expires.
In order to assign default values to a model, we will use the boot() method that is run every time before an Eloquent operation runs.
Create the boot()
method in the Token
class:
use Carbon\Carbon;
protected static function boot() {
parent::boot();
static::creating(function($model) {
$model->expires_at = Carbon::now()->addMinutes(15);
$model->token = bin2hex(openssl_random_pseudo_bytes(16));
});
}
In this boot()
method, when a record is being created, a closure inside of the creating()
method assigns an expiry date and randomly generated hexadecimal token string to it.
If we want to find a valid token that matches our conditions written above, we should create a method so we don't need to type the conditions every time:
public static function valid($token) {
return static::where("token", $token)
->where("expires_at", ">", Carbon::now());
}
In order to execute the query, you need to invoke the ->first()
or
->firstOrFail()
method.
Although we've implemented the logic for token generation, we still need to connect a token with the user.
In the Token
class, create a method called user()
, which finds or creates a user:
public function user() {
$user = User::where("email", $this->email)->first();
return $user ?: User::create([
"email" => $this->email
]);
}
Run php artisan tinker
and try to get a user from a valid token:
And… it's broken. That's because in the auto-generated create_user
migration, it specified that the name field is required. We can either remove it or we can generate the value of it while creating a user.
Let's go to the User
model and create the boot()
method:
protected static function boot() {
parent::boot();
static::creating(function($model) {
$arr = explode("@", $model->email);
array_pop($arr);
$name = implode("@", $arr);
$model->name = $name;
});
}
The first three lines of the code extract the username part from an email. The last line assigns the results of the $name variable to the name field of a record.
Let's try creating a token and getting user from it again.
Here we go! The user()
method has returned the user associated with the token.
Let's go back to the Token
model. We will create another method called authenticate()
to authenticate a user by token:
use Illuminate\Support\Facades\Auth;
public function authenticate() {
Auth::login($this->user(), true);
$this->delete();
}
The second argument of Auth::login
specifies that the authenticated user should be remembered.
After you authenticate the user, you should delete the used token. It's better to wrap this into one method, because it's easy to forget to do the second part.
Enough models, let's focus on the notification service.
In order to deliver e-mails with a sign in link, we need to set up a notification service. But first, we need to set up a route for the sign in link. Open routes/web.php
and add a route:
Route::get("/auth/{token}", "AuthController@authenticate")
->name("auth.authenticate");
This route is named auth.authenticate
. We are using a named route because it will be easier to generate the link for this route.
You may have noticed that we are using AuthController
which we haven't created yet. Don't worry about it for now, we will be covering the controller part later.
Let's generate the service. Run php artisan make:notification AuthenticateUser
and go to the AuthenticateUser@toMail
method. Add the following code:
public function toMail($notifiable) {
$message = new MailMessage();
$url = route("auth.authenticate", $notifiable->token);
return $message
->line("Click on the button below to confirm your sign in.")
->action("Sign in", $url)
->line("This link will expire in 15 minutes.");
}
In this article I won't be covering the creation of Blade views. I'll leave it to you.
Also be sure to not forget to add names to the routes as redirects take advantage of the named routes.
Now that we've created the model and the notification service, let's add the rest of the routes:
Route::get(“/auth”, "AuthController@show")
->name("auth.show");
// this route must be before the route below
Route::get("/auth/sent", "AuthController@notified")
->name("auth.notified");
// we've added this one already
Route::get("/auth/{token}", "AuthController@authenticate")
->name("auth.authenticate");
Route::post("/auth", "AuthController@notify")
->name("auth.notify");
We must create the controller first. Run php artisan make:controller AuthController
.
After we have created the controller, notice that we’ve added routes for our four methods: show
, notified
, authenticate
and notify
. It's time to add them to the AuthController
class.
The show
method returns a view with the authentication form and the notified
method returns a view with a message that the email was sent.
The notify
method creates and sends a notification with a link, then it redirects to a route that displays a message that email was sent successfully:
use App\Notifications\AuthenticateUser;
use App\Token;
public function notify() {
$this->validate(request(), [
"email" => "required"
]);
$data = request()->only("email");
Token::create($data)->notify(new AuthenticateUser);
return redirect()->route("auth.notified");
}
Let's try to sign in! In my Laravel application, I have edited the Welcome page to display the authentication link:
When I click on the “Sign in / Register” link, it shows me a form where I can enter an email address (AuthController@show
):
In your form, enter your e-mail address and submit it. You should now see something like this (AuthController@notified
):
Let's check your inbox and you should see an email with the “Authenticate User” subject with a link to sign in:
Let's see, what happens when you click on the “Sign in” button:
It has thrown an exception because we didn't specify the authenticate()
method. We'll need to make a few changes for this method to work.
Let's add the authenticate()
method to the AuthController
:
use App\Token;
public function authenticate(Token $token) {
$token->authenticate();
return redirect("/");
}
Let's try logging in again. Enter your email to the form and try to click on the link. You should see a different error message:
It says No query results because normally implicitly bound models try to find a record with the find()
function which accepts an id by default.
If we want to customise the resolution logic, we need to do it in RouteServiceProvider@boot
. Go to that method and add a custom resolution logic:
use App\Token;
public function boot() {
parent::boot();
Route::bind("token", function($token) {
return Token::valid($token)->first();
});
}
Now whenever we implicitly bind a Token model to any of our routes, it will query a token with the conditions we have specified while creating the valid()
method instead of the default find()
method.
Let's try authenticating again and you should be successfully authenticated:
Now you can put all the pieces written in this article together and you'll have a functional password-less authentication system.
When users want to use your application, the only thing they will need to do is to enter their email and click on the link. No more forgotten passwords.