diff --git a/internal/web/handlers_auth.go b/internal/web/handlers_auth.go index bb98cb8..605c822 100644 --- a/internal/web/handlers_auth.go +++ b/internal/web/handlers_auth.go @@ -1,13 +1,17 @@ package web import ( - "fmt" + "crypto/rand" + "crypto/sha256" + "encoding/hex" "log" "net/http" "nfeeder/internal/db" + "strconv" "time" "github.com/golang-jwt/jwt/v5" + "github.com/jackc/pgx/v5/pgtype" "golang.org/x/crypto/bcrypt" ) @@ -40,7 +44,7 @@ func (s *Server) handleRegister() http.HandlerFunc { } // Auto-login after reg - s.issueTokenAndRedirect(w, r, fmt.Sprintf("%d", user.ID)) + s.issueTokenAndRedirect(w, r, user.ID) } } @@ -62,7 +66,7 @@ func (s *Server) handleLogin() http.HandlerFunc { return } - s.issueTokenAndRedirect(w, r, fmt.Sprintf("%d", user.ID)) + s.issueTokenAndRedirect(w, r, user.ID) } } @@ -80,12 +84,32 @@ func (s *Server) handleLogout() http.HandlerFunc { } } -func (s *Server) issueTokenAndRedirect(w http.ResponseWriter, r *http.Request, userID string) { +func (s *Server) handleRefresh() http.HandlerFunc { + // TODO: refresh logic + /* + Read the refresh token cookie + Hash it with sha256 + Look up the hash in the DB + Check it's not expired + Delete the old DB record + Call issueTokenAndRedirect — which creates a new JWT and a new refresh token in one go + */ + + return func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/", http.StatusOK) + } +} + +func (s *Server) issueTokenAndRedirect(w http.ResponseWriter, r *http.Request, userID int64) { + nowTime := time.Now() + jwtExpireTime := nowTime.Add(24 * time.Hour) + refreshExpireTime := nowTime.Add((24 * time.Hour) * 3) + claims := jwt.RegisteredClaims{ Issuer: ISSUER, - Subject: userID, - ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), - IssuedAt: jwt.NewNumericDate(time.Now()), + Subject: strconv.FormatInt(userID, 10), + ExpiresAt: jwt.NewNumericDate(jwtExpireTime), + IssuedAt: jwt.NewNumericDate(nowTime), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) @@ -102,7 +126,34 @@ func (s *Server) issueTokenAndRedirect(w http.ResponseWriter, r *http.Request, u HttpOnly: true, Secure: false, // Set to true in prod for HTTPS SameSite: http.SameSiteLaxMode, - Expires: time.Now().Add(24 * time.Hour), + Expires: jwtExpireTime, + }) + + // Generate refresh token + rawToken := rand.Text() + hash := sha256.Sum256([]byte(rawToken)) + hex_token := hex.EncodeToString(hash[:]) + + _, err = s.store.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{ + UserID: userID, + TokenHash: hex_token, + ExpiresAt: pgtype.Timestamptz{Time: refreshExpireTime, Valid: true}, + }) + if err != nil { + // Log the actual error for yourself, send a generic one to the user + log.Printf("failed to create refresh token: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + 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, }) http.Redirect(w, r, "/", http.StatusSeeOther)