Up until this point, we’ve given our users free range to do whatever they want in their app. To be clear, we’ve done this intentionally, because we wanted to make things work, but now's the time to lock things down so only logged in users can perform actions in our app. But first, we need some kind of authentication method.
We’ve been able to see how the Appwrite SDK makes working with these different services easy, but authentication takes this to another level. Authentication can be tricky. When we log a user in, they expect to be able to refresh the page and stay logged in, but to do this, we need to store something like an access token somewhere and there are a lot of security implications around how you do this! But Appwrite handles all of this for us, securely storing a cookie, so that any requests we make with our active session will pass that along and handle it for us.
But for now, we just want to log them in, so we can easily get this process started by kicking off a new Magic Link session, verifying it, and getting our active session ready to go for interacting with it.
We’ll start off by kicking an email to our user by creating a Magic Link session. Through this, we’ll want to also pass in the URL of the location we want our actual Magic Link to take them to when returning to the application, which in our instance, is a session page that will include URL parameters to read. Once they’re there, we’ll use URLSearchParams to read the param values and pass them to the verification endpoint to confirm the session, along with some UI updates to give feedback for their actions.
What You’ll Learn
Instructor: [0:00] Our application is looking pretty solid. We have all of our event data in place, including the images, as well as the individual event pages, being able to add a new event, and even deleting an event. But we have a big problem. We currently don't have any authentication associated with our application, so that means anybody can create an event, and anybody can even delete an event.
[0:20] Now, let's add some authentication. To get started, let's head over to our Appwrite dashboard, where let's go over to the Auth tab. For the first thing that we'll notice, this page is intended to be where we'll see all the users of our application. We can even create a new user if we want, but we want to handle that programmatically, so we're not going to start off there just yet.
[0:38] Let's head over to the Settings tab, where we can see that we actually have a lot of different methods for how we provide this authentication. We even have access to a lot of different OAuth providers.
[0:48] That means if we want to provide Google authentication, or you want people to use their Yahoo accounts, we have a lot of different options for what we can provide.
[0:56] For this lesson, we're going to focus on using Magic URL authentication, which is going to provide a service agnostic way in order to authenticate our users.
[1:04] Optionally, you can go through and uncheck a lot of these different options. It really doesn't matter too much for this lesson, but ultimately, we're going to want to make sure that we have Magic URL enabled.
[1:14] Heading over to our application, if we go to our existing login page, the way that this is going to work is somebody's going to enter in their email address. When they do, we're going to create a new session from that email address. Once they do that, they'll get an email that includes a link that navigates them back to the application.
[1:31] That page is going to be the session page, where there, we'll receive some URL parameters, which we'll extract from the URL, and then use that to validate the existing session that they just created. Once we validate that, we'll send them back to the home page, where they'll then be authenticated.
[1:47] Let's get started by heading over to the Appwrite authentication docs. If we scroll down to explore the different endpoints that we're going to have available, we know that we're going to use Magic URL for this lesson. Let's go ahead and click that and go to the Magic URL section.
[2:00] The way that this is going to work is we'll first send that email by using the createMagicURL session, where we'll pass in a unique ID and their email. When we pass in this unique ID, this is going to assume that this account doesn't currently exist.
[2:15] When it gets to Appwrite, it's going to see if that email does exist. If it does, it will instead associate this session with the existing account and the ID associated with that account.
[2:26] Like we already talked about, once they click that email, we'll be able to extract the information from that link and update that Magic URL session to make sure that it is a valid session and consider them logged in for the application. Let's dig right in.
[2:39] For this, we're going to use the account service. Starting off in our code, we're going to head to lib Appwrite. We now need to make sure that we make that account service actually available. We're going to create an export, our new account constant, along with a new instance where we pass in our client.
[2:56] Next, similar to what we did with events and storage, I want to create a new file where we can include all of the different functions associated with authentication and users. I'm going to create a new file under lib and I'm going to call that auth.ts. Inside, I can get prepared by importing my new account service from @lib/Appwrite.
[3:15] In this first function, we're ultimately going to want to start the login process. We're going to use Create Magic URL session in order to do that. I'm going to export a new async function called logIn(). Inside, I want to await my new account.createMagicURL session.
[3:32] Again, we're going to have two arguments. We're going to have our unique ID, as well as our actual email that we're going to be able to pass into this function.
[3:41] Starting off with our unique ID, similar to what we did in past lessons, we can import our ID helper from Appwrite. With that, we can update our unique ID to ID.unique(). Then we need to create our parameter for our email, so I'm going to add email as our login parameter. That's going to be a string.
[3:59] Just in case we need the results of this later, we can store it inside of a data constant and then return that data. Now that we have this logIn() function, let's get started using it. If I head over to pages, Login.tsx, I have an existing form that includes that input as well as a Submit button.
[4:16] What I want to happen is anytime this form is submitted, I want to grab that email and then pass it to that logIn() function. On my form, I'm going to add an onSubmit handler. Let's call that handleOnSubmit.
[4:28] At the top of my file, I'm going to define an async function called handleOnSubmit(). I know that I'm going to want to grab the information from the submit handler for my form. I want to first define my argument of events for this submit handler. I'm going to type this out as react.SyntheticEvent.
[4:46] Because this is a form and a submit handler, I want to prevent the default action from occurring, which would be submitting this form to the page. I want to make sure that I run e.PreventDefault() to prevent that from happening.
[4:58] If we remember from an earlier lesson, where we were creating a new event, in order to type out the different file inputs, we were able to define this target as well as the different types for all of our fields.
[5:09] To make this easier, I'm going to go ahead and just copy this and paste this into our summit handler. What's happening is we're saying that our target is going to be our event target, but we're going to type it out as the type of our event target as well as the fields that we want to define.
[5:22] Here, we don't need all these fields. We only need one field. That's going to be an email, which is going to be a string. This value is going to be available @target.email.value. Let's go ahead and grab that log in function so we can send it along.
[5:37] At the top of my file, I'm going to import my log in function from @lib/off. I'm going to take that target email value. I'm going to run a wait log in and pass in that email.
[5:49] Before we go any further, let's test this out and see what happens. Back on my log in page, if I enter in my email address and hit submit, we should see a new email arrive in our inbox. It's coming from appWrite.IO.
[6:00] It's going to be that magic URL off session, which includes all those parameters that will later parse to be able to validate that session. We have one big problem here. This appWrite.IO link isn't going to go to our application. We're not going to be able to see this.
[6:13] If we go to the account API documentation and navigate to create magic URL session, we can see that this actually takes in a third argument, which is going to be a URL. We can customize in order to send people to a location that we know is going to exist. We're going to validate that session.
[6:30] One thing to note here, we can see that it says that only URLs from host names in our project will be able to be allowed. The way that we're going to handle this is we're going to pass in the current URL of the page they're on.
[6:41] Whether they're in the local host, they'll get this local host value or if they're in production, they'll get that production URL to send them away. You could alternatively set up an environment variable depending on the environment they're in. You can pass along the URL to use for that Redirect.
[6:56] Inside of our off.ts file, let's head to the end of our create magic URL session. We're going to add that URL. We're going to create a dynamic value that uses window.location.origin. We additionally want to pass in that session path.
[7:11] If we try to submit this log in form again, we should be able to see our new email along with the local host value as our host connected to that session path. Let's go ahead and open this up.
[7:21] In a new tab, I'm going to paste in that URL. As we saw earlier, this is going to be the session page. We validate the information so that we can make sure it is a valid session, and then pass them along as an authenticated user.
[7:32] If we head over to pages and then session, what we're going to want to do is we're going to want to grab the URL parameters from within this page. We're going to fire another function where we pass in those values. Let's first grab those URL parameters.
[7:45] I'm going to import useEffect from React. I'm going to create a new instance of useEffect. I'm only going to want this to run once. I'm going to pass in an empty dependency array. Here, what we want to do is we want to grab this user ID and secret from the URL.
[8:02] The way that we're going to do this is use a new instance of URL search params. We'll pass in the window.location.search, which will include those values as a string. Let's store that in a params constant.
[8:15] I'm going to want to grab my user ID. I'm going to use params.get. I can pass in the key of the param that I want to grab. In this case, it's going to be user ID. I want to also grab the secret.
[8:27] To make sure that this is working, let's go ahead and console log out our user ID, as well as our secret. Once the page reloads, we can see we have both our user ID and our secret. What we'll do is pass these values to our create magic URL session confirmation.
[8:43] Here, I can use the update magic URL session to pass in that user ID and secret in order to validate the session. Inside of lib/Auth.ts, I'm going to create a new function. Let's call this verify session. We know that we're going to want to pass in two things. We're going to have that user ID and the secret.
[9:00] Let's define those different parameters of user ID, which is going to be a string, as well as the secret will also be a string. Instead of create magic URL session, we're going to update that session. We're going to pass the parameters in exactly how we're passing them into the function.
[9:15] Let's get rid of those existing ones, pass in our user ID, and our secret. Back on our session page, let's now import our verify session function from @lib/Auth. We're going to be using an asynchronous function. We want to be able to use the await syntax.
[9:31] We need to wrap that within an async function. I'm going to create a self invoking function called run. Inside is where we're going to run our request. We want to make sure that we invoke that immediately. I can await my verify session request. I'll pass in that user ID and secret.
[9:48] Like most things, we want to make sure that we run this only if these values exist. Before we ever even get to this function, let's say, if type of user ID does not equal string or to make sure that both are strings, we say, if type of secret does not equal string, let's go ahead, just return, and never get there in the first place.
[10:09] Later, we can provide some error handling or some way of redirecting them back to the login page in order to start a new request. For now, all we want to know is we want to try to verify the session if we have that information. If I head back to the page, it might have already refreshed.
[10:22] Let's go ahead and refresh it again. As I suspected, it had already refreshed in the background. We can see that we're now getting a 401 request. That's because we had already validated that session.
[10:32] One thing to keep in mind here is, we are currently using React strict mode, we're also going to run into that situation where two requests are going to happen out of the box. It's going to run once and then once after hydrating. Just to be clear, this will only happen in dev mode because of how React strict mode works.
[10:48] We can even validate that the session is working by heading over to our network tab. We start to look at the request that we're occurring to Appwrite. If we scroll down to the request headers, we're eventually going to see the cookie that has our Appwrite session.
[11:01] The cool thing is, this is a session cookie that's managed completely by Appwrite in the Web SDK. We don't need to touch this at all, which is going to only help our case for making sure that we're doing what we can to provide a secure experience in the application.
[11:14] If we head over quick to the Appwrite dashboard, we can even see that under off, our account does now appear under our users list. What we're going to save interacting with this session until the next lesson, I want to do two things before we wrap this one up.
[11:29] Whenever we submit this login form to begin with, I want to show a message to let the person know that the form was submitted so it doesn't just look broken. On this session page, I want to make sure that I send them back to the home page once I know that it is a valid session.
[11:43] Starting off inside of login, whenever this form is submitted, and our login request finishes, let's update some state that designate that this was submitted. I'm going to import use state from React. We're going to simply create a constant of sent and update that would set sent. We're going to set that as use state where the default will be .
[12:04] After I successfully call that login function, let's say, set sent to true. One thing to note is it is possible that this login and verify session will fail as we saw a second ago where we had already validated that session.
[12:17] In a later lesson, we will learn how we can handle those errors and provide an experience to make sure our users aren't confused when things don't simply work. Whenever it's sent, we'll have that value stored in our sent constant. Let's use that.
[12:29] Starting with this form, I only want to show this if it's not sent. We can say, if it's not sent, and, I know that some people have different preferences for how they do conditional rendering. I'm going to go ahead and wrap this with my form. We can see that it's still showing. It hasn't been sent yet.
[12:45] We also want to provide an experience if it is sent. Let's create a paragraph tag where I'm going to say, check your email for a magic link. To make sure that we're adhering to our styles, let's also add a tail end class of text center. When I submit my email again, we can see that it was updated to check your email, of course, as expected, that email arrived.
[13:04] Back on our session page, whenever we finish verifying that session, as long as it was successful, I want to navigate someone back to the home page. Like we used in previous lessons, I'm going to import my use location hook from wouter, which is going to be our router for this application.
[13:19] I'm going to also grab my navigate function from that use location hook. Once verify session has ran, I can navigate to the home page. While I'm here, if we don't have a user ID or a secret, what we can additionally do is navigate to the login page. Somebody needs to start that new session anyways.
[13:37] Let's start off by testing that session page without the URL parameters. We can see, we get navigated directly to that login page, because we don't have those parameters.
[13:47] If I now copy that link and paste it in my address bar, we can see I do have those parameters. It's going to log me in and then take me to the home page. This doesn't do me much good if I'm not actually trying to access any of my information.
[14:00] Next, we're going to learn how to use the authenticated session to grab information from that user's account including that session object so that we can dynamically show things in the UI based on whether they're logged in or not.