Authentication vs. authorization
Authentication proves who you are. Authorization decides what you're allowed to do once you're in. 401 versus 403.
See it
What it is
Authentication (authn) answers 'who is this?'. Password, passkey, magic link, an SSO handshake with an identity provider: whatever the method, the output is an identity you trust. Authorization (authz) answers a completely different question: 'may this identity do this action on this specific resource?'. The output is allow or deny. They get shortened to the same four letters, which is why the field invented authn and authz to keep them apart in code and in conversation.
HTTP encodes the split, badly. 401 Unauthorized actually means unauthenticated: no valid credentials, go log in. 403 Forbidden means we know exactly who you are and the answer is still no. Getting these backwards sends signed-in users into infinite login redirects, because the client sees a 401 and tries to re-authenticate a user who is already authenticated fine.
The gotcha that ships real vulnerabilities: logging in is not permission. Checking authn in a middleware and then trusting a resource id straight from the request ('GET /invoices/8842') lets any logged-in user read anyone's data. That is broken object level authorization, the top item on the OWASP API Security Top 10, and it is boring rather than clever: the fix is scoping every query by the caller's tenant and ownership, right next to the data access, not up in the router. Related confusion: OAuth scopes are delegated authority. They cap what an application may attempt on someone's behalf, and they are often already narrowed by that person's own privileges, but they are not a resource-level permission check. An operation should run only when the token's scopes allow it and your application's permissions allow it for that specific record.
Ask AI for it
Separate authentication from authorization in this codebase. Add a requireAuth middleware that only verifies the session or bearer token and attaches the user, returning 401 with a WWW-Authenticate header when no valid credential is present. Add a separate can(user, action, resource) authorization check that runs after the record is loaded and returns 403 when the user is known but not permitted. Scope every database query by tenant and ownership rather than trusting ids from the request, and require both the token's scopes and the app's own resource permissions to allow an operation before it runs, so /invoices/:id cannot return another organization's invoice, and add tests covering the three cases: anonymous gets 401, wrong-tenant user gets 403 or 404, owner gets 200.