Let's Go Further Permission-based authorization › Requiring user activation
Previous · Contents · Next
Chapter 16.1.

Requiring user activation

As we mentioned a moment ago, the first thing we’re going to do in terms of authorization is restrict access to our /v1/movies** endpoints — so that they can only be accessed by users who are authenticated (not anonymous), and who have activated their account.

Carrying out these kinds of checks is an ideal task for some middleware, so let’s jump in and make a new requireActivatedUser() middleware method to handle this. In this middleware, we want to extract the User struct from the request context and then check the IsAnonymous() method and Activated field to determine whether the request should continue or not.

Specifically:

So first, let’s head to our cmd/api/errors.go file and add a couple of new helpers for sending those error messages. Like so:

File: cmd/api/errors.go
package main

...

func (app *application) authenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
    message := "you must be authenticated to access this resource"
    app.errorResponse(w, r, http.StatusUnauthorized, message)
}

func (app *application) inactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
    message := "your user account must be activated to access this resource"
    app.errorResponse(w, r, http.StatusForbidden, message)
}

And then let’s create the new requireActivatedUser() middleware for carrying out the checks. The code we need is nice and succinct:

File: cmd/api/middleware.go
package main

...

func (app *application) requireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Use the contextGetUser() helper that we made earlier to retrieve the user 
        // information from the request context.
        user := app.contextGetUser(r)

        // If the user is anonymous, then call the authenticationRequiredResponse() to 
        // inform the client that they should authenticate before trying again.
        if user.IsAnonymous() {
            app.authenticationRequiredResponse(w, r)
            return
        }

        // If the user is not activated, use the inactiveAccountResponse() helper to 
        // inform them that they need to activate their account.
        if !user.Activated {
            app.inactiveAccountResponse(w, r)
            return
        }

        // Call the next handler in the chain.
        next.ServeHTTP(w, r)
    })
}

Notice here that our requireActivatedUser() middleware has a slightly different signature to the other middleware we’ve built in this book. Instead of accepting and returning an http.Handler, it accepts and returns an http.HandlerFunc.

This is a small change, but it makes it possible to wrap our /v1/movie** handler functions directly with this middleware, without needing to make any further conversions.

Go ahead and update the cmd/api/routes.go file to do exactly that, as follows:

File: cmd/api/routes.go
package main

...

func (app *application) routes() http.Handler {
    router := httprouter.New()

    router.NotFound = http.HandlerFunc(app.notFoundResponse)
    router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)

    router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)

    // Use the requireActivatedUser() middleware on our five /v1/movies** endpoints.
    router.HandlerFunc(http.MethodGet, "/v1/movies", app.requireActivatedUser(app.listMoviesHandler))
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.requireActivatedUser(app.createMovieHandler))
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.requireActivatedUser(app.showMovieHandler))
    router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.requireActivatedUser(app.updateMovieHandler))
    router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.requireActivatedUser(app.deleteMovieHandler))

    router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler)
    router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler)

    router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler)

    return app.recoverPanic(app.rateLimit(app.authenticate(router)))
}

Demonstration

Alright, let’s try out these changes!

We’ll begin by calling the GET /v1/movies/:id endpoint as an anonymous user. When doing this you should now receive a 401 Unauthorized response, like so:

$ curl -i localhost:4000/v1/movies/1
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Www-Authenticate: Bearer
Date: Fri, 16 Apr 2021 15:59:33 GMT
Content-Length: 66

{
    "error": "you must be authenticated to access this resource"
}

Next, let’s try making a request as a user who has an account, but has not yet activated. If you’ve been following along, you should be able to use the alice@example.com user to do this, like so:

$ BODY='{"email": "alice@example.com", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/tokens/authentication
{
    "authentication_token": {
        "token": "2O4YHHWDHVVWWDNKN2UZR722BU",
        "expiry": "2021-04-17T18:03:09.598843181+02:00"
    }
}

