Dev.to Security 🔐 Cybersecurity 👁 0 📖 2 min read

401 or 403? The status code shows whether your app keeps login and permissions apart

I work at Permit, and this is based on a post on our blog, so read it with that in mind. Most of it applies whatever you use for authorization. The short version: authentication (AuthN) establishes who the caller is. Au

I work at Permit, and this is based on a post on our blog, so read it with that in mind. Most of it applies whatever you use for authorization.

The short version: authentication (AuthN) establishes who the caller is. Authorization (AuthZ) decides what that caller can do with a specific resource. A user can be fully logged in and still not be allowed to export an invoice from another tenant.

The status code mix-up

HTTP made this confusing by naming 401 "Unauthorized". In practice:

  • 401 means the request isn't authenticated: missing token, expired session, bad API key. The client should log in again or refresh.
  • 403 means the caller is known and policy says no. The client should show access denied or offer a way to request access.

Getting these right helps more than the client. "We don't know who this is" and "we know who this is and they're not allowed" are different events in your logs and during an incident. If both come out as 401, or both as 403, you lose that.

Your IdP doesn't answer the second question

Okta, Auth0, Entra ID and the rest handle login, SSO, MFA and user lifecycle, and you shouldn't rebuild any of that. They can also tell you Alice is in the Finance group. They can't tell you whether Alice may export invoice:987 in tenant:acme on the current plan, because that depends on your product's resources, actions, tenants and workflow state.

So most apps start with something like if user.role == "admin": allow(). That holds up until you add tenants, custom roles, delegated admins and audit requirements. Then the checks spread across endpoints, UI components and background jobs, and nobody can say in one place who can do what.

Ask why, as well as allow or deny

The post also argues that a decision should be explainable after the fact: which policy applied, and which role or attribute produced the result. Support and auditors will ask, and "the code returned false" doesn't answer them.

A quick check you can run on your own API: pick an endpoint, call it with no token, then with a valid token for a user who lacks the permission. You should get 401 and then 403, and the second call should leave a log line saying which rule denied it.

The full post has a table of the two codes, more on the IdP split, and how we approach it at Permit (a policy decision point running next to the app): What's the difference between authentication and authorization?

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.