summaryrefslogtreecommitdiff
path: root/auth.go
diff options
context:
space:
mode:
Diffstat (limited to 'auth.go')
-rw-r--r--auth.go169
1 files changed, 169 insertions, 0 deletions
diff --git a/auth.go b/auth.go
new file mode 100644
index 0000000..1066d9f
--- /dev/null
+++ b/auth.go
@@ -0,0 +1,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{}