Add Authenticate Method0:00
So let's get this answer sent over to the server and our User authenticated. I'm just going to spend a couple of seconds cleaning up our PASKEY controller. I don't need the index method or the create method. We need store. However, we don't need show and we don't need edit or update. So we should just be left with destroy and store. Let's add a new method here. public function authenticate. It'll accept the $request. And then we'll head to web.php.
Define Authenticate Route0:32
It'll accept the request. And then we'll head to web.php. And outside of the authentication middleware, we can add our root. So let's say root post forward slash PASKEYS. And then we'll need to give it another sub-root here. So we'll say forward slash authenticate. And that's going to point to the PASKEYController authenticate method. And we can give it a name, PASKEYS.authenticate, no problem. Now, the body of authenticate is going to be quite similar, at least in the steps that we take, to what we have in the STORE method.
Now, the body of authenticate is going to be quite similar, at least in the steps that we take, to what we have in the STORE method. That is, we'll do a little bit of validation. Then we're going to create a public key credential based on what's been passed from the front end. After we've done that, well, we need to verify that we have not an authenticator attestation response but an authenticator assertion response. Then we create an authenticator assertion response validator and perform the check.
Build Assertion Validation1:22
Then we create an Authenticator assertion response validator and perform the check. And rather than storing the PASKEY at the end of the process, we're going to log the User in at the end of the process. So I'll tell you what. I'm actually going to copy the body of our store method at least to the bottom of the try catch. And I'll paste that in as a nice starting point to authenticate. So first things first. We don't need to validate with bag.
So first things first. We don't need to validate with bag. We can just validate, which means I can get rid of this here. And we're not validating any name. I didn't call it pasKey. I called it answer. So we'll change that too. And why don't we inline that to make it a little bit cleaner. We still need to deserialize the answer. Obviously, we'll need to change this key here. Well, we want a publicKeyCredential at the end of it,
Obviously, we'll need to change this key here. Well, we want a public key credential at the end of it, so that's correct. As I said, we're not looking for an authenticator attestation response in the case of authentication. We're looking for an authenticator assertion response. Those keywords are coming up once again. And this time, we want the opposite effect on line 29 here. Rather than going to the login route, we'd want to go to the profile.edit route.
Rather than going to the login route, we'd want to go to the profile.edit route. Why don't we add that little manage PASKEYS fragment as well so that if this were to happen, they would actually be taken to the correct part of the page. Manage PASKEYS. There we go. In the try catch, we do want to create a publicKeyCredentialSource. But again, we need to change authenticatorAttestationResponseValidator to authenticatorAssertionResponseValidator, which has this create static method, and we can call check.
to AuthenticatorAssertionResponseValidator, which has this create static method, and we can call check. However, the variables that we pass, the signature of the method is quite different. So we have this userHandle that we have to pass. The request, we have had that before. Then the publicKeyCredentialRequestOptions, the AuthenticatorAssertionResponse, and the credentialId. We'll start from the bottom once again. So we have the userHandle.
We'll start from the bottom once again. So we have the user handle. For now, I'm going to set that to null. Then we have the request, which I can leave as request()->getHost(). We're looking for the publicKeyCredentialRequestOptions next. So let's rename this. This will be requestOptions. And I can pass what we stored in the previous call from the session. So let's go to our APIPASKEYController. We'll head down to authenticateOptions.
So let's go to our ApiPaskeyController. We'll head down to authenticate options. And this is what I'm interested in, Paskey authentication options. Let's copy that and paste as the key here. And we can move on. Next, we have our authenticatorAssertionResponse. Well, once again, that's actually what we've checked is the type of publicKeyCredentialResponse. So we can change that to authenticatorAssertionResponse. And sure enough, we can set it to publicKeyCredentialResponse.
So we can change that to authenticatorAssertionResponse. And sure enough, we can set it to publicKeyCredentialResponse. The last parameter we have to pass is a credentialId, which is an instance of a publicKeyCredentialSource. You'll note that the return type of the check method is a publicKeyCredentialSource. So you might think, well, where do we actually pull that information from if it's not been actually provided to us yet? But it has already been provided to us. If you go down to the store method, when we registered a PASKEY, what did we store in a database?
Fetch Passkey From DB4:29
If you go down to the store method, when we registered a PASKEY, what did we store in a database? A public key credential source. So this is actually where we retrieve the PASKEY from the database. And then we pass it into this check method so that the two keys can actually be verified. They can be validated next to each other. Let's go ahead and retrieve that PASKEY from the database. PASKEY equals PASKEY. I'll use firstWhere here.
PASKEY equals PASKEY. I'll use firstWhere here. And we can check for the credential_id column. And I want to grab the credential_id that has public_key_credential. And I'm interested in the raw_id property. Now, if that PASKEY doesn't exist in the database, that would indicate that the user has gone to edit their profile. And they've deleted the PASKEY in their profile. But they've not deleted the PASKEY in their authenticator. So when they get back to the login page,
But they've not deleted the PASKEY in their authenticator. So when they get back to the login page, they attempt to log back in with a deleted PASKEY. And, of course, well, that PASKEY can't be found in the database. So let's go ahead and do something like this. If not PASKEY, we'll throw a validation exception. And we'll set a message on the answer field. And we could just say something simple like, this PASKEY does not exist. No, that's a bit too specific for my liking.
this PASKEY does not exist. No, that's a bit too specific for my liking. This PASKEY is not valid. That doesn't give away too much information to the end user, which I think is a good compromise in case of authentication. With our PASKEY in place, well, we should be able to now pass that to credentialId. So credentialId will be equal to PASKEY. And then we're interested in the data field. Awesome.
And then we're interested in the data field. Awesome. So that's our validator filled out. And it's ready to perform the check to ensure that the answer the user has provided matches the PASSWORD that we have stored in our database. Let's just go over it one more time so that you understand the arguments we're passing. User handle is null for now. We'll come back to that.
User handle is null for now. We'll come back to that. Request is our domain name, which we can grab using $_REQUEST['HTTP_HOST']. Public key credential request options, well, those come from the API call that we've made in the previous episode. So we make a call to authenticateOptions. We flash that to the session. And here we're retrieving it from the session.
We flash that to the session. And here we're retrieving it from the session. Then we have our authenticator assertion response. Well, that's going to essentially take the publicKeyCredential that the user has provided. And then it has a response object on it, which includes information needed to validate that this is indeed the right user. And then finally, we have our credentialId, which is obtained by grabbing the correct paskey from our database.
And then finally, we have our credential ID, which is obtained by grabbing the correct PASKEY from our database and then passing the data that we stored on that PASKEY to our validator. Let's go ahead and handle the catch case here. So something went wrong during verification. We'll throw a validation exception. And let's go ahead and do the exact same thing. I'll pass answer. This PASKEY is not valid.
I'll pass answer. This PASKEY is not valid. Drop that in there. And we don't need an error bag here so I can remove that chained call. But as long as nothing is thrown, we're actually okay to go ahead and authenticate the user. Let's use the auth facade for that. So auth, login using ID. And we can grab the ID from the PASKEY that we retrieved from the database.
So auth, login using ID. And we can grab the ID from the PASKEY that we retrieved from the database. PASKEY user ID. And it'd probably be a good idea to grab the request session and regenerate it. And finally, we could do a redirect. So return to root. And because the user's been logged in, it makes sense to go to the dashboard. Backend complete.
Wire Up Login Form7:59
it makes sense to go to the dashboard. Backend complete. Let's wire it up. Really, we're going to want to do something very similar to what we did for register. That is take the existing form element. This time it will be our login form. Add a little bit of data to it because we need to add our answer. And then submit the form. So I'll copy and paste that.
And then submit the form. So I'll copy and paste that. And we can drop it down here. We need to accept the form element inside Authenticate. So we'll do that now. And then in login, we can pass $L into Authenticate. Just like that. We'll also need to make sure to update our action. So let's do that here. form.action equals /PASKEYs/Authenticate.
So let's do that here. form.action equals /PASKEYs/Authenticate. If I can spell. Very good. Then we're not setting PASKEY, we're setting answer. And we'll set it equal to the ANSWER constant that we created up here. And then finally, we submit the form. The last thing I'll do is update our form to show the input error for our answer. So if we get that validation error thrown, we want to see that on the front end. And I'll literally just stick it underneath the input error for email.
So if we get that validation error thrown, we want to see that on the front end. And I'll literally just stick it underneath the input error for email. I think that will cover us just fine. So let's go to our front end and see if we can log in. If we refresh the page, here's our modal. I'll click sign in. And I'm getting a validation error. The PASKEY is not valid. Okay. Let's try and work out what the problem is here.
Debug CredentialId Type9:21
Okay. Let's try and work out what the problem is here. So it could be coming from here. It could also be coming from here. But I know that this PASKEY does exist in the database. So I'm going to hazard a guess and say that our actual problem is this throwable here. Tell you what, let's go ahead and just dump and die that exception out so we can see what the problem is. And then I'll come back, refresh, hit sign in on the modal. Yeah, here we go. So what's the issue?
Yeah, here we go. So what's the issue? Argument 1 credentialId must be of type publicKeyCredentialSource[] given. What could that be? I know what that is. I know what that is. So in our PASKEY modal, we cast data to JSON, which essentially means when it gets stored in the database, it JSON encodes. When it's retrieved from the database, it JSON decodes. So when we pass PASKEY data here as credentialId,
When it's retrieved from the database, it JSON decodes. So when we pass PASKEY data here as credentialId, which is expecting a publicKeyCredentialSource object, we're just passing a JSON decoded array. So what we actually need to do is cast this using, again, using a WebAuthnSerializerFactory to be a publicKeyCredentialSource object. We could do that here in the controller, but we could also do it with a custom cast, which I think would be nicer. So I'm just going to copy and paste this code, and let's head into the PASKEY modal.
Add Model Data Cast10:46
So I'm just going to copy and paste this code, and let's head into the pascal modal. I'll remove casts entirely, and instead let's have a public function called data, which returns an attribute. There we go. So we'll say return new attribute, and we're going to have a getter, and we're also going to have a setter. We'll sort the getter first.
and we're also going to have a setter. We'll sort the getter first. So the getter will be a closure. That closure is going to receive the string value from the database, so that's the raw JSON, and then I can paste in this SerializerFactory code to build up a WebAuthnSerializerFactory, deserialize. I need to pass in the value that we have in the database, and I don't want a publicKeyCredential. I want a publicKeyCredentialSource object,
and I don't want a publicKeyCredential credential. I want a publicKeyCredentialSource object, which is what we actually stored in the database. That should work. On the other side of things, we have set, which essentially is going to receive a publicKeyCredentialSource object. That's the value, and we could just json_encode that value. There we go. Pass the value in,
There we go. Pass the value in, and I think that should work. So let's go test it again from the front end. I'll hit sign in on our modal. Hey, we just logged in using a passkey for the very first time. Isn't that cool? We just logged in using a passkey. That is incredible. Okay, here's what we're going to try.
Test Multiple Passkeys12:06
That is incredible. Okay, here's what we're going to try. I'm going to go to profile, and I'm going to add another passkey. So let's say testing this one too. Hit enter. I'll save it as a new item. There we go, and then let's go ahead and log out, and we'll log in again, and let's try our second passkey.
and we'll log in again, and let's try our second passkey. It worked. All right, here's another test. We'll go to profile. I'll delete that second passkey, so it's no longer in our database, but it still exists in our authenticator. Then I'll log out, go back to log in, select it again, and there we go.
Then I'll log out, go back to log in, select it again, and there we go. We've got the validation message. This passkey is not valid, but if I select the first passkey, that one's going to let me in. So this is actually working very nicely. I can think of just a little cleanup that might actually be quite nice. When you have a custom attribute,
Cleanup and Update Passkey12:58
that might actually be quite nice. When you have a custom attribute, as we've just created here for data, you can pass the value directly to set, but you can also pass an array, and in that array, I can set multiple attributes at the same time. So of course, I can set data, but I could also set the credentialId, which essentially will be the value
but I could also set the credentialId, which essentially will be the value publicKeyCredentialId property, and that would mean if we go back to our controller and the store method, when we actually store this passkey, we don't need to manually set credentialId. We can just let the CAS do that for us, and it will be automatically configured on our model. We also should remove the dumped ICOL.
and it will be automatically configured on our model. We also should remove the dumped icol that we added in the catch block here. Oh, and one other thing. It's recommended that this publicKeyCredentialSource is used to update the original passkey, just in case any of the properties have changed in the meantime. So that should be as easy as saying passkey update,
So that should be as easy as saying passkey update, we'd be looking for the data attribute, and we just set it to the latest instance of publicKeyCredentialSource, like so. There we go. That is a pretty robust implementation of logging in using a passkey. Oh, we are so close now, but there is just one last thing we need to do.
Oh, we are so close now, but there is just one last thing we need to do for a full implementation. So I'll see you in the next episode.
