در حال بارگذاری ...

Email-only auth overview0:00

So, for the first episode in this series, let's review email-only authentication, which is getting more and more popular. As always, we start from scratch, so we'll call this email-only. Okay, so while that's doing its thing, yeah, the basic idea behind this is, rather than logging in with an email and a password, instead, and we'll use phpStorm today for fun. Anyways, yeah, instead, you provide an email address, we then send an email to that address, and we give you a unique login link. By clicking on it, you confirm that you own that email address, which means you have permission to sign in. So, you take the password aspect completely out of the equation, which is cool for lots

to sign in. So, you take the password aspect completely out of the equation, which is cool for lots of applications. Okay, so how do we tackle this? First, real quick, let me just say php artisan install framework so I can undo what I'm about to show you. Okay, so if you want, you could say php artisan make:auth, and this is going to basically scaffold everything you need to log in, to register, all that stuff. So, for example, if we go to, I have Laravel Valet set up, so if we go to email-only.dev, yeah, you can see it gives you some initial boilerplate you can use to register, log in,

So, for example, if we go to, I have Laravel Valet set up, so if we go to email-only.dev, yeah, you can see it gives you some initial boilerplate you can use to register, log in, reset your password, and all of that stuff. It's really great. However, in this particular situation, just to keep it simpler, I'm not going to do this. And that's because, well, one thing I want to show you. If we were to switch back, by the way, it does add some routing for you. But if I were to visit my AuthController, yeah, it gives you quite a bit of boilerplate here to handle the registration and also the authentication. So for example, if we go to authenticate users, and we come down, yeah, you can see their

Remove password and migrate2:09

So I'm going to say no. That's a cool little alias. If we check that out, all we're doing is resetting hard to the master branch. And then we're also cleaning out any newly created files that aren't being tracked. So now, if I do a git status, or by the way, I just have that alias to GS, all that stuff really saves you a lot of time. Anyways, you can see there's nothing there. We just have our initial commit. Okay, so first step, I think we're going to go, let me open this up for you. Let's go to our migration.

Okay, so first step, I think we're going to go, let me open this up for you. Let's go to our migration. So this is included out of the box no matter what. We know we're no longer going to have a password field, so I will get rid of that entirely. On that note, if we switch to the User model, I can get rid of the password fillable field as well. Cool. So now, let's go into our .env file. And like I often do, we're just going to use an sqlite database. Okay, so touch database/database.sqlite.

Build login routes and form2:59

And like I often do, we're just going to use an sqlite database. Okay, so touch database/database.sqlite. That is the default path. So we touch that file. And then I can say php artisan migrate, and that builds up the users table. And once again, in this case, you don't even need that. So you could delete that migration entirely. Anyways, at this point, let's switch over to our routes file and handle a login route as well as the actual authentication part where they click on the link, we verify that the token is correct, and then we sign them in.

as well as the actual authentication part where they click on the link, we verify that the token is correct, and then we sign them in. Okay, so first one, when you visit the login page, we're going to take you to, well, if we go to app/Http/Controllers/Auth, we still get an AuthController out of the box. We're just going to change it quite a bit. So get rid of PasswordController, that's not relevant. And this will take us to AuthController at login. Okay, so this is the view to sign the User in. All right, let's go in. And like I said, here's what we're going to do.

All right, let's go in. And like I said, here's what we're going to do. Get rid of all of it. We're going to start from scratch. So also I can reformat and get rid of that stuff. All right, so we're going to have a login method. And then another one for, we're not really following REST here. So we'll do login and postLogin. Alternatively, if you really want to follow REST, what people end up doing is something like a SessionsController.

Alternatively, if you really want to follow REST, what people end up doing is something like a SessionsController. So SessionsController create would display the form to sign in. And then SessionsController store would be how you actually perform the login, which is basically this method. But the other one is fine. So login, that's going to return view, let's just call it login. And let's create that. So resources/views/login.blade.php. Okay, so no real master pages, we're being quick here.

