This commit completes Beat 3 of the SequentialThinkingForCHORUS implementation,
adding KACHING JWT policy enforcement with scope checking.
## Deliverables
### 1. JWT Validation Package (pkg/seqthink/policy/)
**jwt.go** (313 lines): Complete JWT validation system
- `Validator`: JWT token validation with JWKS fetching
- `Claims`: JWT claims structure with scope support
- JWKS fetching and caching (1-hour TTL)
- RSA public key parsing from JWK format
- Space-separated and array scope formats
- Automatic JWKS refresh on cache expiration
**Features**:
- RS256 signature verification
- Expiration and NotBefore validation
- Required scope checking
- JWKS caching to reduce API calls
- Thread-safe key cache with mutex
- Base64 URL encoding/decoding utilities
**jwt_test.go** (296 lines): Comprehensive test suite
- Valid token validation
- Expired token rejection
- Missing scope detection
- Space-separated scopes parsing
- Not-yet-valid token rejection
- JWKS caching behavior verification
- Invalid JWKS server handling
- 5 test scenarios, all passing
### 2. Authorization Middleware
**middleware.go** (75 lines): HTTP authorization middleware
- Bearer token extraction from Authorization header
- Token validation via Validator
- Policy denial metrics tracking
- Optional enforcement (disabled if no JWKS URL)
- Request logging with subject and scopes
- Clean error responses (401 Unauthorized)
**Integration**:
- Wraps `/mcp/tool` endpoint (both encrypted and plaintext)
- Wraps `/mcp/sse` endpoint (both encrypted and plaintext)
- Health and metrics endpoints remain open (no auth)
- Automatic mode detection based on configuration
### 3. Proxy Server Integration
**Updated server.go**:
- Policy middleware initialization in `NewServer()`
- Pre-fetches JWKS on startup
- Auth wrapper for protected endpoints
- Configuration-based enforcement
- Graceful fallback if JWKS unavailable
**Configuration**:
```go
ServerConfig{
KachingJWKSURL: "https://auth.kaching.services/jwks",
RequiredScope: "sequentialthinking.run",
}
```
If both fields are set → policy enforcement enabled
If either is empty → policy enforcement disabled (dev mode)
## Testing Results
### Unit Tests
```
PASS: TestValidateToken (5 scenarios)
- valid_token with required scope
- expired_token rejection
- missing_scope rejection
- space_separated_scopes parsing
- not_yet_valid rejection
PASS: TestJWKSCaching
- Verifies JWKS fetched only once within cache window
- Verifies JWKS re-fetched after cache expiration
PASS: TestParseScopes (5 scenarios)
- Single scope parsing
- Multiple scopes parsing
- Extra spaces handling
- Empty string handling
- Spaces-only handling
PASS: TestInvalidJWKS
- Handles JWKS server errors gracefully
PASS: TestGetCachedKeyCount
- Tracks cached key count correctly
```
**All 5 test groups passed (16 total test cases)**
### Integration Verification
**Without Policy** (development):
```bash
export KACHING_JWKS_URL=""
./build/seqthink-wrapper
# → "Policy enforcement disabled"
# → All requests allowed
```
**With Policy** (production):
```bash
export KACHING_JWKS_URL="https://auth.kaching.services/jwks"
export REQUIRED_SCOPE="sequentialthinking.run"
./build/seqthink-wrapper
# → "Policy enforcement enabled"
# → JWKS pre-fetched
# → Authorization: Bearer <token> required
```
## Security Properties
✅ **Authentication**: RS256 JWT signature verification
✅ **Authorization**: Scope-based access control
✅ **Token Validation**: Expiration and not-before checking
✅ **JWKS Security**: Automatic key rotation support
✅ **Metrics**: Policy denial tracking for monitoring
✅ **Graceful Degradation**: Works without JWKS in dev mode
✅ **Thread Safety**: Concurrent JWKS cache access safe
## API Flow with Policy
### Successful Request:
```
1. Client → POST /mcp/tool
Authorization: Bearer eyJhbGci...
Content-Type: application/age
Body: <encrypted request>
2. Middleware extracts Bearer token
3. Middleware validates JWT signature (JWKS)
4. Middleware checks required scope
5. Request forwarded to handler
6. Handler decrypts request
7. Handler calls MCP server
8. Handler encrypts response
9. Response sent to client
```
### Unauthorized Request:
```
1. Client → POST /mcp/tool
(missing Authorization header)
2. Middleware checks for header → NOT FOUND
3. Policy denial metric incremented
4. 401 Unauthorized response
5. Request rejected
```
## Configuration Modes
**Full Security** (Beat 2 + Beat 3):
```bash
export AGE_IDENT_PATH=/etc/seqthink/age.key
export AGE_RECIPS_PATH=/etc/seqthink/age.pub
export KACHING_JWKS_URL=https://auth.kaching.services/jwks
export REQUIRED_SCOPE=sequentialthinking.run
```
→ Encryption + Authentication + Authorization
**Development Mode**:
```bash
# No AGE_* or KACHING_* variables set
```
→ Plaintext, no authentication
## Next Steps (Beat 4)
Beat 4 will add deployment infrastructure:
- Docker Swarm service definition
- Network overlay configuration
- Secret management for age keys
- KACHING integration documentation
- End-to-end testing in swarm
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
179 lines
8.2 KiB
Modula-2
179 lines
8.2 KiB
Modula-2
module chorus
|
|
|
|
go 1.23.0
|
|
|
|
toolchain go1.24.5
|
|
|
|
require (
|
|
filippo.io/age v1.2.1
|
|
github.com/blevesearch/bleve/v2 v2.5.3
|
|
github.com/chorus-services/backbeat v0.0.0-00010101000000-000000000000
|
|
github.com/docker/docker v28.4.0+incompatible
|
|
github.com/docker/go-connections v0.6.0
|
|
github.com/docker/go-units v0.5.0
|
|
github.com/go-redis/redis/v8 v8.11.5
|
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
|
github.com/google/uuid v1.6.0
|
|
github.com/gorilla/mux v1.8.1
|
|
github.com/gorilla/websocket v1.5.0
|
|
github.com/ipfs/go-cid v0.4.1
|
|
github.com/libp2p/go-libp2p v0.32.0
|
|
github.com/libp2p/go-libp2p-kad-dht v0.25.2
|
|
github.com/libp2p/go-libp2p-pubsub v0.10.0
|
|
github.com/multiformats/go-multiaddr v0.12.0
|
|
github.com/multiformats/go-multihash v0.2.3
|
|
github.com/prometheus/client_golang v1.19.1
|
|
github.com/robfig/cron/v3 v3.0.1
|
|
github.com/rs/zerolog v1.32.0
|
|
github.com/sashabaranov/go-openai v1.41.1
|
|
github.com/sony/gobreaker v0.5.0
|
|
github.com/stretchr/testify v1.11.1
|
|
github.com/syndtr/goleveldb v1.0.0
|
|
golang.org/x/crypto v0.24.0
|
|
gopkg.in/yaml.v3 v3.0.1
|
|
)
|
|
|
|
require (
|
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
|
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
|
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
|
github.com/beorn7/perks v1.0.1 // indirect
|
|
github.com/bits-and-blooms/bitset v1.22.0 // indirect
|
|
github.com/blevesearch/bleve_index_api v1.2.8 // indirect
|
|
github.com/blevesearch/geo v0.2.4 // indirect
|
|
github.com/blevesearch/go-faiss v1.0.25 // indirect
|
|
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
|
github.com/blevesearch/gtreap v0.1.1 // indirect
|
|
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
|
github.com/blevesearch/scorch_segment_api/v2 v2.3.10 // indirect
|
|
github.com/blevesearch/segment v0.9.1 // indirect
|
|
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
|
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
|
github.com/blevesearch/vellum v1.1.0 // indirect
|
|
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
|
|
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
|
|
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
|
|
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
|
|
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
|
|
github.com/blevesearch/zapx/v16 v16.2.4 // indirect
|
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
|
github.com/containerd/cgroups v1.1.0 // indirect
|
|
github.com/containerd/errdefs v1.0.0 // indirect
|
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
|
github.com/distribution/reference v0.6.0 // indirect
|
|
github.com/elastic/gosigar v0.14.2 // indirect
|
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
|
github.com/flynn/noise v1.0.0 // indirect
|
|
github.com/francoispqt/gojay v1.2.13 // indirect
|
|
github.com/go-logr/logr v1.4.3 // indirect
|
|
github.com/go-logr/stdr v1.2.2 // indirect
|
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
|
github.com/gogo/protobuf v1.3.2 // indirect
|
|
github.com/golang/protobuf v1.5.3 // indirect
|
|
github.com/golang/snappy v0.0.4 // indirect
|
|
github.com/google/gopacket v1.1.19 // indirect
|
|
github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect
|
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
|
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
|
|
github.com/huin/goupnp v1.3.0 // indirect
|
|
github.com/ipfs/boxo v0.10.0 // indirect
|
|
github.com/ipfs/go-datastore v0.6.0 // indirect
|
|
github.com/ipfs/go-log v1.0.5 // indirect
|
|
github.com/ipfs/go-log/v2 v2.5.1 // indirect
|
|
github.com/ipld/go-ipld-prime v0.20.0 // indirect
|
|
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
|
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
|
github.com/jbenet/goprocess v0.1.4 // indirect
|
|
github.com/json-iterator/go v1.1.12 // indirect
|
|
github.com/klauspost/compress v1.17.2 // indirect
|
|
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
|
github.com/koron/go-ssdp v0.0.4 // indirect
|
|
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
|
github.com/libp2p/go-cidranger v1.1.0 // indirect
|
|
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
|
|
github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect
|
|
github.com/libp2p/go-libp2p-kbucket v0.6.3 // indirect
|
|
github.com/libp2p/go-libp2p-record v0.2.0 // indirect
|
|
github.com/libp2p/go-libp2p-routing-helpers v0.7.2 // indirect
|
|
github.com/libp2p/go-msgio v0.3.0 // indirect
|
|
github.com/libp2p/go-nat v0.2.0 // indirect
|
|
github.com/libp2p/go-netroute v0.2.1 // indirect
|
|
github.com/libp2p/go-reuseport v0.4.0 // indirect
|
|
github.com/libp2p/go-yamux/v4 v4.0.1 // indirect
|
|
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
|
|
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
github.com/miekg/dns v1.1.56 // indirect
|
|
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
|
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
|
github.com/mschoch/smat v0.2.0 // indirect
|
|
github.com/multiformats/go-base32 v0.1.0 // indirect
|
|
github.com/multiformats/go-base36 v0.2.0 // indirect
|
|
github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect
|
|
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
|
|
github.com/multiformats/go-multibase v0.2.0 // indirect
|
|
github.com/multiformats/go-multicodec v0.9.0 // indirect
|
|
github.com/multiformats/go-multistream v0.5.0 // indirect
|
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
|
github.com/nats-io/nats.go v1.36.0 // indirect
|
|
github.com/nats-io/nkeys v0.4.7 // indirect
|
|
github.com/nats-io/nuid v1.0.1 // indirect
|
|
github.com/onsi/ginkgo/v2 v2.13.0 // indirect
|
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
|
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
|
github.com/pkg/errors v0.9.1 // indirect
|
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
github.com/polydawn/refmt v0.89.0 // indirect
|
|
github.com/prometheus/client_model v0.5.0 // indirect
|
|
github.com/prometheus/common v0.48.0 // indirect
|
|
github.com/prometheus/procfs v0.12.0 // indirect
|
|
github.com/quic-go/qpack v0.4.0 // indirect
|
|
github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
|
|
github.com/quic-go/quic-go v0.39.3 // indirect
|
|
github.com/quic-go/webtransport-go v0.6.0 // indirect
|
|
github.com/raulk/go-watchdog v1.3.0 // indirect
|
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
|
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
|
|
go.etcd.io/bbolt v1.4.0 // indirect
|
|
go.opencensus.io v0.24.0 // indirect
|
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
|
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
|
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
|
go.uber.org/dig v1.17.1 // indirect
|
|
go.uber.org/fx v1.20.1 // indirect
|
|
go.uber.org/mock v0.3.0 // indirect
|
|
go.uber.org/multierr v1.11.0 // indirect
|
|
go.uber.org/zap v1.26.0 // indirect
|
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
|
golang.org/x/mod v0.18.0 // indirect
|
|
golang.org/x/net v0.26.0 // indirect
|
|
golang.org/x/sync v0.10.0 // indirect
|
|
golang.org/x/sys v0.35.0 // indirect
|
|
golang.org/x/text v0.16.0 // indirect
|
|
golang.org/x/tools v0.22.0 // indirect
|
|
gonum.org/v1/gonum v0.13.0 // indirect
|
|
google.golang.org/protobuf v1.34.2 // indirect
|
|
lukechampine.com/blake3 v1.2.1 // indirect
|
|
)
|
|
|
|
replace github.com/chorus-services/backbeat => ../BACKBEAT/backbeat/prototype
|