user-scoped identity and authentication for llm and agentic apps (apps sdk + mcp)
until now, most llm systems have relied on shared api keys — not real user identity.
the shift toward agentic systems, personal data, and enterprise governance requires every model call to be tied to a verified user, tenant, and scope.
identifiabl defines this layer.
identifiabl is a user-scoped identity and authentication gateway for llm apps.
it lets you:
📦 implementation:
ai-auth-gateway(published)
as organizations adopt agentic systems and user-specific ai workflows, identity, policy, and governance become mandatory. shared api keys cannot support enterprise-grade access control or compliance.
a new layer is required — the user-scoped trust and governance gateway.
it sits between applications (or agents) and model providers, ensuring that every request is authenticated, authorized, observable, and governed.
gatewaystack defines this layer. identifiabl is the user-scoped identity and authentication module.
a healthcare saas with 10,000+ doctors needs to ensure every ai-assisted diagnosis is tied to the licensed physician who requested it, with full audit trails for hipaa and internal review.
today, most of their ai calls run through a shared openai key:
with identifiabl in front of the model provider:
user_id maps to the specific physician; org_id to the clinic or hospitalrole:physician, scope:diagnosis:write)the result: every ai diagnosis is user-bound, tenant-aware, and fully auditable, without the app having to manually thread identity through every model call.
a global enterprise rolls out an internal copilot that can search confluence, jira, google drive, and internal apis. employees authenticate with sso (okta / entra / auth0), but today the copilot usually calls the llm and tools with a shared api key.
with identifiabl, every request is bound to a specific employee:
sub)this lets security teams enforce “only finance analysts can run this tool”, “legal can see these repositories but not those”, and keep a full identity-level audit trail for every copilot interaction.
a multi-tenant saas platform offers ai features across free, pro, and enterprise tiers. today, most of the ai usage runs through a single openai key per environment — making it impossible to answer basic questions like:
with identifiabl, each model call is tied to a user and tenant:
user_id and org_id are attached to every requestplan:free, plan:pro, feature:advanced-rag)limitabl and explicabl can enforce per-tenant limits and produce human-readable audit trailsthis turns “one big shared key” into per-tenant, per-user accountability without changing your app’s business logic.
today, teams solve this problem in a few different ways:
identity providers (auth0, okta, cognito, entra id)
handle login and token minting, but they stop at the edge of your app. they don’t understand model calls, tools, or which provider a request is going to — and they don’t enforce user identity inside the ai gateway.
api gateways and service meshes (kong, apigee, api gateway, istio, envoy)
great at path/method-level auth and rate limiting, but they treat llms like any other http backend. they don’t normalize user/org/tenant metadata, don’t speak apps sdk / mcp, and don’t provide a model-centric identity abstraction.
cloud ai gateways and guardrails (cloudflare ai gateway, azure openai + api management, vertex, bedrock guardrails)
they focus on provider routing, quota, and safety filters at the tenant or api key level. user identity is usually out-of-band or left to the application.
hand-rolled middleware
many teams glue together jwt validation, headers, and logging inside their app or a thin node/go proxy. it works… until you need to support multiple agents, providers, tenants, and audit/regulatory requirements.
identifiabl is different:
transformabl, validatabl, limitabl, proxyabl, explicabl)you can still run it alongside traditional api gateways — identifiabl is the user-scoped identity slice of the stack.
identifiabl is responsible for establishing the trust chain at the very start of every model call.
it validates identity tokens, extracts normalized user metadata, and binds that identity to all downstream governance modules.
all gatewaystack modules operate on a shared RequestContext object.
identifiabl is responsible for:
Authorization header, jwks endpointidentity (canonical user/org/tenant/roles/scopes), trace_id, identity headers (x-user-id, x-org-id, x-tenant, x-user-scopes)the identity object becomes the foundation for all downstream governance decisions in transformabl, validatabl, limitabl, proxyabl, and explicabl.
identity becomes a piece of runtime metadata that other gatewaystack modules rely on — policy checks (validatabl), routing (proxyabl), cost/budget enforcement (limitabl), and full audit trails (explicabl) all start from this canonical identity object.
transformabl for preprocess content (see transformabl)validatabl to evaluate authorization policies (see validatabl)limitabl to apply rate limits or quotas (see limitabl)proxyabl to perform provider routing (see proxyabl)explicabl to store or ship audit logs by itself (see explicabl)1. verifyToken — verify oidc / apps sdk identity tokens
validates rs256 jwts, audiences, issuers, expirations, and nonce.
a minimal typescript implementation:
// auth/verifyToken.ts
import { createRemoteJWKSet, jwtVerify, JWTPayload } from 'jose';
const ISSUER = process.env.AUTH_ISSUER!;
const AUDIENCE = process.env.AUTH_AUDIENCE!;
const JWKS_URI = process.env.AUTH_JWKS_URI!;
const jwks = createRemoteJWKSet(new URL(JWKS_URI));
export type VerifiedIdentity = {
user_id: string;
org_id?: string;
tenant?: string;
roles: string[];
scopes: string[];
raw: JWTPayload;
};
export async function verifyToken(authorizationHeader: string | undefined): Promise<VerifiedIdentity> {
if (!authorizationHeader?.startsWith('Bearer ')) {
throw new Error('missing or invalid bearer token');
}
const token = authorizationHeader.slice('Bearer '.length).trim();
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
audience: AUDIENCE,
});
return extractIdentity(payload);
}
function extractIdentity(payload: JWTPayload): VerifiedIdentity {
const scopes =
typeof payload.scope === 'string'
? payload.scope.split(' ').filter(Boolean)
: Array.isArray(payload.scope)
? payload.scope.map(String)
: [];
return {
user_id: String(payload.sub ?? ''),
org_id: (payload['org_id'] as string) ?? undefined,
tenant: (payload['tenant'] as string) ?? undefined,
roles: (payload['roles'] as string[]) ?? [],
scopes,
raw: payload,
};
}
2. extractIdentity — normalize user/org/tenant metadata
implemented inside verifyToken above, it returns a canonical structure:
{ user_id, org_id, tenant, roles, scopes }
3. attachIdentity — bind identity to model request metadata
injects identity into headers or context fields for downstream modules.
// middleware/attachIdentity.ts
import type { Request, Response, NextFunction } from 'express';
import { verifyToken, VerifiedIdentity } from '../auth/verifyToken';
declare module 'express-serve-static-core' {
interface Request {
identity?: VerifiedIdentity;
}
}
export async function attachIdentity(req: Request, res: Response, next: NextFunction) {
try {
const authHeader = req.headers['authorization'] as string | undefined;
const identity = await verifyToken(authHeader);
// attach to request for downstream handlers / modules
req.identity = identity;
// inject normalized identity headers for downstream services / proxies
req.headers['x-user-id'] = identity.user_id;
if (identity.org_id) req.headers['x-org-id'] = identity.org_id;
if (identity.tenant) req.headers['x-tenant'] = identity.tenant;
req.headers['x-user-scopes'] = identity.scopes.join(' ');
return next();
} catch (err) {
return res.status(401).json({
error: 'unauthorized',
reason: (err as Error).message ?? 'token validation failed',
});
}
}
then wire it up in your gateway server:
import express from 'express';
import { attachIdentity } from './middleware/attachIdentity';
const app = express();
app.use(attachIdentity);
// all downstream routes now see req.identity and identity headers
4. assertIdentity — enforce presence of user identity
guarantees that no anonymous or shared-key requests pass through.
// middleware/assertIdentity.ts
import type { Request, Response, NextFunction } from 'express';
export function assertIdentity(req: Request, res: Response, next: NextFunction) {
if (!req.identity) {
return res.status(401).json({ error: 'unauthorized', reason: 'missing user identity' });
}
return next();
}
5. logIdentity — produce identity-level audit events
emits structured logs for compliance, analytics, and debugging.
// inside your request pipeline
logger.info('identity_event', {
user_id: req.identity?.user_id,
org_id: req.identity?.org_id,
scopes: req.identity?.scopes,
path: req.path,
action: 'model_request',
});
user
→ identifiabl (who is calling?)
→ transformabl (prepare, clean, classify, anonymize)
→ validatabl (is this allowed?)
→ limitabl (how much can they use? pre-flight constraints)
→ proxyabl (where does it go? execute)
→ llm provider (model call)
→ [limitabl] (deduct actual usage, update quotas/budgets)
→ explicabl (what happened?)
→ response
each module intercepts the request, adds or checks metadata, and guarantees that the call is:
identified, transformed, validated, constrained, routed, and audited.
this is the foundation of user-scoped ai.
identifiabl plugs into gatewaystack and your existing llm stack without requiring application-level changes. it exposes http middleware and sdk hooks for:
for implementation details:
→ ai-auth-gateway quickstart
→ auth0 setup guide
→ integration examples
want to explore the full gatewaystack architecture?
→ view the gatewaystack github repo
want to contact us for enterprise deployments?
→ reducibl applied ai studio
every request flows from your app through gatewaystack's modules before it reaches an llm provider — identified, transformed, validated, constrained, routed, and audited.