Join Evan Johnson as he speaks with security professionals about recent security news!
All right, we are live on Hacker Time, another week on Cloudflare TV. Hacker Time is the number one security show anywhere, and I'm your host, Evan Johnson, coming from the product security team here at Cloudflare.
We have a very exciting show for you today.
I have a pretty large cat sitting on my lap, so I might be a little distracted and she might meow at us.
But we're going to do some programming today and pick up where we left off, building a user login flow, all with Cloudflare workers.
And I'll show you the agenda right here. Here it is.
Oh, my cat just left. She's gone. So it's just us now. And here's our agenda.
So we're going to start by picking up where we left off. We currently have a login, a signup page, but not really a login page.
So we need two separate pages, one where you can sign up, one where you can log in.
And that's going to take a little work.
And then I think we're going to add session cookies. And so without further ado, here's our little signup page.
And let's get started. I've got, let me make it all be on one monitor.
I've got my nice tmux set up here, and we're ready to go.
Let me reposition my windows as well so we can have my tmux on one side and our form on the other and start hacking away with some workers.
OK, so I've got a nice CFTV directory here.
And index.js is where all the magic is happening.
We've got our login thing here. And we've got a we have a separate create account thing.
But we kind of like wrote all the back end code for it. But there's no front end code.
There's no like page you can go to. So let's get that going first.
We should probably have on the home page, login, and then a button that says sign up, which takes you to another page.
And so let's make that happen. I'm going to do this really ugly. I'm just going to be cutting and pasting stuff.
So this will be our login form. Let's add a heading here.
Login. And then here, this will be sign up.
Slash sign up. And then we need to change the form to actually submit our post request to the login API endpoint.
And then we need the sign up form to submit it to create account.
So we've got to, which it already does.
Great. Okay. And then oh, my cat is back.
We need one other heading here that says create account. All right.
This might make two different forms. Let's actually see. So we're going to run Rangler publish and we should see some changes really quickly.
Might make another UI change or two here.
Okay. So we published it. We should have our regular login page. Look at that.
And then slash sign up. The login is enormous. That does not appear to have worked.
I do wrong. So this is a, let's see if this works and then I'll explain why we did this.
Let's see if that actually worked here.
So I'm, I'm ashamed to admit why, why we had to switch those around.
But we'll, we'll just verify first.
This router is one that I built this app docket thing.
I built an express router and an express like router for workers. And because I, yeah, that worked.
And because I did it myself, it's really hacky. And so actually the ordering in which you declare your API endpoints actually matters in my, in my hacky router.
You can probably implement your own, which is less hacky. And I know that we have official routers that are a little better as well.
But this is the one that I'm used to.
I cut and paste a lot of my workers. So it kind of gets, gets used pretty regularly.
Okay. So we have a signup page and a login page. The last thing I want to do is actually have a link to go between them.
And I actually normally implement that like really simply.
We can have what's a good example.
I do this on my personal website with some, some HTML that I kind of like to copy around.
Let me, let me steal some of that. So I think it's just a href tags.
A few of them. Yeah. And a div. This is great. Just cut and paste in some more HTML here of mine.
And then we're just going to have one, two of these. One's going to be login.
One's going to be sign up and it should be good to go. So this will actually look quite nice.
Quite nice to me. I think that's good.
Let's run publish. And we're starting to tie everything together.
I'll also, while we're doing this, load up my Cloudflare dashboard so we can actually see an account you're created and signed up in our dashboard in the, in the actual object store, because we can see all the KVs getting created for each account that gets created in Cloudflare Workers.
KV, the UI. Here we go.
Workers manage workers. All right. Let's see if we have my, my little links at the top.
Oh, I only did it in one place. Look at that.
Okay. That looks fantastic. Why is this slash? I think copy this one other place and then we should be good to go.
This is what happens when you cut and paste your code.
You end up repeating yourself and it just becomes a nasty pattern.
So if this is production code, there's a lot of things you don't want to do that I'm doing.
But this is just for show. Okay. Here are some of my workers.
We are looking for EJCX .net. I'm having trouble seeing it. I have a lot of workers though.
This could be it. Yeah.
This looks like it's it. Okay. Let's test this one more time. We've got our login page.
We've got our signup page and we can kind of go between them and it works great.
Look how fast that is. Serve directly from Cloudflare's edge. Great. Okay.
So we need to find out, yes, this is the right worker. We need to find our KV. And is this it?
Nope. Oh boy.
I just made a KV on accident.
Let's clean it up since I don't actually need it. This is it.
This is the right one. Here are our username and password hashes. Okay. So let us, you'll see EJCX has a password here.
Let's see if I can log in with it. I think my password was also EJCX.
So login, EJCX, EJCX, unable to parse URL. Oh boy. What did I do there?
Well, let's, let's start with signup. Can we sign up a new account? EJCX1, EJCX1.
We have create a new account. Success. Let's see if we see it here in our UI.
I'm going to guess we do, because this code was working.
There we go. EJCX1, password EJCX1. Now let's try to log in. I wonder if I hit an error case there.
Successfully logged in.
Great. I wonder if the parse error was part of a invalid login flow.
Let me try EJCX1 and the wrong password EJCX. Unable to parse URL. Okay. So that's the error we're getting with the wrong password.
So let's try to hunt that down real fast.
So slash login form action goes to slash.
Just doing some cleanup here.
It should actually be just a slash, but it's working because of the way, because this is only listening on posts, login posts.
But why are we getting this unable to parse URL type error?
What are we actually parsing? This thing.
So when we, wait.
So we're logging in with the password. We get the username. We're getting a not success here.
Except it seems to be breaking and redirecting us.
I think it's complaining about this URL because it's just slash and not a full URL.
Okay. So let's try to make it a full URL.
I wonder if it's because it's a, we can do error equals one. I wonder if this is it.
I wonder if I'm doing these redirects improperly this whole time.
That's what it seems to be. I think this should work.
And here we go. All right.
EJCX1. We're going to use the wrong password, EJCX. And that time it worked.
Error one. Okay. So the problem was definitely the redirect URL. And let's just replace all these.
I wonder if it was because I used the 302 instead of the 301.
I wonder if it works with the 301 the other way.
Who knows though.
Takes too long to webpack to find out. I'll just switch them over.
Okay. So we can successfully create an account and log in. Let's do it. Let's do it one more time.
EJCX2. EJCX2. Create. Okay. Success. That's not really what you want to see.
You want to be like seeing a login screen where you're logged in, you see all your data, all of that.
And so let's log in now. EJCX2. EJCX. Error. EJCX2.
EJCX2. Okay. Successfully logged in. So there's a very important thing that happens after you log in.
And that is that you generally get a cookie. That is the way that websites authenticate you.
They give you a session token and a session cookie.
And depending on the programming language you're using, the framework you're using, like Rails is different than PHP quite distinctly.
Depends on how that cookie is implemented. And so here we need some kind of state.
These cookies are given to the user. And the user then when they log into the website or when they go to the website, the browser sends that cookie along with the web request that's being made.
And the server reads that cookie, pulls out from some data source somewhere, state about the user that says your email address or your username or whatever, and then renders the page for you with if it's a social media site, comments and stuff, or if it's Cloudflare, that's how it's rendering my whole UI here.
That's how it knows who I am.
So that's kind of an important part of a login flow. And I want to hack one together really fast in 15 minutes.
So it should be pretty straightforward.
So the things we need to do here, let's pull up our handy dandy to -do list.
To do session cookies, we need to generate a cryptographically secure string, random string.
And this will be the session ID that we give to the user as the cookie.
We need to set a cookie value.
We need to set state within workers KV. And then lastly, on a request, check if user is authenticated.
I think we can get a lot of this done really quickly.
So without further ado, let's go. I'm going to pull up, I like to refer to my csprng.xyz code base for doing anything with a randomness, because this is just a hacky thing that I did once that does a lot with workers.
So it's a nice reference for me, as somebody who doesn't get enough time to program.
Okay, so we have random bytes now.
This will generate our randomness. But I think we probably want to just test the randomness, because this appears to be hexadecimal.
This will be base64.
I don't really want a base64 cookie. I'd rather have hex, like a PHP session.
I think we've done it live on air before. And this usually works. Turn this thing.
Instead of byte array, we need buff.
Let's check real quick. And we're going to get a request. Return response to random bytes.
And we'll pass 16. This will potentially work?
I'm not sure. Okay, we got an error.
Webpack returned. Let r up. That's a very obvious parsing error.
We need an equal sign here, because this is an assignment statement. While we're doing that, let's get a new KB prefix for sessions.
So we'll do user prefix here, and then session prefix here.
While we do that, we need it was called KB prefix.
We need to change that to user. We're doing a lot of repeating ourselves.
We are definitely breaking the dry principle of don't repeat yourself.
But it's because we're doing time crunch programming.
And I'm going to copy all this and paste it and do put session, get session.
And we're going to change user prefix to session.
So we can use these separate functions, and it'll make sure that it's all in a separate namespace when we use these functions.
So when we look in our KB here, it won't all be polluted.
We'll see slash user asdf slash user ejcx, user ejcx1, and then slash session, and then our random string that we generate.
Okay. So let's see if this worked.
ejcx.net slash ran. What did I do? Oh, this is a post. I guess we can still test, but we need curl.
https ejcx.net slash ran dash x post 404. Uh, why would we get that?
Did this error out? Let's try this publish one more time. Did error app.post angry.
I think we had a parse error here. Yes, we did. So we didn't.
And so we can change. This should work now.
So sessions and the login flow really, really nuanced.
I may have touched on this last time I was on air, but I'm just going to test this random and then I'll explain.
But there's all these types of attacks like session fixation.
There's all these times when users need to be logged out.
Yes, this works perfectly. Look at that bunch of randomness here.
Okay. So there's all these times when you need to log users out, like when they create a new account or change their password or sorry, when they change their password or change their email address or, or make state changing second factor authentication requests.
They there's all these times when you want to log a user out and make sure that they're able to log back in and make sure that they're getting a new session.
And also there are some good properties about. Um, I mentioned rails, how it's very different, how a session works.
It's a stateless session where the session and the cookie is actually, uh, an encrypted blob that says who you are and only the server can, can decrypt and verify that session.
So it still identifies you, but it's a way to not make the server need to store the, um, contents of the cookie.
In this case, we're opting for a different model, more of the way the PHP sessions work where PHP sessions, they generate a string and then, um, they actually store.
If you just load up Apache and install the lamp stack and you use the PHP session code, what will end up happening is you have a whole directory filled with all of these session contents.
So, uh, you'll create a session and the user will, we'll get that session.
And then there'll be a directory in the temp directory or the bar directory somewhere, uh, with, with like a file with that PHP session ID, um, that's set as my cookie.
And then when I try to log in or view, uh, view this, the website, um, the PHP, what it'll actually do is read the, um, read my session, look for the file, uh, that contains all the data.
And if it finds it, it'll read all that data, um, that it's storing in my session.
So we're opting for more of that model and we're going to do that all with KB.
So we're generating something random. All we have to do is just write in a little JSON blob.
When somebody logs in and instead of saying success here, you can say, uh, we can create a session and I'm bites 16.
It's to be long and, uh, long enough that it's unguessable and strong enough that it's, uh, cryptographically secure.
So let session equals this. Let, um, let cookie equals this, but session equal just your username, uh, and username here.
We can write that to the, uh, the, uh, put session function we have, right.
Correct. Put session. Okay. So put session and then JSON dot string of five session.
I'd like to finish this before the end of, uh, this, and then we can, let's do a redirect here, a slash slash.
Who knows if this'll work?
Uh, let's just do the full URL that we could have prevented.
Okay. Let's add in a weight here. Okay.
That's how you create an account. Now we want to log in. Uh, do we want to do this on create account?
That's fine. That's fine. Um, I'll work around it.
And then for login, we want to do the same thing where we put a session.
Pretty simple. This isn't going to have all the good properties of a session, but we can go over that in a future, um, episode about what's important in a session.
Okay. We're going to put a session and redirect to the homepage. Is this gonna work?
I'm not sold that this will work on the first try. Oh, well, I don't, I didn't generate the, this though.
Okay. And then anything else when you log in, uh, then on slash, when we render the main page, we want to change this.
We want to check or let's, let's do this first.
We'll want to check if you're logged in and then render who you are.
Oh boy. This needs a trailing. We are quickly running out of time.
I've got to go fast. I gotta go faster and stop talking.
So while we're doing that, the cookie, we have a bunch of code out here.
It always, uh, basically anything I ever need, I can just Google with Cloudflare Workers and it just works.
Honestly. Um, I'm going to copy in this cookie function. That's easy to see.
It's too easy to see. It's too successfully logged in, but we, Oh boy.
We didn't hit our code here. However, we will hit it if we sign up.
So let's try that really quick. EJCX5, EJCX5. And we got redirected to the homepage.
I think that means it worked. When we refresh this, we'll probably see a session somewhere.
Yes, we have generated a session.
Okay. Last but not least, we want to, well, I don't think we'll have time, but we also need to, um, set call.
We're so close. We are so close.
We'll finish this. Uh, we'll finish this next week, but all we have to do is, um, we have to set the cookie value, read the cookie value on request.
Uh, so this one we're setting the state within workers KB.
We're generating this and then read the cookie on request, check if logged in, and then we need to render a homepage.
Uh, this is all the time we have today, but we are so close. This is next week.
Uh, we are so close to finish with this. Uh, and then maybe we can do a little bit of cleanup and really make this code clean.
Cause we're hacking stuff together, but we need to clean it up a little bit.
Thank you for joining me this week.
I will see you next week. Appreciate it. Adios.