So resources views, login.blade.php. Okay, so no real master pages, we're being quick here. And you know what, I actually just paused for a moment. I'm going to paste this in instead so that I don't have to write it out. This is something I just swiped it right from Twitter Bootstrap's website. It's their example login page. And of course, I've removed the password. So most of this stuff, yeah, you don't even need to think about it at all. All you do need to know is that we now have a page to sign in, and it looks like this. Okay.

All you do need to know is that we now have a page to sign in, and it looks like this. Okay. Now for what does actually matter, though, is where this form submits. So I will set the form's method to post, and it'll just post to the current page. And that's fine. This formatting is kind of killing me. Like I said, I just copied it straight from the website from their example page. Okay, anyways, that's all good. So when we fill this out, it's going to post to the current page. So let's say, let's just duplicate that.

So when we fill this out, it's going to post to the current page. So let's say, let's just duplicate that. When we post to login, we're going to hit post login. So let's go back to AuthController, hide the sidebar, and now we're ready to get started. Okay, so this one's a little different. So in the past, yeah, if you did it manually, you would do things like Auth::attempt, and then you would pass in the email address and the password. You would then let Laravel do its thing and proceed accordingly. But now we're doing things a bit differently. So what should happen at this point?

But now we're doing things a bit differently. So what should happen at this point? Okay, well, we take their email address, we send them an email, we include a token, and then when they click on that link from the email, that takes them back to our website. We validate the token, and if it's all good, and if it's connected to that User's email address, we sign them in and they're done. So it sounds complicated, but I promise we can do this pretty quickly. So as I see it, our steps are we'll first validate the request, make sure that email address is correct. Next we need to create a token.

address is correct. Next we need to create a token. So that will be like a unique token that we save to the database and we associate with this User alone. That way when they click on it, well, the only way they could know that token is if they own the email address. And if they own that email account, they should have permission to log in to our site. Anyways, validate the request, create a token, and send it to them. That's it for this step. So nowhere here are we actually logging in a User.

Create AuthenticatesUser service7:26

about object-oriented programming, but many times it is kind of procedural. You're just saying, yeah, do this, and then this, and then this, and then you're done, right? Kind of like this. So for those situations, I like to create classes that are basically verbs. So for example, I might create a class called AuthenticatesUser. That's exactly what it does. It's just a little procedure. It's like a transaction class or a use case. We've reviewed that in the Monstrous Code series.

It's like a Transaction class or a use case. We've reviewed that in the Monstrous Code series. You might take a look at that if you're interested. So let's think about this. If we do want to take that approach, no longer are we thinking about each individual step we need to accomplish. We can zoom out a little bit and just think about how it should feel to work. On the outside, what are we going to do? Well, maybe we have some kind of auth object. Maybe at this point, yeah, we create a token and we send it to them.

Well, maybe we have some kind of auth object. Maybe at this point, yeah, we create a token and we send it to them. We're just inviting them. We're just inviting them to sign in. So maybe I'd call an invite method. And then at this point, we could return. We return a redirect to some page that says, all right, we fired off the email, something like that. But I'll just return a string here to keep it easy. How about sweet?

But I'll just return a string here to keep it easy. How about sweet? Go check that email. Okay, so yeah, we're zooming out. We're not thinking as much about the steps right now. We're just thinking about how it feels to use. We're inviting them. And then later, when they click on that link, yeah, maybe at that point, we have another method on this API called login, and then you pass in the token. That seems good.

method on this API called login, and then you pass in the token. That seems good. Okay, so for now, we're going to accept this AuthenticatesUser class. And that's our next step. So within our app directory, we'll just put it here, AuthenticatesUser. Okay, so we're going to have this method invite here. And now I can paste in those steps, and this is where they will live. Okay, so let's go back to our controller. I can now import this at the top, so use AuthenticatesUser, and this is looking good. So they hit the login page, they fill in their email address, they submit the form, and we

I can now import this at the top, so use authenticateUser, and this is looking good. So they hit the login page, they fill in their email address, they submit the form, and we are now going to invite them as the first step of our authentication process. And then we'll just say, sweet, go check that email. All right, so first step, validate the request. Well, Laravel can help us with this. So why don't we just say use validatesRequest. And that's just a trait that gives us a validate method. And this is exactly what you would use in your controller, it helps. So that means I could just say something like this.

