package auth import ( "context" "fmt" "net/http" "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) type contextKey string const userIDKey contextKey = "userID" func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("hashing password: %w", err) } return string(bytes), nil } func CheckPassword(hash, password string) error { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) } func GenerateToken(userID uuid.UUID, secret string) (string, error) { claims := jwt.MapClaims{ "sub": userID.String(), "exp": time.Now().Add(24 * 7 * time.Hour).Unix(), "iat": time.Now().Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(secret)) } func ValidateToken(tokenString, secret string) (uuid.UUID, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(secret), nil }) if err != nil { return uuid.Nil, fmt.Errorf("parsing token: %w", err) } claims, ok := token.Claims.(jwt.MapClaims) if !ok || !token.Valid { return uuid.Nil, fmt.Errorf("invalid token") } sub, ok := claims["sub"].(string) if !ok { return uuid.Nil, fmt.Errorf("invalid subject claim") } return uuid.Parse(sub) } func Middleware(secret string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err != nil { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } userID, err := ValidateToken(cookie.Value, secret) if err != nil { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } ctx := context.WithValue(r.Context(), userIDKey, userID) next.ServeHTTP(w, r.WithContext(ctx)) }) } } func UserIDFromContext(ctx context.Context) uuid.UUID { id, _ := ctx.Value(userIDKey).(uuid.UUID) return id } func SetSessionCookie(w http.ResponseWriter, token string) { http.SetCookie(w, &http.Cookie{ Name: "session", Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: 7 * 24 * 60 * 60, }) } func ClearSessionCookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ Name: "session", Value: "", Path: "/", HttpOnly: true, MaxAge: -1, }) }