Request context for authentication/authorization
So, with those explanations out of the way, let’s start to use the request context functionality in our application.
We’ll begin by heading back to our internal/models/users.go
file and fleshing out the UserModel.Exists()
method, so that it returns true
if a user with a specific ID exists in our users
table, and false
otherwise. Like so:
package models ... func (m *UserModel) Exists(id int) (bool, error) { var exists bool stmt := "SELECT EXISTS(SELECT true FROM users WHERE id = ?)" err := m.DB.QueryRow(stmt, id).Scan(&exists) return exists, err }
Then let’s create a new cmd/web/context.go
file. In this file we’ll define a custom contextKey
type and an isAuthenticatedContextKey
variable, so that we have a unique key we can use to store and retrieve the authentication status from a request context (without the risk of naming collisions).
$ touch cmd/web/context.go
package main type contextKey string const isAuthenticatedContextKey = contextKey("isAuthenticated")
And now for the exciting part. Let’s create a new authenticate()
middleware method which:
- Retrieves the user’s ID from their session data.
- Checks the database to see if the ID corresponds to a valid user using the
UserModel.Exists()
method. - Updates the request context to include an
isAuthenticatedContextKey
key with the valuetrue
.
Here’s the code:
package main import ( "context" // New import "fmt" "net/http" "github.com/justinas/nosurf" ) ... func (app *application) authenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Retrieve the authenticatedUserID value from the session using the // GetInt() method. This will return the zero value for an int (0) if no // "authenticatedUserID" value is in the session -- in which case we // call the next handler in the chain as normal and return. id := app.sessionManager.GetInt(r.Context(), "authenticatedUserID") if id == 0 { next.ServeHTTP(w, r) return } // Otherwise, we check to see if a user with that ID exists in our // database. exists, err := app.users.Exists(id) if err != nil { app.serverError(w, r, err) return } // If a matching user is found, we know that the request is // coming from an authenticated user who exists in our database. We // create a new copy of the request (with an isAuthenticatedContextKey // value of true in the request context) and assign it to r. if exists { ctx := context.WithValue(r.Context(), isAuthenticatedContextKey, true) r = r.WithContext(ctx) } // Call the next handler in the chain. next.ServeHTTP(w, r) }) }
The important thing to emphasize here is the following difference:
- When we don’t have a valid authenticated user, we pass the original and unchanged
*http.Request
to the next handler in the chain. - When we do have a valid authenticated user, we create a copy of the request with a
isAuthenticatedContextKey
key andtrue
value stored in the request context. We then pass this copy of*http.Request
to the next handler in the chain.
Alright, let’s update the cmd/web/routes.go
file to include the authenticate()
middleware in our dynamic
middleware chain:
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)) // Add the authenticate() middleware to the chain. dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate) 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 := 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) }
The last thing that we need to do is update our isAuthenticated()
helper, so that instead of checking the session data it now checks the request context to determine if a user is authenticated or not.
We can do this like so:
package main ... func (app *application) isAuthenticated(r *http.Request) bool { isAuthenticated, ok := r.Context().Value(isAuthenticatedContextKey).(bool) if !ok { return false } return isAuthenticated }
It’s important to point out here that if there isn’t a value in the request context with the isAuthenticatedContextKey
key, or the underlying value isn’t a bool
, then this type assertion will fail. In that case we take a ‘safe’ fall back and return false (i.e we assume that the user isn’t authenticated).
If you like, try running the application again. It should compile correctly and if you log in as a certain user and browse around the application should work exactly as before.
Then, if you want, open MySQL and delete the record for the user that you’re logged in as from the database. For example:
mysql> USE snippetbox; mysql> DELETE FROM users WHERE email = 'bob@example.com';
And when you go back to your browser and refresh the page, the application is now smart enough to recognize that the user has been deleted, and you’ll find yourself treated as an unauthenticated (logged-out) user.
Additional information
Misusing request context
It’s important to emphasize that request context should only be used to store information relevant to the lifetime of a specific request. The Go documentation for context.Context
warns:
Use context Values only for request-scoped data that transits processes and APIs.
That means you should not use it to pass dependencies that exist outside of the lifetime of a request — like loggers, template caches and your database connection pool — to your middleware and handlers.
For reasons of type-safety and clarity of code, it’s almost always better to make these dependencies available to your handlers explicitly, by either making your handlers methods against an application
struct (like we have in this book) or passing them in a closure (like in this Gist).