Loading
Current section: Authentication 6 exercises
solution

Exercise 03 (Solution)

Transcript

00:00 First, I will install a package called test passkey. I will use it to generate realistic passkeys for my end to end tests. What this package does, it generates a key pair consisting of a public key and a private key and returns two things. The first one being this credential object, this is the part of the passkey that gets stored in the browser, and then the public key which is the part that gets stored on the server. And I will use this package so I don't have to implement all of this in my own tests.

00:27 To install that package, open your terminal and run npm install test dash passkey and provide a flag to install it as a dev dependency. Now I will head to the test file at test slash end to end slash authentication-passkeys.test.ts and complete this first test case. So first, I would need to create a test user. I would use the same function we used before, create user, and store it in a disposable object called user. Now I need to generate a test passkey and this is where the test passkey package comes in.

01:00 So I will head here to the imports and import from test passkey package, the function called create test passkey. Then I will call that function over here, create test passkey and assign it result to a variable called passkey. This function requires a single argument, which is an object containing a key r p I d. This stands for relying party I d. And in a passkey, it represents a domain for which this key was issued.

01:25 That is done for security considerations so you couldn't use this passkey on a different domain, for example, through phishing. So here we want to bind this passkey to our app And for that, we need to grab the current URL of our app and get it hold its host name by wrapping it in the URL constructor and grabbing the host name property. And because we're relying on the page URL, we need to have our app already open in the test before creating the task passkey. So I will head here and navigate to the login page. And by doing so, the created passkey will already be associated with the host name of our app.

02:01 Now the next thing I want to do is create a record in the database that collocates this user and this passkey. To do this, I will use a utility called create passkey that I already have in my db slash utils. This utility is extremely similar to the ones we implemented before, like create user. And all it does is creates a record in the database using the Prisma client, returns that passkey, and also has this async dispose callback to delete the created passkey when the test is done. The important bit here is that it accepts the public key, which is a public part of the passkey, and also a user ID to associate that passkey with.

02:38 So now I need to provide that data in the test. So here when I call this function, which returns a promise, I need to provide a bunch of things. Let's start from the public key, which we can get from the test passkey, dot public key. Then the ID of this passkey will be passkey dot credential dot credential ID. The user ID will be user dot ID, which is our test user.

02:59 And the a a g u ID will be passkey credentials a a g u I d or an empty string if it's not set. And now because this create passkey utility returns a disposable object, I will consume it appropriately by calling await using and using underscore as the name because once again, I don't need the return database entry in this test. And now I will log in using a passkey in the UI by locating the button by role page role button and its accessible name. Log in with a passkey and I will click that button to initiate this login flow. And as the expectation, once again, I want to make sure that the user profile link is visible in the UI, confirming that the login was successful.

03:42 Now there's just one thing missing from this equation. So we created the user and created a passkey for our app and created the entry in the server associating the user and the public part of this passkey and what's missing is storing that passkey in the browser because passkeys are stored on the device and we can use Playwright's API to do that. Adding a passkey to the browser will consist of two steps. First, adding your web authentication client and then adding our passkey to that client so the browser can use it. I will head to the top of this file and create a new utility which will be an asynchronous function, called create web authentication client.

04:20 This utility will accept a Playwright page of the type page from Playwright, and then I will call page dot context to get the browser context and the new c d p session method there. Calling this method creates a new session in Chrome DevTools protocol. This is a special protocol that Playwright itself uses to communicate with Chrome and automate it. We will use this protocol to register a new virtual authenticator that will use our passkeys. So it's an asynchronous method, and we will store the result of it method in a variable called session.

04:50 Now I will call session dot send web authon enable and await this promise. Sending this argument to the CDP session will enable web authentication. Next, I will create a virtual authenticator by calling session dot send with this argument which is web authentication dot add virtual authenticator. And I will use the following options. The important one here is the last one, automatic presence simulation, set to true.

05:17 By setting this option to true, playwright will automatically select the first evaluable passkey from the list of passkeys, which is exactly what we need. I will store this virtual authenticator in a variable called authenticator. And from this entire utility, I will return two things which is our CDP session and the authenticator ID, which is authenticator authenticator ID. And the only thing left to do now is to add the test passkey to this authenticator. So I'll head back to my test.

