User authorization
Being able to authenticate the users of our application is all well and good, but now we need to do something useful with that information. In this chapter we’ll introduce some authorization checks so that:
- Only authenticated (i.e. logged in) users can create a new snippet; and
- The contents of the navigation bar changes depending on whether a user is authenticated (logged in) or not. Specifically:
- Authenticated users should see links to ‘Home’, ‘Create snippet’ and ‘Logout’.
- Unauthenticated users should see links to ‘Home’, ‘Signup’ and ‘Login’.
As I mentioned briefly in the previous chapter, we can check whether a request is being made by an authenticated user or not by checking for the existence of an "authenticatedUserID"
value in their session data.
So let’s start with that. Open the cmd/web/helpers.go
file and add an isAuthenticated()
helper function to return the authentication status like so:
package main ... // Return true if the current request is from an authenticated user, otherwise // return false. func (app *application) isAuthenticated(r *http.Request) bool { return app.sessionManager.Exists(r.Context(), "authenticatedUserID") }
That’s neat. We can now check whether or not the request is coming from an authenticated (logged in) user by simply calling this isAuthenticated()
helper.
The next step is to find a way to pass this information to our HTML templates, so that we can toggle the contents of the navigation bar appropriately.
There are two parts to this. First, we’ll need to add a new IsAuthenticated
field to our templateData
struct:
package main import ( "html/template" "path/filepath" "time" "snippetbox.alexedwards.net/internal/models" ) type templateData struct { CurrentYear int Snippet models.Snippet Snippets []models.Snippet Form any Flash string IsAuthenticated bool // Add an IsAuthenticated field to the templateData struct. } ...
And the second step is to update our newTemplateData()
helper so that this information is automatically added to the templateData
struct every time we render a template. Like so:
package main ... func (app *application) newTemplateData(r *http.Request) templateData { return templateData{ CurrentYear: time.Now().Year(), Flash: app.sessionManager.PopString(r.Context(), "flash"), // Add the authentication status to the template data. IsAuthenticated: app.isAuthenticated(r), } } ...
Once that’s done, we can update the ui/html/partials/nav.tmpl
file to toggle the navigation links using the {{if .IsAuthenticated}}
action like so:
{{define "nav"}} <nav> <div> <a href='/'>Home</a> <!-- Toggle the link based on authentication status --> {{if .IsAuthenticated}} <a href='/snippet/create'>Create snippet</a> {{end}} </div> <div> <!-- Toggle the links based on authentication status --> {{if .IsAuthenticated}} <form action='/user/logout' method='POST'> <button>Logout</button> </form> {{else}} <a href='/user/signup'>Signup</a> <a href='/user/login'>Login</a> {{end}} </div> </nav> {{end}}
Save all the files and try running the application now. If you’re not currently logged in, your application homepage should look like this:

Otherwise — if you are logged in — your homepage should look like this:

Feel free to have a play around with this, and try logging in and out until you’re confident that the navigation bar is being changed as you would expect.
Restricting access
As it stands, we’re hiding the ‘Create snippet’ navigation link for any user that isn’t logged in. But an unauthenticated user could still create a new snippet by visiting the https://localhost:4000/snippet/create
page directly.
Let’s fix that, so that if an unauthenticated user tries to visit any routes with the URL path /snippet/create
they are redirected to /user/login
instead.
The simplest way to do this is via some middleware. Open the cmd/web/middleware.go
file and create a new requireAuthentication()
middleware function, following the same pattern that we used earlier in the book:
package main ... func (app *application) requireAuthentication(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // If the user is not authenticated, redirect them to the login page and // return from the middleware chain so that no subsequent handlers in // the chain are executed. if !app.isAuthenticated(r) { http.Redirect(w, r, "/user/login", http.StatusSeeOther) return } // Otherwise set the "Cache-Control: no-store" header so that pages // require authentication are not stored in the users browser cache (or // other intermediary cache). w.Header().Add("Cache-Control", "no-store") // And call the next handler in the chain. next.ServeHTTP(w, r) }) }
We can now add this middleware to our cmd/web/routes.go
file to protect specific routes.
In our case we’ll want to protect the GET /snippet/create
and POST /snippet/create
routes. And there’s not much point logging out a user if they’re not logged in, so it makes sense to use it on the POST /user/logout
route as well.
To help with this, let’s rearrange our application routes into two ‘groups’.
The first group will contain our ‘unprotected’ routes and use our existing dynamic
middleware chain. The second group will contain our ‘protected’ routes and will use a new protected
middleware chain — consisting of the dynamic
middleware chain plus our new requireAuthentication()
middleware.
Like this:
package main ... func (app *application) routes() http.Handler { mux := http.NewServeMux() fileServer := http.FileServer(http.Dir("./ui/static/")) mux.Handle("GET /static/", http.StripPrefix("/static", fileServer)) // Unprotected application routes using the "dynamic" middleware chain. dynamic := alice.New(app.sessionManager.LoadAndSave) mux.Handle("GET /{$}", dynamic.ThenFunc(app.home)) mux.Handle("GET /snippet/view/{id}", dynamic.ThenFunc(app.snippetView)) mux.Handle("GET /user/signup", dynamic.ThenFunc(app.userSignup)) mux.Handle("POST /user/signup", dynamic.ThenFunc(app.userSignupPost)) mux.Handle("GET /user/login", dynamic.ThenFunc(app.userLogin)) mux.Handle("POST /user/login", dynamic.ThenFunc(app.userLoginPost)) // Protected (authenticated-only) application routes, using a new "protected" // middleware chain which includes the requireAuthentication middleware. protected := dynamic.Append(app.requireAuthentication) mux.Handle("GET /snippet/create", protected.ThenFunc(app.snippetCreate)) mux.Handle("POST /snippet/create", protected.ThenFunc(app.snippetCreatePost)) mux.Handle("POST /user/logout", protected.ThenFunc(app.userLogoutPost)) standard := alice.New(app.recoverPanic, app.logRequest, commonHeaders) return standard.Then(mux) }
Save the files, restart the application and make sure that you’re logged out.
Then try visiting https://localhost:4000/snippet/create
directly in your browser. You should find that you get immediately redirected to the login form instead.
If you like, you can also confirm with curl that unauthenticated users are redirected for the POST /snippet/create
route too:
$ curl -ki -d "" https://localhost:4000/snippet/create HTTP/2 303 content-security-policy: default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com location: /user/login referrer-policy: origin-when-cross-origin server: Go vary: Cookie x-content-type-options: nosniff x-frame-options: deny x-xss-protection: 0 content-length: 0 date: Wed, 18 Mar 2024 11:29:23 GMT
Additional information
Without using alice
If you’re not using the justinas/alice
package to manage your middleware that’s OK — you can manually wrap your handlers like this instead:
mux.Handle("POST /snippet/create", app.sessionManager.LoadAndSave(app.requireAuthentication(http.HandlerFunc(app.snippetCreate))))