113 lines
3.5 KiB
Go
113 lines
3.5 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/prabhupadasays/spsays-bff/internal/repository"
|
|
"github.com/prabhupadasays/spsays-bff/pkg/auth"
|
|
"github.com/prabhupadasays/spsays-bff/pkg/response"
|
|
)
|
|
|
|
type ctxKey string
|
|
|
|
const (
|
|
userIDKey ctxKey = "user_id"
|
|
tokenVersionKey ctxKey = "tv"
|
|
)
|
|
|
|
// RequireAuth is the middleware applied to every protected route.
|
|
//
|
|
// It pulls the JWT from the cookie, verifies signature + expiry, then asks
|
|
// the UserRepo for the current token_version and rejects if the JWT carries
|
|
// a stale version (password-change or forced logout invalidates older JWTs).
|
|
//
|
|
// On success it stuffs (user_id, token_version) into the request context;
|
|
// handlers read them via UserID() / TokenVersion().
|
|
func RequireAuth(issuer *auth.Issuer, users *repository.UserRepo) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
raw := auth.Read(r)
|
|
if raw == "" {
|
|
response.Error(w, http.StatusUnauthorized, "not authenticated")
|
|
return
|
|
}
|
|
|
|
claims, err := issuer.Parse(raw)
|
|
if err != nil {
|
|
response.Error(w, http.StatusUnauthorized, "invalid or expired session")
|
|
return
|
|
}
|
|
|
|
u, err := users.FindByID(r.Context(), claims.UserID)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrUserNotFound) {
|
|
response.Error(w, http.StatusUnauthorized, "account no longer exists")
|
|
return
|
|
}
|
|
response.Error(w, http.StatusInternalServerError, "auth check failed")
|
|
return
|
|
}
|
|
if u.TokenVersion != claims.TokenVersion {
|
|
response.Error(w, http.StatusUnauthorized, "session expired — please log in again")
|
|
return
|
|
}
|
|
|
|
ctx := context.WithValue(r.Context(), userIDKey, claims.UserID)
|
|
ctx = context.WithValue(ctx, tokenVersionKey, claims.TokenVersion)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|
|
|
|
// OptionalAuth is the soft-auth sibling of RequireAuth. Same logic, but
|
|
// invalid / missing sessions don't reject the request — they just skip
|
|
// the context population. Use it on routes that are reachable
|
|
// anonymously but want to personalise the response when the viewer
|
|
// happens to be signed in (e.g. `viewer_has_reshared` on a public
|
|
// post).
|
|
func OptionalAuth(issuer *auth.Issuer, users *repository.UserRepo) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
raw := auth.Read(r)
|
|
if raw == "" {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
claims, err := issuer.Parse(raw)
|
|
if err != nil {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
u, err := users.FindByID(r.Context(), claims.UserID)
|
|
if err != nil || u.TokenVersion != claims.TokenVersion {
|
|
// Stale or invalid session — proceed anonymously rather
|
|
// than failing the request. RequireAuth would 401 here.
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
ctx := context.WithValue(r.Context(), userIDKey, claims.UserID)
|
|
ctx = context.WithValue(ctx, tokenVersionKey, claims.TokenVersion)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|
|
|
|
// UserID returns the authenticated user's id from the request context.
|
|
// Returns "" if RequireAuth has not run.
|
|
func UserID(ctx context.Context) string {
|
|
if v, ok := ctx.Value(userIDKey).(string); ok {
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// TokenVersion returns the JWT's token_version claim from context.
|
|
func TokenVersion(ctx context.Context) int {
|
|
if v, ok := ctx.Value(tokenVersionKey).(int); ok {
|
|
return v
|
|
}
|
|
return 0
|
|
}
|