05:48 After I created this passkey record, I will call await create web auth and client, my utility, provide the page as an argument, and here I will destructure this return object to grab the session and the authenticator ID. And now to add the pass key to this authenticator, I will call await session dot send and provide it this argument web authentication dot add credential. And I will describe what kind of credential I want to add in the second argument which is the options. Here, I'm telling the CDP session to add this credential to this virtual authenticator by ID and this is the credential itself. Here, I'm referencing my test passkey and spreading its credential object including the username from my test user, user handle is the user ID encrypted in base 64, and also the user display name if they have one.

06:36 If not, it will be an email. And now this concludes the setup for this test. So let's recap quickly. We created a test user like before. We went to the login page, so our application is already open in playwright.

06:48 We created a test pass key binding it to our host name, to the host name of our app, and then we created the server side record in the database connecting the pass key, its public part, with our user ID. And finally, we used the virtual authenticator to add this passkey as a credential object for our test. Now let's run the test to see if it's passing. So in the terminal npm run test end to end. I can see that there's an error with the test, and if I scroll up, I can see why it's failing.

07:18 Playwright is complaining that I was expected to pass a page or a frame to this new CDP session method. So let's scroll up to this utility, and here, even TypeScript is warning us. So let's add page here as an argument. This is the same page that we passed as an argument in the test over here. Now that I have this fixed and the CTP session should be created correctly, let's run the tests again, npm run test end to end, and now hopefully they will pass.

07:46 That's exactly what's happening. If you scroll to the server logs, you can see that we're hitting the web authentication methods because we're using passkey as the authentication. And naturally, all the steps that we are describing from adding the passkey and to choosing the proper login method are passing including our expectations to be signed in. Let me draw your attention to how our test looks like. So here are the lines that we spent automating the browser and acting as the user on the page and all of these lines are part of our test setup.

08:16 You can see how much effort we spend arranging all these pieces just to have a simple test afterward. And this is the right way to approach complexity in tests. Because this scenario is moderately complex, and one of the common mistakes developers make is they rush in head on into the test juggling all the pieces together instead of approaching it methodically. Understand what moving pieces you have in your test case. Where is the main root of the complexity here?

08:42 Like in our case, it was to understand that the passkey will be stored on the device and also on the server, so we arranged those setup steps in order to have a simple test afterward, which is just one action followed by a single assertion. And I will finish by adding another test case that makes sure that our app displays an error by authenticating via a passkey fails. So here I will still need our navigate fixture and the page fixture. To reproduce this scenario, I don't really need a test user so I will head straight to the login page. And now I still need to tell Playwright that the user has no passkey selected or in other words user is not verified to use passkeys.

09:31 If I don't do this and try to go through the normal logging flow, I will get an error message from Playwright that this authentication method is not configured. This is not an error that the actual user would get. So to do that, I will use our create webauthin client for this page. I'll grab back the Session and our Authenticator ID and I will call await session send and I will send this command set user verified That is bound to this authenticator ID and has the is user verified property set to false. By doing this, I'm letting Playwright know that I'm simulating the scenario when the user either doesn't have pass keys or hasn't selected any during the login flow.

10:15 And from here on out, let's just repeat the same steps. Gonna click on the button, log in with a passkey, but my expectation here will be different. I'm expecting to see an error message. So I expect a page element by role alert to have the following error message visible on the page. So in other words, when we're trying to go through this scenario, we navigate into the login page, we're letting the playwright know that there are no passkeys configured for this user, or the user hasn't selected any passkeys.

10:45 And when they try to go through this logging flow, they will see the following error message that the application failed to authenticate them using a passkey. Now let's run both of these scenarios to make sure that they're passing. PM run, test, and to end. And now we've got both of them covered. I cannot stress the importance of the test setup enough when it comes to end to end tests because you have this big complex thing on your hands which is your app and the proper way to address all of that complexity is in the setup.

11:16 So make sure to invest enough effort in writing those utilities, into understanding every test case, what it entails, and what makes it complex. Because once you have those setup blocks implemented and ready, every test case becomes a matter of combining them together and acting as the user would.