Zero-Trust Security

Security is one of the most consistently broken areas in AI-generated code. Studies show that nearly half of AI-produced code contains security vulnerabilities — missing authorization checks, hardcoded credentials, and overly permissive access controls are the most common offenders. Understanding Zero-Trust Security is your first line of defense as an AI-assisted builder: if you cannot articulate what correct security looks like, you will not catch it when the AI gets it wrong. For a detailed security guide covering AI-specific threats, developer checklists, and secure coding practices, refer to the Security Guide.

The Old Model Is Broken#

The traditional security model was built on a perimeter: once you were inside the corporate network or VPN, you were implicitly trusted. Everything outside was suspect; everything inside was assumed safe.

That model fails in three ways that are especially relevant today:

  1. Cloud and microservices — modern systems are distributed across many services, cloud providers, and networks. There is no meaningful "inside."
  2. Remote work and SaaS — users access systems from personal devices on untrusted networks. The perimeter no longer maps to physical location.
  3. AI-generated services — when AI agents generate microservices that call other microservices, each inter-service call is a potential attack surface. Without explicit verification at every hop, a single compromised service can move laterally through the entire system.

The replacement framework is Zero Trust.

The Three Pillars of Zero Trust#

Zero Trust is a security philosophy, not a product. It rests on three core principles that apply at every layer of your system.

PrincipleWhat It MeansConcrete Example
Verify ExplicitlyAuthenticate and authorize every request, every time, based on all available data — identity, device, location, time of day. Never trust based on network location alone.An internal microservice calling another must present a valid token on every request — not just at startup. Being 'on the internal network' is not sufficient authorization.
Use Least PrivilegeGrant only the minimum permissions needed to do the job. Access should be scoped narrowly, time-limited, and revoked automatically.A service that reads from the analytics database should only have SELECT on the specific tables it needs — not a blanket admin credential it inherited from a copy-pasted config.
Assume BreachDesign as if attackers are already inside. Encrypt all traffic (even internal), segment networks to limit blast radius, and monitor for anomalous behavior.Encrypt traffic between microservices even within your private network. If one service is compromised, the attacker should not be able to read traffic flowing to other services.

Perimeter Security vs. Zero Trust

The perimeter model trusts everything inside the network. Zero Trust treats every request as potentially hostile — regardless of origin. This is the shift that Zero Trust forces: from 'is the request coming from inside?' to 'who is making this request, and are they authorized for exactly this action?'

Rendering diagram...

Authentication vs. Authorization#

Before diving into tokens and protocols, it is important to distinguish two concepts that are often conflated — even by AI-generated code.

Authentication answers: Who are you? It verifies identity — the process of checking credentials and confirming that a user or service is who they claim to be.

Authorization answers: What are you allowed to do? It verifies permissions — even after authentication succeeds, the system checks whether the authenticated entity has the right to perform the requested action on a specific resource.

AI-generated code often implements authentication correctly but omits authorization entirely. A common pattern is a login flow that validates credentials and issues a token, but then returns data to any authenticated user without checking whether they own that specific resource. This is the IDOR (Insecure Direct Object Reference) vulnerability: an authenticated user accessing /api/orders/42 can see any order just by changing the ID — because the code verifies the JWT but never checks whether the order belongs to them.

The fix is always the same: after verifying the token, check the claim. Both steps are required.

OAuth 2.0: The Authorization Framework#

OAuth 2.0 (RFC 6749) is the industry standard for delegated authorization. It lets a user or service grant a third party access to resources without sharing raw credentials. Modern auth in production systems is built on OAuth 2.0.

The Core Roles#

RoleWhat It IsExample
Resource OwnerThe user who owns the dataA user with photos stored in a cloud account
ClientThe app requesting access on the user's behalfA photo-editing app that wants to read those photos
Authorization ServerIssues tokens after verifying identity and consentAuth0, Okta, Cognito, or your own auth service
Resource ServerThe API that holds the protected resourceThe photo storage API that the editing app wants to call

The Authorization Code + PKCE Flow#

For user-facing applications (web apps, mobile apps), the correct OAuth flow is Authorization Code with PKCE (Proof Key for Code Exchange). The older Implicit Flow — where tokens were returned directly in the browser URL — is deprecated because tokens exposed in URLs are easily leaked via browser history, referrer headers, and server logs.

