diff options
Diffstat (limited to 'auth.go')
-rw-r--r-- | auth.go | 62 |
1 files changed, 62 insertions, 0 deletions
@@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "strings" "time" ) @@ -78,12 +79,14 @@ func (a Auth[T]) Required(handler http.Handler) http.Handler { cleartext, ok := a.enc.Decrypt(cookie.Value) if !ok { + a.Clear(w) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } var data T if err := a.config.SerDes.Deserialize(bytes.NewBuffer(cleartext), &data); err != nil { + a.Clear(w) http.Error(w, "Server error", http.StatusInternalServerError) return } @@ -108,12 +111,14 @@ func (a Auth[T]) Optional(handler http.Handler) http.Handler { cleartext, ok := a.enc.Decrypt(cookie.Value) if !ok { + a.Clear(w) handler.ServeHTTP(w, r) return } var data T if err := a.config.SerDes.Deserialize(bytes.NewBuffer(cleartext), &data); err != nil { + a.Clear(w) http.Error(w, "Server error", http.StatusInternalServerError) return } @@ -145,6 +150,63 @@ func (a Auth[T]) Set(w http.ResponseWriter, data T) error { return nil } +// removeSetCookieHeaders removes any existing Set-Cookie headers that match the given cookie name. +// This ensures we don't send multiple Set-Cookie headers with the same cookie name, which +// violates RFC 6265 recommendations. +func removeSetCookieHeaders(w http.ResponseWriter, cookieName string) { + headers := w.Header() + setCookieHeaders := headers["Set-Cookie"] + if len(setCookieHeaders) == 0 { + return + } + + // Filter out headers that match our cookie name + var filteredHeaders []string + for _, header := range setCookieHeaders { + // Parse the cookie name from the Set-Cookie header + // Format: "name=value; other=attributes" + if idx := strings.Index(header, "="); idx > 0 { + headerCookieName := strings.TrimSpace(header[:idx]) + if headerCookieName != cookieName { + filteredHeaders = append(filteredHeaders, header) + } + } else { + // Keep malformed headers as-is + filteredHeaders = append(filteredHeaders, header) + } + } + + // Replace the Set-Cookie headers with the filtered list + if len(filteredHeaders) == 0 { + headers.Del("Set-Cookie") + } else { + headers["Set-Cookie"] = filteredHeaders + } +} + +// Clear removes the authentication cookie by setting it to expire immediately. +// +// This effectively logs out the user by invalidating their authentication cookie. +// If there are existing Set-Cookie headers for the same cookie name, they are removed +// to comply with RFC 6265 recommendations against multiple Set-Cookie headers with +// the same cookie name. +func (a Auth[T]) Clear(w http.ResponseWriter) { + // Remove any existing Set-Cookie headers for this cookie name + removeSetCookieHeaders(w, a.config.CookieName) + + cookie := &http.Cookie{ + Name: a.config.CookieName, + Value: "", + Path: a.config.URLPath, + Domain: a.config.URLDomain, + MaxAge: -1, + Secure: a.config.HTTPSOnly, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + } + w.Header().Add("Set-Cookie", cookie.String()) +} + // Get retrieves authentication data from the request context. // // Returns the data and true if authentication data is present and valid, |