And this is exactly what you would use in your controller, it helps. So that means I could just say something like this. Well, to start, I could put this all in the invite method. But here's another good advantage to writing it out like I often do in the videos. Many times, these steps can just translate to method names, especially for these transactional or these use case type classes. So you could even do something like a fluent API. So you could say this, validate request, next, create a token, and then finally, send it to them. So notice when we write it in this way, what happens?

to them. So notice when we write it in this way, what happens? The documentation, the comments there, redundant, get rid of it entirely. And that looks good. So let's see if this works. Let's go ahead and add our method. And phpStorm defaults to private. My thoughts on private, I don't know, I always end up defaulting to protected. Just as an aside, I get the benefit to private, but sometimes it feels like treating other developers like babies, like you don't want to allow them to do anything.

Just as an aside, I get the benefit to private, but sometimes it feels like treating other developers like babies, like you don't want to allow them to do anything. If somebody wants to extend this and do something, then be my guest. So I sometimes default to protected unless I really have a good reason to use private. Anyways, if this is going to be a fluent API, then I need to make sure I return the instance. Next, we're going to create a token. So I will add that as well. And let's get started. So how do you validate the request? We just say this->validate($request), we're going to have to inject that against this.

So how do you validate the request? We just say this, validate a request, we're going to have to inject that against this criteria. And in our case, we'll just say the email address is required. It should be of type email. So it should be a valid email address. And it should exist on our users table. So for example, if they enter an email address for somebody who hasn't registered yet, then validation will fail. And that's it in this case.

validation will fail. And that's it in this case. So yeah, as you may know, if this fails, an exception is going to be thrown. And then Laravel will automatically redirect back to the previous page accordingly. And you know what, why don't we just try that out right now? Okay, so why don't we do this, I'm not going to bother with a registration page. Ain't nobody got time for that. So why don't we build up a factory. And in fact, let me go to bear with me for a minute, get rid of the password field on our factory class.

And in fact, let me go to bear with me for a minute, get rid of the password field on our factory class. Okay, so anyways, bring it back up, and we'll say factory for a User create. So yeah, this is just simulating somebody who has registered for our site, in this case, Nakia Douglas. So we're going to use that in just a bit. But real quick, let's go back to, let's see, authenticatesUser. So we're trying to reference this request, but it hasn't been passed in yet. Let's do that in the constructor. So we'll say protected request, and then build up a constructor that accepts that.

Let's do that in the constructor. So we'll say protected $request, and then build up a constructor that accepts that. And this will just be an instance of Illuminate\Http\Request. Now if you're wondering how we pass this in, well, remember, with our AuthController, when we type into here, Laravel will automatically handle injecting any of the dependencies there. So in this case, we're declaring we need a $request. So Laravel will go, okay, well, I know what that is. So I'm going to pass that in for you. And I'm just going to remove doc blocks to keep the code a little bit shorter. Okay, so let's review the workflow.

And I'm just going to remove doc blocks to keep the code a little bit shorter. Okay, so let's review the workflow. We go to the login page. We have this little dummy bootstrap form. They fill it out. Oh, and on this note, we should also include a csrf field to protect ourselves against cross-site request forgery, of course. Anyways, they fill in their email address. They submit the form. It will then post to the current page.

They submit the form. It will then post to the current page. That will hit this endpoint. So if we now go to AuthController, we then hit this method where we invite the User. We take a look at that, and all it's doing right now is it's validating the request. Okay, so let's enter a gibberish email address. And actually, real quick, let's make sure that it does have a name. Type IDE class. It does not. It's weird they didn't include that.

see, yeah, we get that suite, go check that email, even though we haven't gotten to that part yet. All right, so we're making good progress. We invite the User. We validate the request. Next, we create the token. And once again, let's try to make this as readable as we can. So while we need to find the User, right, so we have an email address, why don't we say $user, and we could do one of these things, where $email equals this, $request->email, firstOrFail.

say User, and we could do one of these things, where $email equals this, request, email, first or fail. But for things like this, especially if I'm going to use it in multiple places, I might just change it to something like userByEmail, and then pass that through. So why don't we do that? Switch over to User, come down here. We want a static method called byEmail that accepts the $email address. And now here is where I can return that logic. Static where email is what you give us, and then return to me the very first one. Yeah.

