Handling Name Validation0:00
So, I managed to get validation working on the options request. If I click create passkey and I've not entered a name, it doesn't go and create a passkey for me, it just shows me that name validation. And I'll show you how I got that working, just in case you were struggling. So, in the Alpine register passkey component, I've added a name field and an errors field. I make sure to reset the errors field at the top of that register function. But then further down, when we make the options request, I'm going to, let's say, hack Axios and say that actually a valid status includes both a 200 status code and a 422, which is Laravel's validation error code. If a 422 happens, well, then I'm essentially going to do an early return and set the errors equal to what we have from the request, what Laravel returns inside a validation error. And then inside manage passkeys, you'll see that I've
to do an early return and set the $errors equal to what we have from the $request, what Laravel returns inside a validation error. And then inside managePasskeys, you'll see that I've got my standard input error, but I also created a new component, which is this Alpine input error, where I can pass in, in this case, $errors->name, which is exactly what we have here, $errors, and then it will be an object that has a name property, which is what the error is taking place on. All in all, that leads to, well, the experience we now have. Before we ever get to the second request to store the passkey, we already validate whether the name is actually going to work or not. And, of course, if I fill this out for real, let's say onePassword, hit enter, there is no validation error, so I can go ahead and create a new passkey item, and you can see that it's added here. Of course, if you have
Adding WebAuthn Support Checks1:30
let's say one password, hit enter, there is no validation error, so I can go ahead and create a new passkey item, and you can see that it's added here. Of course, if you have a different solution and it's working, go ahead with that solution. That's absolutely fine. No problems there. If you just want to get ahead, then you can absolutely pull from the Episode 4 start branch, and you'll have all the code that I've just shown you. There are a couple of things I want to add to this register function before we move on. One thing to keep in mind, not all browsers support WebAuthn, so there is, with the WebAuthn package in JavaScript, a method called browserSupportsWebAuthn. What I'm actually going to do is include that in the register passkey data object in Alpine. I'll use shorthand for this, and then here I'll just be able to say, if not this.browserSupportsWebAuthn,
to do is include that in the register passkey data object in Alpine. I'll use shorthand for this, and then here I'll just be able to say, if not this.browser supports WebAuthn, I want to return early, essentially. I want to make sure that we don't perform any of the other requests, because, well, the browser's not actually going to support it anyway. And then what we can do in addition to that, we can go to manage passkeys, and on this form here, I'll essentially say that we're only going to show this form if the browser supports WebAuthn. I'd also like to make this line here a little more bulletproof, because it's completely possible for registration to fail on the browser's side. Of course, we could wrap this in a try catch. Why don't we do that? And we'll accept an error, and then I think we'll just do an early return, but along with doing an early return, we can say
wrap this in a try catch. Why don't we do that? And we'll accept an error, and then I think we'll just do an early return, but along with doing an early return, we can say this.errors equals, and then we'll set the name object to an array, and then we can provide a single error message in here, something like passkey creation failed, please try again. And then let's test this out on the frontend. So if we come back, and let's add test, but then I'm going to cancel this, and cancel the browser WebAuthn modal as well. There we go. passkey creation failed. Please try again. Just a bit of a nicer flow for the user. It can also fail if the request times out. They'll see a similar message now. So we're just covering those edge cases, making it nicer for the end user to understand what's actually going on, rather than it being a bit of a black box that they just can't see.
Recapping Registration Flow3:38
we're just covering those edge cases, making it nicer for the end user to understand what's actually going on, rather than it being a bit of a black box that they just can't see into. So now that we have a full registration flow, let's just go ahead and recap how it works. You enter a passkey name and click create passkey. That's going to make an asynchronous request to the register options endpoint that we have here. We'll do a little bit of validation on the name, and then we're going to create some options that are needed to actually generate the passkey. So, for example, what is the name of our application? What's the URL of our application? And this is referred to as the relying party. Also, we need some information about the user, such as their email, a unique ID that's non-personal, and a friendly display.
our application? And this is referred to as the relying party. Also, we need some information about the user, such as their email, a unique ID that's non-personal, and a friendly displayName that they can use. We give it a challenge. That's important if you're doing what's known as attestation, which we're not, but we still have to stand the challenge anyway. And then finally, you can specify authenticator selection, which is what type of authenticators can actually be used. In the case of passkeys, you probably want no preference. In other words, it can be either platform or cross-platform based, but we're going to require a resident key in order to make the authentication flow smoother. We flash that to the session and we send those options back to the browser. The browser takes those options and it generates a passkey for you in your authenticator of choice. Then with the information you've been given from
options back to the browser. The browser takes those options and it generates a passkey for you in your authenticator of choice. Then with the information you've been given from the authenticator, you send that back to your server, in this case, to this store endpoint. We're going to do some basic validation. Then we deserialize the passkey we've been provided from the front end. We check that it's the right type. And then finally, we're going to create a validator in order to make sure that the data hasn't been tampered with and that this is actually okay to use as a credential. Once we have that credential source, well, we're safe to actually store that in our database against the User. And then finally, we're going to do a redirect. And that's exactly what we see here in the front end when we get a new passkey added to our account. So that handles all of the registration flow.
Introducing Authentication Flow5:44
going to do a redirect. And that's exactly what we see here in the front end when we get a new passkey added to our account. So that handles all of the registration flow. And we can now focus on potentially the more exciting part of passkeys, which is authentication, logging in using one of these safe passkeys. If we head to the login page, you'll see that in the case of 1Password at least, it's already saying, oh, I have some information stored that you can autofill. But if we click one of these options, it's just going to fill in our email. Nothing else happens because we need to implement the rest of the logic. Much as the registration of a passkey takes place in two steps, options, and then storing in the database, authentication also takes place in two steps, but for a slightly different reason. This time, I want you to picture a 1950s spy thriller, where we have a dingy
in the database, authentication also takes place in two steps, but for a slightly different reason. This time, I want you to picture a 1950s spy thriller, where we have a dingy bookshop and in walks a secret agent holding an umbrella and wearing a trilby. He walks on over to the front desk and starts making small talk with the shopkeeper. The shopkeeper looks him up and down and thinks, well, this may be a secret agent. And so the shopkeeper says, terrible weather we're having today. And then the secret agent looks around and says, good job, I have my umbrella. And all of a sudden, the dingy shop transforms into the MI5 headquarters. What's going on? Well, the shopkeeper offered a challenge. That challenge was the phrase, terrible weather we're having today. And the response from the secret agent would essentially tell the shopkeeper whether or not he was a trusted agent, whether he
was the phrase, terrible weather we're having today. And the response from the secret agent would essentially tell the shopkeeper whether or not he was a trusted agent, whether he was somebody that should be able to access the MI5 headquarters. That's sort of what's happening with authentication in passkeys. In other words, your server is the shopkeeper and your server is going to say, okay, here's a challenge that I'm going to present to your passkey. The passkey knows how to answer that challenge. And so it will respond. And then it will send that information back to the server, back to the shopkeeper, who will decide whether or not that is a correct response. And if it is a correct response, well, you're going to be allowed into your account. So the first thing we need to do in tackling authentication is provide the browser, the authenticator, the secret agent with a challenge.
Building Authentication Options Endpoint7:57
going to be allowed into your account. So the first thing we need to do in tackling authentication is provide the browser, the authenticator, the secret agent with a challenge to answer. Let's add our new options endpoint to the ApiPasskeyController. So underneath registration, we can add a new public function. And let's go ahead and call this authenticateOptions. This needs to build up another object. So we can say options equals, and it's a new instance of PublicKeyCredentialRequestOptions, not CreationOptions, RequestOptions. You'll be glad to know that it requires a lot less effort to build than the CreationOptions. The first thing we need to pass is a challenge. This is just a cryptographically secure random string. So I'll use stringRandom for this. And then we also can pass an RPID, which is a relying party identifier. It needs to use the exact same logic that
secure random string. So I'll use string random for this. And then we also can pass an RPID, which is a relying party identifier. It needs to use the exact same logic that we used for creation, passing the app.URL inside our config and extracting just the PHP URL host. So I'll drop that in there. And then there are other options we can pass, such as allowCredentials, but we only have to pass these in certain circumstances. So I'll come back to that particular one in a later episode. Much like the creation options, requestOptions is JSON serializable. So we can return this directly inside the controller and it will automatically be converted to JSON. But before we do, we'll need access to this challenge in the subsequent request. So much as we flashed our creation options to the session, I'm also going to flash the authenticationOptions.
Wiring Frontend Authentication9:28
we do, we'll need access to this challenge in the subsequent request. So much as we flashed our creation options to the session, I'm also going to flash the authentication options to the session. Passkey authentication options can be the name of the key, and then we'll pass the options object in directly. Let's now head to the api.php file and we'll add our new root. That's Route::get, /passkeys/authenticate. And that needs to go to the PasskeyController and our new authenticate options endpoint. Of course, you don't want to add the middleware here because this needs to be accessible by a guest. Now we can turn our attention to the front-end. I'm going to create a new Alpine.data component for authentication. And I'll go ahead and paste that just down here. Alpine.data, let's say authenticatePasskey, and then inside
I'm going to create a new Alpine data component for authentication. And I'll go ahead and paste that just down here. Alpine.data, let's say authenticatePasskey, and then inside here we can start performing our actual logic. So we could have an async function called authenticate. And the first thing we need to do is go and grab those options. So we'll say const options equals, and let's use await here. await axios.get, /api/passkeys/authenticate. Once again, we'll start nice and simple. I'm just going to console.log those options out to make sure that the endpoint is configured correctly. And let's jump to login.blade.php, and I'll wire up the login form to actually listen for this x-data component. x-data, authenticatePasskey, and then we need the options to actually be retrieved pretty much as soon as the page has loaded. So I'll use
listen for this X data component. X data, authenticate passkey, and then we need the options to actually be retrieved pretty much as soon as the page has loaded. So I'll use X.init to make sure that as soon as this form is available, that request is actually made. We'll want to call the authenticate function that we've just created. And that should be everything we need for the moment to wire this up. So let's go to the front-end. And sure enough, we have our response. One thing I forgot to do, if we go back to app.js, is we want to log options.data rather than just options. And yeah, there you go. We have our challenge, and we have the relying party ID, which is our domain name. So now we can pass this to the WebAuthn API to actually request the correct passkey to
our challenge, and we have the relying party ID, which is our domain name. So now we can pass this to the WebAuthn API to actually request the correct passkey to answer the challenge. Let's get rid of console.log here, and we can say const, and I'm going to call it answer because this is answering the challenge that the options have provided. So const answer equals await, and then we'll call startAuthentication, passing in options.data. And startAuthentication, once again, is being provided by the simple WebAuthn package. So make sure you import that at the top. Okay. So we await startAuthentication, which should prompt us to actually select the passkey we're interested in. Let's see if that's the case from the front end. If I refresh the page, you'll see our authenticator prompts us with a modal and says, which passkey do
we're interested in. Let's see if that's the case from the front end. If I refresh the page, you'll see our authenticator prompts us with a modal and says, which passkey do you want to authenticate using? I have three registered for this website, so I can choose any of these three. If I click one of these, well, nothing's going to happen, but in the background, it's used that passkey to answer the challenge that our server provided. So in the next episode, we're going to take that answer, send it back to the server, and then verify that it is actually correct before logging the User in.
