This document describes how to add new features to the project, what's already wired up, and the planned roadmap.
| Layer | Location | Rule |
|---|---|---|
| Public API | pkg/ |
Everything here is importable by users |
| Internal | internal/ |
Only for the standalone service — not part of the library |
| Interfaces | pkg/providers/provider.go, pkg/tenant/resolver.go |
Define here, implement in subdirs |
| Tests | next to source file (_test.go) |
Unit tests live with the code they test |
| Integration tests | tests/integration/ (create when needed) |
Require external services |
- Define the interface/struct in the appropriate
pkg/package - Implement in a sub-package or the same file
- Wire into the service in
cmd/iam-service/main.go(if service-level) - Add CLI command in
cmd/iam-cli/main.go(if user-facing) - Update policy YAML if the feature adds new policy fields
- Document in
docs/
File: pkg/core/token/dpop.go
The skeleton is in place. What's missing:
- Full JWK →
crypto.PublicKeyextraction usinglestrrat-go/jwx/v2 - JWT signature verification against the embedded public key
cnf.jktclaim extraction from the access token to match DPoP proof key
// To implement: pkg/core/token/dpop.go
import "github.com/lestrrat-go/jwx/v2/jwk"
func VerifyDPoPSignature(proof *DPoPProof, rawJWT string) error {
key, err := jwk.ParseKey(proof.JWK, ...)
// verify signature of the DPoP JWT
}Enable in middleware by setting EnableDPoP: true in Config.
New file: pkg/core/token/jwtvalidator.go
Currently all tokens go through RFC 7662 introspection. Add a local validator using JWKS:
type JWTValidator struct {
jwks *jwk.Set
}
func (v *JWTValidator) Validate(ctx context.Context, rawToken string) (*CommonClaims, error) {
// parse + verify signature using lestrrat-go/jwx
// extract claims → MapToCommon
}Use case: High-throughput services where remote introspection is too slow.
New file: pkg/core/rar/types.go
Add authorization_details claim support in policy evaluation:
# Policy YAML extension
policies:
- name: account-transfer
resources: [/api/transfer]
require_authorization_details:
- type: account_transfer
min_amount: 0
max_amount: 10000New file: pkg/core/jar/
For services that need to send signed authorization requests to the AS.
File: internal/admin/handler.go
Currently the /admin/ API has no authentication. Add:
- Bearer token validation
- IP allowlist middleware
- Optional mTLS
// internal/admin/handler.go
func (h *Handler) routes() {
h.mux.Handle("/tenants", requireAdminToken(h.handleTenants))
// ...
}File: pkg/core/token/cache.go
The RedisCache struct and RedisClient interface are defined, but not wired into the service. To complete:
// cmd/iam-service/main.go
import "github.com/redis/go-redis/v9"
rdb := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
})
// Adapt go-redis to our RedisClient interface
cache := token.NewRedisCache(&redisAdapter{rdb}, "iam:token:")The redisAdapter just needs to bridge go-redis method signatures to RedisClient.
Add passkey as an AMR value and policy option:
require_passkey: true # AMR must include "hwk" or "fido"Track which scopes users have consented to and enforce at the resource level.
Allow services to exchange tokens for narrower-scoped tokens (actor tokens, delegation).
Auto-sync users/groups from the AS when a tenant is registered.
A web-based policy editor that writes policy.yaml and triggers hot-reload.
Currently audit events are written to slog. Add pluggable sinks:
- Elasticsearch
- S3 (JSONL)
- PostgreSQL
New interface: pkg/telemetry/audit.go
type AuditSink interface {
Write(ctx context.Context, event *AuditEvent) error
}// pkg/middleware/grpc/interceptor.go
func UnaryServerInterceptor(cfg Config) grpc.UnaryServerInterceptor
func StreamServerInterceptor(cfg Config) grpc.StreamServerInterceptorFinancial-grade API profile — add PAR (RFC 9126), mTLS sender-constrained tokens.
- Create
pkg/providers/myprovider/adapter.go - Embed
generic.Adapter(or implementproviders.Providerfrom scratch) - Build the discovery URL for your provider
- Add claims mapping in
pkg/providers/claims_mapper.goif needed - Add a test using
localasas the mock - Document in
docs/providers.md
- Add to
pkg/tenant/resolver.go - Implement
tenant.Resolverinterface (one method:Resolve(*http.Request) (string, error)) - Add to
NewChainResolverusage example in docs
- Add field to
Policystruct inpkg/core/policy/types.go - Add check in
engine.check()inpkg/core/policy/engine.go - Add the corresponding
PolicyRequestfield intypes.go - Update YAML tag + docs
- Add a test case in the simulator
- Unit tests: Use
tokenfactoryto generate tokens with specific claims. No network calls. - Integration tests: Use
localasas the AS. No external dependencies. - Policy tests: Use
simulator.RunTable()for a clear, readable test matrix. - E2E tests: Spin up
localas+ your service + test client in the same test process.
// Pattern: full E2E in a single test function
func TestFullFlow(t *testing.T) {
as, _ := localas.New()
baseURL, _ := as.Start()
defer as.Stop(ctx)
// setup provider → middleware → handler
// issue token from localas
// make HTTP request
// assert response
}