Static where email is what you give us, and then return to me the very first one. Yeah. Now I move it in here, and I make the outside interface just a little bit easier to work with. So now that I have the User, we can generate a token for them, because the User exists, so we can move on to the next step. So maybe, well, we could either add a trait here, so we could do something like this if you want, or we could let a different class handle that. Like maybe we have a database table that specifically contains these tokens. This is how it seems to often be done.

Like maybe we have a database table that specifically contains these tokens. This is how it seems to often be done. So maybe we could have a LoginToken model, and then I could say generate a new token for the given User. That seems pretty readable, right? So we come back up. When we invite the User, we validate the request, and then we create a token where we just track down the User, and then we generate a new token for that User. Okay, so that's the next step. We need this model.

Add LoginToken model16:21

Okay, so that's the next step. We need this model. php artisan make:model LoginToken, and I do want a migration with it as well. Okay, so let's first go to our migration. How should this look? Well, we're going to have the integer for the userId, so this will be just a foreign key for the User who is associated with the token. I'll make that an index. You also actually might want to make that unique, possibly. Let's do an index for now, though.

You also actually might want to make that unique, possibly. Let's do an index for now, though. Next we want the token itself. So let's make that 50 characters, and once again, add an index to that so that we can easily track it down. And yeah, that looks good to me. So let's migrate the database, and we now have a dedicated table that stores these tokens. Because remember, it makes sense, right? If we are sending tokens to 50 different people who are signing in this hour, well, we have to track down the actual token somewhere, so we throw them into a table.

If we are sending tokens to 50 different people who are signing in this hour, well, we have to track down the actual token somewhere, so we throw them into a table. What's the next step? Well, if we go back, we have a LoginToken model, so we just need to create a generate method. So that will accept the User. And now how do we create a token? Well, pretty simple. We just create a new row within that table. We assign a random token, and that's basically it.

We just create a new row within that table. We assign a random token, and that's basically it. Very simple. Return static create, where the userId, we could use some kind of fancy Eloquent relationship, but let's just do it like this. And then the token will be just a random string. This is fine. No problem there. So does this all make sense to you? When we generate a new token, all we're doing is creating a new row in a database table,

So does this all make sense to you? When we generate a new token, all we're doing is creating a new row in a database table, assigning a random token, and associating that with the current user's email address. So let's see what the next step is. Now, we could return this. Let me show you two options here. So if we want this to continue being fluent, we return this, and then we have another method here called send, and damn it, that should be protected too. Anyways, we have a protected method called send, and then here you could inject a mailer if you want.

Anyways, we have a protected method called send, and then here you could inject a mailer if you want. You could use the facade here, something like that. That's an option. Or I kind of like the idea of the LoginToken model itself being responsible for this. It kind of makes sense, right? Can a token be sent to somebody? Yeah. So maybe we can expose that behavior. So in that case, maybe we don't return the object.

So maybe we can expose that behavior. So in that case, maybe we don't return the object. We return the login token object, and then we call a send method off of that. All right? So let's go to the model, create a new method called send, and now the token is sending itself. It's exposing the behavior that it can be sent. So we're going to do this by, we'll reference the Mail facade, and we could do send, but you know what? Let's just do a raw email so that I don't have to create a view for it.

you know what? let's just do a raw email so that I don't have to create a view for it. Next, you know what? In the past, using the Mail facade in an Eloquent model, I don't know why this specifically. I've talked to a lot of people who feel the same way. This kind of felt wrong, like this isn't right. You shouldn't be doing this. But you know what? I just don't care anymore. It's never been a problem for me in the past, so I think it's actually nice to put it here.

associated User's email address, right? So if a token belongs to a User, well, then I should be able to say, once we hook up our relationship, get me the associated User for the token, and I want to send it to their email address. Then, I can say, set the subject equal to login to, and let's just imagine it's Lyricast. This is what I would do. Okay, so URL variable, that will just be a URL to. What should the URL be when the User clicks on this link? Now, I've seen everything across the board. Some people do something like auth/token/confirm.

