Authentication and authorization
Who you are versus what you may do, password hashing with bcrypt or Argon2, sessions versus tokens, MFA, and account enumeration.
Two words that get used interchangeably and mean different things, and almost every access-control bug lives in the gap between them.
- Authentication — who are you? Proving identity. It happens once per request, at the edge.
- Authorisation — may you do this? Checking permission. It happens per operation, and it needs to know what is being operated on.
A system can get the first perfectly right and still hand one customer another customer's invoice, because those are different questions asked at different moments.
The bug that follows from confusing them
@GetMapping("/orders/{id}")
public OrderView get(@PathVariable Long id) {
return toView(orders.findById(id).orElseThrow());
}The endpoint is behind a login. A request without a valid session is rejected before this method runs, so the code is authenticated. It is not authorised: nothing checks that this order belongs to the caller. Change 42 to 43 in the URL and you are reading somebody else's order.
This is the single most common serious flaw in real APIs, it has a name — broken object level authorisation, first on the OWASP API list — and it is invisible to every test that logs in and checks a 200.
The fix is to make the question part of the query:
Order order = orders.findByIdAndCustomerId(id, currentUser.getCustomerId())
.orElseThrow(OrderNotFoundException::new);Note the shape: not fetch, then check, but fetch the thing this user is allowed to fetch. There is no window in which the object exists in a variable without the check having happened, and the failure is a 404 rather than a 403 — which is also the answer that does not confirm the order exists.
Where the check belongs
The rule that survives contact with a real codebase: authenticate at the edge, authorise where the data is.
Authentication is a filter's job — one place, one mechanism, every request. Authorisation is not, because a filter knows the URL and the method and nothing about which order, whose account, what state it is in. Pushing it into the controller is better; pushing it into the service layer, beside the query, is better still, because that is where every caller passes.
Roles, authorities and what they actually model
- A role is a coarse label for a kind of user:
ADMIN,SUPPORT,CUSTOMER. - An authority (or permission) is a specific action:
order:refund,user:read.
Spring Security treats both as GrantedAuthority and only the naming convention differs — a role is an authority with a ROLE_ prefix. That is worth knowing because it explains why hasRole("ADMIN") and hasAuthority("ROLE_ADMIN") mean the same thing, which otherwise looks like a bug.
Which to use is a design question with a practical answer. Roles multiply badly. The moment somebody needs "support, but also allowed to issue refunds under ₹500", a role-only system grows SUPPORT_WITH_REFUNDS, and then a third variant, and then nobody can say what any of them can do. Permissions assigned to roles — the user has a role, the role grants a set of permissions, and the code checks permissions — keeps the check in the code stable while the policy moves.
There is a third model worth naming because you will meet it as a requirement rather than as a choice: attribute-based access control, where the decision depends on the data. "A manager may approve an expense, but not their own" is not a role, not a permission, and not expressible in an annotation. It is a rule about this user and this object, and it belongs in the service.
Principle of least privilege, applied to things that are not users
The idea is familiar for people. It is routinely forgotten everywhere else, and each of these is a real incident shape:
- The database user your application connects as. It almost never needs
DROP, and frequently does not needDELETE. An injection flaw in an application connected as the schema owner is a very different day from one connected as a user withSELECTandINSERTon eight tables. - The service account calling another service. Scope the token to what that integration does.
- The deploy key, the CI token, the cloud role. Each one is a credential that will eventually be exfiltrated by something, and its blast radius is whatever you granted it.
Failing closed
When an authorisation check cannot be completed — the permissions service is down, a claim is missing from the token, the role is unrecognised — there are two possible defaults and only one of them is defensible.
// fails open: an unrecognised role is treated as harmless
if (user.getRole() == Role.ADMIN) return DENY_NOTHING;
return checkPermissions(user);
// fails closed: anything not explicitly permitted is refused
if (!permitted(user, action, resource)) throw new AccessDeniedException();Default-deny is the property to design for, and the test that proves it is the one people do not write: a request with no permissions at all should be refused by every endpoint. A new endpoint added next year is then secure by omission rather than insecure by omission.