Your app has login. That proves it can recognize a user.
It does not prove that the user can reach only their own invoices, projects, files, teams, exports, or administrative actions.
Authentication answers who are you? Authorization answers may you do this to this object now?
Confusing the two is one of the most dangerous shortcuts in a fast-built app.
A valid session is not permission to every record the server can find.
Begin with the promise, not the auth library
Write the access rules in sentences a founder can inspect.
For example:
- A signed-out visitor can read public reports, but not private reports.
- A member can read projects belonging to their current team.
- An editor can change a project, but a viewer cannot.
- A team administrator can invite members and change roles.
- No team administrator can read another team’s data.
- A support operator can inspect account metadata, but not private document contents.
- A deleted membership removes access to new requests immediately.
These are product rules. Your authentication provider cannot invent them from a session cookie.
OWASP’s current Authorization Cheat Sheet recommends defining users, resources, and permitted operations before choosing the enforcement mechanism. That order matters in AI-generated code. If the prompt says only “add team auth,” the model must guess what a team member is allowed to do.
Draw a small access matrix
List the actors down the left and the important objects across the top.
Actors might include:
- signed-out visitor;
- ordinary account owner;
- team viewer;
- team editor;
- team administrator;
- support operator;
- background worker;
- provider webhook.
Objects might include:
- account profile;
- project;
- uploaded file;
- report;
- team membership;
- billing subscription;
- export;
- audit record.
For each pair, mark whether the actor may create, read, update, delete, share, or administer the object. Add conditions such as “only when the object belongs to the current team” or “only after fresh confirmation.”
Keep the first matrix small. If you cannot explain the access model on one page, adding more roles will not make it safer.
Create two users, not one founder account
The fastest useful authorization test needs:
- Account A with its own records and files;
- Account B with different records and files;
- one administrative or team-owner account when the product has roles;
- a clean signed-out browser.
Give the records memorable harmless names such as “A private launch” and “B private launch.” Do not use real customer data.
Then run the same actions from both accounts:
- List records.
- Open one record directly.
- Change it.
- Delete or archive it.
- Download its files.
- Create and download an export.
- Share or invite another user.
- Repeat after a role change or removal.
Account A should never see Account B’s object, even when A knows its identifier or direct URL.
This test finds the class of bug hidden by a founder’s single warm account. The interface looks correct because every record in the development database belongs to the same person.
Test the server, not only the menu
Hiding a button is interface design. It is not authorization.
A user can call an endpoint without clicking your button. They can alter a request, reopen an old link, repeat a form submission, or use a client version that still displays an action.
The trusted server path must decide:
- Which authenticated actor is making the request?
- Which object are they trying to reach?
- What action are they attempting?
- Which relationship or policy permits it now?
- What safe result follows when permission is absent?
OWASP’s 2025 description of broken access control specifically includes changing URL parameters, reaching APIs with missing checks, viewing another user’s object through its identifier, and relying on client-side controls. Its prevention guidance places enforcement in trusted server-side code or serverless APIs.
Check every route that changes or returns consequential data. One protected page wrapped around an unprotected API is still an unprotected product.
Do not accept ownership from the browser
A common generated pattern looks safe because every row has a userId or teamId. The danger is where that value comes from.
Do not let an ordinary client choose the owner by sending:
userId;accountId;teamId;role;isAdmin;- another ownership field the server could derive.
The server should derive the actor from the verified session, load the target object, and check the required relationship. A team selector in the interface may tell the server which team the user intends to use. It does not prove membership in that team.
OWASP’s Business Logic Security guidance advises rechecking ownership on every request and treating every request field, including hidden and JavaScript-set fields, as input rather than authority.
Random identifiers do not grant permission
A long unpredictable ID is useful. It reduces accidental discovery and casual guessing.
It is not an access-control rule.
If Account A obtains Account B’s object ID through a shared email, log entry, browser history, analytics event, screenshot, referrer, or support conversation, the server must still refuse the request.
Do not write “nobody can guess the URL” as the reason a private object is private. Share links are a separate product design. If possession of a secret link grants access, define its scope, expiry, revocation, indexing behavior, and consequences plainly.
The website trust signals guide explains why privacy promises must match the actual path by which records are reached and shared.
Test every form of the same object
Protecting the main database query is not enough when the object appears elsewhere.
For each private record, check:
- detail and list pages;
- search results;
- recent-item menus;
- notifications;
- email links;
- previews and thumbnails;
- uploaded originals;
- generated downloads;
- CSV or PDF exports;
- activity feeds;
- audit logs;
- cached responses;
- API responses;
- support tools.
An application may deny /projects/B while still returning Project B through global search or an export job created by Account A.
Ask one severe question: where else can this object be represented?
Files need the same ownership rule
Private files often escape the application’s checks because object storage uses a public URL or a long filename.
Test whether Account A can:
- open Account B’s stored object directly;
- reuse an old signed download URL after removal;
- change only the file key in a download request;
- see a thumbnail whose original is protected;
- access a file through a CDN cache;
- list a bucket or infer neighboring objects.
Keep private storage private. Generate narrowly scoped access only after the application verifies the actor and object. Choose expiry based on the product’s use, and understand that a signed URL may remain usable until it expires even after a team role changes.
Never put secrets or sensitive record contents into a URL. URLs leak through logs, history, referrers, screenshots, and support messages.
Trace work that happens after the request
Authorization can disappear between the button and the consequence.
A user starts an export. The server places a job on a queue. A worker later loads the requested project and writes a file. Which identity and permission travel with that job?
Decide whether the worker should:
- recheck that the initiating actor still has access;
- operate under a narrow system permission;
- stop when the team or object was deleted;
- record who initiated the action;
- prevent the result from being delivered to a different account.
Run the uncomfortable sequence: start a job, remove the user from the team, then let the job complete. Decide the correct outcome before testing it.
Apply the same reasoning to scheduled tasks, webhooks, email digests, AI agents, imports, and retry queues. A trusted worker still needs a defined scope.
Test role changes as state changes
Permissions are not static.
Test:
- accepting an invitation twice;
- accepting an expired or revoked invitation;
- inviting an address already attached to another account;
- lowering an administrator to an editor;
- removing the last owner;
- leaving a team while a sensitive page is open;
- using an old browser tab after removal;
- signing out and replaying a prior request;
- changing a password while other sessions remain active;
- deleting an account that owns shared resources.
Decide whether access should end immediately or when a short-lived credential expires. Make the behavior consistent with the promise you show users.
The safest rule is usually to grant the least access required and make expansion explicit. OWASP recommends denying by default and being able to explain why a permission was granted.
Treat support and admin access as a separate product
An administrative page is not safe because only founders know the URL.
List each support action: view account metadata, resend verification, change an address, refund a charge, restore a record, assume a user’s view, or inspect private content. Give each one an authorization rule and audit record.
High-risk actions may need fresh authentication, a second approver, or a reason recorded before execution. Avoid broad “super admin” access when a narrow support tool can solve the job.
If the founder can impersonate a user, make the state unmistakable, prevent silent destructive actions, log entry and exit, and decide whether the user should be notified. Do not let support convenience become an invisible master key.
Fail without revealing more than necessary
When access is denied, the response should not expose the private object in the error message, log, preview, or page title.
For many private-object routes, returning “not found” may reveal less than confirming that the object exists but belongs to somebody else. The correct response depends on the product, but the decision should be deliberate.
Record enough server-side context to investigate:
- request or trace identifier;
- actor identifier;
- attempted action;
- object type and internal identifier;
- policy result;
- safe reason category;
- timestamp.
Do not log session tokens, passwords, private file contents, or entire request bodies by default.
Repeated denied requests may deserve an alert or rate limit. One denial may simply be an old tab. Logging should help the team distinguish the two.
Ask the AI for a proof, not reassurance
“Is my auth secure?” invites a broad answer and borrowed confidence.
Give the coding agent the access rules and ask for bounded work:
Map every server route, action, job, storage operation, and cache that can create, read, update, delete, share, or export a project. For each path, identify how the actor is authenticated, how ownership or role is checked, and what test proves Account A cannot operate on Account B’s object. Do not change code yet. Report missing or client-only enforcement with file references.
Then ask it to implement tests from the matrix. Review the tests before trusting their result. A test that never substitutes Account B’s identifier does not prove isolation.
Use the AI-generated code review guide to trace the invariant from the public request to the database, storage, queue, and response.
Run the pre-launch isolation drill
Before opening access, prove these paths on the public deployment with controlled test data:
- A signed-out browser cannot reach private objects.
- Account A cannot list, read, edit, delete, share, or export Account B’s objects.
- A viewer cannot perform editor or administrator actions.
- A removed member loses access through pages, APIs, files, caches, and old tabs.
- Private uploads and generated exports obey the same ownership rules as database rows.
- Background work retains a narrow, auditable authorization context.
- Support tools grant only the access each task requires.
- Denials reveal no private record contents.
- Permission failures are logged without logging secrets.
- Automated tests repeat the most important separations on every change.
If one user can reach another user’s data, stop the affected path. Do not publish the direct URL, invite more testers, or treat the issue as a beta rough edge.
Add the corrected path to the production checklist for an AI-built app, then test it again after changes to teams, roles, storage, exports, caching, or background jobs.
Login is the beginning of the question
An authentication service can save a startup from inventing passwords, sessions, and identity flows. That is valuable. A YC discussion about security for startups makes the sensible case for using established authentication rather than rebuilding it carelessly.
But the library does not know which report belongs to which customer. It does not know whether a viewer may refund an invoice, whether a removed member may download yesterday’s export, or whether a support operator may open a private file.
Those are the product’s rules.
Write them down. Enforce them on the server. Test them with two real accounts. Repeat the test wherever the same object appears.
A user who has proved their identity has answered one question. Your app must still answer the next one, every time.
