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.

Common constraints

              

Draft updates as you toggle; final policy still follows threat model and compliance.

Back to skills More skills