authkit initial

This commit is contained in:
juancwu 2026-04-26 01:36:53 +00:00
commit 134393fbca
43 changed files with 5188 additions and 1 deletions

138
middleware/middleware.go Normal file
View file

@ -0,0 +1,138 @@
package middleware
import (
"encoding/json"
"net/http"
"git.juancwu.dev/juancwu/authkit"
)
// Options configures auth middleware. Auth is required; the rest fall back
// to defaults: BearerExtractor, a JSON 401 on auth failure, and a JSON 403
// on authz failure.
type Options struct {
Auth *authkit.Auth
Extractor authkit.Extractor
OnUnauth func(w http.ResponseWriter, r *http.Request, err error)
OnForbidden func(w http.ResponseWriter, r *http.Request, err error)
}
func (o Options) extractor() authkit.Extractor {
if o.Extractor != nil {
return o.Extractor
}
return authkit.BearerExtractor()
}
func (o Options) onUnauth() func(w http.ResponseWriter, r *http.Request, err error) {
if o.OnUnauth != nil {
return o.OnUnauth
}
return defaultJSONError(http.StatusUnauthorized)
}
func (o Options) onForbidden() func(w http.ResponseWriter, r *http.Request, err error) {
if o.OnForbidden != nil {
return o.OnForbidden
}
return defaultJSONError(http.StatusForbidden)
}
func defaultJSONError(status int) func(w http.ResponseWriter, r *http.Request, err error) {
return func(w http.ResponseWriter, _ *http.Request, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{
"error": http.StatusText(status),
})
}
}
// RequireSession authenticates the request via an opaque session string. The
// extractor is consulted first; if no extractor is set the default Bearer
// extractor is used. For cookie-based session lookup, set
// Options.Extractor = authkit.CookieExtractor(cfg.SessionCookieName).
func RequireSession(opts Options) func(http.Handler) http.Handler {
return requireWith(opts, func(r *http.Request, raw string) (*authkit.Principal, error) {
return opts.Auth.AuthenticateSession(r.Context(), raw)
})
}
// RequireJWT authenticates the request via an HS256 JWT.
func RequireJWT(opts Options) func(http.Handler) http.Handler {
return requireWith(opts, func(r *http.Request, raw string) (*authkit.Principal, error) {
return opts.Auth.AuthenticateJWT(r.Context(), raw)
})
}
// RequireAPIKey authenticates the request via an opaque API secret.
func RequireAPIKey(opts Options) func(http.Handler) http.Handler {
return requireWith(opts, func(r *http.Request, raw string) (*authkit.Principal, error) {
return opts.Auth.AuthenticateAPIKey(r.Context(), raw)
})
}
// RequireAny tries each method in order until one succeeds. Useful for routes
// that accept either a session cookie or an API key.
func RequireAny(opts Options, methods ...authkit.AuthMethod) func(http.Handler) http.Handler {
if len(methods) == 0 {
methods = []authkit.AuthMethod{
authkit.AuthMethodSession,
authkit.AuthMethodJWT,
authkit.AuthMethodAPIKey,
}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
raw, ok := opts.extractor()(r)
if !ok || raw == "" {
opts.onUnauth()(w, r, authkit.ErrSessionInvalid)
return
}
var (
p *authkit.Principal
lastErr error
)
for _, m := range methods {
switch m {
case authkit.AuthMethodSession:
p, lastErr = opts.Auth.AuthenticateSession(r.Context(), raw)
case authkit.AuthMethodJWT:
p, lastErr = opts.Auth.AuthenticateJWT(r.Context(), raw)
case authkit.AuthMethodAPIKey:
p, lastErr = opts.Auth.AuthenticateAPIKey(r.Context(), raw)
}
if lastErr == nil && p != nil {
next.ServeHTTP(w, r.WithContext(withPrincipal(r.Context(), p)))
return
}
}
opts.onUnauth()(w, r, lastErr)
})
}
}
// requireWith is the shared scaffolding for the single-method Require*
// middlewares.
func requireWith(opts Options, authn func(r *http.Request, raw string) (*authkit.Principal, error)) func(http.Handler) http.Handler {
if opts.Auth == nil {
panic("authkit/middleware: Options.Auth is required")
}
extractor := opts.extractor()
onUnauth := opts.onUnauth()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
raw, ok := extractor(r)
if !ok || raw == "" {
onUnauth(w, r, authkit.ErrSessionInvalid)
return
}
p, err := authn(r, raw)
if err != nil {
onUnauth(w, r, err)
return
}
next.ServeHTTP(w, r.WithContext(withPrincipal(r.Context(), p)))
})
}
}