Your JWT Is Valid and Your API Is Still Vulnerable
Login works, the access token is valid, and Alice can still read Bob's data by changing one URL id. This is the resource-authorization gap in AI-generated backends, plus the provider-neutral and database-neutral pattern that closes it.
I have seen this demo many times. The React app has a polished login screen. The identity provider redirects back. A JWT appears. The dashboard loads. Everybody relaxes because the padlock icon has entered the meeting.
Then I change /projects/alice-1 to /projects/bob-1. The API returns Bob's project. Login was perfect. Authorization was on holiday.
This is Broken Object Level Authorization, usually shortened to BOLA. Many developers also know the same family of bugs as IDOR. It is especially easy to generate with AI because the endpoint looks complete: authentication, a scope check, validation, a database query, and a nice JSON response. Every requested feature is there. The missing feature is the one the prompt did not name.
The working endpoint that is not safe
The route answers one question correctly: may this caller use the project-reading function? It never answers the next question: may this caller read this specific project?
- 01identityWho are you?valid token
- 02functionMay you read projects?projects:read
- 03objectMay you read this project?id + owner + tenant
- 04propertyWhich fields may leave?response schema
Three layers, three different jobs
Function-level authorization is usually a scope or role. It decides whether the caller may attempt an operation such as reading projects or refunding an order.
Object-level authorization connects the principal to one record. It decides whether Alice may read project 42, not whether Alice may read projects in general.
Property-level authorization decides which fields the caller may read or change. A project owner may rename a project without being allowed to set ownerId, tenantId, approvedAt, or role. Giving someone the Edit button does not mean handing them the database row and a pen.
Normalize identity once
The solution should not depend on one identity vendor. Auth0, Cognito, Entra ID, Clerk, Keycloak, Zitadel, Ory, or a signed session can all produce the same application principal:
Map the pair (issuer, subject) to one immutable application user ID. Do not use email as your ownership key. Email is contact information wearing a fake moustache and pretending to be a primary key.
Put ownership into the data operation
The safest repository is one that makes owner-scoped operations boring and obvious. This interface works with any ORM, SQL builder, document database, key-value store, or in-memory test double:
A real implementation should combine the resource ID and trusted owner or tenant in the same database query. Avoid loading by ID, checking in application code, then mutating by ID in a separate operation. That pattern is easy to weaken during refactoring and can create a time-of-check to time-of-use gap.
Return the same 404 for a missing project and a project owned by somebody else when revealing existence would be sensitive. A 403can tell Alice that Bob's secret project ID is real. Sometimes that disclosure is acceptable. It should be a decision, not an accident.
The client does not choose ownership
Creation endpoints have a second common problem: mass assignment. The AI passes the whole request body to the ORM, so a helpful caller includes ownerId, role, or isApproved. Very efficient. The attacker appreciates the developer experience.
Strict request schemas reject privileged fields. Response schemas perform the other half of the job by preventing internal flags, deleted records, billing metadata, or credentials from leaving the API.
One-user tests cannot prove isolation
A happy-path test proves Alice can use the product. It says nothing about Bob's data. Every user-owned or tenant-owned resource needs at least two principals in the test suite.
Repeat the same idea for list, update, and delete operations. Add a cross-tenant case when the product has organizations. Add an explicit admin case when support staff need access, and verify that the bypass creates an audit event.
Why AI keeps generating this bug
Business owners ask for login because login is visible. They ask for CRUD because CRUD is visible. They rarely ask for object-level authorization because they do not know the term, and they should not need an application-security vocabulary to request a normal product.
The coding agent optimizes for the prompt. It reaches the happy path, sees a valid token, and moves on. A bigger pile of tutorials does not automatically help if those tutorials also stop at "add JWT middleware, job done."
This rule belongs in framework documentation, executable tutorials, project scaffolds, AI instructions, and tests. Repeating it is not glamorous, but neither is explaining a cross-customer data leak to every customer at once.
What the framework can and cannot do
DaloyJS can verify identity, enforce scopes, validate identifiers, reject unexpected properties, validate response bodies, redact production errors, and carry a typed principal into the handler. It cannot know whether an invoice belongs to a user, an organization, both, or neither. That is business policy.
The framework can still make the policy difficult to forget. The official guidance now classifies identifier routes, favors owner-scoped repository operations, rejects client-selected ownership, and requires Alice-versus-Bob tests in generated agent instructions.
A short note about Django, TypeScript, and hiring
I would not reject Django because it is old. Mature software is not a yoghurt with an expiry date. Django's integrated security features are useful.
I would still choose a TypeScript backend when the available team already builds React and TypeScript every day, or when hiring for that stack is materially easier for the business. Sharing language, types, generated clients, and mental models across the frontend and backend is a legitimate operational advantage.
That staffing decision is not a security control. React developers do not receive object-level authorization through osmosis. Choose the stack your team can maintain, then make the authorization policy structural in that stack.
The takeaway
A valid JWT proves that the identity provider recognizes the caller. A scope proves that the caller may attempt an operation. Neither proves ownership of the ID in the URL.
Scope every user-owned and tenant-owned data operation using trusted principal data. Reject privileged ownership fields. Test with two users. Make admin bypasses explicit and audited. If Alice can change one character in a URL and become Bob, the login screen was only expensive decoration.
Read the complete resource authorization guide and build the multi-user projects tutorial. The tutorial includes the attack tests, because an authorization guide without an attacker is just optimistic documentation.
About the author: Filipino fullstack developer in Norway. Has spent around 12 years learning that a green login button is not an authorization policy, no matter how confidently the demo presenter clicks it.