Files
librenotes/internal/httpapi/context.go
T
libretechandClaude Opus 4.7 03bc16571d Add tenant-aware HTTP middleware and router
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>
2026-04-28 22:19:09 +02:00

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
}