PKCE solves a subtle problem: public clients (web apps, mobile apps) cannot safely store a secret. Unlike a backend server that runs in a controlled environment, a browser app's source code is fully visible to anyone who opens the browser's developer tools — so any "secret" baked into the app is effectively public. PKCE gives these clients a way to prove they initiated the flow without needing a secret. Here is how it works:

Rendering diagram...

Why PKCE matters: If an attacker intercepts the authorization_code in step 5 (for example, via a malicious redirect URI), they cannot exchange it for tokens. They do not have the code_verifier — only the app that generated it does. The server verifies this in step 7, ensuring the entity completing the flow is the same one that started it.

Tokens: Access vs. Refresh#

Token TypeLifetimePurposeWhere to Store
Access TokenShort — 5 to 15 minutesSent with every API request as proof of authorizationIn-memory only (never localStorage)
Refresh TokenLonger — hours to daysUsed to get a new access token when the current one expires, without requiring the user to log in againhttpOnly, Secure, SameSite=Strict cookie
ID TokenShort — same as access tokenContains user profile info for the client to display (an OpenID Connect addition on top of OAuth 2.0)In-memory; decode client-side, never send to APIs

Never store access tokens in localStorage. JavaScript running in the same origin — including scripts injected by third-party packages — can read localStorage. An XSS attack that injects a script can exfiltrate every stored token instantly. Store access tokens in memory; store refresh tokens in httpOnly cookies, which JavaScript cannot access at all.

JWT: The Token Format#

OAuth 2.0 defines the authorization flow — it does not specify what tokens look like. In practice, most systems use JWT (JSON Web Token, RFC 7519) as the token format. A JWT is a compact, self-contained string that carries verifiable claims.

Structure: Three Parts Joined by Dots#

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9    ← Header
.eyJzdWIiOiJ1c2VyXzEyMyIsImlzcyI6Im... ← Payload (Claims)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV... ← Signature

Each part is Base64url-encoded. The payload is not encrypted — anyone can decode it. Only the signature makes it tamper-proof: if anyone modifies the payload after the token is issued, the signature will no longer match and validation will fail. Never put secrets or sensitive PII in a JWT payload.

Key Claims in the Payload#

ClaimNameMeaning
subSubjectThe unique ID of the user or service the token represents (e.g., user_123)
issIssuerThe URL of the authorization server that issued the token (e.g., https://auth.example.com)
audAudienceThe intended recipient of the token — your API should reject tokens not addressed to it
expExpirationUnix timestamp after which the token is invalid — always validate this
iatIssued AtUnix timestamp when the token was created — useful for detecting replay attacks
scopeScopesSpace-separated list of permissions granted (e.g., read:orders write:profile)
jtiJWT IDA unique ID for this token — enables token revocation via a blocklist

Signing Algorithms#

The signature guarantees that no one tampered with the payload after it was issued. The algorithm is specified in the header.

AlgorithmTypeWhen to Use
HS256 / HS512HMAC — symmetric (the same secret is used to both sign and verify)Only for internal services where you fully control both the issuer and the verifier. Never share the secret with external parties.
RS256 / RS512RSA — asymmetric (private key signs, public key verifies)Preferred for production APIs. The authorization server holds the private key; resource servers verify using the public key, published at a /.well-known/jwks.json endpoint. This way, no service needs access to the private key.
ES256 / ES384ECDSA — asymmetric (same trust model as RSA)Same security model as RSA but with smaller key sizes and faster verification — a modern alternative to RS256.

How a Resource Server Validates a JWT#

Token validation is not optional, and it is not just checking the signature. Every claim matters:

Rendering diagram...

AI-generated auth code commonly only checks the signature and skips the remaining claims. Skipping exp, iss, or aud validation creates real vulnerabilities: an expired token from a compromised account is still accepted, or a token issued by a different authorization server for a completely different API is treated as valid.

A critical pitfall — the alg: none attack: Some JWT libraries historically accepted tokens with "alg": "none" in the header, meaning no signature at all. An attacker could craft any payload they wanted without a signing key. Always explicitly allowlist the valid algorithms your server accepts — never let the incoming token header dictate which algorithm to use for verification.

Machine-to-Machine (M2M) Authentication#

In modern systems, services frequently need to call other services — a payment microservice calling a fraud-detection service, a CI/CD pipeline deploying to a cloud environment, a monitoring agent posting metrics. These calls have no human involved: there is no user to log in and no browser to redirect.

The standard OAuth 2.0 pattern for this is the Client Credentials Flow (RFC 6749 §4.4). Instead of a user, the service itself is the authenticated principal.

OAuth 2.0 Client Credentials Flow (M2M)

Each service is registered with the authorization server as a 'client' with its own client_id and client_secret. The service authenticates with these credentials to get a short-lived access token, then uses that token to call other services. No human is involved in the loop — the service acts as its own principal.

Rendering diagram...

M2M Security Pitfalls#

PitfallWhat Goes WrongThe Fix
Hardcoded secretsThe client_secret is committed to git and is now permanently leaked — even deleting it from git history does not help if the repo was ever cloned or pushed to a remoteStore secrets in a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager). Inject at runtime via environment variables, never at build time.
Shared credentialsAll services use the same client_id/client_secret. One compromised service means every service's tokens must be rotated simultaneously — often causing an outageOne credential per service, no exceptions.
Long-lived access tokensA leaked access token is valid for hours. The attacker has a large window to use it before it expiresKeep access token lifetime at 5–15 minutes. Services cache and auto-refresh.
Fetching a new token per requestThe authorization server gets hammered with token requests. At high throughput, this becomes a bottleneck and a rate-limit riskCache the access token. Track its exp claim and refresh it ~60 seconds before it expires.
No scope validation on the serverService B accepts any valid token, regardless of what scopes it carries. A token issued for Service C's scope works on Service B tooValidate the required scope on every endpoint — not just that the token is valid, but that it authorizes this specific action.
Skipping key rotationSigning keys are never rotated. If the private key leaks, all tokens ever issued under that key are compromised — with no recovery path short of rotating all credentialsRotate signing keys regularly. Use a dual-key strategy: publish the new key before switching to it, and keep the old key active until all tokens signed with it have expired.

