Sign a token at login; check it once, in one middleware, for every protected route.
3a
Embed jwt.RegisteredClaims for the standard fields and add only what you need.
type Claims struct { Role string `json:"role"` jwt.RegisteredClaims // sub, iat, exp, jti⦠} func IssueAccess(userID, role string, ttl time.Duration) (string, error) { now := time.Now() c := Claims{ Role: role, RegisteredClaims: jwt.RegisteredClaims{ Subject: userID, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(ttl)), }, } t := jwt.NewWithClaims(jwt.SigningMethodHS256, c) return t.SignedString(secret) }
MapClaims β no claims["sub"].(string) assertions later.CheckPassword succeeds β typically with 15*time.Minute.secret is loaded from the environment at startup (session 10).3b
Five real requests hit GET /me. Watch which check stops each one.
func RequireAuth(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { h := r.Header.Get("Authorization") raw, ok := strings.CutPrefix(h, "Bearer ") if !ok { http.Error(w, "missing token", 401) return } c := &Claims{} _, err := jwt.ParseWithClaims(raw, c, keyFn, jwt.WithValidMethods([]string{"HS256"})) if err != nil { http.Error(w, "invalid token", 401) return } ctx := context.WithValue(r.Context(), ctxKey{}, c) next.ServeHTTP(w, r.WithContext(ctx)) } return http.HandlerFunc(fn) } // wiring mux.Handle("GET /me", RequireAuth(http.HandlerFunc(me)))
no header β 401 missing token
garbage β 401 invalid token
expired β 401 invalid token
alg:none β 401 invalid token
valid β 200 hello user 42 (user)
reason: token has invalid claims: token is expired
reason: token signature is invalid: signing method none is invalid
WithValidMethods pins the algorithm β a token claiming alg: none is refused.invalid token; log the real reason server-side./login and /register stay public.3c
The middleware verified once; handlers just ask the context who is calling.
type ctxKey struct{} // unexported: no collisions func ClaimsFrom(ctx context.Context) *Claims { c, _ := ctx.Value(ctxKey{}).(*Claims) return c } func me(w http.ResponseWriter, r *http.Request) { c := ClaimsFrom(r.Context()) fmt.Fprintf(w, "hello user %s (%s)", c.Subject, c.Role) }
ClaimsFrom, is the only way in β easy to fake in tests.c.Role and c.Subject from here to authorize.Trusting a user id from the request body or query string. The only trustworthy identity is c.Subject from a verified token.
GET /mePOST /login returns {"access_token": "β¦"} on success.GET /me in RequireAuth; return the user from c.Subject.httptest cases for all five requests above.Recap
jwt.RegisteredClaims in a typed struct; sign with HS256 at login.WithValidMethods; answer every failure with 401.ClaimsFrom(ctx) β identity never comes from user input.