Server-side Auth with Remix, Prisma, and the Web Platform

Bookmark

In this talk, we'll get a live coded demo of building custom hand-rolled authentication. When you have the right tools (and we do), authentication can be quite simple and secure. This is more (and better) than just: "Install this library and you're good to go." When we're done we'll have our own auth code that can evolve with our ever-changing requirements without a need to learn some library-specific APIs. We'll be leveraging the Web Platform the way it was meant to be done to give us simple and secure server-side authentication for the web.


You can check the slides for Kent's talk here as well as demo code.



Transcription


Hey Node Congress, my name is Kent C. Dodds and I'm really excited to give this talk, server-side auth with remix, prisma, and the web platform. And we're working in Node because Node is awesome and remix runs on Node and I love remix. So if you aren't familiar with me, I work at remix as the director of developer experience. So if you've tried remix and your developer experience wasn't good, it's my bad, sorry. I'm working on making all that better. I also am the creator of EpicReact.dev and TestingJavaScript.com. And you can check out my website, KentC.Dodds.com, because it's pretty legit. So what this talk is, is a live code demo of adding auth to an existing app. And we're actually basing this off of part of the tutorial from remix. So if you've gone through the jokes tutorial in remix, that's kind of what we're going through here. This is not complete. I only get about 20 minutes with you. And so yeah, it's not going to have everything, but it should give you the right idea of how remix approaches authentication. And I think it's in a really good way. So let's go ahead and get started, since we don't have a lot of time. Let's jump right into the coding. So here, we're actually going to take the full screen off, get rid of this. remix you can find at remix.run, and you can read all about it and scroll through this. It's actually a pretty cool scrolling experience. To go through that and get an idea of what remix is, this is the app that we're going to be working on, remix. So great, it's funny. It's a jokes app where you can get random jokes and it'll just have a bunch of different jokes for you. It's pretty fun. And then you have permalinks and stuff. And in the tutorial, we add the ability to delete the jokes. You can add your own. And this actually works right now. But the problem is that there's no authentication going on right here. So anybody in the world could add their own jokes. We never know who did it. And people who add jokes, they can't remove those jokes. And we don't want to add that capability until they can log in because we don't want people removing other people's jokes. So that is what we're going to be adding to our app is just the ability to authenticate. So we're going to start out in our schema.prisma file. So we've already got prisma up and running. That's holding all of our jokes. And we have a seed to get all of these jokes put into the database already. But we want to add user authentication. And so to do that, we're going to need a user model so we can associate jokes to specific users. So we'll say model user. And Copilot's going to help us out a lot with this stuff. Not all this is what we want, though. So we get an ID. I like the UUID personally. Thanks a lot, Copilot. We do want the created app. We want the updated app. And then we're going to have a username, not just a name. And this needs to be unique. And then we don't really need an email here. So we'll get rid of that. We do want a password, though. But we're not storing raw passwords. We're going to store a hash of the passwords to make it nice and secure. And then we have a relationship with the joke model inside of this user. So we'll have jokes. And that is an array of jokes. And it looks like my Copilot didn't quite get this right. So this is going to default to now. And there we go. So now we're getting a red underline here because the joke model is not associated to the user model. And, whew, that was a close call. We had two Ns right there. So now we want to add a jokester ID. This is going to be a string. And then we'll have the jokester, which in this case is going to be a user. And we're going to add a relation with the fields for this relation is just the jokester ID. There we go. Thank you. And the references is just the ID. And then on delete, we'll cascade. So when we delete the jokester, all their jokes are going to be gone. And let's make sure we spell this correctly. There we go. Okay, cool. So we've gotten prisma updated. Let's go ahead and run a couple of prisma scripts. So we'll run npx prisma db push to push all of these changes. Yes, we were going to delete all of the things. And now we should have some type errors right in here, because we're now seeding the database with jokes that don't have jokesters associated. So let's go ahead and make our first jokester. And this is going to be Cody, we're going to await prisma user create. And the data for this is going to have the username of Cody. And the password hash is actually going to be Twix rocks. And I have this pasted over here because I, you don't want to watch me type that out. But this is basically Twix rocks hashed. And so that's going to be our password for for our Cody user. And now that we have Cody, we can use Cody for generating or creating all of these jokes. So we'll say our data is going to be all of the joke properties plus the jokester ID of Cody dot ID. And so that can be our data. And now our typescript stuff, hopefully will go away. I'm not sure why it's not. Let's just figure out what's going on here. jokester ID, am I misspelling something jokester ID? There we go. I misspell things right in programming gets kind of funny if you don't, or rather consistently doesn't have to be right, but it has to be consistent. We know that from the refer header. Thank you very much. Okay, so then I've got this script here called npm run set up DB. This is going to push and then regenerate everything. And of course it blows up because the user doesn't exist in the database. So we're going to prisma DB reset. Oh shoot, no, it's prisma DB push. I thought that I'd done that already. Now it's prisma DB migrate dev or no, it's just prisma. Oh man, I'm falling all over myself. Ah, what am I doing? Yes. And this is going to be users. Sweet. Okay, so now we've been able to seed our database and now we have users and everything and everything actually still works. So yeah, I'm actually technically still running this. So the app, whoops, app still works technically until we try to create a joke. Now creating a joke will not work because you need to have a user ID to create jokes, but each one of these jokes is associated to our Cody user. And so now all we need to do is make it so that we can log in as our Cody user so we can add additional jokes and stuff to our app. So with that in mind, let's go ahead and go to our login, which is already all coded out for us. We go to slash login, then we'll see we have both login and register in here. I don't have a lot of time, so I pre-coded all of this stuff up and we've already got our action for handling our form submission. So you can see the form submission right there. We've got our validation being handled here to show validation errors and all of that stuff. That's already coded up for us. So all we need to do is write the logic for logging a user in, and then we'll write some logic for getting the user's information when they're creating a new joke. So let's make a new file to handle all of our session stuff. So we'll go to utils and we'll create a new util called session server TS. So this is only a server file. That's why it's called session.server. And we're going to export a new function. That's an async function called login. And this is going to accept a username and password. And that's username is a string and the password also a string. Both of these things are required. And now we just need to let's bring in the database actually. So DB is going to come from our DB server. This is our connection to prisma. And we'll say user.findfirst. And we'll say where username. And now we'll have our user is a wait. So now if we don't have a user, then there is no user by that username. And so we're going to just return null and then our UI will handle it and say your username or password is messed up. If we do have a user, we need to double check that the password is correct. And we're going to we used to bcrypt to generate the hash. We can use bcrypt to compare the hash. So we'll say bcrypt from bcrypt.js. And with that, we can say cost is correct password equals bcrypt. We're going to await this. It's async because it's slow. And that's like, that's the feature actually, is that it's slow. And that's why it's secure. Fancy that. So if there is not, if this is not the correct password, then we'll return null and let our UI say username or password is messed up. And now if all of those things are fine, then we can return the user. So that's that is authentication. That's login when you've got your own database, and you're just hashing stuff with bcrypt. It's actually really quite simple. And so we can log in, we can get a user, but we need to create a session. And so we're gonna we'll do that next. But let's go ahead and continue with this for now. So let's get our user. User is await login, we'll bring that in username and password. And if there is no user, then we're going to return a bad request with a form error that says, hey, this user does not exist or whatever we want to say. So like, user or yeah, incorrect username or password. Thank you, copilot. That helps out a lot. Because we're if if there is actually a user, then we can actually redirect and everything. We'll do that later. Let's just make sure that we can get the user first. So it's console log, the user. And now that console log is going to show up in our log output here. So I'm going to just clear some space, we'll say Cody and Twix rocks. And it's not implemented yet, we're not actually redirecting. But on the server side, we are getting the user. So that totally works. Huzzah. That's awesome. And even our validation works as well. So if I go to register, and we're down to like just a couple of things, we're going to get all of our validation logic and everything that we kind of built that before. So sweet. So now what we need to do is create the user session and redirect them to the jokes page. So let's do that. In our session server, we the way that we manage sessions in remix is to use cookies. And we have built in support for this web platform api. So we're going to import a couple of things from remix. And the thing we want is create cookie session storage. And here we'll get our storage from create cookie session storage. And there are a couple of properties that you're going to set for this. And they are all on cookie. So this is how we configure our cookie. So there are a couple of things that we want to do when configuring our cookie. First we need to give it a name. And you can call it whatever you want. It's namespaced to your domain. So I'm going to call it the remix jokes session cookie. That seems to make sense to me. And then also we want to encode this cookie. So even if the user pops open their dev tools and a look at the cookie, they'll have no idea what this thing is. So we're going to encode it. And we also means that even if they try to change it or something, we'll know that they messed it up. So we do have a secret. And this is processenv session secret. And with that, of course, typescript is mad at us about this. So we're going to make it constant and call this session secret. And then we'll say if that's not defined, then we'll throw a new error that says, come on Copilot, thank you. Session secret must be set. Sweet. So we've got our secret set up. We want to set this to HTTP only so that users can't or third party scripts can't access this cookie in the browser. That's one of the big problems with using JOTS in local storage. So yeah, HTTP only. We also want it to only apply to secure connections. So no man in the middle attacks there. Or rarely, I suppose. And then max age. Let's make this go, no, not seven days. Let's do 30 days. Let's log into a JOTS app every seven days. Not me. No. Okay, cool. So now we've configured our storage. So now we can create sessions with this storage. So we're going to make a function called create user session with the user ID as a string. And we could also have a redirect to so that redirect to is configurable. But we'll just always redirect them to slash. You know what? Let's do it. Let's say redirect to string. So they can control where they want the user to go when we create this user session. Also let's capitalize this properly. There we go. Okay, sweet. So let's make our session. We've got our session equals await storage get session. So what this is going to do is it's automatically going to create a brand new session for us. So we're not getting a session from an existing cookie. We're not passing an existing cookie. So it'll just create a session object for us. This is also async. So we'll need to make that async. And now we'll say session set user ID, user ID. So we don't have to do any sort of like database lookup or anything like that. It all just lives in the cookie. And we know that if it's in the cookie, then the user is authenticated. And there was no other way for that to get in there other than going through our session secret encoding and all of that stuff. So we're this is actually very cool, like quite secure. It's awesome. So now that we have the user ID set into the session, we're going to return a redirect to wherever they wanted to redirect to. But an important part of all of this is that we set the set cookie header so that the browser will set the cookie in the browser's cookies registry or whatever. So we'll have some headers right here with a set cookie. And we'll use the storage to commit the session. And here's our session. And the session is a sync. So we'll wait that. And then this redirect is a utility from remix that just is like a helps you create response objects. So that's what we're returning from this create user session. We'll export this. And now we can use create user session right here. We'll just return create user session with user ID and redirect to which is coming from the UI. So based on how we got into this UI in the first place, the pages that can redirect to the login will set the redirect to. So the user can go right back to where they started from. OK, cool. So let's try this. So we're on login. I can say, Cody, Twix, Rox, and boom. Now I am on the jokes page. And I've got my application right here. So I can look at my cookies right here. And boom, there it is. That's my RJ session. It's all encoded and stuff. I have no idea what this is as a user. And if I tried to change it or anything, it wouldn't work because my server wouldn't be able to decode it at all. So that's sweet. Now when I want to go to the jokes slash new page, I want to be able to enter in a joke here and add that. It all blows up. I don't have good error handling on this page yet. And the reason that it's blowing up is because we're not getting the user's ID yet. So let's go to that really quick in the last couple of minutes that we've got here. And we'll say, hey, yeah, thank you, typescript. You would have saved me from a terrible doom. What I really want to do is at the very top of my action of handling this form, I don't want to go through all the validation stuff and say, hey, you got this wrong. You got this wrong. And then they finally get it right. And only then do they realize, oh, shoot, I'm actually supposed to be logged in. So we're going to just put this at the very top to get the user ID. And we'll say, oh, wait, get user ID. And actually, let's call this require user ID so that we know that if they don't have it, we need to redirect them. Because some routes, it might be optional to have a user ID. So in our case, it's required. So we're going to say required here. And we'll pass in the request. So require user ID is going to come from our session server. And we'll export a function called require user ID that takes the request. And the request is what holds the cookie. And so this is a request object. And that's a web standard right there, request. That's coming from the web fetch api, the web api. Super cool. So there's the web part of everything that we're doing. Also cookies. But yeah, so let's get our session. Wait. Storage. Get session from the request headers. Get cookie. So the request is automatically going to have the cookie on it because that's just the way the browser works. And if we have the user ID, or rather, if we don't have the user ID, then we know that the user's not logged in. And instead of throwing a new error, we can actually throw a redirect to say redirect to login. So user's not supposed to be here. Let's redirect them. And you can throw responses in remix, which means that this is really nice and abstractable. You don't have to do a bunch of logic here like, is there a user ID? There's not. So let me redirect or whatever. You don't have to do that in every place you need the user ID. You can just do it right here and remix will handle that for you. It's really, really awesome. And you can do that for are they authorized and a bunch of other things too. It's really cool. Okay. So if they do have the user ID, then we'll just return that user ID. And we'll come over here, grab that from the server. Now we've got the user ID right there. And that is going to be our jokester ID. Boom. Sweet. So now I can say, let's just refresh here, milk, and let's see milk. Milk is also the fastest liquid on earth. It's pasteurized before you even see it. Sweet. And so now we have a joke that we were able to create and it's associated to our user. And that's awesome. Unfortunately, I don't have enough time to show you like, let's add registration and log out and stuff. But you should get a good idea, like basically log out is a matter of instead of commit session, you destroy session. And that works. It's pretty simple. But what I really want to point out here is that this is actually quite simple. It's using platform APIs. You're able to talk to the database directly. It's all like seamless between the client and the server. You also, I just really love that because we've implemented it, we see all of this code. And it's actually because it's simple, we can add and change whatever we want to. So that's the beauty of remix is it makes it so that you are in control and you can change things over time as needed. And I just really appreciate that simplicity of remix. So that is the demo. Feel free to go to the repo to take a look at the code. And like I said, this is actually coming straight from the remix docs. Oops, not dos, the docs. If you go to the jokes app deep dive. You can go through this whole thing. We go from nothing to setting up the prisma database and everything to mutations and authentication, all of that stuff. It's awesome. Oh, and by the way, we also talked about setting up javascript. You may, you probably didn't notice, but if you look at the network tab here and look at the javascript that's loaded on the page, there is no javascript on the page. So we did all of that without any javascript on the page, which is just kind of a fun trick. Of course, we do want javascript on the page for accessibility and better user experience. But that's just kind of a fun spoiler alert there for you as you're going through the tutorial. So that's it. Thank you all so much. I hope you have an awesome time at Node Congress and we'll see you all in the future. Bye. Hello. Hey Ken. Good to have you. Thanks for this amazing talk. So most people, 48% said blue, green, yellow, pink, red. Give us the word. That's the correct order. That is correct. So well done, 48% of you. I can't, well, I can't. People remember this. I just remember the font, but that's me. And the shadow. It's a nice logo, but I don't remember the order of colors. I know the colors from the Coca-Cola logo. There you go. That's a bit easier. So Ken, we're going to jump to the questions from our audience, if that's okay with you. Or if you have any other plans. No, I'm happy to answer your questions. And I'm looking at the chat here. We've got a couple already. I'd love to hear more. So keep them coming. So the first one you already answered in chat, but for the people that haven't been reading the chat is from Argentyle1990. The data in the cookie, is that encrypted or encoded? Yeah, that's a great question. And I answered it before realizing that I was supposed to wait to answer it now. No worries. But yeah, so it is signed with the secret. So people can't make their own or change it or whatever. And so when it comes back to the server, you can verify that it actually was created by the server. Okay, cool. Thanks. Oh, you just advanced to level one on the Discord server. Congratulations. Oh, really important question from CCC Krish. Is remix ready for production? Yes, it totally is. So my website is a production website has approaching 3000 users. And that's like people who have created accounts and everything. I get around a half a million page views a month. And over 1 to 200,000 unique viewers a month. So that's pretty production. It's not like what I built at PayPal, but I would like I would be so happy to have remix when I worked at PayPal. So yes, it's when we hit 1.0, that was us signaling to the world that remix is ready for production. And we are actively maintaining it and working on it now too. And then as a follow up on that, so let's say Elon Musk came to you and he said, I'm starting PayPal all over again, would you choose remix then? Oh, yeah. Are you kidding? For sure. Yeah, yeah. Yeah, I would 100% like the entire PayPal property is very well suited for remix. And then actually on that note, since you mentioned Elon, he hasn't reached out to me or anything, but I am in conversations with engineers at Tesla. I can tell you that because our conversation started on Twitter. So I know that it's public. But yeah, so we're talking with people at Tesla and various other companies that I'm not sure I'm at liberty to tell you, so I won't mention. But yeah, a lot of people are very interested. Super cool. Yeah. Well, I might have to join the engineering team of Tesla here in Amsterdam. Next question is from Argentile1990 again. So it works like a JWT where you can decode the data, but there's nothing you can do with it on the server. I mean, you can do stuff with it. You can pull the user ID out of it. You could store all of the application state in the cookie if you wanted to. And for some use cases, that makes sense. You are limited to the size of cookies. And you could do multiple cookies without any trouble. Just keep in mind that when you store stuff in a cookie, every single request is going to have that cookie in it. So probably don't store all of your app state in the cookie. But you could if you wanted to. And so yeah, like the cookies as a technology, a web platform technology, like this has very little to do with remix other than remix gives you a really nice api for working with cookies. But outside of that, it's all web technologies. And so yeah, you can do whatever you want to because it's a cookie. Max, I'd like a cookie. Next question is from Hail to the Wood. What are you most excited about that remix can provide for enterprise size apps? So nice piggyback on the previous question. Yeah, yeah, that's a super question. So I think for any enterprise, like one of the biggest problems with enterprise is not actually the technology. It's how do we get this to work with a team of developers? You have like, or even a half a dozen teams all working on the same project. That was probably the biggest, one of the biggest problems that I had when I was at PayPal. Certainly the biggest problem I had when I was at USAA. Just coordinating effort on a single project with people who don't ever talk to each other and don't even review each other's code because they're in different areas of the app. The cool thing about remix is that the mental model is the same everywhere in the app. And so the state management isn't really a question when you're working with remix. In my talk, I mentioned how you don't have to worry about state management here because remix is just, basically your Redux store is the database. And so you're not thinking about how do I keep the client in sync with the server? You're building your app and remix ensures that those things stay in sync just by nature of how it's built. And so the mental model is, it's basically a web 1.0 mental model with a web 4 developer experience. I don't want to say web 3 because that actually has meaning now. But yeah, so it's a really awesome development experience with a very simple mental model. And so when you're working with a team of developers, like enterprise apps are, I can't think of a simpler framework to go with. On top of that, the user experience is stellar. And the out of the box user experience is really good. And so you're doing... End user or developer experience? Yeah, end user. So the developer experience is amazing, mental model is simple, all of that. But then the user experience, end user experience is really awesome as well. And so you don't have to spend so much time trying to make the user experience awesome because just the default user experience is really good. And so, I mean, of course, you're going to want to think about focus management and stuff like that for accessibility. There's only so much that a framework could do. You won't be able to build an abstraction for focus management. Maybe in some ways, but react use effects plus refs are a really nice way to do focus management. And all of react works just fine with remix. And I guess I'll take this opportunity to say in the future, other UI libraries will likely work with remix as well. And so it's not a react framework, it's a web framework. And yeah, anyway, that's a long answer to that simple question. But enterprises will love remix. Nice, well, that's the answer we want, right? Yeah, for everyone. So my call MC is asking if you can direct questions to my son, but let's keep it with Kent. I think, well, if you came to see my son, then let me know. But I think people are here for you. CCC Chris is asking, cookies is the default auth choice. There might be other options that might be better for certain scenarios or not needed. Yeah. So I can't think of other situations where you'd want to use something else for a web app. But remix can also be used for other types of apps as well. So like, you could actually take your COA or your Express REST server and rewrite it in remix, like re-implement the whole thing in remix. You could have a graphql api that's written with remix. And actually, the really cool aspect of doing that is that eventually you decided, hey, we've got this REST api server. I want to have some sort of admin interface for it or something to inspect some of the data or whatever. Then you could build that because you built it with remix without having to completely re-architect something or run another thing alongside or do some sort of hacky thing to like generate html or whatever. And so, yeah, in those situations, if you're supporting and actually you could have your existing web app that's built with remix and say, hey, I want to have my mobile client be able to interact with this. You can have what's called a resource route that can do anything that you want to respond to any request with any kind of response and put a graphql endpoint on your web app. And then you have mobile clients. And so all of that to say that for those use cases, other authentication strategies might make more sense. But if you're just building a web app, if that's all that your web app is supporting, I can't imagine why you'd want to do a different authentication strategy. This is the web and that's how we do on the web. So just use the platform. Stick with the platform is usually the best choice. Yeah. We have time for one more question and it's from Eugena Edelstein. We have two minutes for discussion. What would you say is the most unique feature that sets remix apart from other frameworks? Good question. Yeah. Yeah. So for folks wondering, a lot of people compare remix to next.js and we don't like to talk about that a lot just because it's distracting. But people do wonder. So we wrote a blog post about it and I'll share a link to the blog post in the notes here later or in the Discord server. But if you just Google remix versus Next and look at the remix blog post, you can see a very in-depth and objective comparison of the two. But the biggest thing for me that sets these two or any framework apart from remix is that remix builds a super, super solid foundation over the network chasm between the front end and the back end. And that is one of the biggest challenges for all frameworks or for all web apps. That network chasm is the reason why state management is hard for UIs. It's the reason why performance is a problem for UIs. And so with a really nice network bridge, you can drastically simplify the mental model, improve performance, and reduce the amount of stuff that you're having to manage yourself. So that's the biggest thing for me. Another pretty significant one is nested routing where most frameworks, like gatsby and Next, I'm thinking of specifically, they have file-based nested routing where you can put files in ‑‑ nest your files and stuff. But that doesn't have anything to do with where you are requesting data and where you're rendering UI, you have to render everything everywhere. But with remix, we have true nested routing. And that's a really huge feature that allows for a really nice developer experience and also a good user experience because we can optimize things because of nested routing. Yeah. Yeah, I remember reading how it works, the routing, and I was really impressed with that. So thanks a lot, Kent, for this amazing session. We unfortunately don't have a spatial chat for Kent's speaker room, but there is a discussion room coming up later today. I can't say the time because times are different for everyone. But be sure to join that if you want to talk more about remix with Kent. Kent, thanks a lot for joining us, and it was really a blast to have you here. Thank you so much for having me. Appreciate your questions. And say hello to your son for me. Greetings from him.
34 min
18 Feb, 2022

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic