Middleware Implementation
Status
Section titled “Status”This document is FINAL and ENFORCEABLE for backend middleware implementation.
It translates the architecture and access-control rules into practical code patterns.
It complements:
2.1.-Backend-Auth.md2.2.-Backend-Core.md2.2.1.-Backend-Core-API.md2.3.-Backend-Base.md2.4.-Access-Control-Integration.md2.5.-Access-Matrix.md
1. Purpose
Section titled “1. Purpose”This document defines the actual backend middleware pattern for:
- JWT validation
x-orgenforcement- Auth access resolution
- module enforcement
- permission enforcement
- fail-closed behavior
- request context propagation
It includes:
- Node.js / Express-style examples
- Go / Chi-style examples
- reusable guard helpers:
requireModule()requirePermission()
2. Final enforcement model
Section titled “2. Final enforcement model”All tenant-scoped backend routes must follow this flow:
- validate JWT
- validate
x-org - resolve effective access from Auth
- verify module
- verify permission
- execute or deny
2.1 Final security rule
Section titled “2.1 Final security rule”Backends do not compute access.Backends do not call Core for authorization.Backends do not trust frontend state.Backends enforce Auth-resolved access only.3. Expected Auth contract
Section titled “3. Expected Auth contract”Middleware assumes Auth exposes an access-resolution contract like:
GET /auth/me/accessAuthorization: Bearer <USER_ACCESS_TOKEN>x-org: <COMPANY_ID>X-Internal-API-Key: <SERVICE_KEY>And returns a shape similar to:
{ "userId": "uuid", "companyId": "uuid", "tenantRole": "ADMIN", "modules": ["basic", "finance"], "permissions": [ "basic.dashboard.view", "finance.expense.view" ], "delegation": { "canManageUsers": true, "canBuyAddons": false }, "meta": { "tokenVersion": 2, "entitlementVersion": 8 }}4. Request context model
Section titled “4. Request context model”Your backend should attach a resolved request context after middleware succeeds.
Recommended normalized shape:
type RequestAccessContext = { userId: string companyId: string tenantRole: string | null modules: string[] permissions: string[] delegation?: { canManageUsers?: boolean canBuyAddons?: boolean grantableModules?: string[] grantablePermissions?: string[] } meta?: { tokenVersion?: number entitlementVersion?: number generatedAt?: string }}Equivalent idea in Go:
type AccessContext struct { UserID string CompanyID string TenantRole string Modules []string Permissions []string Delegation map[string]any Meta map[string]any}5. Error contract
Section titled “5. Error contract”Use these rules consistently:
401→ invalid/missing JWT400→ missing/invalidx-org403→ access denied (missing module / permission / membership)503→ Auth access resolution unavailable
6. Node.js / Express-style implementation
Section titled “6. Node.js / Express-style implementation”Below is a practical Node.js implementation pattern.
6.1 Shared types
Section titled “6.1 Shared types”import type { Request, Response, NextFunction } from "express";
export type AccessContext = { userId: string; companyId: string; tenantRole?: string | null; modules: string[]; permissions: string[]; delegation?: { canManageUsers?: boolean; canBuyAddons?: boolean; grantableModules?: string[]; grantablePermissions?: string[]; }; meta?: { tokenVersion?: number; entitlementVersion?: number; generatedAt?: string; };};
export interface AuthenticatedRequest extends Request { jwt?: { sub: string; rawToken: string; }; access?: AccessContext;}6.2 Utility: send structured errors
Section titled “6.2 Utility: send structured errors”function sendError( res: Response, status: number, code: string, message: string) { return res.status(status).json({ success: false, error: { code, message }, });}6.3 Middleware: validate JWT locally
Section titled “6.3 Middleware: validate JWT locally”This middleware validates the token format and signature locally.
In production you would wire this to JWKS / Auth public keys.
import jwt from "jsonwebtoken";
const JWT_PUBLIC_KEY = process.env.JWT_PUBLIC_KEY?.replace(/\\n/g, "\n") ?? "";const JWT_ISSUER = process.env.JWT_ISSUER ?? "auth.kisum.io";const JWT_AUDIENCE = process.env.JWT_AUDIENCE ?? "kisum-apps";
export function validateJwt() { return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { const auth = req.header("authorization"); if (!auth || !auth.startsWith("Bearer ")) { return sendError(res, 401, "unauthorized", "missing bearer token"); }
const rawToken = auth.slice("Bearer ".length).trim(); if (!rawToken) { return sendError(res, 401, "unauthorized", "missing bearer token"); }
const decoded = jwt.verify(rawToken, JWT_PUBLIC_KEY, { algorithms: ["RS256"], issuer: JWT_ISSUER, audience: JWT_AUDIENCE, }) as jwt.JwtPayload;
if (!decoded.sub || typeof decoded.sub !== "string") { return sendError(res, 401, "unauthorized", "invalid token subject"); }
req.jwt = { sub: decoded.sub, rawToken, };
return next(); } catch (err) { return sendError(res, 401, "unauthorized", "invalid or expired token"); } };}6.4 Middleware: require x-org
Section titled “6.4 Middleware: require x-org”export function requireCompanyContext() { return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { const xOrg = req.header("x-org");
if (!xOrg || !xOrg.trim()) { return sendError(res, 400, "validation_error", "missing x-org header"); }
// Optional UUID format check here if required. return next(); };}6.5 Auth client for access resolution
Section titled “6.5 Auth client for access resolution”type AuthAccessResponse = { success: boolean; data?: AccessContext; error?: { code: string; message: string; };};
const AUTH_BASE_URL = process.env.AUTH_BASE_URL ?? "https://auth.kisum.io";const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY ?? "";
export async function fetchAccessContext( rawToken: string, companyId: string): Promise<AccessContext> { const response = await fetch(`${AUTH_BASE_URL}/auth/me/access`, { method: "GET", headers: { Authorization: `Bearer ${rawToken}`, "x-org": companyId, "X-Internal-API-Key": INTERNAL_API_KEY, Accept: "application/json", }, });
if (response.status === 401) { throw Object.assign(new Error("invalid token"), { status: 401 }); }
if (response.status === 403) { throw Object.assign(new Error("forbidden"), { status: 403 }); }
if (response.status >= 500) { throw Object.assign(new Error("auth unavailable"), { status: 503 }); }
const body = (await response.json()) as AuthAccessResponse;
if (!body.success || !body.data) { throw Object.assign(new Error("access resolution failed"), { status: 503 }); }
return body.data;}6.6 Middleware: resolve access from Auth
Section titled “6.6 Middleware: resolve access from Auth”export function resolveAccessContext() { return async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { if (!req.jwt?.rawToken) { return sendError(res, 401, "unauthorized", "missing validated JWT"); }
const companyId = req.header("x-org"); if (!companyId) { return sendError(res, 400, "validation_error", "missing x-org header"); }
const access = await fetchAccessContext(req.jwt.rawToken, companyId); req.access = access;
return next(); } catch (err: any) { if (err?.status === 401) { return sendError(res, 401, "unauthorized", "invalid or expired token"); }
if (err?.status === 403) { return sendError(res, 403, "forbidden", "access forbidden"); }
return sendError( res, 503, "service_unavailable", "unable to resolve access from auth" ); } };}6.7 Guard helper: requireModule()
Section titled “6.7 Guard helper: requireModule()”export function requireModule(moduleKey: string) { return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { const modules = req.access?.modules ?? [];
if (!modules.includes(moduleKey)) { return sendError( res, 403, "forbidden", `missing module access: ${moduleKey}` ); }
return next(); };}6.8 Guard helper: requirePermission()
Section titled “6.8 Guard helper: requirePermission()”export function requirePermission(permissionKey: string) { return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { const permissions = req.access?.permissions ?? [];
if (!permissions.includes(permissionKey)) { return sendError( res, 403, "forbidden", `missing permission: ${permissionKey}` ); }
return next(); };}6.9 Example route composition
Section titled “6.9 Example route composition”import express from "express";
const app = express();
app.get( "/api/finance/expenses", validateJwt(), requireCompanyContext(), resolveAccessContext(), requireModule("finance"), requirePermission("finance.expense.view"), async (_req, res) => { return res.json({ success: true, data: { items: [], }, }); });
app.post( "/api/finance/expenses", validateJwt(), requireCompanyContext(), resolveAccessContext(), requireModule("finance"), requirePermission("finance.expense.create"), async (_req, res) => { return res.status(201).json({ success: true, data: { created: true, }, }); });6.10 Optional helper: combine guards
Section titled “6.10 Optional helper: combine guards”export function requireAccess(moduleKey: string, permissionKey: string) { return [ validateJwt(), requireCompanyContext(), resolveAccessContext(), requireModule(moduleKey), requirePermission(permissionKey), ];}Usage:
app.get( "/api/basic/events", ...requireAccess("basic", "basic.event.view"), async (_req, res) => { return res.json({ success: true, data: [] }); });7. Go / Chi-style implementation
Section titled “7. Go / Chi-style implementation”Below is a practical Go pattern for services using Chi.
7.1 Types
Section titled “7.1 Types”package access
type AccessContext struct { UserID string `json:"userId"` CompanyID string `json:"companyId"` TenantRole string `json:"tenantRole"` Modules []string `json:"modules"` Permissions []string `json:"permissions"` Delegation map[string]any `json:"delegation,omitempty"` Meta map[string]any `json:"meta,omitempty"`}7.2 Request context keys
Section titled “7.2 Request context keys”package access
type contextKey string
const ( CtxJWTTokenKey contextKey = "jwt_raw_token" CtxUserIDKey contextKey = "user_id" CtxCompanyIDKey contextKey = "company_id" CtxAccessKey contextKey = "access_context")7.3 Error writer
Section titled “7.3 Error writer”package access
import ( "encoding/json" "net/http")
func WriteError(w http.ResponseWriter, status int, code, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]any{ "success": false, "error": map[string]any{ "code": code, "message": message, }, })}7.4 Middleware: validate JWT
Section titled “7.4 Middleware: validate JWT”This example assumes you already have a JWT validation function.
package access
import ( "context" "net/http" "strings")
type ValidatedJWT struct { Subject string RawToken string}
func VerifyJWT(rawToken string) (*ValidatedJWT, error) { // Plug in your real RS256/JWKS verification here. // Must validate signature, exp, iss, aud. return &ValidatedJWT{ Subject: "user-subject", RawToken: rawToken, }, nil}
func ValidateJWT(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { WriteError(w, http.StatusUnauthorized, "unauthorized", "missing bearer token") return }
rawToken := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")) if rawToken == "" { WriteError(w, http.StatusUnauthorized, "unauthorized", "missing bearer token") return }
validated, err := VerifyJWT(rawToken) if err != nil { WriteError(w, http.StatusUnauthorized, "unauthorized", "invalid or expired token") return }
ctx := context.WithValue(r.Context(), CtxJWTTokenKey, validated.RawToken) ctx = context.WithValue(ctx, CtxUserIDKey, validated.Subject)
next.ServeHTTP(w, r.WithContext(ctx)) })}7.5 Middleware: require x-org
Section titled “7.5 Middleware: require x-org”package access
import ( "context" "net/http" "strings")
func RequireCompanyContext(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { companyID := strings.TrimSpace(r.Header.Get("x-org")) if companyID == "" { WriteError(w, http.StatusBadRequest, "validation_error", "missing x-org header") return }
ctx := context.WithValue(r.Context(), CtxCompanyIDKey, companyID) next.ServeHTTP(w, r.WithContext(ctx)) })}7.6 Auth client for access resolution
Section titled “7.6 Auth client for access resolution”package access
import ( "encoding/json" "fmt" "net/http" "time")
type AuthClient struct { BaseURL string InternalAPIKey string HTTPClient *http.Client}
func NewAuthClient(baseURL, internalAPIKey string) *AuthClient { return &AuthClient{ BaseURL: baseURL, InternalAPIKey: internalAPIKey, HTTPClient: &http.Client{ Timeout: 10 * time.Second, }, }}
func (c *AuthClient) FetchAccess(rawToken, companyID string) (*AccessContext, int, error) { req, err := http.NewRequest(http.MethodGet, c.BaseURL+"/auth/me/access", nil) if err != nil { return nil, http.StatusServiceUnavailable, err }
req.Header.Set("Authorization", "Bearer "+rawToken) req.Header.Set("x-org", companyID) req.Header.Set("X-Internal-API-Key", c.InternalAPIKey) req.Header.Set("Accept", "application/json")
resp, err := c.HTTPClient.Do(req) if err != nil { return nil, http.StatusServiceUnavailable, err } defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized { return nil, http.StatusUnauthorized, fmt.Errorf("unauthorized") }
if resp.StatusCode == http.StatusForbidden { return nil, http.StatusForbidden, fmt.Errorf("forbidden") }
if resp.StatusCode >= 500 { return nil, http.StatusServiceUnavailable, fmt.Errorf("auth unavailable") }
var body struct { Success bool `json:"success"` Data AccessContext `json:"data"` }
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return nil, http.StatusServiceUnavailable, err }
if !body.Success { return nil, http.StatusServiceUnavailable, fmt.Errorf("access resolution failed") }
return &body.Data, http.StatusOK, nil}7.7 Middleware: resolve access context
Section titled “7.7 Middleware: resolve access context”package access
import ( "context" "net/http")
func ResolveAccessContext(client *AuthClient) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rawToken, _ := r.Context().Value(CtxJWTTokenKey).(string) companyID, _ := r.Context().Value(CtxCompanyIDKey).(string)
if rawToken == "" { WriteError(w, http.StatusUnauthorized, "unauthorized", "missing validated JWT") return }
if companyID == "" { WriteError(w, http.StatusBadRequest, "validation_error", "missing x-org header") return }
access, status, err := client.FetchAccess(rawToken, companyID) if err != nil { switch status { case http.StatusUnauthorized: WriteError(w, http.StatusUnauthorized, "unauthorized", "invalid or expired token") case http.StatusForbidden: WriteError(w, http.StatusForbidden, "forbidden", "access forbidden") default: WriteError(w, http.StatusServiceUnavailable, "service_unavailable", "unable to resolve access from auth") } return }
ctx := context.WithValue(r.Context(), CtxAccessKey, access) next.ServeHTTP(w, r.WithContext(ctx)) }) }}7.8 Guard helper: requireModule()
Section titled “7.8 Guard helper: requireModule()”package access
import "net/http"
func RequireModule(moduleKey string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { access, _ := r.Context().Value(CtxAccessKey).(*AccessContext) if access == nil { WriteError(w, http.StatusServiceUnavailable, "service_unavailable", "missing access context") return }
for _, m := range access.Modules { if m == moduleKey { next.ServeHTTP(w, r) return } }
WriteError(w, http.StatusForbidden, "forbidden", "missing module access: "+moduleKey) }) }}7.9 Guard helper: requirePermission()
Section titled “7.9 Guard helper: requirePermission()”package access
import "net/http"
func RequirePermission(permissionKey string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { access, _ := r.Context().Value(CtxAccessKey).(*AccessContext) if access == nil { WriteError(w, http.StatusServiceUnavailable, "service_unavailable", "missing access context") return }
for _, p := range access.Permissions { if p == permissionKey { next.ServeHTTP(w, r) return } }
WriteError(w, http.StatusForbidden, "forbidden", "missing permission: "+permissionKey) }) }}7.10 Example route composition (Chi)
Section titled “7.10 Example route composition (Chi)”package main
import ( "encoding/json" "net/http"
"github.com/go-chi/chi/v5" "yourapp/access")
func main() { authClient := access.NewAuthClient( "https://auth.kisum.io", "your-internal-service-key", )
r := chi.NewRouter()
r.Route("/api/finance", func(r chi.Router) { r.Use(access.ValidateJWT) r.Use(access.RequireCompanyContext) r.Use(access.ResolveAccessContext(authClient))
r.With( access.RequireModule("finance"), access.RequirePermission("finance.expense.view"), ).Get("/expenses", func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{ "success": true, "data": []any{}, }) })
r.With( access.RequireModule("finance"), access.RequirePermission("finance.expense.create"), ).Post("/expenses", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(map[string]any{ "success": true, "data": map[string]any{ "created": true, }, }) }) })
_ = http.ListenAndServe(":8080", r)}8. Optional caching layer
Section titled “8. Optional caching layer”Caching is optional but recommended for performance.
8.1 Cache rule
Section titled “8.1 Cache rule”Cache must never become the source of truth.
Use cache only if:
- userId matches
- companyId matches
- tokenVersion matches
- entitlementVersion matches (if available)
If Auth is unavailable:
- do not allow requests from stale cache blindly
- fail closed with
503
8.2 Suggested Node cache wrapper
Section titled “8.2 Suggested Node cache wrapper”type CachedAccess = AccessContext & { cacheKey: string;};
function buildAccessCacheKey( userId: string, companyId: string, tokenVersion?: number, entitlementVersion?: number) { return `access:${userId}:${companyId}:${tokenVersion ?? 0}:${entitlementVersion ?? 0}`;}8.3 Suggested Go cache key
Section titled “8.3 Suggested Go cache key”func BuildAccessCacheKey(userID, companyID string, tokenVersion, entitlementVersion int) string { return fmt.Sprintf("access:%s:%s:%d:%d", userID, companyID, tokenVersion, entitlementVersion)}9. Recommended middleware order
Section titled “9. Recommended middleware order”For tenant-scoped routes, use this order:
validateJwtrequireCompanyContextresolveAccessContextrequireModulerequirePermission
This ensures:
- identity is validated first
- company context is validated second
- access is resolved once
- guards run on resolved context
10. What middleware must never do
Section titled “10. What middleware must never do”Middleware must never:
- call Platform Core for user authorization
- compute
entitlements ∩ grants - trust frontend access claims
- allow optimistic fallback if Auth fails
- allow stale access silently when validation signals mismatch
- derive module access from role alone
11. Common examples
Section titled “11. Common examples”11.1 Basic dashboard
Section titled “11.1 Basic dashboard”Requires:
- module:
basic - permission:
basic.dashboard.view
11.2 Basic events list
Section titled “11.2 Basic events list”Requires:
- module:
basic - permission:
basic.event.view
11.3 Finance expenses list
Section titled “11.3 Finance expenses list”Requires:
- module:
finance - permission:
finance.expense.view
11.4 Finance expense create
Section titled “11.4 Finance expense create”Requires:
- module:
finance - permission:
finance.expense.create
11.5 Market contracts approve
Section titled “11.5 Market contracts approve”Requires:
- module:
market - permission:
market.contract.approve
12. Test checklist
Section titled “12. Test checklist”You should test at minimum:
JWT and company context
Section titled “JWT and company context”- missing token →
401 - invalid token →
401 - missing
x-org→400
Access resolution
Section titled “Access resolution”- Auth unavailable →
503 - user not in company →
403
Module checks
Section titled “Module checks”- module not granted →
403
Permission checks
Section titled “Permission checks”- permission not granted →
403
Success
Section titled “Success”- valid token + valid company + valid module + valid permission → request succeeds
13. Final implementation rule
Section titled “13. Final implementation rule”Auth resolves access.Middleware enforces access.Handlers execute business logic only after access succeeds.14. Final one-line summary
Section titled “14. Final one-line summary”A backend route is allowed only after JWT validation, company resolution, Auth access resolution, module check, and permission check all succeed.