> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.adid.dev/llms.txt.
> For full documentation content, see https://docs.adid.dev/llms-full.txt.

# Server-Side Enforcement

The authoritative RBAC code lives in `packages/api/internal/middleware/rbac.go`. Every protected route is wrapped with `middleware.RequireRoles(roles...)` in `packages/api/internal/router/router.go`.

#### 13.4.1 Middleware contract

```go
// packages/api/internal/middleware/rbac.go (verified)
func RequireRoles(roles ...string) func(http.Handler) http.Handler {
    allowed := make(map[string]bool, len(roles))
    for _, r := range roles {
        allowed[r] = true
    }
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            userRole := GetAuthRole(r.Context())

            // Admin always passes.
            if userRole == "admin" {
                next.ServeHTTP(w, r); return
            }
            // API-key and DID-Auth schemes pass through (handler enforces ownership).
            scheme := GetAuthScheme(r.Context())
            if scheme == "apikey" || scheme == "didauth" {
                next.ServeHTTP(w, r); return
            }
            if userRole == "" {
                writeRBACError(w, http.StatusUnauthorized, "authentication required"); return
            }
            if !allowed[userRole] {
                writeRBACError(w, http.StatusForbidden, "insufficient permissions for this resource"); return
            }
            next.ServeHTTP(w, r)
        })
    }
}
```

Three things to know:

1. **`admin` is hard-wired** as a superset role at the middleware layer — it bypasses every check.
2. **`apikey` and `didauth` schemes pass through.** This is intentional — both schemes already proved an identity (a long-lived secret or a fresh DID signature). The handler is then responsible for ownership / resource-scope checks.
3. **Empty role yields `401`, wrong role yields `403`.** Clients should distinguish: 401 = "log in"; 403 = "this account can't do that".

#### 13.4.2 Wiring in the router

```go
// packages/api/internal/router/router.go (illustrative; matches actual routes)
r.Route("/api/v1", func(r chi.Router) {
    // Public — no auth, no rate-limit aware of identity
    r.Get("/health", h.healthH.Get)
    r.Get("/.well-known/agent.json", h.agentH.WellKnownCard)
    r.Post("/auth/register", h.authH.Register)
    r.Post("/credentials/verify", h.vcH.Verify)
    r.Post("/presentations/verify", h.vcH.VerifyPresentation)

    // Authenticated
    r.Group(func(r chi.Router) {
        r.Use(h.authMW.Handler)

        // Holder-and-up endpoints
        r.Get("/dashboard/stats", h.dashboardH.Stats)
        r.Post("/dids", h.didH.Create)

        // Issuer-only
        r.Group(func(r chi.Router) {
            r.Use(middleware.RequireRoles("issuer"))
            r.Post("/credentials/schemas", h.vcH.CreateSchema)
            r.Post("/credentials/issue",   h.vcH.Issue)
            r.Post("/credentials/revoke",  h.vcH.Revoke)
        })

        // Verifier-only
        r.Group(func(r chi.Router) {
            r.Use(middleware.RequireRoles("verifier"))
            r.Post("/verifications",                h.verifierH.Create)
            r.Post("/verifier/trusted-issuers",     h.verifierH.AddTrustedIssuer)
            r.Delete("/verifier/trusted-issuers/{id}", h.verifierH.RemoveTrustedIssuer)
        })
    })
})
```

#### 13.4.3 Owner-only enforcement inside handlers

Some endpoints (e.g. `PUT /dids/{did}`) accept any authenticated caller at the **router** layer but enforce **resource ownership** inside the handler. The handler typically:

```go
// service/did.go (illustrative)
func (s *DIDService) UpdateDID(ctx context.Context, did string, doc DIDDocument) error {
    callerSub  := middleware.GetAuthSubject(ctx)
    callerDID  := middleware.GetAuthDID(ctx)
    callerRole := middleware.GetAuthRole(ctx)

    rec, err := s.repo.GetDID(ctx, did)
    if err != nil {
        return ErrDIDNotFound
    }
    // Admins skip ownership.
    if callerRole == "admin" {
        return s.update(ctx, rec, doc)
    }
    // Owner check: either DID-Auth principal == DID, or owner.user_id == callerSub.
    if callerDID == did || rec.OwnerUserID == callerSub {
        return s.update(ctx, rec, doc)
    }
    return ErrForbidden
}
```

> **Best practice for new endpoints:** if the resource has an owner column, always perform the owner check **after** RBAC, never before. RBAC restricts the *role*; ownership restricts the *instance*.

#### 13.4.4 Error response shape

Failed RBAC checks return the canonical error envelope (verified in `rbac.go:73-81`):

```http
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "success": false,
  "error": {
    "code": "FORBIDDEN",
    "message": "insufficient permissions for this resource"
  }
}
```

A failed auth (no header, malformed scheme, expired JWT) instead returns:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: DIDAuth realm="IDA Platform"
Content-Type: application/json

{
  "error": "invalid or expired token",
  "status": 401
}
```

> **Why two shapes?** RBAC errors are produced by `rbac.go:writeRBACError` and follow the new `{ success, error: { code, message } }` envelope. Auth errors are produced by `auth.go:writeAuthError` and use the legacy `{ error, status }` envelope. SDK clients should accept both shapes; new endpoints should standardise on the RBAC shape.

***