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:
- Cloud and microservices — modern systems are distributed across many services, cloud providers, and networks. There is no meaningful "inside."
- Remote work and SaaS — users access systems from personal devices on untrusted networks. The perimeter no longer maps to physical location.
- 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.
| Principle | What It Means | Concrete Example |
|---|---|---|
| Verify Explicitly | Authenticate 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 Privilege | Grant 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 Breach | Design 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?'
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#
| Role | What It Is | Example |
|---|---|---|
| Resource Owner | The user who owns the data | A user with photos stored in a cloud account |
| Client | The app requesting access on the user's behalf | A photo-editing app that wants to read those photos |
| Authorization Server | Issues tokens after verifying identity and consent | Auth0, Okta, Cognito, or your own auth service |
| Resource Server | The API that holds the protected resource | The 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:
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 Type | Lifetime | Purpose | Where to Store |
|---|---|---|---|
| Access Token | Short — 5 to 15 minutes | Sent with every API request as proof of authorization | In-memory only (never localStorage) |
| Refresh Token | Longer — hours to days | Used to get a new access token when the current one expires, without requiring the user to log in again | httpOnly, Secure, SameSite=Strict cookie |
| ID Token | Short — same as access token | Contains 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#
| Claim | Name | Meaning |
|---|---|---|
sub | Subject | The unique ID of the user or service the token represents (e.g., user_123) |
iss | Issuer | The URL of the authorization server that issued the token (e.g., https://auth.example.com) |
aud | Audience | The intended recipient of the token — your API should reject tokens not addressed to it |
exp | Expiration | Unix timestamp after which the token is invalid — always validate this |
iat | Issued At | Unix timestamp when the token was created — useful for detecting replay attacks |
scope | Scopes | Space-separated list of permissions granted (e.g., read:orders write:profile) |
jti | JWT ID | A 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.
| Algorithm | Type | When to Use |
|---|---|---|
| HS256 / HS512 | HMAC — 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 / RS512 | RSA — 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 / ES384 | ECDSA — 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:
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.
M2M Security Pitfalls#
| Pitfall | What Goes Wrong | The Fix |
|---|---|---|
| Hardcoded secrets | The 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 remote | Store secrets in a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager). Inject at runtime via environment variables, never at build time. |
| Shared credentials | All services use the same client_id/client_secret. One compromised service means every service's tokens must be rotated simultaneously — often causing an outage | One credential per service, no exceptions. |
| Long-lived access tokens | A leaked access token is valid for hours. The attacker has a large window to use it before it expires | Keep access token lifetime at 5–15 minutes. Services cache and auto-refresh. |
| Fetching a new token per request | The authorization server gets hammered with token requests. At high throughput, this becomes a bottleneck and a rate-limit risk | Cache the access token. Track its exp claim and refresh it ~60 seconds before it expires. |
| No scope validation on the server | Service B accepts any valid token, regardless of what scopes it carries. A token issued for Service C's scope works on Service B too | Validate the required scope on every endpoint — not just that the token is valid, but that it authorizes this specific action. |
| Skipping key rotation | Signing 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 credentials | Rotate 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 Default | What You Should Ask For Instead |
|---|---|
Checks that a JWT is present but does not validate exp, iss, or aud | Ask: '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 ownership | Ask: '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 git | Ask: '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 secret | Ask: '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 call | Ask: '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#
| Concept | The Key Point |
|---|---|
| Zero Trust | Never trust based on network location. Verify every request explicitly, use least-privilege access, and design as if attackers are already inside. |
| Authentication vs. Authorization | Authentication verifies identity; authorization verifies permission. Both are required. AI-generated code routinely implements auth without authz. |
| OAuth 2.0 | The industry-standard delegation framework. Use Authorization Code + PKCE for user-facing flows. Never use the deprecated Implicit Flow. |
| JWT | A 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 tokens | Access 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 Credentials | Services authenticate as principals using client_id + client_secret. One credential per service. Cache tokens and refresh before expiry. |
| Secret management | Never hardcode secrets. Store them in a secrets manager and inject at runtime. Rotate signing keys regularly using a dual-key strategy. |
| AI review checklist | Verify 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:
- Zero Trust Architecture for Platform Engineers
- What is Zero Trust? | Microsoft Learn
- OAuth 2.0 Authorization Framework | RFC 6749
- JSON Web Token | RFC 7519
- JWT Best Practices | Curity
- Authorization Code Flow with PKCE | Auth0 Docs
- Client Credentials Flow | Auth0 Docs
- The Complete Guide to M2M Authentication | Stytch