Auth Middleware

Check the credential once in a middleware, put the identity in the request context, and let handlers read it from there.

Syntaxfunc requireUser(next http.Handler) http.Handler

Every protected handler needs to know who is calling. Doing that lookup inside each handler means the same twenty lines in twenty places, and the one you forget is the hole. So you authenticate once, in a middleware.

A middleware is a function that takes an http.Handler and returns another http.Handler wrapped around it. The wrapper runs first. It can answer the request itself, or call next.ServeHTTP to pass it on. For authentication the wrapper reads the cookie or the Authorization header, turns it into a user, and either rejects the request with 401 or hands it onward.

Carrying the identity

The wrapper cannot pass an extra argument, because ServeHTTP has a fixed shape. It uses the request context instead. context.WithValue(parent, key, value) returns a new context holding one more pair, and r.WithContext(ctx) returns a copy of the request carrying it. A handler reads it back with r.Context().Value(key).

Use an unexported key type of your own, such as type userKey struct{}, not a string. Two packages that both used the string "user" would overwrite each other. An unexported type cannot collide with anything outside your package.

The same handler, written two ways

http.HandlerFunc(f) is a type conversion, not a call. The standard library declares type HandlerFunc func(http.ResponseWriter, *http.Request) with a method ServeHTTP(w, r) that calls f(w, r). Converting a function is therefore exactly the same as writing your own type with a ServeHTTP method, in one line instead of six.

Value returns any, so reading it back needs a type assertion, and that assertion can fail. Treat a missing identity as not signed in rather than trusting it.

Example

Example · go
package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
)

// userKey is the key the identity is stored under. Its type is unexported, so
// no other package can use the same key by accident.
type userKey struct{}

func requireUser(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		c, err := r.Cookie("session")
		if err != nil || c.Value != "s_8f3c" { // a real app looks the id up
			http.Error(w, "unauthorised", http.StatusUnauthorized)
			return
		}
		ctx := context.WithValue(r.Context(), userKey{}, "u_42")
		next.ServeHTTP(w, r.WithContext(ctx)) // the copy, not the original
	})
}

func account(w http.ResponseWriter, r *http.Request) {
	user, ok := r.Context().Value(userKey{}).(string)
	if !ok {
		http.Error(w, "unauthorised", http.StatusUnauthorized)
		return
	}
	fmt.Fprintln(w, "account for", user)
}

func main() {
	h := requireUser(http.HandlerFunc(account))

	signed := httptest.NewRequest("GET", "/account", nil)
	signed.AddCookie(&http.Cookie{Name: "session", Value: "s_8f3c"})
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, signed)
	fmt.Print(rec.Body.String())

	bare := httptest.NewRecorder()
	h.ServeHTTP(bare, httptest.NewRequest("GET", "/account", nil))
	fmt.Println("no cookie:", bare.Code)
}

// Output:
// account for u_42
// no cookie: 401

When to use it

  • Protect a group of routes with one wrapper instead of a check inside each handler.
  • Resolve the session into a user once per request and share it with every handler.
  • Return 401 for a missing or expired credential before any handler code runs.
  • Log rejected requests by putting the logging middleware outside the auth one.

More examples

The longhand the shorthand stands for

Both forms are http.Handler and both behave identically, which is the point. You will meet the struct form in libraries that need to hold configuration beside the next handler, and the function form almost everywhere else. Recognising them as the same thing means a codebase that uses one is not a codebase you have to relearn.

Example · go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
)

// The long way: a named type holding the next handler, with a ServeHTTP method.
type requireHeader struct {
	next http.Handler
}

func (m requireHeader) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Header.Get("Authorization") == "" {
		http.Error(w, "unauthorised", http.StatusUnauthorized)
		return
	}
	m.next.ServeHTTP(w, r)
}

// The short way: the same logic, with http.HandlerFunc supplying ServeHTTP.
func requireHeaderFunc(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") == "" {
			http.Error(w, "unauthorised", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "ok")
	})

	for _, h := range []http.Handler{requireHeader{next: ok}, requireHeaderFunc(ok)} {
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
		fmt.Println(rec.Code)
	}
}

// Output:
// 401
// 401

One place that stores the identity, one that reads it

Write this pair once per package and never type a context key or a type assertion again. It also gives you one place to change if the stored value grows from a user id into a whole struct. The second call shows why userFrom returns a bool: a handler reached without the middleware, in a test or after a routing mistake, must be able to tell.

Example · go
package main

import (
	"context"
	"fmt"
)

type userKey struct{}

// withUser and userFrom are a pair, and nothing else touches the key. The type
// written in and the type read out therefore cannot drift apart.
func withUser(ctx context.Context, id string) context.Context {
	return context.WithValue(ctx, userKey{}, id)
}

func userFrom(ctx context.Context) (string, bool) {
	id, ok := ctx.Value(userKey{}).(string)
	return id, ok
}

func main() {
	ctx := withUser(context.Background(), "u_42")
	id, ok := userFrom(ctx)
	fmt.Println(id, ok)

	// A context that never went through withUser has nothing to read.
	_, ok = userFrom(context.Background())
	fmt.Println("anonymous:", ok)
}

// Output:
// u_42 true
// anonymous: false

Order matters: log outside, authenticate inside

Read a chain from the outside in: the outermost wrapper runs first and finishes last. Put logging outside authentication and you see the requests that were turned away, which is exactly the traffic you want a record of. Put it inside and failed sign-in attempts leave no trace. The same reasoning applies to panic recovery, which belongs outside everything.

Example · go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
)

func logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("start", r.URL.Path)
		next.ServeHTTP(w, r)
		fmt.Println("done", r.URL.Path)
	})
}

func requireUser(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if _, err := r.Cookie("session"); err != nil {
			fmt.Println("rejected")
			http.Error(w, "unauthorised", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	page := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})

	// logging wraps requireUser, so a rejected request is still logged.
	h := logging(requireUser(page))
	h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/admin", nil))
}

// Output:
// start /admin
// rejected
// done /admin

Discussion

  • Be the first to comment on this lesson.