Identity in Context
RequireToken from the last lesson answers one question: is this request
allowed through at all? That's enough for a single shared secret, but most
real services have more than one caller, and downstream code usually needs
to know which one — whose data to load, whose action to log, whose limits
apply. That's a different question: not "is this allowed" but "who is
this." Answering it means resolving credentials to an identity, and making
that identity available to whatever handler runs next.
Resolving a token to a user
The header parsing is the same strings.CutPrefix move from bearer auth,
but instead of comparing against one fixed secret, the token is now a key
into a lookup:
token, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
user, known := tokens[token]
Two things can go wrong, and both mean the same outcome: no Bearer prefix
(!ok), or a prefix with a token that isn't in the map (!known). Either
way, the request stops with 401 before it does anything else. When both
checks pass, user holds the resolved identity — a plain string here, but
in a real system it might be a user ID, a struct, or a claims object decoded
from a JWT. The shape doesn't matter for what comes next.
Storing identity the way you stored the request ID
Chapter 5's request-ID middleware faced a problem this lesson's middleware
also faces: a value produced early in the chain needs to reach code much
further down, without threading it through every function signature in
between. The answer is the same: context.WithValue, with an unexported
key type so nothing outside this package can collide with it or fake the
value.
ctx := context.WithValue(r.Context(), currentUser, user)
next.ServeHTTP(w, r.WithContext(ctx))
currentUser is a package-level value of an empty struct type declared
just for this purpose — type userKey struct{}. Its only job is to be a
key nothing else in the program can produce by accident, the same
discipline the request-ID key followed. Any handler further down the chain
that wants to know who's calling reads it back with a helper, rather than
re-parsing the Authorization header itself:
func UserFrom(ctx context.Context) string {
u, _ := ctx.Value(currentUser).(string)
return u
}
The comma-ok read
ctx.Value returns any, so pulling a string back out needs a type
assertion. Written as u, _ := ctx.Value(currentUser).(string), the
comma-ok form never panics: if the key isn't present, ctx.Value returns
nil, the assertion fails, ok comes back false, and u is left at its
zero value — "" for a string. UserFrom doesn't even need to check ok
explicitly, because an empty string is exactly the right answer for "no
identity here." That makes UserFrom safe to call on any context,
authenticated or not, which matters once other middleware or tests start
passing contexts that never went through Authenticate.
Why not just pass the username as an argument
It's worth being precise about when this pattern is the right tool. Context
values are for data that's scoped to a single request and produced by
something the handler didn't call directly — request IDs, deadlines,
authenticated identity. They are not a substitute for ordinary function
arguments. If a value is required for a function to do its job and the
caller already has it in hand, pass it as a parameter; burying it in a
context makes the dependency invisible and the function harder to test.
Identity earned that context slot because middleware produces it before the
handler exists to receive it as an argument — exactly the same reasoning
that put the request ID there in Chapter 5, and the same one the
Fundamentals course used when it first introduced context.WithValue.
Practice
1. Identity in Context
IntermediateAuth usually needs to tell the handler who the caller is. Store the authenticated identity in the request context — the same context mechanism as request IDs, now carrying something handlers actually branch on.
The
tokensmap (token → username) and the private context key are given. Implement:Authenticate(next http.Handler) http.Handler— pull the bearer token, look it up intokens. Unknown or missing →401. Known → put the username in the context and callnextwith the updated request.UserFrom(ctx context.Context) string— return the stored username, or"".
Now any downstream handler can call
UserFrom(r.Context())to know who's asking, without re-parsing the header.package main import ( "context" "net/http" ) type userKey struct{} var currentUser userKey // tokens maps bearer tokens to usernames — a stand-in for a real user store. var tokens = map[string]string{ "tok-alice": "alice", "tok-bob": "bob", } // Authenticate resolves the bearer token to a username, stores it in the // request context, and rejects unknown tokens with 401. func Authenticate(next http.Handler) http.Handler { // TODO return next } // UserFrom returns the authenticated username, or "" if unauthenticated. func UserFrom(ctx context.Context) string { // TODO return "" } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.