$ curl -i -H "Authorization: Bearer 2O4YHHWDHVVWWDNKN2UZR722BU" localhost:4000/v1/movies/1
HTTP/1.1 403 Forbidden
Content-Type: application/json
Vary: Authorization
Date: Fri, 16 Apr 2021 16:03:45 GMT
Content-Length: 76

{
    "error": "your user account must be activated to access this resource"
}

Great, we can see that this now results in a 403 Forbidden response from our new inactiveAccountResponse() helper.

Finally, let’s try making a request as an activated user.

If you’re coding along, you might like to quickly connect to your PostgreSQL database and double-check which users are already activated.

$ psql $GREENLIGHT_DB_DSN 
psql (15.4 (Ubuntu 15.4-1.pgdg22.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

greenlight=> SELECT email FROM users WHERE activated = true;
       email       
-------------------
 faith@example.com
(1 row)

In my case the only activated user is faith@example.com, so let’s try making a request as them. When making a request as an activated user, all the checks in our requireActivatedUser() middleware will pass and you should get a successful response. Similar to this:

$ BODY='{"email": "faith@example.com", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/tokens/authentication
{
    "authentication_token": {
        "token": "ZFIKQ344EYM5KEP6JL2RHLRPJQ",
        "expiry": "2021-04-17T18:04:57.513348573+02:00"
    }
}

$ curl -H "Authorization: Bearer ZFIKQ344EYM5KEP6JL2RHLRPJQ" localhost:4000/v1/movies/1
{
    "movie": {
        "id": 1,
        "title": "Moana",
        "year": 2016,
        "runtime": "107 mins",
        "genres": [
            "animation",
            "adventure"
        ],
        "version": 1
    }
}

Splitting up the middleware

At the moment we have one piece of middleware doing two checks: first it checks that the user is authenticated (not anonymous), and second it checks that they are activated.

But it’s possible to imagine a scenario where you only want to check that a user is authenticated, and you don’t care whether they are activated or not. To assist with this, you might want to introduce an additional requireAuthenticatedUser() middleware as well as the current requireActivatedUser() middleware.

However, there would be some overlap between these two middlewares, as they would both be checking whether a user is authenticated or not. A neat way to avoid this duplication is to have your requireActivatedUser() middleware automatically call the requireAuthenticatedUser() middleware.

It’s hard to describe in words how this pattern works, so I’ll demonstrate. If you’re following along go ahead and update your cmd/api/middleware.go file like so:

File: cmd/api/middleware.go
package main

...

// Create a new requireAuthenticatedUser() middleware to check that a user is not 
// anonymous.
func (app *application) requireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user := app.contextGetUser(r)

        if user.IsAnonymous() {
            app.authenticationRequiredResponse(w, r)
            return
        }

        next.ServeHTTP(w, r)
    })
}

// Checks that a user is both authenticated and activated.
func (app *application) requireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
    // Rather than returning this http.HandlerFunc we assign it to the variable fn.
    fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user := app.contextGetUser(r)

        // Check that a user is activated.
        if !user.Activated {
            app.inactiveAccountResponse(w, r)
            return
        }

        next.ServeHTTP(w, r)
    })

    // Wrap fn with the requireAuthenticatedUser() middleware before returning it.
    return app.requireAuthenticatedUser(fn)
}

The way that we’ve set this up, our requireActivatedUser() middleware now automatically calls the requireAuthenticatedUser() middleware before being executed itself. In our application this makes a lot of sense — we shouldn’t be checking if a user is activated unless we know exactly who they are!

You can go ahead and run the application again now — everything should compile and continue to work just like it did before.


Additional information

In-handler checks

If you only have a couple of endpoints where you want to perform authorization checks, then rather than using middleware it can often be easier to do the checks inside the relevant handlers instead. For example:

func (app *application) exampleHandler(w http.ResponseWriter, r *http.Request) {
    user := app.contextGetUser(r)

    if user.IsAnonymous() {
        app.authenticationRequiredResponse(w, r)
        return
    }

    if !user.Activated {
        app.inactiveAccountResponse(w, r)
        return
    }
    
    // The rest of the handler logic goes here...
}