Now, I've seen everything across the board. Some people do something like authTokenConfirm. I've seen that in the wild. Why don't we stick with loginConfirm? I don't know. Do we want to do that? loginToken, I don't know. This is the fun part, is just figuring out what you want. You know what? Let's stick with authTokenConfirm.

You know what? Let's stick with auth token confirm. Let's keep it like this. Yeah, that would make sense, because we'll pass through a token in the process. They're going to click on a link that goes to auth/token/50 random characters, and that will take them to AuthController, and we'll call this authenticate. Yeah. The important thing now is that we have our endpoint here, which means if we come back, auth token, and then we're not going to do that, of course. We're just going to tack it on here.

auth token, and then we're not going to do that, of course. We're just going to tack it on here. This token. Okay. Now that's going to spit out, like I said, auth token, and then 50 random characters for the user's token. Then we insert that into the email, and we fire it off to the user's email address. I think we may have made a mistake, but I think that'll take care of this. The only remaining step, of course, is to create this relationship. Let's say at the bottom, a User, and what is the relationship between a loginToken

The only remaining step, of course, is to create this relationship. Let's say at the bottom, a User, and what is the relationship between a LoginToken and a User? Well, the token belongs to the User. I could say this belongs to a User, and that should do it. Let's go back and see our progress. We invite a User by validating the request. We then create a token. We've learned how to do that. That just creates a new record and saves it to the database, and then finally, we send

We've learned how to do that. That just creates a new record and saves it to the database, and then finally, we send the invitation, and that's handled here as well, or put it wherever you want, wherever you feel most comfortable. If you think that belongs on the Service class, if you want to even call it a Service class, then yeah, you can add a send method here, fire an event, or whatever makes you sleep at night. Now, of course, in this case, I don't actually want to send an email, so I'm going to set the mail driver equal to log so that I can then just visit my storage/logs/laravel.log file.

the mail driver equal to log so that I can then just visit my storage/logs/laravel.log file. Let's get rid of some old errors, and the email should be spit out right here. Okay, let's try it out. So login, we're going to enter something that doesn't exist. It's not going to work. We're not going to send an email. However, if we ... Let's find that User again, our one registered User. So this person signs in. Ah, looks like we forgot to set up the fillable fields for the LoginToken model.

So this person signs in. Ah, looks like we forgot to set up the fillable fields for the LoginToken model. All right, fair enough. Let's clear the error. Go back to LoginToken, and here we are. Oh, actually, first, let's make sure we return that relationship, and then, yeah, fair enough. Let's add our fillable fields, and we want a userId and a token. Cool. Okay, so one more shot. Give it a refresh.

Okay, so one more shot. Give it a refresh. We run it, sign in, sweet, go check that email, which means if I come back and we go back to our laravel.log file, here it is. So login to laracasts, and like I said, we just have a single URL. Okay, so let's come back to Chrome, paste this in, and notice once again the endpoint, auth/token, and then a unique sign in token. So before we hit this, let's just see what's going to happen. Back to routes. We have this endpoint.

Token login and binding24:09

Back to routes. We have this endpoint. We load an AuthController, and now we add our authenticate method. Now I'm going to show you a little trick here. Why don't we set it up so that this token, well, when it comes to our method, it's already in the form of a LoginToken model. So it should be in the format of an object of this type. Okay, here's how we're going to do that. I'm going to begin by just type hinting it, and do note that it got injected right up here, and then I'm just going to dd($token).

I'm going to begin by just type hinting it, and do note that it got injected right up here, and then I'm just going to dd the token. Now right now, it's not going to work. Let me show you. Hit this, and we get not found. So here's what's happening. This is what we call implicit model binding. So in our routes file, we have a wildcard here, and then in the AuthController, we're trying to type hint it. So Laravel's thinking, oh, you want an instance of LoginToken.

trying to type hint it. So Laravel's thinking, oh, you want an instance of LoginToken. I'm going to try to find the record that has this ID. But notice I said ID. That's not really what we want. We want to track it down by the token. So if you still want to use implicit model binding, here's what you do. Go to your LoginToken model, and then you're going to add a method called getRouteKeyName. And this is just going to return a string for which attribute should be the column that we hunt down the record by.

