1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
package kate
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
)
// Auth provides secure cookie-based authentication for HTTP applications.
//
// It uses generic type T to allow storage of any serializable data in encrypted cookies.
type Auth[T any] struct {
enc encryption
config AuthConfig[T]
}
// AuthConfig holds configuration settings for the Auth instance.
//
// It specifies how data is serialized, cookie properties, and security settings.
type AuthConfig[T any] struct {
// SerDes handles serialization and deserialization of authentication data
SerDes SerDes[T]
// CookieName is the name of the HTTP cookie used for authentication
CookieName string
// URLPath restricts the cookie to a specific path on the server (optional)
URLPath string
// URLDomain restricts the cookie to a specific domain (optional)
URLDomain string
// HTTPSOnly when true, requires cookies to be sent only over HTTPS connections
HTTPSOnly bool
// MaxAge determines how long in seconds the authentication cookie remains valid
MaxAge time.Duration
}
// SerDes defines the interface for serializing and deserializing authentication data.
//
// Implementations must handle conversion between type T and byte streams.
type SerDes[T any] interface {
// Serialize writes the data of type T to the provided writer
Serialize(io.Writer, T) error
// Deserialize reads data from the reader and populates the provided pointer
Deserialize(io.Reader, *T) error
}
// New creates a new Auth instance with the given private key and configuration.
//
// The private key must be a hex-encoded string used for cookie encryption.
// Panics if the private key is invalid.
func New[T any](privkey string, config AuthConfig[T]) Auth[T] {
enc, err := encryptionFromHexKey(privkey)
if err != nil {
panic(err.Error())
}
return Auth[T]{enc: enc, config: config}
}
// Required is an HTTP middleware that enforces authentication.
//
// It checks for a valid authentication cookie and makes the authenticated data
// available in the request context. Returns 401 Unauthorized if authentication fails.
func (a Auth[T]) Required(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(a.config.CookieName)
if errors.Is(err, http.ErrNoCookie) {
http.Error(w, "Authentication missing", http.StatusUnauthorized)
return
}
cleartext, ok := a.enc.Decrypt(cookie.Value)
if !ok {
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
var data T
if err := a.config.SerDes.Deserialize(bytes.NewBuffer(cleartext), &data); err != nil {
http.Error(w, "Server error", http.StatusInternalServerError)
return
}
handler.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), key, data)))
})
}
// Optional returns an HTTP middleware that allows optional authentication.
//
// It checks for a valid authentication cookie and makes the authenticated data
// available in the request context if present. Unlike Required, this middleware
// allows requests to proceed even when authentication is missing or invalid.
// Returns 500 Internal Server Error only if deserialization fails on valid authentication data.
func (a Auth[T]) Optional(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(a.config.CookieName)
if errors.Is(err, http.ErrNoCookie) {
handler.ServeHTTP(w, r)
return
}
cleartext, ok := a.enc.Decrypt(cookie.Value)
if !ok {
handler.ServeHTTP(w, r)
return
}
var data T
if err := a.config.SerDes.Deserialize(bytes.NewBuffer(cleartext), &data); err != nil {
http.Error(w, "Server error", http.StatusInternalServerError)
return
}
handler.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), key, data)))
})
}
// Set creates and sets an authentication cookie containing the provided data.
//
// The data is serialized, encrypted, and stored in an HTTP cookie.
// Returns an error if serialization fails.
func (a Auth[T]) Set(w http.ResponseWriter, data T) error {
buf := &bytes.Buffer{}
if err := a.config.SerDes.Serialize(buf, data); err != nil {
return fmt.Errorf("Auth.Set: %w", err)
}
cookie := &http.Cookie{
Name: a.config.CookieName,
Value: a.enc.Encrypt(buf.Bytes()),
Path: a.config.URLPath,
Domain: a.config.URLDomain,
MaxAge: int(a.config.MaxAge / time.Second),
Secure: a.config.HTTPSOnly,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
w.Header().Add("Set-Cookie", cookie.String())
return nil
}
// Get retrieves authentication data from the request context.
//
// Returns the data and true if authentication data is present and valid,
// otherwise returns the zero value and false. Should be called within handlers
// protected by the Required middleware.
func (a Auth[T]) Get(ctx context.Context) (T, bool) {
var zero T
val := ctx.Value(key)
if val == nil {
return zero, false
}
switch v := val.(type) {
case T:
return v, true
default:
return zero, false
}
}
type keyt struct{}
var key = keyt{}
|