مرور آخرین مانع0:00
We have one last hurdle. Let's jump it together. Watch what happens when I dismiss this passkey modal. Nothing. Nothing happens. In other words, if I dismiss that modal, there is no way I can now authenticate using my passkey. I would have to reload the page. You might think, well, is that really a big deal? Well, your users don't necessarily know that they'd have to reload the page. And also, if you want to support a wider range of authenticators, then you need to allow for authenticators that don't have a resident key. Authenticators that don't have a resident key won't support this popup modal anyway.
Planning Passkey Fallback0:32
then you need to allow for authenticators that don't have a resident key. Authenticators that don't have a resident key won't support this popup modal anyway. So it's best practice to also implement a fallback mechanism where we can enter our email or our username manually and then request a passkey. So we're going to implement that fallback method in this episode to round the series off. Let's get started. I'm going to update the frontend and turn this login form into a two-step process so that we only see the password field if we actually need to enter our password. So in Alpine here, in our AuthenticatePasskey object, perhaps I'll have a showPasswordField property.
Two-Step Login UI1:12
So in Alpine here, in our AuthenticatePasskey object, perhaps I'll have a ShowPasswordField property. And let's set a default here of false. We'll probably change that a little further on in the episode. Then we'll jump into our Login.blade.php field. I'm going to wrap our password field in a template so that I can do an x-if statement, which is going to check for this ShowPasswordField property. We'll drop that in, and then we'll make sure this entire div is wrapped in that template. In fact, at the same time, we should probably ensure that the ForgotYourPassword field also only shows in certain circumstances.
In fact, at the same time, we should probably ensure that the ForgotYourPassword field also only shows in certain circumstances. So for that, we could just use XShow, I think. XShow equals ShowPasswordField. And there we go. So we just see the email field to start. The User will go ahead and enter their email address as usual, and they'll submit this form. We need to capture that event and then send this email over to our AuthenticateOptions request in order to retrieve the relevant passkeys for this given username.
Submitting Email for Options2:05
We need to capture that event and then send this email over to our AuthenticateOptions request in order to retrieve the relevant passkeys for this given username. Let's tackle that. So now in our Login.Form, we can say X.on. We want to listen for the submit event, and we'll prevent the default action from taking place. And then we want to call the authenticate function, passing in the current form element. And, you know, I want to actually be able to differentiate between an authenticate from X.init and an authenticate from submitting the form manually. So I'll pass a Boolean flag into authenticate as well, and we could accept that as a second parameter in authenticate here.
So I'll pass a boolean flag into Authenticate as well, and we could accept that as a second parameter in Authenticate here. Let's call it ManualSubmission. And by default, that will be set to false. All right, what's next? We need to send the username across to our AuthenticateOptions, as I already mentioned. So let's track the username in Alpine. We'll create a field called Email. In our Login.Form, we can then add x-model to our text input for our email, and that will be equal to the email attribute that we've just added in Alpine.
In our LoginForm, we can then add x-model to our text input for our email, and that will be equal to the email attribute that we've just added in Alpine. And then when I make the axios.get request, I'll pass in options. And I think there's a property in axios called params. Yes, there is. So I'll set params equal to an object where email is going to be equal to this.email. Okay, that's the front-end taken care of as far as our AuthenticateOptions are concerned. We now need to turn our attention to our controller for AuthenticateOptions to take this email into account. Remember a few episodes ago I mentioned this allowCredentials parameter.
Filtering Allowed Credentials3:40
to take this email into account. Remember a few episodes ago I mentioned this AllowCredentials parameter to our PublicKeyCredentialRequestOptions? Well, we need to make use of that now. So let's add that in, AllowCredentials, and looking at the signature, it expects an array of PublicKeyCredentialDescriptors. You can grab these descriptors from the PassKeyData object. So let's say that the AllowedCredentials are going to be equal to, and we need to find the PassKeys that relate to the given email address from the User. So we'll say PassKey where relation, we're looking for the user relation, the email field,
and we need to find the PassKeys that relate to the given email address from the User. So we'll say PassKey where relation, we're looking for the user relation, the email field, and we want to set this to whatever email has been passed through in the request. So we'll accept the request up here, and we can pass in request email just like that. Let's go ahead and grab those. So that will give us a collection, and we want to map over that collection. Each of these instances will be a PassKey, and we'll return PassKeyData. Now, if we go into PassKey, remember that we created this attribute here, which means that the data property is going to be an instance of a PublicKeyCredentialSource. So after we've mapped to data, let's do another map.
which means that the data property is going to be an instance of a PublicKeyCredentialSource. So after we've mapped to data, let's do another map. This time we'll have a PublicKeyCredentialSource object, and this has a method on it called getPublicKeyCredentialDescriptor, which is exactly what we're looking for to pass to allowedCredentials. Now, one important thing to remember is that this authenticateOptions endpoint is now used in two different places. One is the automatic request where no username is provided, and now we have this additional request where the username is provided. So we need to make sure we only pass allowedCredentials if an email has been passed along.
and now we have this additional request where the username is provided. So we need to make sure we only pass allowed credentials if an email has been passed along. Let's say request filled. We're going to look for the email field, and if you have filled out the email, then yeah, we'll go ahead and grab the allowed credentials. Otherwise, we'll just return an empty array here, and then we can pass those allowed credentials into our PublicKeyCredentialRequestOptions. So now check out what happens when I cancel the first modal, and I'll cancel the browser pop-up as well, and then I'll enter my email address, test@example.com,
and I'll cancel the browser pop-up as well, and then I'll enter my email address, test@example.com, followed by submitting the form. Note that we receive another modal now because it's going to fetch those options. It will return the correct authenticated keys and provide those in this pop-up here, and if I hit Sign In, I'm able to sign in without having to enter any password. Of course, whilst this works, and it works really well, there is a slight issue now that if I try to log in to my account without pass keys, let's say test@example.com, go ahead and hit Enter, nothing happens because there are no pass keys in the authenticated
Password Fallback on Errors6:23
let's say test at example.com, go ahead and hit Enter, nothing happens because there are no pass keys in the authenticated, there are no pass keys in my profile or account, and I can't actually log in with my password. So we need to make sure as a last resort that the User is actually able to authenticate using their password. So to fix this, let's go ahead and wrap our call to options and then the answer from startAuthentication in a try-catch. We're just going to go ahead and catch any error that gets thrown. And then we could say, well, we want to show the password field in this instance.
We're just going to go ahead and catch any error that gets thrown. And then we could say, well, we want to show the password field in this instance. So this showPassword field will be set equal to true. And then why don't we say at the top of this function, look, if the password field is already visible, so if this showPassword field is equal to true, we actually want to go ahead with a standard form submission here. So I will return form.submit. As discussed in the previous episode, this answer will now not work because the try-catch is a separate logic block.
As discussed in the previous episode, this answer will now not work because the try-catch is a separate logic block. So let's create a let up here called answer, and then we'll set answer equal to startAuthentication, meaning that it can be used later down the line as well. Of course, one other thing to keep in mind, if we set the password field to true, we don't want to then create this action. So why don't we go ahead and return early to make sure that that doesn't happen. Now when we head back to the login page,
So why don't we go ahead and return early to make sure that that doesn't happen. Now when we head back to the login page, note that the password field shows immediately. That's because our account doesn't have a passkey registered against it. So let's log in, test@example.com, password, hit enter, and then we'll go ahead and create a passkey for this account. So let's say onepass, hit enter, save, and then we'll go back to our login and try this again. Here we go. So we get in our pop-up.
Here we go. So we get in our pop-up. If I click sign in, I'm signed in immediately. That's great. Let's go back in again. This time I'm going to cancel the modal that comes up, and it shows me the password field. So again, this isn't what we want because we want to allow for a second chance to enter a passkey before it falls back to password.
because we want to allow for a second chance to enter a passkey before it falls back to password. Perhaps we can fix that. So remember we passed this manualSubmission parameter so that we know whether it's manually submitting for a passkey or whether it's automated. We could use that, right? So we could wrap the catch here, and we could say if manualSubmission, only then should you go ahead and set showPasswordField to true.
and we could say if manual submission, only then should you go ahead and set showPasswordField to true. So now we should be able to cancel the initial modal that shows, then log in by entering our email and submitting the form, which will cause the passkey modal to pop up again. But if I were to go to my profile, and let's go ahead and remove the passkey, then let's log out, go to log in, cancel the initial modal, then we go ahead and enter our email, cancel the second modal, which will cause an error,
then we go ahead and enter our email, cancel the second modal, which will cause an error, and that will cause the password field to show up as a fallback, and of course we can go ahead, enter our default password, and be logged in as normal. Perfect. Just a few little things before we wrap up. I think showPasswordField would actually make more sense as not browser supports WebAuthn. So if your browser is older, passkeys aren't supported,
Final Tweaks and Wrap-Up9:37
as not browser supports WebAuthn. So if your browser is older, passkeys aren't supported, well, we just show the password field right out of the box. Also in login.blade.php, where we have our autocomplete on the email field here, it's important that you also add WebAuthn as an autocomplete option. So you can keep username in there, but you should also add WebAuthn. And then finally, now that we've added this fallback option, inside our API where we have the register options endpoint, you could actually change requireResidentKey to false
inside our API where we have the registerOptions endpoint, you could actually change requireResidentKey to false because you no longer actually need a resident key to be able to sign in using WebAuthn. In fact, seeing as that's kind of the default in the WebAuthn package, you could just remove this option entirely to be left with a more simple options object. And well, with that done, you've created passkey support in your Laravel app. It wasn't that hard, was it?
you've created passkey support in your Laravel app. It wasn't that hard, was it? So a User can create a passkey, a User can sign in automatically without entering any information using biometrics, a User can fall back to entering their email or username followed by a passkey. And if all else fails, we even have a fallback option where they can go ahead and enter their username and password, just as you always have done in your Laravel apps.
where they can go ahead and enter their username and password, just as you always have done in your Laravel apps. So I'm sure you'll agree. Once you wrap your head around it, it isn't that difficult. And within an hour or two, you can actually have this up and running in your apps. You'll want to test it thoroughly. You want to make sure it works correctly in your chosen stack. But that's the basics. That's really all there is to it.
But that's the basics. That's really all there is to it. And the rest is just whatever syntactic sugar, whatever magic you want to add on top. I hope you really enjoyed the series and that it was useful to you. I can't wait to see passkeys grow and expand as more apps start integrating them, particularly in the Laravel space. And if you have a nice example of passkey integration in your Laravel app, be sure to post it in the comments below in this episode.
And if you have a nice example of passkey integration in your Laravel app, be sure to post it in the comments below in this episode so that we can check it out and take inspiration. Alright, thanks for watching. I'll see you all soon.
