internal/httpapi/ provides:
- Tenant{UserID, Email} carried on context.Context, with
WithTenant / TenantFrom helpers and ErrNoTenant for the
programming-error case (route reached without middleware).
- AuthMiddleware verifies an Authorization: Bearer <jwt> on every
request via auth.Signer.Verify (which already enforces HS256
and rejects alg=none). On failure: 401, with the underlying
reason logged server-side but not exposed to the client.
- RequireTenantOwnership(ownerID) compares the request's tenant
against the resource owner; returns 403 on mismatch. Handlers
that touch tenant-owned resources call this guard.
- Server.Routes() mounts /auth/* unauthenticated and wraps
/api/* with the middleware. /api/whoami is included as the
canonical example of a tenant-scoped endpoint.
Tests cover: valid JWT pass-through, missing/empty Authorization,
wrong scheme, malformed JWT, tampered signature, JWT signed with
a different secret (cross-tenant key confusion), and the 200/403
matrix for RequireTenantOwnership.
Closes #11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
40 lines
1.1 KiB
Go
40 lines
1.1 KiB
Go
// Package httpapi provides the HTTP-facing layer for the multi-tenant
|
|
// backend: auth middleware, tenant context propagation, and route
|
|
// wiring for the auth and note endpoints.
|
|
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
)
|
|
|
|
// Tenant carries the per-request tenant identity extracted from a
|
|
// validated JWT. It is the only thing handlers need to know about
|
|
// "who is this request for".
|
|
type Tenant struct {
|
|
UserID string
|
|
Email string
|
|
}
|
|
|
|
type ctxKey struct{}
|
|
|
|
// ErrNoTenant indicates that handler code expected a tenant on the
|
|
// request context but found none. This is always a programming error
|
|
// (the route was reached without going through AuthMiddleware).
|
|
var ErrNoTenant = errors.New("httpapi: no tenant in context")
|
|
|
|
// WithTenant returns a derived context carrying t.
|
|
func WithTenant(ctx context.Context, t Tenant) context.Context {
|
|
return context.WithValue(ctx, ctxKey{}, t)
|
|
}
|
|
|
|
// TenantFrom retrieves the tenant from ctx. Panics are avoided by
|
|
// returning ErrNoTenant when the value is missing.
|
|
func TenantFrom(ctx context.Context) (Tenant, error) {
|
|
v, ok := ctx.Value(ctxKey{}).(Tenant)
|
|
if !ok {
|
|
return Tenant{}, ErrNoTenant
|
|
}
|
|
return v, nil
|
|
}
|