summaryrefslogtreecommitdiff
path: root/auth.go
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-07-05 09:41:47 -0600
committerT <t@tjp.lol>2025-07-05 09:55:47 -0600
commitcaf5bb2ee84079365996a622ab8fc5ed510ef9a7 (patch)
tree5caf8bcbcda5ab5c8d70782fb733923c0ac70af3 /auth.go
parent639ad6a02cbb4b713434671ec09f309aa5410921 (diff)
Add Auth.Clear() method and enhance middlewares to clear invalid cookiesHEADmain
- Add Auth.Clear() method that creates expired cookies (MaxAge: -1) to log out users - Enhance Clear() to remove existing Set-Cookie headers to comply with RFC 6265 - Update Required() and Optional() middlewares to automatically clear invalid cookies - Add comprehensive tests for all new functionality - Update documentation with Auth.Clear() usage examples
Diffstat (limited to 'auth.go')
-rw-r--r--auth.go62
1 files changed, 62 insertions, 0 deletions
diff --git a/auth.go b/auth.go
index 1066d9f..ac1fc57 100644
--- a/auth.go
+++ b/auth.go
@@ -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,