Files
nfeeder/internal/web/middleware.go
T

86 lines
2.4 KiB
Go

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))
})
}