CSRF Protection Implementation
Cross-Site Request Forgery protection is added using Echo middleware. A CSRF cookie is configured with strict same-site, HTTP-only settings, and skipped for asset routes. This ensures requests are validated as originating from trusted sources.
Session and Cookie Management
A helper function is introduced to set authenticated session data in a secure cookie. The session stores the user email and uses encrypted values, with configurable lifetime and proper invalidation logic for logout.
Login Controller Logic
A login handler binds form data, retrieves the user by email, and validates the password using a pepper value from configuration. On success, a session cookie is set and the user is redirected; on failure, a generic error message is returned.
Login View and Routes
A login page is created with email and password fields using existing UI components. Separate GET and POST routes handle rendering the form and processing authentication, with rate limiting applied to prevent brute-force attempts.
Manual User Seeding
Instead of implementing a public signup flow, a user is created directly in the database via seed data. This avoids unnecessary exposure and ensures only authorized admin access.
Logout Functionality
A logout route invalidates the session by setting the cookie’s max age to a negative value. A flash message confirms logout, and navigation elements are conditionally rendered based on authentication state.
Authentication Flow Overview
The system now includes CSRF protection, rate limiting, password validation, secure session cookies, login/logout flows, and protected admin access. Remaining enhancements for a full production-ready authentication system are deferred to a later module.
All right, let's actually finish this thing. We need a login form. We need our way to log out. We need to hide some routes so we can see them when we are logged in. And then we also have a little security hole that we need to fix. So let's tackle the security hole first and then implement the form and all the rest. So we can start actually having a secure admin that only we can use.
Right, so the security hole we have now is that we have no protection against cross-site request forgery. So we don't prevent malicious site from submitting forms on our behalf. This is something we definitely want to have. And the way we're going to add this is to rely on EchoStack once more. We're going to add an import called echo.mb for echo middleware.
say github.com lab stack echov4 and then middleware. Then we jump down and say euse. And in here we will say echo mv csrf with config. And in here we provide it with a csrf config.
where we will fill in some values. Of course, we are again, gonna skip the assets route. So we say return strings has prefix, see request, there we go. See request context and URL.path, routes, assets,
Assets, prefix. Now we are missing, you don't have URL on the, it's on the request, not on the context. Okay. So there we go. So this whole, this basically puts in our token value into all of request. They can validate if something came from our
application or not. And we don't want this to run on our assets, on our any of assets route, even though we might not see this in your crystal, but it's nice to have this skipper in place so that we don't, we don't have any underlying failings because because of our assets prefix route. So we then want to say token lookup and it should lookup cookie with a name of
underscore CRF, CSRF is hard to pronounce. Right, we want the cookie path, the cookie path to be slash. Like this, we want them to have cookie HTTP only to set to true. And then cookie, same site.
And it said to HTTP, same site strict mode. Right. Indicate same site mode of the CSRF cookie. So this again is just a cookie that gets added with a value that gets included on every request. Ego then checks this value and validates that this is coming from our server. So, right. That took not long at all.
We don't, again, we skip the assets because they don't do any modification to the state. So it's technically not really necessary, but other than that, we have a few more options we need to configure when we prepare to go to production, but we will leave that for the next module. Just think of this as a cookie that has a name of underscore CSRF that gets included on all requests and that has a token value. We can validate that the request is coming from our server.
Oh, not from our server. It's a token that has been signed from our server, and you know the request comes from a place that we allow it to come from. Next, I am in the off.go file under cookies, where we will create a new function called setApp that takes in the ego context. And it also takes the email, because that's the only extra thing we have on the app struct.
Then we say sys, error equals session get, and we say string, app key, pass the echo context, just as before, give that a save, return the error if there is any error. Right, all of this should be feeling very familiar now. Then we want the values, and we can see here this is a map of an interface interface, so we can pass whatever we want. This is where we use this
this constant we defined in the previous episode so that we up here can pull out the right values. So user email and this is going to be email. Then we'll return, says save, pass the echo request and the echo response. Nothing new happens here.
This is simply a helper method we can use in our controller whenever we are actually authenticating ourselves. If we were to add anything else to this cookie, we could add it to this setup function, right? Great. Now we need to update the controller and then we need to update, create the view and we are ready to actually authenticate ourselves.
So in controller.go here we need to extend the struct because we need access to the config.config struct so we can pull out the pepper value. We just update the new function and update the controller and now we have access to whatever is in the config. Let's just quickly update our main.go file here because it also needs, we need to pass the
the config, right? Then we need, we already have to route. Let's just double check because yes, we have to log in page, good. So now if we go back to our controller, our controllers, we go to the bottom and then we have here, let's create it here and say,
Login. Oh, it's going to be the controller. We're going to say log in page. Easy. Echo. Context. And here we return or we render the views log in page. That will take in views.
field props that we have not created yet. We have to field props, but we don't have to log in page. Let's also quickly create our, actually our login handler or login controller. So log is just gonna call this one log in.
We pass the echo context again, this is all stuff we have seen before. We now need to define our payload, very similar to how we did the article form payload up here. So we will say type login form payload struct. We have email of string and we have password also of string.
This is also form elements, so we have form email and form password. And since we can technically have errors, we should maybe consider adding the two log, like the mapping we have up here, where we say two article props.
However, what we really want to do is just to say that the login failed. We don't really want to specify if the email is wrong or the password is wrong, right? So I think what I want to do instead is just to remove it from here. And then we save our payload. And we say payload is of type login form. We say bind.
payload, write, return, and error as always. Then we try to grab the user. Models find user by email. And we pass the values as normal. There we go. Then with the user, we can say what would we actually do here? Let's just say is.
valid or error. Then we say user, valid password, then we pass the payload, payload.password, then we pass the pepper value. Again, if we have an error, we return it. Then we say if, if not, or if it's valid, it's false. We will say return, render, easy, use,
login page that we still don't have. But if it's valid, we will say if error equals to cookies, set app past echo context and say user email. Again, if you have an error, turn the error. And at this point, we will probably save if flash error cookies at flash.
cookies, flash success, log in success, log in successfully, log in successful, log in success, full level. Again, return the error. If there is an error, finally, let's just say easy redirect to
ACDP status, see other routes, admin, not admin prefix, but admin page. And again, it's not ACDPs. So what we have here is we render the login page. We get a form request.
Just as we create articles and update articles, we pull out the values, we find the user, we try to validate the password. If it's not valid, the password, we return to the login page. If it is valid, we set the cookie, and then we add a flash, and redirect to the admin page. We should probably also just up here say flash.
Flash error is not an error, flash info, login on successful. Yeah, I like that for now. We could technically create some, return some styling around the border saying wrong password or we could add a little note that the password was, the password or the details provided was wrong, but I think this is a really good place to
to just add a flag. When we get to the next module, we will start to add some proper interactivity and we can potentially fix it there if you want to. Great. Now let's deal with this login page here. So we go into views, create a new file called login.timble and package views as always. This will be a Timble login page. Double again, extend the base layout.
Let's say login. And in here, we create a main element, close it, say class. Actually, let's just take what we have from home because I believe this is gonna be the same. Do we want, no. Let's create it ourselves. So we're gonna add some marching
Sorry, on the y-axis, we're gonna say container. We will say lg, max with md, flex one, mx auto. Do we want anything else? We also want to have it be a flex, in a flex call. And then we create an h1.
I'm blasting through this because there's nothing new going on here. This is something we have seen before many times. So I don't think it's really worth to go into detail form. And what we want text-based content. And what should we say? Just say login. Yeah. Then we create a form again. And the form will have class, grids, gap six, p4,
BG base 300 and rounded. And what more do we need? We also need to specify that this is a method post and the action should go to routes login. Nope, that was too much. So say routes login.
We haven't created that route yet. We will do that in just a second. Let me just grab the import. Great. Then we simply create the diff for the input fields. We need one for email, and we need one for password. Do we want to know? Let's just use the input fields here. We say email, email.
We have name, title, value, empty, string, input type, empty, and then also field, prop, empty. Repeat this for the password. And here we want to say password because this is distinct type that our input field can have. We also don't want to set a value and we don't have any field prop here.
Finally, let's create a button. Close the button. So the class is going to be what do we want? We want to say px4, py2, border, rounded, pg, primary. Text is going to be primary content and hover. We're going to say bg.
primary 70 to make a stand out bit and then say log in. All right. All of this should again, should feel familiar. We are just reusing our components here. You could argue maybe you should try and make a form into components. Maybe the button should be a component. But for now, let's just stick with this. I think this is more than sufficient. Let's create this login route next.
So in routes.go, we need to create the login route. And that one is going to be exactly the same as the login page route. The only difference is that this one will have a get and this one will have a post. We could reuse this one, but I think it's much clearer to have two of them, but even if they have the same value, sort of be down here, I can say e get and say routes, login page, goes to controller, login page.
page and e.post goes to routes login, controller login. So this just makes it more clear that this is actually two different routes when we read it. We also need to add the IP rate limiter so that we count how many times an IP address has tried to login. Great. I think it's time that we try and run the development server.
and see what we get back. Cannot use. Okay, so let's jump into the controller and see whatever we have. We need to call it. That's right. Do we have it in any place? Yes, we have some errors here because this is not error. This is flash error. And the same thing here. Great. Now can we run the application?
You can. Let's give it a refresh. It works. Let's try and go to slash log in. We see our beautiful looking form here with the email and the password. And if I do something like just give it random values and click, you can see we get some browser validation here that this is not a valid email. When we type in our password, it hides it for us so that if we log in in a public place, no one can see the password. Great.
Now we need to actually create a user. And we are starting to run into one of the limitations of the setup I've made here because we are missing some pieces. We will go into more details in the next episode about those missing pieces. But since it's only us that are going to be using this admin page, I don't want to spend the time to create a proper sign up flow because we need to technically validate emails, which means sending an email. And there's a lot of things.
Also, it doesn't make sense to have a signup form that people could potentially find and misuse and create a lot of accounts. So we are going to be creating an account directly against the database, just like how we did with our seed data for articles. This will also work when we go to production, where we can just interact directly through the command entry here and create a user in the production.
database. That's also why we don't have this. It's validated or something you might have seen before. It is intentionally left out. But we still need a user. So I want to create a user here in the seed data where actually let's just say if error and say models create user pass in the
dbconwrite and then we need to specify also the pepper. Yes. And then the data, I'm going to fill out this first. So we have an error with just panic because this is seed data. Right. Let's fill in this. We're going to say mvv, add mvv, not mvvlabs, but let's use masterfullstackgolang.com.
In here, we didn't have the password. Password. Password. For developer purpose, this is perfectly fine, but create a secure password when we go to production. And let's just run reset migrations to have a clean slate. Just up migrations now. Yes, we are back and then run just seed.
we have seeded successfully. Let's just jump into the seed and remove this code to create a user. We don't need this to run multiple times because it will fail since we have this column or this rule that emails must be unique. Actually, let's just try and validate that it works. Just seed, we panic, duplicate key, valuation, unique constraint, user email key. So our
rules are actually working. Reset migrations, just up migrations, just seed, and we are back. After I have commented this part out, there we go. Now we should technically be able to run the application, go into the browser, and log in with the user we just created. All right, let's test this thing. So it was MVV at master,
that go lang.com and password. Let's give this a go. And we are locked in now. We can access that article. We can edit. Can we actually update this? Upcoming features updated. Let's try and give this a save. We can also update it. Let's try and see. Yes. There we go. So now we have this cookie. You can see here in our
Development Tools, this app underscore cookie. Maybe we should rename it to something like UI, UA is a typical name, UR, UA. Yeah. This is the cookie that we set. And you can also see the values are not the values that we set in the code, but this is encrypted data. Great. We can now log in and navigate the admin and do the observations that we need.
but we need to log out and we also need to be able to be able to, we want to hide this admin here whenever your user is not authenticated and then also have a log out button. So we can kill our session if we want to. Let's begin by adding the log out route. So we're gonna say log out and it goes to log.
Lockout, I can't spell, there we go. Then in our controller layer, we can jump to the bottom here. And then we say, funk ccontroller. Lockout, echo, echo context, error. And then we can simply grab the session. So let's say off, off cookie. No, let's call it app cookie.
error and then we say cookies gets app and use the echo context return the error if there is any then we say if app cookie ah that's right we are lacking a method here because we need to invalidate the session so actually what I want to do instead is to say
What should we call this? InvalidateApp. Yes, I think that's good. So instead, we will say create this invalidateApp function, say sysOptions maxH. And we can set that to minus one and set authenticated to false. And let's also set email to an empty string just to be completely sure that we have
We have killed the cookie. So setting this maxH to one will make the browser remove it. But also, technically, you should have this up here. So we can see the maxH is how long the cookie will live, and it will be deleted after the browser sessions. And so instead of minus one and refresh the page or load a new page, it's killed, it's gone, it no longer exists. It is given in seconds. So let's say we want to have
Let's say we want to have maybe a week, a week, two weeks. Let's just go with a week. So that would be 600 and 4,800 seconds. So this should be valid for one week. Then when we invalidate, we set it to minus one, false, empty string. Save it. And then we have to log in again to interact with the admin page.
right back to the controller. Now we can say invalidate app. That will just have an error. So if the error is not equal to nil. And of course we also don't need the email here. There we go. Then let's just add a flash that says
Not success, but I think info. Logout successful here. Seems about right. Let me say redirect to HTTP status. See other and routes. We can do login page. We can also do home page. Let's just do home. I think that's nicer. Right.
In our router, we will accept the post request to log in no log out. And we'll use the log out controller and no middleware. Again, this doesn't really fit into the method we should be using. We should technically be using a delete. We'll fix that in the upcoming module where we add some modern hypermedia. But for now, we are forced to use this dot post method here.
Final thing, we need to hide the admin and we need to show a logout button on that element whenever we are in an authenticated session. And then to do this, we can use our cookies, get appctx past the context. I say is authenticated. And actually, if we go out and
Did I add something? No. And run. We should still see the admin. We still see the admin. And just to illustrate, if we go back into base layout and say, if not authenticated, and run, or we just reverse it, we can see that it's gone. So regular users, when they don't have an authenticated cookie, will not see the admin page.
Great, let's go in and undo that, undo that. And then we're gonna grab this because we need to wrap this in a form so we can send the log out request. We could extract this and then accept like a children that we do here and to reuse this,
Again, I think it's a bit overkill. So method is gonna be post, action is gonna go to routes.logout, right? Then we're gonna have our button, cursor to point up, basically we want to have this.
So add this and say, log out. And we also need a type of submit because we are submitting a form. And let me just format this a little bit better. Let's run it one more time and see what shows up. We see the admin and we see the log out. Now, we can see we have the app cookie here. If I click log out,
it goes away and you can see now we only have the CSRF cookie and the bleeding edge flash key plus we see log out successful. I am also going to hide the login page just because we don't need users to necessarily see this in our navigation bar. But we can still go to our login, mbv, add master, full stack, go lang.com, password, enter, and we are back in again.
This is how we do authentication. We have been through CSRF protection. We have been through rate limiting, password security, session security, cookies. We have covered a lot of ground here. But as also mentioned, we are still technically lacking some elements that you will have in a full end to end authentication flow. And let's just discuss them in the next episode so you're aware of them.
If you want to implement them yourself, feel free to go ahead. I will also link the Copenhagen book in the show notes or episode notes. So you can use that as a basis for extending what we have already built here.