diff --git a/internal/web/handlers_auth.go b/internal/web/handlers_auth.go index 605c822..a74509d 100644 --- a/internal/web/handlers_auth.go +++ b/internal/web/handlers_auth.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "crypto/sha256" "encoding/hex" + "encoding/json" "log" "net/http" "nfeeder/internal/db" @@ -17,7 +18,10 @@ import ( const ISSUER = "nfeeder-app" -var jwtKey = []byte("very_super_duper_secret") +type AuthResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} func (s *Server) handleRegister() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -44,7 +48,7 @@ func (s *Server) handleRegister() http.HandlerFunc { } // Auto-login after reg - s.issueTokenAndRedirect(w, r, user.ID) + s.issueToken(w, r, user.ID) } } @@ -66,7 +70,7 @@ func (s *Server) handleLogin() http.HandlerFunc { return } - s.issueTokenAndRedirect(w, r, user.ID) + s.issueToken(w, r, user.ID) } } @@ -100,7 +104,7 @@ func (s *Server) handleRefresh() http.HandlerFunc { } } -func (s *Server) issueTokenAndRedirect(w http.ResponseWriter, r *http.Request, userID int64) { +func (s *Server) issueToken(w http.ResponseWriter, r *http.Request, userID int64) { nowTime := time.Now() jwtExpireTime := nowTime.Add(24 * time.Hour) refreshExpireTime := nowTime.Add((24 * time.Hour) * 3) @@ -113,22 +117,12 @@ func (s *Server) issueTokenAndRedirect(w http.ResponseWriter, r *http.Request, u } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString(jwtKey) + tokenString, err := token.SignedString(s.jwtSecret) if err != nil { http.Error(w, "Internal Error", http.StatusInternalServerError) return } - http.SetCookie(w, &http.Cookie{ - Name: "nfeeder_token", - Value: tokenString, - Path: "/", - HttpOnly: true, - Secure: false, // Set to true in prod for HTTPS - SameSite: http.SameSiteLaxMode, - Expires: jwtExpireTime, - }) - // Generate refresh token rawToken := rand.Text() hash := sha256.Sum256([]byte(rawToken)) @@ -146,15 +140,15 @@ func (s *Server) issueTokenAndRedirect(w http.ResponseWriter, r *http.Request, u return } - http.SetCookie(w, &http.Cookie{ - Name: "nfeeder_refresh", - Value: string(rawToken), - Path: "/", - HttpOnly: true, - Secure: false, // Set to true in prod for HTTPS - SameSite: http.SameSiteLaxMode, - Expires: refreshExpireTime, - }) + resp := AuthResponse{ + AccessToken: tokenString, + RefreshToken: rawToken, + } - http.Redirect(w, r, "/", http.StatusSeeOther) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(resp); err != nil { + log.Printf("json encoding failed: %v", err) + return + } } diff --git a/internal/web/middleware.go b/internal/web/middleware.go index 2740baf..893da8e 100644 --- a/internal/web/middleware.go +++ b/internal/web/middleware.go @@ -2,6 +2,7 @@ package web import ( "context" + "encoding/json" "fmt" "net/http" @@ -14,21 +15,37 @@ const userIDKey contextKey = "userID" func (s *Server) hasAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("nfeeder_token") - if err != nil { - http.Redirect(w, r, "/login", http.StatusSeeOther) + 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(cookie.Value, claims, func(t *jwt.Token) (interface{}, error) { - // check alg + 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 jwtKey, nil + return s.jwtSecret, nil }) /// golang-jwt the library internally runs a Valid(): @@ -36,13 +53,17 @@ func (s *Server) hasAuth(next http.Handler) http.Handler { /// 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 { - http.Redirect(w, r, "/login", http.StatusSeeOther) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid or expired token"}) return } // Verify issuer if claims.Issuer != ISSUER { - http.Redirect(w, r, "/login", http.StatusSeeOther) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid issuer"}) return }