diff options
author | T <t@tjp.lol> | 2025-06-26 11:42:17 -0600 |
---|---|---|
committer | T <t@tjp.lol> | 2025-07-01 17:50:49 -0600 |
commit | 639ad6a02cbb4b713434671ec09f309aa5410921 (patch) | |
tree | 7dde9cce8136636d11f2f7c961072984cfc705e7 /auth.go |
Create authentic_kate: user authentication for go HTTP applications
Diffstat (limited to 'auth.go')
-rw-r--r-- | auth.go | 169 |
1 files changed, 169 insertions, 0 deletions
@@ -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{} |