Let's Go User authentication › User authorization
Previous · Contents · Next
Chapter 10.6.

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:

  1. Only authenticated (i.e. logged in) users can create a new snippet; and
  2. 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:

File: cmd/web/helpers.go
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:

File: cmd/web/templates.go
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:

File: cmd/web/helpers.go
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:

File: ui/html/partials/nav.tmpl
{{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:

10.06-01.png

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

10.06-02.png

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:

File: cmd/web/middleware.go
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:

File: cmd/web/routes.go
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))))