Cut user-owned API keys; redesign subject model
Removes the APIKey primitive entirely (Auth.IssueAPIKey/AuthenticateAPIKey/
RevokeAPIKey, APIKeyStore, Deps.APIKeys, Stores.APIKeys, Tables.APIKeys,
ErrAPIKeyInvalid, AuthMethodAPIKey, Principal.{APIKeyID, Abilities, HasAbility},
prefixAPIKey, RequireAPIKey, and the 6 SQL templates). Migration
0003_drop_api_keys.sql hard-drops authkit_api_keys.
The new subject model: *Principal carries identity only (sessions, JWTs);
*ServiceKey is the only abilities-bearing credential and gains a
HasAbility(name) method. RequireAbility now reads *ServiceKey from context
(user principals 403 by design). RequireRole/RequirePermission stay
Principal-only. New RequireServiceKey + ServiceKeyFrom + MustServiceKey,
and a heterogeneous RequireAnyOrServiceKey for routes that accept either.
RequireAny is now Principal-only (default [Session, JWT]).
Adds 7 middleware tests (auth, revoked, ability accept/reject across
subjects, role rejects service key, RequireAnyOrServiceKey both paths) and
1 (*ServiceKey).HasAbility unit test. Existing API-key tests deleted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4942e4dbdc
commit
7f1db871bc
24 changed files with 773 additions and 496 deletions
|
|
@ -65,21 +65,27 @@ func RequireJWT(opts Options) func(http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
// RequireServiceKey authenticates the request via an opaque service token
|
||||
// secret. On success the resolved *authkit.ServiceKey is placed on the
|
||||
// request context; downstream handlers retrieve it via ServiceKeyFrom. Note
|
||||
// that this middleware does NOT place a *Principal on the context — service
|
||||
// tokens have no user — so user-bound authz middleware (RequireRole,
|
||||
// RequirePermission) will reject service-key requests with 403.
|
||||
func RequireServiceKey(opts Options) func(http.Handler) http.Handler {
|
||||
return requireWithServiceKey(opts, func(r *http.Request, raw string) (*authkit.ServiceKey, error) {
|
||||
return opts.Auth.AuthenticateServiceKey(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.
|
||||
// RequireAny tries each user-bound method in order until one succeeds. The
|
||||
// default set is [Session, JWT]; service tokens are NOT included because
|
||||
// they yield a different subject type. For routes that accept either a user
|
||||
// credential or a service token, use RequireAnyOrServiceKey.
|
||||
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 {
|
||||
|
|
@ -99,8 +105,6 @@ func RequireAny(opts Options, methods ...authkit.AuthMethod) func(http.Handler)
|
|||
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)))
|
||||
|
|
@ -112,8 +116,56 @@ func RequireAny(opts Options, methods ...authkit.AuthMethod) func(http.Handler)
|
|||
}
|
||||
}
|
||||
|
||||
// requireWith is the shared scaffolding for the single-method Require*
|
||||
// middlewares.
|
||||
// RequireAnyOrServiceKey tries the user-bound methods first (default
|
||||
// [Session, JWT]); on failure, falls through to a service-key lookup. The
|
||||
// downstream handler sees either a *Principal or a *ServiceKey on context —
|
||||
// retrieve via PrincipalFrom or ServiceKeyFrom and dispatch accordingly.
|
||||
func RequireAnyOrServiceKey(opts Options, methods ...authkit.AuthMethod) func(http.Handler) http.Handler {
|
||||
if opts.Auth == nil {
|
||||
panic("authkit/middleware: Options.Auth is required")
|
||||
}
|
||||
if len(methods) == 0 {
|
||||
methods = []authkit.AuthMethod{
|
||||
authkit.AuthMethodSession,
|
||||
authkit.AuthMethodJWT,
|
||||
}
|
||||
}
|
||||
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 lastErr error
|
||||
for _, m := range methods {
|
||||
var p *authkit.Principal
|
||||
switch m {
|
||||
case authkit.AuthMethodSession:
|
||||
p, lastErr = opts.Auth.AuthenticateSession(r.Context(), raw)
|
||||
case authkit.AuthMethodJWT:
|
||||
p, lastErr = opts.Auth.AuthenticateJWT(r.Context(), raw)
|
||||
}
|
||||
if lastErr == nil && p != nil {
|
||||
next.ServeHTTP(w, r.WithContext(withPrincipal(r.Context(), p)))
|
||||
return
|
||||
}
|
||||
}
|
||||
k, err := opts.Auth.AuthenticateServiceKey(r.Context(), raw)
|
||||
if err == nil && k != nil {
|
||||
next.ServeHTTP(w, r.WithContext(withServiceKey(r.Context(), k)))
|
||||
return
|
||||
}
|
||||
if lastErr == nil {
|
||||
lastErr = err
|
||||
}
|
||||
opts.onUnauth()(w, r, lastErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// requireWith is the shared scaffolding for the single-method user-bound
|
||||
// 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")
|
||||
|
|
@ -136,3 +188,28 @@ func requireWith(opts Options, authn func(r *http.Request, raw string) (*authkit
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// requireWithServiceKey is the service-key analogue of requireWith. It places
|
||||
// a *ServiceKey (not a *Principal) on the request context.
|
||||
func requireWithServiceKey(opts Options, authn func(r *http.Request, raw string) (*authkit.ServiceKey, 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.ErrServiceKeyInvalid)
|
||||
return
|
||||
}
|
||||
k, err := authn(r, raw)
|
||||
if err != nil {
|
||||
onUnauth(w, r, err)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(withServiceKey(r.Context(), k)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue