Authentication & authorization
Draw a clear line between proving identity and granting capability across systems and Agent call chains—avoid scattering checks in prompts or tool args; this page ties RBAC / ABAC choices to the main flow.
Authentication answers “who are you”: credential forms (password, MFA, OAuth token, mTLS) and issuers must be traceable; authorization answers “what may you access”: roles, policies, and resource-level checks belong on the server, not model discretion alone.
A SKILL can require Agents to list auth assumptions for protected resources (e.g. Bearer scopes, tenant id) before generating call samples; never hard-code long-lived secrets in examples or logs.
In human–AI flows, separate end-user identity from service principals: when an Agent acts as an app, document delegation scope and audit fields.
- Non-negotiables: second confirmation for sensitive ops, least privilege, deny by default.
- Align with API design: consistent 401/403 semantics; errors must not enable account enumeration.
- Sessions and tokens: document expiry, rotation, and revocation in runbooks.
Request path: authentication → authorization (skill-flow-block)
[ Client / Agent presents credentials (header, cookie, mTLS, signature) ]
│
▼
[ Authentication: validate credential, resolve principal (sub, client_id, tenant claims) ]
│
┌────────┴────────┐
▼ ▼
[ Fail → 401 ] [ Success → auth context: roles / permissions / attributes ]
│ │
│ ▼
│ [ Policy: resource + action + environment (ABAC) or role binding (RBAC) ]
│ │
└────────┬────────┘
▼
[ Deny → 403 ] or [ Allow → business logic + audit fields ]
Authenticate before authorize: 401 when the principal is unknown; 403 when known but disallowed. Generated code should centralize checks in gateway or service middleware, not copy-paste into every handler.
Authentication vs authorization: 401 / 403 and anti-patterns
- 401 Unauthorized: missing credential, expired, bad signature—client should refresh or re-login; do not conflate with "insufficient permission."
- 403 Forbidden: identity known but action or resource not allowed—optional finer error codes per team convention without leaking resource existence.
- Anti-patterns: 200 +
{ "allowed": false }to hide auth failure; bodies like "email not registered" for enumeration; telling the model to "decide if allowed" instead of server policy.
JWT full validation flow — all checks must be performed in order; skipping any step is a vulnerability:
// Node.js: JWT correct validation flow (verify signature → check exp → check iss → check aud)
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true, cacheMaxEntries: 5, cacheMaxAge: 600000
});
async function verifyJWT(token: string): Promise<JWTPayload> {
// Step 1: Decode header to get kid, do NOT trust payload content yet
const decoded = jwt.decode(token, { complete: true });
if (!decoded) throw new Error('Invalid token format');
// Step 2: Verify signature — fetch public key from JWKS, never use alg:none or symmetric keys
const key = await client.getSigningKey(decoded.header.kid);
const publicKey = key.getPublicKey();
return new Promise((resolve, reject) => {
jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Step 2: algorithm allowlist, reject HS256/none
issuer: 'https://auth.example.com', // Step 3: verify iss, prevent cross-service token reuse
audience: 'api.example.com', // Step 4: verify aud, prevent token reuse on other services
clockTolerance: 30, // Step 5: exp check with 30-second clock skew tolerance
}, (err, payload) => {
if (err) reject(new AuthError('JWT verification failed', 401));
else resolve(payload as JWTPayload);
});
});
}
// Common vulnerabilities to avoid:
// ❌ No signature verification: jwt.decode(token) — anyone can forge the payload
// ❌ No exp check: jwt.verify(token, key, { ignoreExpiration: true })
// ❌ Confused identity source: not checking iss, service A's token accepted by service B
RBAC: role-permission mapping and middleware implementation
Role-based access control bundles permissions into roles inherited via membership. Fits orgs with clear boundaries and enumerable actions; pitfalls include role explosion and same-role different visibility per tenant, often needing role × tenant matrices or hierarchies.
// RBAC implementation: role-permission mapping table + Express permission check middleware
type Permission = 'read:articles' | 'write:articles' | 'delete:articles' | 'admin:users';
type Role = 'viewer' | 'editor' | 'admin';
// Role-permission mapping table: single source of truth, changes require approval
const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
viewer: ['read:articles'],
editor: ['read:articles', 'write:articles'],
admin: ['read:articles', 'write:articles', 'delete:articles', 'admin:users'],
};
// Permission check middleware: mounted on route layer, not scattered in handlers
function requirePermission(permission: Permission) {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = req.user?.role as Role;
if (!userRole) return res.status(401).json({ error: 'Unauthenticated' });
const allowed = ROLE_PERMISSIONS[userRole]?.includes(permission) ?? false;
if (!allowed) {
// 403: identity known but insufficient permissions; do not leak resource existence
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage: deny by default, explicit permission grant required
app.delete('/articles/:id', authenticate, requirePermission('delete:articles'), handler);
- Role naming and audit: role grants must be traceable (who, when, what scope).
- Deny by default: no role or binding means deny — avoid "new route forgot policy."
- Map OAuth scopes to internal RBAC in docs; avoid overly broad scopes.
ABAC: attribute-based access control implementation
Attribute-based access control combines subject, resource, action, and environment attributes (often with a policy engine or DSL). Fits owner/department rules, dynamic tags, and compliance; cost is readability, test cases, and per-request evaluation latency.
// ABAC simple implementation: policy evaluation based on subject/resource/action/environment attributes
interface AccessContext {
subject: { userId: string; role: string; department: string; tenantId: string };
resource: { ownerId: string; tenantId: string; classification: 'public' | 'internal' | 'confidential' };
action: 'read' | 'write' | 'delete';
environment: { ipRegion: string; mfaVerified: boolean };
}
// Policy functions: versioned, testable, each rule has a unique name for audit logs
const policies = [
{
name: 'owner-can-always-read-own',
evaluate: (ctx: AccessContext) =>
ctx.action === 'read' && ctx.subject.userId === ctx.resource.ownerId
&& ctx.subject.tenantId === ctx.resource.tenantId,
},
{
name: 'confidential-requires-mfa',
evaluate: (ctx: AccessContext) =>
ctx.resource.classification === 'confidential'
? ctx.environment.mfaVerified // environment attribute: MFA verified
: true, // other classifications not restricted by this policy
},
{
name: 'tenant-isolation',
evaluate: (ctx: AccessContext) =>
ctx.subject.tenantId === ctx.resource.tenantId, // tenant isolation: from token claims
},
];
function checkAccess(ctx: AccessContext): { allowed: boolean; reason: string } {
for (const policy of policies) {
if (!policy.evaluate(ctx)) {
return { allowed: false, reason: policy.name }; // deny by default, log reason
}
}
return { allowed: true, reason: 'all-policies-passed' };
}
- Trust attribute sources: token claims must match IdP or directory — clients must not forge.
- Version policies with rollback; keep allow/deny matrices and tests for critical rules.
- Hybrid with RBAC: coarse roles plus ABAC for resource-level patches.
When RBAC fits
- Permissions follow job function
- Operations and APIs are relatively stable
- Auditors need “who acted under which role”
When ABAC fits
- Rules depend on resource attributes (owner, classification, region)
- Same role needs different outcomes by context
- Compliance needs configurable policy with fewer code changes
APIs and Agent toolchains
Tool definitions should declare credential type and minimum scopes; the runtime injects short-lived tokens instead of models concatenating secrets. For multi-step delegation, log "acting principal + end user" or an equivalent audit dimension.
// Agent tool definition: declare required credential type and minimum scope
const tool = {
name: 'read_user_profile',
description: 'Read user profile, requires read:profile scope',
// Security metadata: runtime injects short-lived token, model does not concatenate
auth: {
type: 'bearer',
requiredScopes: ['read:profile'], // least privilege principle
tokenSource: 'runtime-injected', // forbidden to read from prompt
},
execute: async ({ userId }, { token }) => {
// Runtime provides short-lived token, log dual-identity audit dimension
const response = await fetch(`/api/users/${userId}/profile`, {
headers: { Authorization: `Bearer ${token}` },
});
// Audit log: decision result + acting principal, do NOT log full token
auditLog({ action: 'read_profile', subject: token.sub, targetUser: userId });
return response.json();
}
};
- SKILL requirement: before calls, list assumed auth mechanism and authorization checkpoints.
- Logs and traces: sample allow/deny reason codes; never log full tokens or passwords.
SKILL snippet
---
name: authn-authz-pattern
description: Design or review authentication vs authorization boundaries
---
# Authentication steps (JWT)
1. Verify signature: fetch public key from JWKS endpoint, algorithm allowlist only RS256/ES256
2. Check exp: must not set ignoreExpiration; clock tolerance at most 30s
3. Check iss: must exactly match trusted issuer URL
4. Check aud: must include this service's audience identifier
5. Parse sub/claims: do not trust payload fields without signature verification
# Authorization steps
6. RBAC: evaluate from single role-permission mapping table, unified middleware mount
7. ABAC: combine subject/resource/action/environment attributes, policy functions independently testable
8. Deny by default: no explicit allow rule returns 403
9. Log decision reason code for audit and debugging
# Security constraints
10. Strict 401 vs 403 semantics, do not leak account enumeration info
11. Agent tools do not read secrets from prompt, runtime injects short-lived tokens
12. Multi-tenant: tenant_id from token claims, do not trust client-supplied params
13. Logs: record sub + action, do not record full token or password
14. Long-lived secrets must not appear in examples, comments, or version history
15. Sensitive operations (delete, payment) require second auth factor or MFA confirmation
Policy draft lab (auth-page JS)
Check constraints to generate a local draft for design docs suggesting RBAC, ABAC, or hybrid—no network calls.
Draft updates as you toggle; final policy still follows threat model and compliance.