And this is just going to return a string for which attribute should be the column that we hunt down the record by. So like I said, by default, it's ID. So we basically say static where ID is equal to the wildcard that you gave us. But now we're going to return token instead, which means behind the scenes, it'll say static where token is equal to the wildcard, and then give me the first result. That's exactly what we want. So let's give it another shot. Reload this page, and now it's working. So it took care of the work of tracking down the model that contains the token and the

Reload this page, and now it's working. So it took care of the work of tracking down the model that contains the token and the associated User. OK. So that means, well, we have this relationship set up, right? So if I come back to AuthController, and we die and dump the token user, this will give us the associated User for the token, and it does. Sweet. So if we come back, what's our next step? Well, we started by inviting the User.

So if we come back, what's our next step? Well, we started by inviting the User. They accepted the invitation. So now we can log in the User, and I'll pass in the token instance here. Now we have a couple options. Once again, I could pass this through again, and that would work. Or if you don't want to do that, and you find yourself injecting this in two or three or more places, at that point, you might want to use the constructor. So protected auth. Why don't we do it at this point?

So protected auth. Why don't we do it at this point? And now that wants an authenticatesUser instance. OK. So now down here, I can get rid of that entirely, and then say this auth invite, and then this auth logIn. Make sense? Cool. Let's go ahead and add the method. And why don't we...

Let's go ahead and add the method. And why don't we... Yeah, it can stay up here. So this will accept the loginToken. And now what do we do here? At this point, don't worry. Not much to do really at all. As I see it, we log in the User associated with this token, and then delete the token. And that's really it at that point, right? Nothing else to do.

And that's really it at that point, right? Nothing else to do. OK. So how do we log in the User? It's like you've done it for years with Laravel. You could use the facade. You could use the helper function. You could inject an authenticator. Once again, whatever you feel comfortable with. Why don't we use the facade in this case?

Once again, whatever you feel comfortable with. Why don't we use the Auth facade in this case? login token, and then give me the associated User. And we'll go ahead and import that at the top. So that takes care of that step. And then finally, delete the token. Fair enough. token delete. So yeah, they've signed in. It's a single use token.

So yeah, they've signed in. It's a single use token. So the next time they sign in, they would need to check their email again. And that's why generally for these sorts of things, you have a long expiration date. So you would remember them for a long time. Now check this out. If we come back to AuthController, this ends up actually being pretty simple stuff. Let's reformat real quick. The login method displays a view to sign in. We submit the form.

The login method displays a view to sign in. We submit the form. We invite the User. And we redirect somewhere to inform them, go check your email. When they do, they click on a link that takes them to this method where we log in the User. And in this case, that consists of deferring to Laravel's login functionality. And then we delete that token from the database. So why don't we try this? We still have this link. Remember, we died and dumped immediately.

Let's do this. Let's truncate it and start from scratch. Okay, nothing there. We're going to have the User log in again. I still have that email address. Paste it in. Great. So if we run it again, there's our token. And we also received an email. Or we could just grab this token and say, authToken, and paste that in.

And we also received an email. Or we could just grab this token and say, authToken, and paste that in. Okay, so now, remember, we didn't redirect anywhere. However, we should currently be signed in. So if we come back, the token should be gone. And it is. So that means, yeah, in our authenticate method, here's where you redirect to the User's dashboard anywhere you want. I'm just going to return a string that says, you are now signed in. And then let's concat auth user name.

I'm just going to return a string that says, you are now signed in. And then let's concat auth user name. And then also, maybe, let's add a dashboard as well. So Route::get dashboard, return, welcome, once again, and the exact same thing. And we'll add a middleware to that. And let's check that out. Oh, and then also, real quick, let's add a logout link. Auth and AuthController at logout. Okay, back to AuthController. Down here at the bottom, this rounds it out.

Okay, back to AuthController. Down here at the bottom, this rounds it out. Oh, and by the way, you can also do some try and catch stuff here. Like catch if you could not find a token, and then send them a message or flash something, whatever you want to do. Auth logout, return, redirect to the homepage. Or again, for stuff like this, like if you still want to defer to our little authenticateUser object, then you could do that as well. All right, so let's go to the dashboard. We're signed in.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