What AI Gets Wrong Here — and How to Fix It#

AI agents, when asked to generate auth code, consistently make predictable mistakes. Knowing these patterns lets you write better prompts and review AI output more systematically.

What AI Generates by DefaultWhat You Should Ask For Instead
Checks that a JWT is present but does not validate exp, iss, or audAsk: 'Validate all standard JWT claims: signature, expiration, issuer, audience, and required scopes. Reject the request if any check fails.'
Grants access based on authentication alone, without checking resource ownershipAsk: 'After validating the token, check that the authenticated user owns or has permission to access the specific resource by ID before returning it.'
Stores service credentials in a .env file that is committed to gitAsk: 'Read the client_secret from an environment variable injected by the secrets manager at runtime. Never hardcode it or read it from a file in the repository.'
Issues tokens with no expiry (exp claim omitted)Ask: 'Issue access tokens with a 15-minute expiry. Refresh tokens should expire after 7 days and be rotated on use.'
Uses HS256 with a hardcoded string as the secretAsk: 'Use RS256 with a private key stored in the secrets manager. Expose the public key at /.well-known/jwks.json for clients to verify tokens.'
Fetches a new M2M token on every API callAsk: 'Cache the access token in memory. Track the exp claim and refresh the token 60 seconds before it expires. Never fetch a new token per request.'

Summary#

ConceptThe Key Point
Zero TrustNever trust based on network location. Verify every request explicitly, use least-privilege access, and design as if attackers are already inside.
Authentication vs. AuthorizationAuthentication verifies identity; authorization verifies permission. Both are required. AI-generated code routinely implements auth without authz.
OAuth 2.0The industry-standard delegation framework. Use Authorization Code + PKCE for user-facing flows. Never use the deprecated Implicit Flow.
JWTA signed, self-contained token format. The payload is readable by anyone — never store secrets in it. Validate all claims: signature, exp, iss, aud, and scope.
Access vs. Refresh tokensAccess tokens are short-lived (5–15 min) and sent with every request. Refresh tokens are longer-lived and stored in httpOnly cookies that JavaScript cannot read.
M2M / Client CredentialsServices authenticate as principals using client_id + client_secret. One credential per service. Cache tokens and refresh before expiry.
Secret managementNever hardcode secrets. Store them in a secrets manager and inject at runtime. Rotate signing keys regularly using a dual-key strategy.
AI review checklistVerify that AI-generated auth validates all JWT claims, enforces resource ownership, reads secrets from the environment, and uses short token lifetimes.

Zero Trust is not a feature you add at the end of a project — it is a design constraint you specify before the AI writes a single line. The best time to enforce it is in your architectural spec. The second best time is in the prompt that follows.

Sources: