package web import ( "context" "encoding/json" "errors" "fmt" "net/http" "github.com/golang-jwt/jwt/v5" ) type contextKey string const userIDKey contextKey = "userID" func (s *Server) hasAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var tokenString string // Authorization Header Bearer token authHeader := r.Header.Get("Authorization") if len(authHeader) > 7 && authHeader[:7] == "Bearer " { tokenString = authHeader[7:] } // Fallback to Cookie (future Web Frontend) if tokenString == "" { if cookie, err := r.Cookie("nfeeder_token"); err == nil { tokenString = cookie.Value } } if tokenString == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) return } // parse and validate claims := &jwt.RegisteredClaims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) { // check/confirm alg if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return s.jwtSecret, nil }) /// golang-jwt the library internally runs a Valid(): /// Time Check: It compares time.Now() against the ExpiresAt value. /// Not Before Check: It checks if the nbf (Not Before) time has passed. /// Issued At Check: It ensures the iat isn't in the future. if err != nil || !token.Valid { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) // Check if the error is specifically because the token expired if errors.Is(err, jwt.ErrTokenExpired) { json.NewEncoder(w).Encode(map[string]string{ "error": "token_expired", "message": "Please use your refresh token to get a new session", }) return } json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) return } // Verify issuer if claims.Issuer != ISSUER { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "invalid issuer"}) return } // Success! Store userID in context for handlers to use ctx := context.WithValue(r.Context(), userIDKey, claims.Subject) next.ServeHTTP(w, r.WithContext(ctx)) }) }